fabro/docs/public/reference/dot-language.mdx
fabro-sh-0530[bot] 3fb4b5bc1b
Add output_schema validation with same-context repair for agent and pro… (#374)
## Summary

Adds `output_schema` and `output_retries` node attributes that validate
structured LLM output and perform corrective repair turns inside the
same conversation context before failing the node. Also adds sortable
columns (Repo, Title, Workflow, Changes) to the runs list view and hides
the pager when the result set is small.

### Plan Summary

- **Task 1**: `Node::output_schema()` / `Node::output_retries()`
accessors in `fabro-types`, with `@`-prefix file-reference support in
static validation and file inlining.
- **Task 2**: New `handler/structured_output.rs` module —
`OutputSchemaKind` (Routing / JsonSchema), balanced JSON scanning,
validation, repair-message generation, `apply_validated_output`, and
`exhausted_failure_outcome`.
- **Task 3**: `extract_status_fields` moved to `structured_output.rs`;
agent routing fallback chain (response → `status.json` → last file
touched) preserved and delegated to `validate_agent_output_sources`.
- **Task 4/5**: `one_shot` (prompt) and `run` (agent) both loop over LLM
calls, appending the prior assistant response and a corrective user turn
on validation failure, up to `output_retries` times.
- **Task 6**: ACP backend rejects `output_schema` immediately with a
clear error before launching any process.
- **Task 7**: `outputs.mdx` and `dot-language.mdx` updated with
attribute docs, repair semantics, and `output.{node_id}` context key.

## What changed and why

```mermaid
TB
  graph

  A[Node attrs\noutput_schema / output_retries] --> B[structured_output.rs\nparse / validate / repair]
  B --> C{OutputSchemaKind}
  C -->|Routing| D[validate routing fields\n→ outcome routing]
  C -->|JsonSchema| E[jsonschema validator\n→ context_updates.output.node_id]
  B --> F[exhausted_failure_outcome\nterminal, non-retryable]

  G[prompt handler\none_shot loop] --> B
  H[agent handler\nrun loop + session.process_input] --> B
  I[ACP backend] -->|output_schema present| J[Validation error\nno process launched]
```

**`output_schema="routing"`** tightens existing loose routing
extraction: malformed fields now fail validation and trigger a repair
turn rather than being silently ignored. The fallback priority (response
text → `status.json` → last file touched) is preserved but only for the
`NoJsonObject`/`NoRelevantJsonObject` error kinds that allow it.

**Custom schemas** (`@path` inlined to JSON Schema) validate the last
JSON object in the response against a precompiled
`jsonschema::Validator`. On success, the parsed value is stored at
`output.{node_id}` in `context_updates` for downstream nodes.

**Repair loop** — prompt nodes keep the prior assistant response in the
message list and append a corrective user message; agent API sessions
call `session.process_input` on the live session. Both paths aggregate
token usage across all turns. Exhausting `output_retries` returns a
terminal `OutputSchemaValidation` error (non-retryable, deterministic
failure category) that does not consume `max_retries`.

**ACP guardrail** rejects `output_schema` before spawning any
subprocess, with a clear `"output_schema is not supported with
backend=\"acp\" in this release"` message.

The `one_shot` refactor also extracted `complete_one_shot_request` and
`OneShotCompletion` to separate fallback-chain logic from the repair
loop, removing duplication.


### Fabro Details

<details>
<summary>Ran 9 stages in 74m 9s for $31.92</summary>

| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 1s | – | 0 |
| preflight_compile | 2m 16s | – | 0 |
| preflight_lint | 2m 28s | – | 0 |
| implement | 24m 59s | $17.96 | 0 |
| simplify_opus | 16m 36s | $9.95 | 0 |
| simplify_gpt | 3m 20s | $1.75 | 0 |
| verify | 6m 29s | – | 0 |
| fixup | 17m 13s | $2.26 | 0 |
| **Total** | **74m 9s** | **$31.92** | **0** |

</details>

<details>
<summary>Ran <code>ImplementPlan.fabro</code> (11 nodes and 14
edges)</summary>

```dot
digraph ImplementPlan {
    graph [
        goal="Implement and simplify",
        model_stylesheet="
            * { model: claude-opus-4-7; }
        "
    ]
    rankdir=LR

    start [shape=Mdiamond, label="Start"]
    exit  [shape=Msquare, label="Exit"]

    toolchain         [label="Toolchain", shape=parallelogram, script="command -v cargo >/dev/null || { curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && sudo ln -sf $HOME/.cargo/bin/* /usr/local/bin/; }; cargo --version 2>&1", max_retries=0]
    preflight_compile [label="Preflight Compile", shape=parallelogram, script="cargo check -q --workspace 2>&1", max_retries=0]
    preflight_lint    [label="Preflight Lint", shape=parallelogram, script="cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1", max_retries=0]
    fix_lints         [label="Fix Lints", prompt="The preflight lint step failed. Read the build output from context and fix all clippy lint warnings.", max_visits=3]
    implement         [label="Implement", prompt="Read the plan file referenced in the goal and implement every step. Make all the code changes described in the plan. Use red/green TDD.", model="gpt-55", reasoning_effort="xhigh"]
    simplify_opus     [label="Simplify (Opus)", prompt="@prompts/simplify.md"]
    simplify_gpt      [label="Simplify (GPT-55)", prompt="@prompts/simplify.md", model="gpt-55"]
    verify            [label="Verify", shape=parallelogram, script="git fetch origin main 2>&1 && git merge --no-edit --no-stat origin/main 2>&1 && cargo +nightly-2026-04-14 fmt --all 2>&1 && cargo dev docs refresh 2>&1 && cargo +nightly-2026-04-14 fmt --check --all 2>&1 && ! rg -n 'AuthMode::Disabled|RunAuthMethod|RunSubjectProvenance|\bActorRef\b|\bActorKind\b|AuthenticatedSubject|AuthenticatedService|AuthorizeRunScoped|AuthorizeRunBlob|AuthorizeStageArtifact|AuthorizeCommandLog|auth_method\s*==\s*\"disabled\"' lib/crates apps lib/packages docs/public/api-reference/fabro-api.yaml 2>&1 && cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings 2>&1 && cargo nextest run --workspace --status-level slow --profile ci 2>&1 && cargo dev docs check 2>&1 && bun install --frozen-lockfile 2>&1 && (cd apps/fabro-web && bun run typecheck) 2>&1 && (cd apps/fabro-web && bun run test) 2>&1 && (cd lib/packages/fabro-api-client && bun run typecheck) 2>&1 && cargo dev build -- -p fabro-cli --release 2>&1", goal_gate=true, retry_target="fixup"]
    fixup             [label="Fixup", prompt="The verify step failed. Read the build output from context and fix all format, clippy, Rust test, docs, TypeScript typecheck/test, and build failures.", max_visits=3]

    start -> toolchain
    toolchain -> preflight_compile [condition="outcome=succeeded"]
    toolchain -> exit
    preflight_compile -> preflight_lint [condition="outcome=succeeded"]
    preflight_compile -> exit
    preflight_lint -> implement [condition="outcome=succeeded"]
    preflight_lint -> fix_lints
    fix_lints -> preflight_lint
    implement -> simplify_opus -> simplify_gpt -> verify
    verify -> exit  [condition="outcome=succeeded"]
    verify -> fixup
    fixup -> verify
}

```

</details>

⚒️ Generated with [Fabro](https://fabro.sh)

---------

Co-authored-by: Fabro <noreply@fabro.sh>
Co-authored-by: Fabro <fabro@fabro.sh>
Co-authored-by: Bryan Helmkamp <bryan@brynary.com>
2026-05-23 19:45:16 -04:00

450 lines
18 KiB
Text

---
title: "Fabro Language"
description: "Complete reference for Fabro's Graphviz workflow language"
---
Fabro workflows are written in a subset of the [Graphviz DOT language](https://graphviz.org/doc/info/lang.html) with extensions for agent orchestration. This page is the complete syntax reference. For conceptual introductions, see [Workflows](/core-concepts/workflows) and [Nodes & Stages](/workflows/stages-and-nodes).
## File structure
Every workflow is a `digraph` (directed graph) with a name and a body of statements:
```dot title="my-workflow.fabro"
digraph MyWorkflow {
graph [goal="Describe the project"]
rankdir=LR
start [shape=Mdiamond, label="Start"]
exit [shape=Msquare, label="Exit"]
scan [label="Scan Files", shape=parallelogram, script="find . -type f | head -30"]
analyze [label="Analyze", shape=tab, prompt="Summarize the project structure."]
start -> scan -> analyze -> exit
}
```
Only `digraph` is supported — `graph` (undirected) and `strict` are not. The graph name is required. Semicolons after statements are optional.
## Comments
```dot
// Line comment — everything to end of line
/* Block comment
spanning multiple lines */
```
Comments inside quoted strings are preserved as literal text.
## Value types
Attribute values in `[key=value]` blocks can be:
| Type | Syntax | Examples |
|---|---|---|
| String | Double-quoted | `"Run tests"`, `"line1\nline2"` |
| Integer | Bare digits, optional sign | `42`, `-1`, `0` |
| Float | Digits with decimal point | `3.14`, `-0.5`, `.5` |
| Boolean | Keywords | `true`, `false` |
| Duration | Integer with unit suffix | `250ms`, `30s`, `15m`, `2h`, `1d` |
| Bare string | Identifier with hyphens/dots | `claude-sonnet-4-5`, `gpt-5.2-codex` |
| Identifier | Bare word | `LR`, `box`, `Mdiamond` |
**Escape sequences** in quoted strings: `\"`, `\\`, `\n`, `\t`.
**Duration units:** `ms` (milliseconds), `s` (seconds), `m` (minutes), `h` (hours), `d` (days).
## Statements
The body of a digraph can contain these statement types:
### Graph attributes
Set workflow-level configuration:
```dot
// Block syntax
graph [goal="Build a feature", model_stylesheet="* { model: claude-haiku-4-5; }"]
// Declaration syntax
rankdir=LR
```
| Attribute | Type | Description |
|---|---|---|
| `goal` | String | Workflow objective — guides agent behavior |
| `rankdir` | Identifier | Layout direction: `LR` (left-to-right) or `TB` (top-to-bottom) |
| `model_stylesheet` | String | CSS-like rules for model assignment (see [Model Stylesheets](/workflows/stylesheets)) |
| `default_max_retries` | Integer | Default retry count for all nodes (default: 0) |
| `retry_target` | String | Default node ID to jump to on retry |
| `fallback_retry_target` | String | Fallback retry target if primary target fails |
| `default_fidelity` | String | Default [fidelity level](/execution/context) for all nodes |
| `default_thread` | String | Default thread ID for all nodes |
| `max_node_visits` | Integer | Max visits per node across the run (0 = unlimited) |
| `stall_timeout` | Duration | Timeout for stalled workflows (default: `1800s`, 0 = disabled) |
| `loop_restart_signature_limit` | Integer | Max times the same failure signature can repeat before aborting (default: 3) |
### Node defaults
Apply default attributes to all subsequently declared nodes:
```dot
node [shape=box, timeout="900s"]
```
Defaults are scoped to their enclosing subgraph. Explicit attributes on individual nodes override defaults.
### Edge defaults
Apply default attributes to all subsequently declared edges:
```dot
edge [weight=5]
```
### Node declarations
Declare a node with optional attributes:
```dot
plan [label="Plan", prompt="Create an implementation plan."]
```
**Node identifiers** must start with a letter or underscore, followed by letters, digits, or underscores (e.g. `run_tests`, `gate_1`, `_private`).
Nodes referenced in edges are auto-created if not explicitly declared.
### Edge declarations
Connect nodes with directed edges:
```dot
start -> plan -> implement -> exit
```
Chained edges like `A -> B -> C` expand to individual edges `A -> B` and `B -> C`, all sharing the same attributes.
Edges can have attributes:
```dot
gate -> exit [label="Pass", condition="outcome=succeeded"]
gate -> implement [label="Fix"]
```
### Subgraphs
Group nodes visually and apply scoped defaults:
```dot
subgraph cluster_impl {
label = "Implementation"
node [thread_id="impl", fidelity="full"]
plan [label="Plan"]
implement [label="Implement"]
review [label="Review"]
}
```
When a subgraph has a `label`, it is converted to a CSS class name and applied to all nodes within the subgraph (e.g. `"Implementation"` becomes class `implementation`, `"Loop A"` becomes `loop-a`). This enables [stylesheet](/workflows/stylesheets) targeting.
Node and edge defaults declared inside a subgraph are scoped — they don't leak to the outer graph. Edges can cross subgraph boundaries.
## Node types
Each node's `shape` attribute determines its execution behavior. See [Nodes & Stages](/workflows/stages-and-nodes) for detailed documentation of each type.
| Shape | Handler | Purpose |
|---|---|---|
| `Mdiamond` | start | Workflow entry point (exactly one required) |
| `Msquare` | exit | Workflow terminal (exactly one required) |
| `box` (default) | agent | Multi-turn LLM with tool access |
| `tab` | prompt | Single LLM call, no tools |
| `parallelogram` | command | Execute a shell script |
| `hexagon` | human | Human-in-the-loop decision gate |
| `diamond` | conditional | Route based on conditions |
| `component` | parallel | Fan-out to concurrent branches |
| `tripleoctagon` | parallel.fan_in | Merge parallel branch results |
| `insulator` | wait | Pause for a duration |
| `house` | stack.manager_loop | Sub-workflow orchestration |
The `type` attribute can also be set explicitly to override the shape-based mapping.
Start nodes can also be identified by ID (`start` or `Start`). Exit nodes can be identified by ID (`exit`, `Exit`, `end`, or `End`).
## Node attributes
### All nodes
| Attribute | Type | Description |
|---|---|---|
| `label` | String | Display name in the graph visualization |
| `shape` | Identifier | Graphviz shape — determines handler type (see table above) |
| `type` | String | Explicit handler type (overrides shape) |
| `class` | String | Comma-separated classes for [stylesheet](/workflows/stylesheets) targeting |
| `timeout` | Duration | Execution timeout (e.g. `900s`) |
| `max_visits` | Integer | Max times this node can execute in a run. Overrides the graph-level `max_node_visits` for this node. |
| `max_retries` | Integer | Override default retry count |
| `retry_policy` | String | Named preset: `none`, `standard`, `aggressive`, `linear`, `patient` |
| `retry_target` | String | Node ID to jump to on retry |
| `fallback_retry_target` | String | Fallback node ID if primary `retry_target` is unreachable |
| `goal_gate` | Boolean | When `true`, workflow fails if this node didn't finish with `succeeded` or `partially_succeeded`. See [Node Outcomes](/execution/outcomes#goal-gate-interaction). |
| `auto_status` | Boolean | When `true`, overrides any non-`succeeded`/non-`skipped` outcome to `succeeded` after the handler completes. See [Node Outcomes](/execution/outcomes#auto_status). |
| `allow_partial` | Boolean | When `true` and retries are exhausted on a retry-requesting failure, promotes the outcome to `partially_succeeded` instead of `failed`. Default `false`. See [Node Outcomes](/execution/outcomes#allow_partial). |
| `selection` | String | Edge tiebreaking strategy: `deterministic` (default) or `random` (weighted-random). Cannot be combined with conditional edges. |
### Agent and prompt nodes
| Attribute | Type | Description |
|---|---|---|
| `prompt` | String | Task instructions for the LLM. Supports file references with `@path/to/file.md` |
| `reasoning_effort` | String | `low`, `medium`, or `high` (default: `high`) |
| `max_tokens` | Integer | Maximum output tokens |
| `fidelity` | String | How much prior context is passed: `compact`, `full`, `summary:high`, `summary:medium`, `summary:low`, `truncate` |
| `thread_id` | String | Groups nodes into a shared conversation thread |
| `model` | String | Explicit model ID (overrides stylesheet) |
| `provider` | String | Explicit provider name (overrides stylesheet). Auto-inferred from the model catalog when omitted. |
| `project_memory` | Boolean | When `true` (default), prompt nodes discover and include project docs (`AGENTS.md`, `CLAUDE.md`, etc.) as a system prompt. Set to `false` to disable. |
| `output_schema` | String | Optional structured output validation. Use `routing` for Fabro's built-in routing directive schema, or `@path/to/schema.json` for a JSON Schema file. Supported on agent and prompt nodes. |
| `output_retries` | Integer | Corrective structured-output turns inside the same prompt conversation or agent session. Default `2`; `0` validates once and fails without repair. Separate from `max_retries`. |
| `backend` | String | Agent execution backend: `api` (default) or `acp`. `api` runs Fabro's tool loop through provider APIs; `acp` runs an Agent Client Protocol stdio agent inside the active sandbox. Prompt nodes are API-only. See [Agents — Backends](/core-concepts/agents#backends). |
| `acp.command` | String | Shell command for nodes with `backend="acp"`. Mutually exclusive with `acp.config`. The value is always parsed as a command string, not JSON. |
| `acp.config` | String | JSON stdio ACP config for nodes with `backend="acp"`. Mutually exclusive with `acp.command`. |
#### Structured output validation
`output_schema` opts an agent or prompt node into strict JSON validation:
```dot
review [
shape=tab,
output_schema="routing",
output_retries=2
]
audit [
shape=tab,
output_schema="@schemas/audit-result.schema.json",
output_retries=2
]
```
- `output_schema="routing"` requires a JSON object with at least one recognized routing field: `preferred_next_label`, `outcome`, `failure_reason`, `suggested_next_ids`, or `context_updates`.
- `output_schema="@schemas/audit-result.schema.json"` loads a JSON Schema file using workflow file-reference rules and validates the final JSON object in the response text.
- On validation failure, Fabro sends validation feedback to the same active context before failing: prompt nodes keep the prior assistant response in the message list, and API-backed agent nodes repair in the same live session.
- `output_retries` defaults to `2` and controls only these corrective structured-output turns. It is not the same as `max_retries` and does not consume workflow retry attempts.
- Custom schema output is stored in context at `output.{node_id}`. Routing schema output updates routing fields and any `context_updates`.
- `backend="acp"` with `output_schema` is unsupported in this release.
### Command nodes
| Attribute | Type | Description |
|---|---|---|
| `script` | String | Shell command to execute |
| `language` | String | `"shell"` (default) or `"python"` |
### Parallel (fan-out) nodes
| Attribute | Type | Description |
|---|---|---|
| `join_policy` | String | When the merge can proceed: `wait_all` (default), `first_success` |
| `max_parallel` | Integer | Maximum concurrent branches (default: 4) |
### Wait nodes
| Attribute | Type | Description |
|---|---|---|
| `duration` | Duration | How long to pause (required). E.g. `"30s"`, `"2m"` |
### Human nodes
| Attribute | Type | Description |
|---|---|---|
| `question_type` | String | Optional interview question type override: `yes_no`, `confirmation`, `multiple_choice`, `multi_select`, or `freeform`. Defaults to `freeform` when the gate only has a freeform edge; otherwise defaults to `multiple_choice`. |
| `human.default_choice` | String | Target node to use when the question times out. |
### Manager loop (sub-workflow) nodes
| Attribute | Type | Description |
|---|---|---|
| `stack.child_workflow` | String | Path to child workflow `.fabro` file (required unless inline source given) |
| `stack.child_dot_source` | String | Inline DOT source for the child workflow (alternative to file path) |
| `manager.poll_interval` | Duration | How often the manager checks the child workflow (default: `45s`) |
| `manager.max_cycles` | Integer | Maximum polling cycles before timeout (default: 1000) |
| `manager.stop_condition` | String | Condition expression — when true, the child run is stopped early |
## Edge attributes
| Attribute | Type | Description |
|---|---|---|
| `label` | String | Display text; also used for human gate option matching |
| `condition` | String | Boolean expression for conditional routing (see below) |
| `weight` | Integer | Priority for tiebreaking (higher wins, default: 0) |
| `fidelity` | String | Override fidelity level for this transition |
| `thread_id` | String | Override thread ID for this transition |
| `loop_restart` | Boolean | Mark this edge as a loop restart point |
| `freeform` | Boolean | When `true` on a human-gate edge, accept free-text input instead of fixed choices |
## Condition expressions
Edge conditions are boolean expressions evaluated against the stage outcome and run context. See [Transitions](/workflows/transitions) for the full routing logic.
### Grammar
```
Expr ::= OrExpr
OrExpr ::= AndExpr ('||' AndExpr)*
AndExpr ::= UnaryExpr ('&&' UnaryExpr)*
UnaryExpr ::= '!' UnaryExpr | Clause
Clause ::= Key Op Value | Key (bare key = truthy check)
Value ::= BareWord | '"' QuotedString '"'
Op ::= '=' | '!=' | '>' | '<' | '>=' | '<='
| 'contains' | 'matches'
```
### Keys
| Key | Resolves to |
|---|---|
| `outcome` | Stage outcome: `succeeded`, `failed`, `partially_succeeded`, or `skipped`. See [Node Outcomes](/execution/outcomes#outcome-in-edge-conditions). |
| `preferred_label` | Label selected by a human gate or LLM routing directive |
| `context.KEY` | Value from the run context |
| `KEY` | Shorthand for context lookup (without the `context.` prefix) |
### Operators
| Operator | Example | Description |
|---|---|---|
| `=` | `outcome=succeeded` | Equality |
| `!=` | `outcome!=failed` | Inequality |
| `>` | `context.score > 80` | Greater than (numeric) |
| `<` | `context.count < 5` | Less than (numeric) |
| `>=` | `context.score >= 80` | Greater than or equal |
| `<=` | `context.count <= 10` | Less than or equal |
| `contains` | `context.message contains error` | Substring match or array membership |
| `matches` | `context.version matches ^v\d+` | Regular expression match |
| `&&` | `a=1 && b=2` | Logical AND (binds tighter than `\|\|`) |
| `\|\|` | `a=1 \|\| b=2` | Logical OR |
| `!` | `!outcome=failed` | Logical NOT |
A bare key with no operator is a **truthiness check** — it passes if the value is non-empty, not `"false"`, and not `"0"`.
### Examples
```dot
// Simple outcome check
gate -> exit [condition="outcome=succeeded"]
// Compound condition
gate -> deploy [condition="outcome=succeeded && context.tests_passed=true"]
// Either outcome
gate -> proceed [condition="outcome=succeeded || outcome=partially_succeeded"]
// Negation
gate -> retry [condition="!outcome=succeeded"]
// Numeric comparison
gate -> fast_path [condition="context.score > 80"]
// Substring search
gate -> alert [condition="context.log contains error"]
// Regex match
gate -> v2 [condition="context.version matches ^v2\\."]
// Unconditional fallback (no condition attribute)
gate -> slow_path
```
## Prompt and schema file references
Instead of inlining long prompts or JSON Schemas, reference an external file:
```dot
simplify [label="Simplify", prompt="@prompts/simplify.md"]
audit [shape=tab, output_schema="@schemas/audit-result.schema.json"]
```
The `@` prefix tells Fabro to load the referenced file relative to the workflow file. Paths support `~` (home directory) and `..` (parent directory):
```dot
shared [prompt="@~/shared-prompts/review.md"]
parent [prompt="@../common/plan.md"]
```
Untracked `@file` references (files not committed to git) are inlined into the Graphviz source at prepare time, so they work even inside sandboxes that only see the git tree.
Fabro validates `@file` references at parse time — if the referenced file does not exist, validation fails with a clear error pointing to the bad reference.
## Validation
Fabro validates workflows at parse time and reports diagnostics. Key rules:
- Exactly one start node and one exit node
- All nodes reachable from start
- No incoming edges to start, no outgoing edges from exit
- Edge targets reference existing nodes
- Condition expressions parse correctly
- Stylesheet syntax is valid
- LLM nodes (agent, prompt) have a `prompt` attribute
- `@file` references point to existing files
- Conditional nodes have multiple outgoing edges with conditions
- Retry targets reference existing nodes
- Goal gates have retry configuration
- `thread_id` requires `fidelity="full"` (session reuse depends on full fidelity)
- Known handler types only
## Complete example
```dot title="implement-feature.fabro"
digraph ImplementFeature {
graph [
goal="Implement a feature with tests and code review",
model_stylesheet="
* { model: claude-haiku-4-5;reasoning_effort: low; }
.coding { model: claude-sonnet-4-5;reasoning_effort: high; }
#review { model: claude-sonnet-4-5;reasoning_effort: high; }
"
]
rankdir=LR
start [shape=Mdiamond, label="Start"]
exit [shape=Msquare, label="Exit"]
// Planning phase
plan [label="Plan", shape=tab, prompt="Create a detailed implementation plan for: {{ goal }}"]
// Human approval
approve [shape=hexagon, label="Approve Plan"]
// Implementation (threaded for context continuity)
subgraph cluster_impl {
label = "Implementation"
node [thread_id="impl", fidelity="full"]
implement [label="Implement", class="coding", prompt="Implement the approved plan."]
test [label="Write Tests", class="coding", prompt="Write comprehensive tests."]
}
// Validation
validate [label="Run Tests", shape=parallelogram, script="cargo test 2>&1 || true"]
gate [shape=diamond, label="Tests passing?"]
// Review
review [label="Code Review", shape=tab, prompt="Review the implementation for correctness."]
// Wiring
start -> plan -> approve
approve -> implement [label="[A] Approve"]
approve -> plan [label="[R] Revise"]
implement -> test -> validate -> gate
gate -> review [label="Pass", condition="outcome=succeeded"]
gate -> implement [label="Fix"]
review -> exit
}
```