Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions .github/capsule-pipeline/task-runner.dot
Original file line number Diff line number Diff line change
Expand Up @@ -396,12 +396,12 @@ digraph BacklogTaskRunner {
package [shape=box, class="maker",
prompt="Both gates passed. Prepare the work for handoff in $target_dir: ensure runner artifacts (.ai/) are excluded from version control via .git/info/exclude (do NOT commit them); create or reuse a branch named task/<task id> (the id is in the task file's frontmatter at $task_file); commit all task changes with a clear conventional commit message stating WHAT changed and WHY (draw on .ai/brief.md and .ai/critique.md; include the standard Amplifier co-authored-by attribution); and write .ai/SHIPPED.md summarizing the goal, what shipped, where the evidence lives (.ai/verify.log, .ai/convergence.jsonl, run events), and anything a reviewer should know."]

// TRACKED-.ai LEAK CHANNEL (T1-7 council): the porcelain grep excludes
// only UNTRACKED .ai entries (^?? .ai); a worker that git-adds/commits
// .ai/ mid-run evades that filter and ships runner artifacts. First
// assertion: `git ls-files .ai/` must be EMPTY, else dirty -> escalate.
// TRACKED-.ai LEAK CHANNEL (T1-7 council): assert first that
// `git ls-files -- .ai/` is EMPTY, then use an explicit pathspec to ask
// porcelain about every path except `.ai/`; either nonempty result means
// dirty -> escalate.
ship_check [shape=parallelogram, class="gate", max_retries=0,
tool_command="[ -z \"$(git ls-files .ai/)\" ] && [ -z \"$(git status --porcelain | grep -v -E '^\\?\\? \\.ai')\" ] && git log --oneline -1 | grep -q . && printf shipped || printf dirty"]
tool_command="tracked=$(git ls-files -- .ai/) || { printf dirty; exit 0; }; [ -z \"$tracked\" ] || { printf dirty; exit 0; }; status=$(git status --porcelain -- . ':(exclude).ai/') || { printf dirty; exit 0; }; [ -z \"$status\" ] && git rev-parse --verify -q HEAD^{commit} >/dev/null 2>&1 && printf shipped || printf dirty"]

// ---------- budget exhaustion: a decision point, not a fuse ----------
// MUST_WRITE + S2 END-STATE (T1-7 council, 4-1; sam's S3 dissent
Expand Down
13 changes: 10 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -161,13 +161,14 @@ jobs:
#
# THE MOVE HAS NOW HAPPENED (the P4 slim, attractor-28x), and Track A
# did its job: loop-pipeline is gone from this repo and this job still
# runs the same guard corpus, green, having installed nothing but
# pytest. That is the design working, not a coincidence.
# runs the guard corpus without installing engine modules. Its test
# dependencies are pytest, PyYAML, and Graphviz for DOT render proof.
#
# Deliberately independent of every module/, by construction: every
# moved guard was refactored (where needed) to assert FILE CONTENT --
# doc text, YAML/dot files, script logic loaded by path -- rather than
# importing engine code, so this job installs nothing but pytest itself.
# importing engine code. Graphviz checks the documented DOT syntax
# independently; it does not introduce an engine-module dependency.
# A handful of guards that genuinely could not be decoupled from the
# live engine parser/linter (e.g. the examples/ lint-clean sweep, the
# shipped-graph structural contract checks) deliberately stayed behind
Expand All @@ -185,6 +186,12 @@ jobs:
- name: Install uv
uses: astral-sh/setup-uv@v4

- name: Install graphviz
run: |
sudo apt-get update
sudo apt-get install -y graphviz
dot -V

- name: Install pytest
run: uv pip install --system pytest pyyaml

Expand Down
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ That document also carries the per-change-class evidence table, the four-layer d
## Key directories

- `modules/tool-report-outcome/` — the one module this repo owns.
- `tests/` — the root guard harness (the `opinionated-guards` CI job). Asserts on repo-root docs/, examples/, skills/, agents/, context/, bundles/, behaviors/ content, and installs nothing but pytest by construction.
- `tests/` — the root guard harness (the `opinionated-guards` CI job). Asserts on repo-root docs/, examples/, skills/, agents/, context/, bundles/, behaviors/ content, and installs only pytest plus Graphviz for DOT-render proof.
- `examples/pipelines/` — canonical pipeline patterns. Useful as live test fixtures when verifying engine changes.
- `specs/` — our spec extensions and the canonical attractor reference.
- `docs/CONTRACTS.md` — engine-level contracts: M5 substitution, fail-fast policy, structural concurrency, and cross-consumer guidance.
Expand Down
7 changes: 4 additions & 3 deletions examples/pipelines/11-manager-child-dotfile-hitl/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ steps:
## DOT parser note

