Demonstrates the "fail-fast, no fix-up loop" pattern.
REQ-DOD-VALIDATE-ERRORSREQ-EXEC-EDGE-ROUTING
description
Demonstrates the "fail-fast, no fix-up loop" pattern.
Per SPEC §6.1, when a node reports FAILURE the engine requires an
explicit `label="FAILURE"` outgoing edge to continue; if none
exists the run ends with status `incomplete`. The last paragraph
of §6.1 spells this out as a legitimate design choice:
Terminating on failure (run ends `incomplete`, `captured_output`
preserved on the worktree branch for inspection) is a legitimate
workflow design: not every author wants fix-up loops everywhere.
So this is NOT an invalid graph — it parses, it validates, it
describes a workflow where any verify failure ends the run cleanly
for human triage. The fixture lives at `workflows/` (not under
`invalid/`) to make that explicit.
`attractor validate workflows/failure-no-route.dot` MUST accept
the graph. The "missing FAILURE edge" condition is a runtime
check, not a structural one (§6.1 last paragraph).
REQs exercised: REQ-EXEC-EDGE-ROUTING (the runtime side: SUCCESS
may take the unlabeled default edge; FAILURE without a labeled
edge ends the run incomplete). The validator-time piece of
REQ-DOD-VALIDATE-ERRORS is covered by the genuinely-invalid
fixtures still under `workflows/invalid/`.
Fetch and implement a GitHub issue, then verify with Python checks.
Requirements:
gh must be installed and authenticated in the run environment.
Outputs:
issue.json - raw metadata from `gh issue view`.
issue.md - normalized issue title/body/metadata for agent nodes.
Run:
uv run attractor run workflows/github-issue.dot https://github.com/<owner>/<repo>/issues/<number>
Expected behaviour:
start -> fetch_issue -> plan -> approve_plan -> implement -> verify
verify SUCCESS -> approve_diff -> exit
verify FAILURE -> fixup -> verify
REQs exercised: REQ-EXEC-RUN-ARGUMENTS, REQ-HUMAN-GATE,
REQ-EXEC-VERIFY-FIXUP-IDIOM, REQ-EXEC-GOAL-GATES,
REQ-EXEC-EDGE-ROUTING, REQ-EXEC-REVISIT-CONTEXT.
Implement a feature described in goal.md, verify with pytest + ruff + pyright.
description
Implement a feature described in goal.md, verify with pytest + ruff + pyright.
Inputs (placed in the run worktree before launch):
goal.md — what to build (issue body, SPEC excerpt, free-form)
Outputs (left on the worktree branch refs/heads/attractor/run/<id>/worktree):
plan.md — implementation plan, written by `plan` node
<code edits> — committed at each successful node boundary (§6.4)
Run: attractor run workflows/implement.dot --input goal.md=path/to/goal.md
Diff: git diff main..attractor/run/<run-id>/worktree
A workflow with three human gates including a revise loop. The middle
gate expects the user to supply `--reason <text>` when choosing `revise`
(§11); the rewrite agent reads that reason out of its entry-edge context
as `predecessor_reason` (§6.3) and uses it to steer the rewrite.
Inputs (placed in the worktree before launch):
brief.md — what to draft.
Expected behaviour:
start -> draft -> gate_outline -> gate_review (revise/--reason loops
back to rewrite -> gate_review) -> gate_publish -> exit.
Three gates total:
gate_outline — approve/revise (no reason needed; revise loops to draft)
gate_review — approve/revise; revise expects --reason; loops via rewrite
gate_publish — approve/abandon; abandon ends the run incomplete is OK
because no goal_gate=true node exists; here run completes
cleanly via exit either way.
REQs exercised: REQ-HUMAN-GATE, REQ-EXEC-REVISIT-CONTEXT
(predecessor_reason on visit 2 of `rewrite`), REQ-EXEC-EDGE-ROUTING,
REQ-CLI-RUN-LIFECYCLE (run respond --reason).
Parallel agents + agent merge pattern (SPEC §6.10.3).
Three agents work independently on separate modules; a merger agent
combines their branch refs into the main worktree; verification
confirms the merged result.
Run: attractor run workflows/parallel-agents-merge.dot
Parallel verifiers + fixup pattern (SPEC §6.9.5).
Runs pytest, ruff, and pyright concurrently; if any fail the fixup
agent reads the combined captured output and loops back. The join's
max_visits=3 bounds the retry loop at 3 verification attempts.
Run: attractor run workflows/parallel-verifiers.dot
Two verify/fixup pairs surrounding the implementation stage, per §6.6's
"preflight verify before implementation, final verify before exit" idiom.
Each verify is goal_gate=true and has its own FAILURE→fixup→verify loop
bounded by max_visits.
Inputs (placed in the worktree before launch):
goal.md — feature description.
Expected behaviour:
start -> preflight -> [preflight_fixup loop if env broken] -> implement
-> verify -> [fixup loop if tests/clippy fail] -> exit
With max_visits=4 on each verify node, each loop bounds at 4 verify
executions + 3 fixup executions before run ends `incomplete` (§6.3
worked example).
REQs exercised: REQ-EXEC-VERIFY-FIXUP-IDIOM, REQ-EXEC-GOAL-GATES,
REQ-EXEC-MAX-VISITS, REQ-EXEC-REVISIT-CONTEXT (fixup reads verify's
captured_output via the FAILURE-edge payload).
Exercises §10's specificity rules end-to-end.
Stylesheet (most-specific last for readability, but specificity is by
selector type, not source order):
* { model: claude-haiku-4-5; reasoning_effort: low; }
.heavy { model: claude-sonnet-4-6; reasoning_effort: high; }
.heavy { reasoning_effort: medium; } // last-wins within bucket
#critical_node { model: claude-opus-4-7; }
Plus one per-node `model="..."` attribute that overrides the stylesheet.
Expected effective config per node (assuming the engine resolves
stylesheet even on non-agent nodes but applies model/reasoning_effort
only to agents, per §10):
universal_node model=haiku-4-5, reasoning_effort=low
(only universal selector matches)
classed_node model=sonnet-4-6, reasoning_effort=medium
(.heavy beats *; the second .heavy rule
overrides reasoning_effort within the class
bucket via last-wins)
critical_node model=opus-4-7, reasoning_effort=medium
(#id beats .class beats *; opus from id rule.
reasoning_effort: §10 ¶2 last-wins-per-
declaration applies inside the .heavy bucket,
so the second .heavy rule rewrites it to
medium; #critical_node does not set
reasoning_effort and so cannot recover the
earlier `high`.)
override_node model=gpt-5.2, reasoning_effort=high
(per-node `model=` beats stylesheet; class
contributes reasoning_effort=medium then the
per-node attr does not touch effort... but
per-node `reasoning_effort=high` here makes
the override explicit)
Inputs: none.
Expected behaviour: linear traversal, all four agent nodes invoked with
the model resolved per the cascade above.
REQs exercised: REQ-ROUTING-STYLESHEET (universal, class, id; specificity;
last-wins within bucket; per-node override), REQ-EXEC-CONFIG-CASCADE
(graph defaults → stylesheet → per-node attrs).
A workflow with zero agent (box) nodes — only tool (parallelogram) and
human (hexagon) handlers. Exercises traversal, goal_gate satisfaction,
FAILURE-edge routing, and human-gate routing without ever invoking the
LLM client. Useful as an integration-test fixture: runs with no provider
keys set.
Inputs: none.
Outputs: `release.txt` and `build.log` left on the worktree branch.
Expected behaviour:
start -> clean -> build -> test -> approve_release -> exit (happy path)
On `test` FAILURE the run ends `incomplete` (no fixup loop here — the
test node is goal_gate=true and there is no FAILURE→fixup→test loop).
On `approve_release=reject` the run loops back to `build`.
Cross-shell contract: every `script` here uses only commands that behave
identically under `sh -c` (Unix) and `pwsh -NoProfile -Command` (Windows
per SPEC §6.8): `echo`, `cat`, `&&`, and `>` redirection. No `rm -rf`,
`test`, `[ ]`, or other POSIX-only builtins — they trip pwsh.
REQs exercised: REQ-DOT-SHAPE-HANDLERS (parallelogram, hexagon, Mdiamond,
Msquare), REQ-DOT-ATTRIBUTES-V01 (script, goal_gate, max_visits, label),
REQ-EXEC-NODE-LIFECYCLE, REQ-EXEC-EDGE-ROUTING (SUCCESS unlabeled default,
FAILURE explicit), REQ-EXEC-GOAL-GATES, REQ-EXEC-LOCAL-ENV,
REQ-EXEC-TOOL-CAPTURE, REQ-HUMAN-GATE.
A diagnose agent that deliberately decides the issue isn't auto-fixable
and calls report_outcome("FAILURE", "<reason>") (§6.2 case b, §8.2).
The FAILURE edge routes to a downstream agent which reads the reason
out of its entry-edge context payload (§6.3 entry-edge context with
predecessor_outcome + captured_output) and uses it to brief a human
reviewer at the next gate.
Inputs (placed in the worktree before launch):
bug.md — symptom description (e.g. "intermittent CI flake on test_x").
Expected behaviour:
start -> diagnose
- SUCCESS (root cause is in the code): -> patch -> verify -> exit
- FAILURE (e.g. external infra / not-our-bug):
-> brief_reviewer (reads captured_output) -> human_review
-> abandon | force_patch
REQs exercised: REQ-EXEC-OUTCOMES-DYNAMIC (case b: agent calls
report_outcome), REQ-EXEC-EDGE-ROUTING (FAILURE requires explicit edge),
REQ-EXEC-REVISIT-CONTEXT (entry-edge context: predecessor_node,
entry_edge_label, predecessor_outcome, captured_output), REQ-HUMAN-GATE,
REQ-AGENT-TOOLS-V01 (report_outcome).
INVALID FIXTURE.
Violates §6.3: the `verify` node sits in a cycle with `fixup` but
neither the node nor the graph supplies a `max_visits` /
`default_max_visits` bound. Without a cap the engine would loop
forever on persistent failures.
`attractor validate workflows/invalid/cycle-no-max-visits.dot` should
reject this with an actionable diagnostic (per REQ-DOD-VALIDATE-ERRORS),
e.g.:
error: node `verify` participates in a cycle (verify -> fixup -> verify)
with no max_visits attribute and no graph default_max_visits; bound
the loop with `max_visits=<N>` on `verify` or add
`default_max_visits=<N>` to the graph attributes
No line/column applies — the error is whole-graph (cycle detection),
not token-level. A nice-to-have: the diagnostic could point at the
offending node's declaration line.
REQs exercised (negative case): REQ-EXEC-MAX-VISITS, REQ-EXEC-VERIFY-FIXUP-IDIOM,
REQ-DOD-VALIDATE-ERRORS.
INVALID FIXTURE.
Violates §5.3: "Exit. Exactly one per graph." This file declares a
start node and an agent node but no `Msquare` node — there is nowhere
for traversal to terminate.
`attractor validate workflows/invalid/missing-exit.dot` should reject
this with an actionable diagnostic (per REQ-DOD-VALIDATE-ERRORS), e.g.:
error: no exit node found (expected exactly one node with shape=Msquare
or type="exit"); add `exit [shape=Msquare, label="Exit"]` and route
to it from a terminal stage
No line/column applies — the error is graph-level, not token-level.
REQs exercised (as a negative case): REQ-DOT-SHAPE-HANDLERS (exactly
one Msquare per graph), REQ-DOD-VALIDATE-ERRORS.