Attribute keys containing dots (`manager.max_cycles`, `stack.child_dotfile`) are
written **without** surrounding double-quotes -- the attractor DOT parser stores a
quoted key with its quote characters, which breaks the bare-string lookups the
handlers use. Correct: `manager.max_cycles=1`. Wrong: `"manager.max_cycles"="1"`.
quoted Graphviz keys: `"manager.max_cycles"=1` is correct. The runtime also accepts
bare `manager.max_cycles=1`, but Graphviz rejects it. The parser strips the key
delimiters before handler lookup, so quoting keeps `manager.max_cycles` as the
lookup key.
92 changes: 92 additions & 0 deletions tests/test_doc_consistency.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
"""

import re
import subprocess
from pathlib import Path

# Root of the bundle repo relative to this test file
Expand Down Expand Up @@ -195,6 +196,97 @@ def test_house_llm_classification_is_indirect():
)


# ---------------------------------------------------------------------------
# D-336: manager-child DOT parser guidance must match its adjacent DOT example
# ---------------------------------------------------------------------------

_MANAGER_CHILD_README_REL = "examples/pipelines/11-manager-child-dotfile-hitl/README.md"
_MANAGER_CHILD_PARENT_REL = (
"examples/pipelines/11-manager-child-dotfile-hitl/parent.dot"
)
_MANAGER_CHILD_PARSER_NOTE_HEADING = "## DOT parser note"
_QUOTED_MANAGER_MAX_CYCLES = '"manager.max_cycles"=1'
_BARE_MANAGER_MAX_CYCLES = "manager.max_cycles=1"


def _manager_child_parser_note() -> str:
"""Return the example's one parser-note section, refusing heading dodges."""
readme = _read(_MANAGER_CHILD_README_REL)
headings = re.findall(
rf"^{re.escape(_MANAGER_CHILD_PARSER_NOTE_HEADING)}$",
readme,
flags=re.MULTILINE,
)
assert len(headings) == 1, (
f"{_MANAGER_CHILD_README_REL}: expected exactly one "
f"'{_MANAGER_CHILD_PARSER_NOTE_HEADING}' heading, found {len(headings)}. "
"Keep the parser teaching in its named final section (D-336)."
)
return readme.split(_MANAGER_CHILD_PARSER_NOTE_HEADING, 1)[1].strip()


def test_manager_child_parser_note_teaches_the_quoted_parent_attribute():
"""The README must teach the Graphviz-valid form its adjacent parent uses (D-336)."""
note = _manager_child_parser_note()
parent = _read(_MANAGER_CHILD_PARENT_REL)

parent_attr = re.search(
r'^\s*(?P<key>"manager\.max_cycles")=1,$', parent, flags=re.MULTILINE
)
assert parent_attr is not None, (
f"{_MANAGER_CHILD_PARENT_REL}: quoted manager.max_cycles fixture attribute "
"not found; the README's parser guidance needs an adjacent executable witness."
)
assert _QUOTED_MANAGER_MAX_CYCLES in note, (
f"{_MANAGER_CHILD_README_REL}: parser note must teach the exact quoted "
f"attribute used by parent.dot: `{_QUOTED_MANAGER_MAX_CYCLES}`."
)
assert _BARE_MANAGER_MAX_CYCLES in note, (
f"{_MANAGER_CHILD_README_REL}: parser note must retain the bare-form "
f"counterexample `{_BARE_MANAGER_MAX_CYCLES}`."
)
assert re.search(r"runtime also accepts\s+bare", note), (
f"{_MANAGER_CHILD_README_REL}: parser note must distinguish runtime "
"acceptance from Graphviz syntax validity."
)
assert "Graphviz rejects it" in note, (
f"{_MANAGER_CHILD_README_REL}: parser note must say the bare dotted key "
"is invalid Graphviz, not merely omit it."
)
assert re.search(r"strips the key\s+delimiters", note), (
f"{_MANAGER_CHILD_README_REL}: parser note must explain that quoted key "
"delimiters do not enter handler lookup keys."
)


def test_manager_child_parser_note_rendering_witnesses_match_its_teaching():
"""Real Graphviz must render the documented positive form and reject the negative."""

def render(source: str) -> subprocess.CompletedProcess[str]:
return subprocess.run(
["dot", "-Tsvg"],
input=source,
text=True,
capture_output=True,
check=False,
)

quoted = render(f"digraph {{ manager [{_QUOTED_MANAGER_MAX_CYCLES}] }}")
assert quoted.returncode == 0, (
"Graphviz rejected the quoted dotted-key form the parser note teaches:\n"
f"{quoted.stderr}"
)
assert "<svg" in quoted.stdout, "Graphviz succeeded without emitting SVG output."

bare = render(f"digraph {{ manager [{_BARE_MANAGER_MAX_CYCLES}] }}")
assert bare.returncode != 0, (
"Graphviz accepted the bare dotted-key form that the parser note calls invalid."
)
assert "syntax error" in bare.stderr.lower(), (
f"The negative Graphviz witness failed for an unexpected reason:\n{bare.stderr}"
)


# ---------------------------------------------------------------------------
# D-240: README's suggested_next_ids note vs the shipped coercion (DR-CORE-001)
# ---------------------------------------------------------------------------
Expand Down
Loading