Add SVGs and simplify DoD example to two specs

Remove the third attractor spec layer from both single-model and
multi-model variants, keeping just unified-llm and agent-loop.
Add workflow diagram SVGs with dark mode support.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Bryan Helmkamp 2026-03-06 09:13:01 -05:00
parent 4a908db67e
commit 14a8071dc1
3 changed files with 268 additions and 94 deletions

View file

@ -16,12 +16,16 @@ This pattern is useful when you have detailed specs with acceptance criteria (De
## Single-model variant
<Frame>
<img src="/images/example-dod.svg" alt="Definition of Done workflow: Start → Audit LLM → Audit Agent → Triage → Fix → Build → Verify → Review → Exit, with Fix self-loop, Build/Build Fix loop, and Verify back to Triage" />
</Frame>
The simpler variant uses one model throughout, with sequential audits across multiple specs:
```dot title="spec-dod.dot"
digraph SpecDoD {
graph [
goal="Satisfy every Definition of Done checkbox across all three attractor specs (unified-llm-spec.md, coding-agent-loop-spec.md, attractor-spec.md). The implementation is in Rust under crates/. Do NOT modify the spec files. Only modify implementation code.",
goal="Satisfy every Definition of Done checkbox across both specs (unified-llm-spec.md, coding-agent-loop-spec.md). The implementation is in Rust under crates/. Do NOT modify the spec files. Only modify implementation code.",
default_max_retry="3",
retry_target="triage",
model_stylesheet="
@ -81,27 +85,6 @@ Respond with ONLY a JSON object (no prose) -- don't write it out as a file:
\"failed_items\": [ {\"section\": \"9.1\", \"text\": \"...\", \"reason\": \"...\"} ]
}
Be thorough. Check every single checkbox."
]
audit_attractor [
label="Audit: Attractor Pipeline DoD",
shape=box,
class="audit",
prompt="Read docs/specs/attractor-spec.md Section 11 (Definition of Done) in full. Then read every source file under crates/attractor/.
For EACH checkbox in sections 11.1 through 11.13, evaluate whether the current Rust implementation satisfies it. Be strict.
Respond with ONLY a JSON object (no prose) -- don't write it out as a file:
{
\"spec\": \"attractor\",
\"sections\": { ... },
\"total\": N,
\"passed\": M,
\"failed\": K,
\"failed_items\": [ {\"section\": \"11.1\", \"text\": \"...\", \"reason\": \"...\"} ]
}
Be thorough. Check every single checkbox."
]
@ -112,7 +95,7 @@ Be thorough. Check every single checkbox."
triage [
label="Triage & Prioritize",
shape=box,
prompt="You have three audit reports in context (from audit_llm, audit_agent, audit_attractor). Merge them into a single prioritized list of ALL failing DoD checkboxes.
prompt="You have two audit reports in context (from audit_llm, audit_agent). Merge them into a single prioritized list of ALL failing DoD checkboxes.
Group failures into:
1. IMPLEMENTABLE — can be fixed by writing/modifying Rust code (functions, handlers, logic)
@ -206,10 +189,9 @@ Output the fixes applied and ensure the code will compile cleanly with: cargo bu
final_audit [
label="Final Verification Audit",
shape=box,
prompt="This is a verification pass. Re-read all three spec DoD sections:
prompt="This is a verification pass. Re-read both spec DoD sections:
- docs/specs/unified-llm-spec.md Section 8
- docs/specs/coding-agent-loop-spec.md Section 9
- docs/specs/attractor-spec.md Section 11
And re-read the implementation files that were changed in this iteration.
@ -244,8 +226,7 @@ Otherwise set preferred_next_label to \"More work needed\"."
/* Sequential audit chain */
audit_llm -> audit_agent
audit_agent -> audit_attractor
audit_attractor -> triage
audit_agent -> triage
/* Triage decision */
triage -> exit [label="Done", condition="preferred_label=Done"]
@ -291,12 +272,16 @@ Otherwise set preferred_next_label to \"More work needed\"."
## Multi-model variant
<Frame>
<img src="/images/example-dod-multimodel.svg" alt="Multi-model Definition of Done workflow: Start → Dual Audit (Opus+GPT) → Critique → Consensus → Triage (Opus+GPT) → Fix Cycle (Codex↔Opus) → Build → Verify (Opus+GPT) → Review → Exit, with Fix Cycle self-loop, Build/Build Fix loop, and Verify back to Triage" />
</Frame>
The multi-model variant applies the same audit-triage-fix-verify structure but uses independent assessments from two models (Claude Opus and GPT-5.2) at each phase, with cross-critique and consensus merging. This catches blind spots that a single model might miss.
```dot title="spec-dod-multimodel.dot"
digraph SpecDoDMultiModel {
graph [
goal="Satisfy every Definition of Done checkbox across all three attractor specs (unified-llm-spec.md, coding-agent-loop-spec.md, attractor-spec.md). The implementation is in Rust under crates/. Do NOT modify the spec files. Only modify implementation code. Uses multi-model consensus: Opus 4.6 and GPT-5.2 compete on audits and planning, GPT-5.2-codex and Opus 4.6 alternate on implementation.",
goal="Satisfy every Definition of Done checkbox across both specs (unified-llm-spec.md, coding-agent-loop-spec.md). The implementation is in Rust under crates/. Do NOT modify the spec files. Only modify implementation code. Uses multi-model consensus: Opus 4.6 and GPT-5.2 compete on audits and planning, GPT-5.2-codex and Opus 4.6 alternate on implementation.",
default_max_retry="3",
retry_target="triage_merge",
default_fidelity="full",
@ -315,7 +300,7 @@ digraph SpecDoDMultiModel {
/*========================================================================
* PHASE 1 — Dual Independent Audits (interleaved, fidelity-isolated)
*
* Each spec is audited by both models before moving to the next spec.
* Each spec is audited by both models before moving to the next.
* Audit nodes use fidelity="truncate" so they only see the graph goal
* and NOT each other's responses — prevents anchoring bias.
* Full responses are still stored as response.<node_id> for later use.
@ -420,54 +405,6 @@ Respond with ONLY a JSON object (no prose) -- don't write it out as a file:
\"failed_items\": [ {\"section\": \"9.1\", \"text\": \"...\", \"reason\": \"...\"} ]
}
Be thorough. Check every single checkbox."
]
/* ---- Attractor spec: both models ---- */
audit_attractor_opus [
label="Opus: Audit Attractor DoD",
shape=box,
class="opus",
fidelity="truncate",
prompt="Read docs/specs/attractor-spec.md Section 11 (Definition of Done) in full. Then read every source file under crates/attractor/.
For EACH checkbox in sections 11.1 through 11.13, evaluate whether the current Rust implementation satisfies it. Be strict.
Respond with ONLY a JSON object (no prose) -- don't write it out as a file:
{
\"spec\": \"attractor\",
\"model\": \"opus\",
\"sections\": { ... },
\"total\": N,
\"passed\": M,
\"failed\": K,
\"failed_items\": [ {\"section\": \"11.1\", \"text\": \"...\", \"reason\": \"...\"} ]
}
Be thorough. Check every single checkbox."
]
audit_attractor_gpt [
label="GPT-5.2: Audit Attractor DoD",
shape=box,
class="gpt",
fidelity="truncate",
prompt="Read docs/specs/attractor-spec.md Section 11 (Definition of Done) in full. Then read every source file under crates/attractor/.
For EACH checkbox in sections 11.1 through 11.13, evaluate whether the current Rust implementation satisfies it. Be strict.
Respond with ONLY a JSON object (no prose) -- don't write it out as a file:
{
\"spec\": \"attractor\",
\"model\": \"gpt-5.2\",
\"sections\": { ... },
\"total\": N,
\"passed\": M,
\"failed\": K,
\"failed_items\": [ {\"section\": \"11.1\", \"text\": \"...\", \"reason\": \"...\"} ]
}
Be thorough. Check every single checkbox."
]
@ -483,17 +420,15 @@ Be thorough. Check every single checkbox."
shape=box,
class="gpt",
fidelity="full",
prompt="You have all six audit reports available in context. The full outputs are in these context keys:
prompt="You have all four audit reports available in context. The full outputs are in these context keys:
OPUS AUDITS:
- response.audit_llm_opus — Opus's audit of unified-llm-spec.md Section 8
- response.audit_agent_opus — Opus's audit of coding-agent-loop-spec.md Section 9
- response.audit_attractor_opus — Opus's audit of attractor-spec.md Section 11
GPT AUDITS (your own):
- response.audit_llm_gpt — your audit of unified-llm-spec.md Section 8
- response.audit_agent_gpt — your audit of coding-agent-loop-spec.md Section 9
- response.audit_attractor_gpt — your audit of attractor-spec.md Section 11
Compare them item by item. For every DoD checkbox where the two models DISAGREE (one says pass, the other says fail), re-read the relevant spec section and source file to determine who is correct.
@ -522,17 +457,15 @@ Be rigorous. When in doubt, fail the checkbox — strictness prevents false conf
shape=box,
class="opus",
fidelity="full",
prompt="You have all six audit reports available in context. The full outputs are in these context keys:
prompt="You have all four audit reports available in context. The full outputs are in these context keys:
GPT AUDITS:
- response.audit_llm_gpt — GPT-5.2's audit of unified-llm-spec.md Section 8
- response.audit_agent_gpt — GPT-5.2's audit of coding-agent-loop-spec.md Section 9
- response.audit_attractor_gpt — GPT-5.2's audit of attractor-spec.md Section 11
OPUS AUDITS (your own):
- response.audit_llm_opus — your audit of unified-llm-spec.md Section 8
- response.audit_agent_opus — your audit of coding-agent-loop-spec.md Section 9
- response.audit_attractor_opus — your audit of attractor-spec.md Section 11
Compare them item by item. For every DoD checkbox where the two models DISAGREE (one says pass, the other says fail), re-read the relevant spec section and source file to determine who is correct.
@ -570,7 +503,7 @@ Be rigorous. When in doubt, fail the checkbox — strictness prevents false conf
fidelity="full",
prompt="You have all prior audit and critique outputs in context. The key inputs are:
SIX AUDIT REPORTS (context keys response.audit_llm_opus, response.audit_agent_opus, response.audit_attractor_opus, response.audit_llm_gpt, response.audit_agent_gpt, response.audit_attractor_gpt)
FOUR AUDIT REPORTS (context keys response.audit_llm_opus, response.audit_agent_opus, response.audit_llm_gpt, response.audit_agent_gpt)
TWO CROSS-CRITIQUES (context keys response.critique_by_gpt, response.critique_by_opus)
@ -584,8 +517,7 @@ Respond with JSON (not by writing out a file):
{
\"spec_results\": {
\"unified-llm\": { \"total\": N, \"passed\": M, \"failed\": K, \"failed_items\": [...] },
\"coding-agent-loop\": { ... },
\"attractor\": { ... }
\"coding-agent-loop\": { ... }
},
\"consensus_total\": N,
\"consensus_passed\": M,
@ -814,10 +746,9 @@ Output the fixes applied and ensure the code will compile cleanly with: cargo bu
fidelity="full",
prompt="This is a verification pass. The items that were previously failing are listed in context key response.triage_merge (the implementable list). The fixes applied are in response.fix_codex and response.review_fix_opus.
Re-read all three spec DoD sections:
Re-read both spec DoD sections:
- docs/specs/unified-llm-spec.md Section 8
- docs/specs/coding-agent-loop-spec.md Section 9
- docs/specs/attractor-spec.md Section 11
And re-read the implementation files that were changed in this iteration.
@ -840,10 +771,9 @@ Respond with JSON (not by writing out a file):
fidelity="full",
prompt="This is a verification pass. The items that were previously failing are listed in context key response.triage_merge (the implementable list). The fixes applied are in response.fix_codex and response.review_fix_opus.
Re-read all three spec DoD sections:
Re-read both spec DoD sections:
- docs/specs/unified-llm-spec.md Section 8
- docs/specs/coding-agent-loop-spec.md Section 9
- docs/specs/attractor-spec.md Section 11
And re-read the implementation files that were changed in this iteration.
@ -904,11 +834,9 @@ Otherwise set preferred_next_label to \"More work needed\"."
audit_llm_opus -> audit_llm_gpt
audit_llm_gpt -> audit_agent_opus
audit_agent_opus -> audit_agent_gpt
audit_agent_gpt -> audit_attractor_opus
audit_attractor_opus -> audit_attractor_gpt
/* Phase 2: Cross-critique (now sequential — GPT critiques Opus, then Opus critiques GPT) */
audit_attractor_gpt -> critique_by_gpt
audit_agent_gpt -> critique_by_gpt
critique_by_gpt -> critique_by_opus
/* Phase 3: Consensus */
@ -957,7 +885,7 @@ Otherwise set preferred_next_label to \"More work needed\"."
**Cross-critique** — after all audits complete, each model reviews the other's work. Disagreements are resolved by re-reading the spec and code. This adversarial step catches both false positives (a model said "pass" when the feature is incomplete) and false negatives (a model said "fail" when the code is actually correct).
**Consensus merging** — a dedicated merge node combines the six audit reports and two critiques into a single truth. The merge rules are conservative: when in doubt, fail the checkbox.
**Consensus merging** — a dedicated merge node combines the four audit reports and two critiques into a single truth. The merge rules are conservative: when in doubt, fail the checkbox.
**Dual triage** — both models independently classify and prioritize failures. The merge takes the more actionable classification (IMPLEMENTABLE > STRUCTURAL > DEFERRED) and averages ranks. Different models weight risks differently, so consensus produces a stronger work plan.

View file

@ -0,0 +1,129 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="1060" height="170" viewBox="0 0 1060 170" xmlns="http://www.w3.org/2000/svg">
<style>
@media (prefers-color-scheme: dark) {
[fill="#1a1a1a"] { fill: #e5e5e5; }
[fill="#666666"] { fill: #aaaaaa; }
[stroke="#666666"] { stroke: #aaaaaa; }
[stroke="#999999"] { stroke: #777777; }
}
</style>
<g>
<!-- START (Mdiamond) at (40, 48) -->
<polygon fill="none" stroke="#357f9e" points="40,30 10,48 40,66 70,48"/>
<polyline fill="none" stroke="#357f9e" points="20,42 20,54"/>
<polyline fill="none" stroke="#357f9e" points="30,60 50,60"/>
<polyline fill="none" stroke="#357f9e" points="60,54 60,42"/>
<polyline fill="none" stroke="#357f9e" points="50,36 30,36"/>
<text xml:space="preserve" text-anchor="middle" x="40" y="52" font-family="Helvetica,sans-Serif" font-size="12.00" fill="#1a1a1a">Start</text>
<!-- DUAL AUDIT (ellipse) at (155, 48) -->
<ellipse fill="none" stroke="#357f9e" cx="155" cy="48" rx="42" ry="18"/>
<text xml:space="preserve" text-anchor="middle" x="155" y="52" font-family="Helvetica,sans-Serif" font-size="12.00" fill="#1a1a1a">Dual Audit</text>
<text xml:space="preserve" text-anchor="middle" x="155" y="80" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#666666">Opus + GPT</text>
<!-- CRITIQUE (ellipse) at (280, 48) -->
<ellipse fill="none" stroke="#357f9e" cx="280" cy="48" rx="34" ry="18"/>
<text xml:space="preserve" text-anchor="middle" x="280" y="52" font-family="Helvetica,sans-Serif" font-size="12.00" fill="#1a1a1a">Critique</text>
<!-- CONSENSUS (ellipse) at (395, 48) -->
<ellipse fill="none" stroke="#357f9e" cx="395" cy="48" rx="40" ry="18"/>
<text xml:space="preserve" text-anchor="middle" x="395" y="52" font-family="Helvetica,sans-Serif" font-size="12.00" fill="#1a1a1a">Consensus</text>
<!-- TRIAGE (ellipse) at (510, 48) -->
<ellipse fill="none" stroke="#357f9e" cx="510" cy="48" rx="28" ry="18"/>
<text xml:space="preserve" text-anchor="middle" x="510" y="52" font-family="Helvetica,sans-Serif" font-size="12.00" fill="#1a1a1a">Triage</text>
<text xml:space="preserve" text-anchor="middle" x="510" y="80" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#666666">Opus + GPT</text>
<!-- FIX CYCLE (ellipse) at (620, 48) -->
<ellipse fill="none" stroke="#357f9e" cx="620" cy="48" rx="36" ry="18"/>
<text xml:space="preserve" text-anchor="middle" x="620" y="52" font-family="Helvetica,sans-Serif" font-size="12.00" fill="#1a1a1a">Fix Cycle</text>
<text xml:space="preserve" text-anchor="middle" x="620" y="80" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#666666">Codex ↔ Opus</text>
<!-- BUILD (parallelogram) at ~(740, 48) -->
<polygon fill="none" stroke="#357f9e" points="710,30 778,30 770,66 702,66"/>
<text xml:space="preserve" text-anchor="middle" x="740" y="52" font-family="Helvetica,sans-Serif" font-size="12.00" fill="#1a1a1a">Build</text>
<!-- VERIFY (ellipse) at (845, 48) -->
<ellipse fill="none" stroke="#357f9e" cx="845" cy="48" rx="28" ry="18"/>
<text xml:space="preserve" text-anchor="middle" x="845" y="52" font-family="Helvetica,sans-Serif" font-size="12.00" fill="#1a1a1a">Verify</text>
<text xml:space="preserve" text-anchor="middle" x="845" y="80" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#666666">Opus + GPT</text>
<!-- REVIEW (hexagon) at (940, 48) -->
<polygon fill="none" stroke="#357f9e" points="912,48 922,30 958,30 968,48 958,66 922,66"/>
<text xml:space="preserve" text-anchor="middle" x="940" y="52" font-family="Helvetica,sans-Serif" font-size="12.00" fill="#1a1a1a">Review</text>
<!-- EXIT (Msquare) at (1030, 48) -->
<polygon fill="none" stroke="#357f9e" points="1015,30 1045,30 1045,66 1015,66"/>
<polyline fill="none" stroke="#357f9e" points="1025,30 1015,40"/>
<polyline fill="none" stroke="#357f9e" points="1015,56 1025,66"/>
<polyline fill="none" stroke="#357f9e" points="1035,66 1045,56"/>
<polyline fill="none" stroke="#357f9e" points="1045,40 1035,30"/>
<text xml:space="preserve" text-anchor="middle" x="1030" y="52" font-family="Helvetica,sans-Serif" font-size="12.00" fill="#1a1a1a">Exit</text>
<!-- BUILD FIX (ellipse) at (740, 132) -->
<ellipse fill="none" stroke="#357f9e" cx="740" cy="132" rx="36" ry="18"/>
<text xml:space="preserve" text-anchor="middle" x="740" y="136" font-family="Helvetica,sans-Serif" font-size="12.00" fill="#1a1a1a">Build Fix</text>
<!-- === FORWARD EDGES === -->
<!-- start → dual_audit -->
<path fill="none" stroke="#666666" d="M70,48 L103,48"/>
<polygon fill="#666666" stroke="#666666" points="103,44.5 113,48 103,51.5"/>
<!-- dual_audit → critique -->
<path fill="none" stroke="#666666" d="M197,48 L236,48"/>
<polygon fill="#666666" stroke="#666666" points="236,44.5 246,48 236,51.5"/>
<!-- critique → consensus -->
<path fill="none" stroke="#666666" d="M314,48 L345,48"/>
<polygon fill="#666666" stroke="#666666" points="345,44.5 355,48 345,51.5"/>
<!-- consensus → triage -->
<path fill="none" stroke="#666666" d="M435,48 L472,48"/>
<polygon fill="#666666" stroke="#666666" points="472,44.5 482,48 472,51.5"/>
<!-- triage → fix_cycle -->
<path fill="none" stroke="#666666" d="M538,48 L574,48"/>
<polygon fill="#666666" stroke="#666666" points="574,44.5 584,48 574,51.5"/>
<text xml:space="preserve" text-anchor="middle" x="561" y="43" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#666666">Fix</text>
<!-- fix_cycle → build -->
<path fill="none" stroke="#666666" d="M656,48 L694,48"/>
<polygon fill="#666666" stroke="#666666" points="694,44.5 704,48 694,51.5"/>
<!-- build → verify [OK] -->
<path fill="none" stroke="#666666" d="M774,48 L807,48"/>
<polygon fill="#666666" stroke="#666666" points="807,44.5 817,48 807,51.5"/>
<text xml:space="preserve" text-anchor="middle" x="796" y="43" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#666666">OK</text>
<!-- verify → review -->
<path fill="none" stroke="#666666" d="M873,48 L902,48"/>
<polygon fill="#666666" stroke="#666666" points="902,44.5 912,48 902,51.5"/>
<!-- review → exit [Accept] -->
<path fill="none" stroke="#666666" d="M968,48 L1005,48"/>
<polygon fill="#666666" stroke="#666666" points="1005,44.5 1015,48 1005,51.5"/>
<text xml:space="preserve" text-anchor="middle" x="991" y="43" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#666666">Accept</text>
<!-- === LOOPS === -->
<!-- fix_cycle self-loop (above) -->
<path fill="none" stroke="#666666" d="M612,32 C606,10 634,10 628,32"/>
<polygon fill="#666666" stroke="#666666" points="624,26 628,36 632,26"/>
<!-- build → build_fix [Fail] -->
<path fill="none" stroke="#666666" d="M748,66 L748,104"/>
<polygon fill="#666666" stroke="#666666" points="744.5,104 748,114 751.5,104"/>
<text xml:space="preserve" text-anchor="middle" x="762" y="91" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#666666">Fail</text>
<!-- build_fix → build (curved left) -->
<path fill="none" stroke="#666666" d="M714,120 C688,105 688,72 710,58"/>
<polygon fill="#666666" stroke="#666666" points="706,62 710,52 714,62"/>
<!-- verify → triage [More work] (backward arc below) -->
<path fill="none" stroke="#666666" d="M840,66 C840,162 515,162 515,66"/>
<polygon fill="#666666" stroke="#666666" points="511,72 515,62 519,72"/>
<text xml:space="preserve" text-anchor="middle" x="678" y="162" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#666666">More work</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 7.4 KiB

117
docs/images/example-dod.svg Normal file
View file

@ -0,0 +1,117 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="870" height="170" viewBox="0 0 870 170" xmlns="http://www.w3.org/2000/svg">
<style>
@media (prefers-color-scheme: dark) {
[fill="#1a1a1a"] { fill: #e5e5e5; }
[fill="#666666"] { fill: #aaaaaa; }
[stroke="#666666"] { stroke: #aaaaaa; }
[stroke="#999999"] { stroke: #777777; }
}
</style>
<g>
<!-- START (Mdiamond) at (40, 50) -->
<polygon fill="none" stroke="#357f9e" points="40,32 10,50 40,68 70,50"/>
<polyline fill="none" stroke="#357f9e" points="20,44 20,56"/>
<polyline fill="none" stroke="#357f9e" points="30,62 50,62"/>
<polyline fill="none" stroke="#357f9e" points="60,56 60,44"/>
<polyline fill="none" stroke="#357f9e" points="50,38 30,38"/>
<text xml:space="preserve" text-anchor="middle" x="40" y="54" font-family="Helvetica,sans-Serif" font-size="12.00" fill="#1a1a1a">Start</text>
<!-- AUDIT LLM (ellipse) at (155, 50) -->
<ellipse fill="none" stroke="#357f9e" cx="155" cy="50" rx="42" ry="18"/>
<text xml:space="preserve" text-anchor="middle" x="155" y="54" font-family="Helvetica,sans-Serif" font-size="12.00" fill="#1a1a1a">Audit LLM</text>
<!-- AUDIT AGENT (ellipse) at (278, 50) -->
<ellipse fill="none" stroke="#357f9e" cx="278" cy="50" rx="48" ry="18"/>
<text xml:space="preserve" text-anchor="middle" x="278" y="54" font-family="Helvetica,sans-Serif" font-size="12.00" fill="#1a1a1a">Audit Agent</text>
<!-- TRIAGE (ellipse) at (390, 50) -->
<ellipse fill="none" stroke="#357f9e" cx="390" cy="50" rx="28" ry="18"/>
<text xml:space="preserve" text-anchor="middle" x="390" y="54" font-family="Helvetica,sans-Serif" font-size="12.00" fill="#1a1a1a">Triage</text>
<!-- FIX (ellipse) at (470, 50) -->
<ellipse fill="none" stroke="#357f9e" cx="470" cy="50" rx="18" ry="18"/>
<text xml:space="preserve" text-anchor="middle" x="470" y="54" font-family="Helvetica,sans-Serif" font-size="12.00" fill="#1a1a1a">Fix</text>
<!-- BUILD (parallelogram) at ~(560, 50) -->
<polygon fill="none" stroke="#357f9e" points="530,32 598,32 590,68 522,68"/>
<text xml:space="preserve" text-anchor="middle" x="560" y="54" font-family="Helvetica,sans-Serif" font-size="12.00" fill="#1a1a1a">Build</text>
<!-- VERIFY (ellipse) at (660, 50) -->
<ellipse fill="none" stroke="#357f9e" cx="660" cy="50" rx="28" ry="18"/>
<text xml:space="preserve" text-anchor="middle" x="660" y="54" font-family="Helvetica,sans-Serif" font-size="12.00" fill="#1a1a1a">Verify</text>
<!-- REVIEW (hexagon) at (752, 50) -->
<polygon fill="none" stroke="#357f9e" points="724,50 734,32 770,32 780,50 770,68 734,68"/>
<text xml:space="preserve" text-anchor="middle" x="752" y="54" font-family="Helvetica,sans-Serif" font-size="12.00" fill="#1a1a1a">Review</text>
<!-- EXIT (Msquare) at (840, 50) -->
<polygon fill="none" stroke="#357f9e" points="825,32 855,32 855,68 825,68"/>
<polyline fill="none" stroke="#357f9e" points="835,32 825,42"/>
<polyline fill="none" stroke="#357f9e" points="825,58 835,68"/>
<polyline fill="none" stroke="#357f9e" points="845,68 855,58"/>
<polyline fill="none" stroke="#357f9e" points="855,42 845,32"/>
<text xml:space="preserve" text-anchor="middle" x="840" y="54" font-family="Helvetica,sans-Serif" font-size="12.00" fill="#1a1a1a">Exit</text>
<!-- BUILD FIX (ellipse) at (560, 132) -->
<ellipse fill="none" stroke="#357f9e" cx="560" cy="132" rx="36" ry="18"/>
<text xml:space="preserve" text-anchor="middle" x="560" y="136" font-family="Helvetica,sans-Serif" font-size="12.00" fill="#1a1a1a">Build Fix</text>
<!-- === FORWARD EDGES === -->
<!-- start → audit_llm -->
<path fill="none" stroke="#666666" d="M70,50 L103,50"/>
<polygon fill="#666666" stroke="#666666" points="103,46.5 113,50 103,53.5"/>
<!-- audit_llm → audit_agent -->
<path fill="none" stroke="#666666" d="M197,50 L220,50"/>
<polygon fill="#666666" stroke="#666666" points="220,46.5 230,50 220,53.5"/>
<!-- audit_agent → triage -->
<path fill="none" stroke="#666666" d="M326,50 L352,50"/>
<polygon fill="#666666" stroke="#666666" points="352,46.5 362,50 352,53.5"/>
<!-- triage → fix -->
<path fill="none" stroke="#666666" d="M418,50 L442,50"/>
<polygon fill="#666666" stroke="#666666" points="442,46.5 452,50 442,53.5"/>
<text xml:space="preserve" text-anchor="middle" x="435" y="45" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#666666">Fix</text>
<!-- fix → build -->
<path fill="none" stroke="#666666" d="M488,50 L514,50"/>
<polygon fill="#666666" stroke="#666666" points="514,46.5 524,50 514,53.5"/>
<!-- build → verify [OK] -->
<path fill="none" stroke="#666666" d="M594,50 L622,50"/>
<polygon fill="#666666" stroke="#666666" points="622,46.5 632,50 622,53.5"/>
<text xml:space="preserve" text-anchor="middle" x="614" y="45" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#666666">OK</text>
<!-- verify → review -->
<path fill="none" stroke="#666666" d="M688,50 L714,50"/>
<polygon fill="#666666" stroke="#666666" points="714,46.5 724,50 714,53.5"/>
<!-- review → exit [Accept] -->
<path fill="none" stroke="#666666" d="M780,50 L815,50"/>
<polygon fill="#666666" stroke="#666666" points="815,46.5 825,50 815,53.5"/>
<text xml:space="preserve" text-anchor="middle" x="802" y="45" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#666666">Accept</text>
<!-- === LOOPS === -->
<!-- fix self-loop (above) -->
<path fill="none" stroke="#666666" d="M462,34 C456,12 484,12 478,34"/>
<polygon fill="#666666" stroke="#666666" points="474,28 478,38 482,28"/>
<!-- build → build_fix [Fail] -->
<path fill="none" stroke="#666666" d="M568,68 L568,104"/>
<polygon fill="#666666" stroke="#666666" points="564.5,104 568,114 571.5,104"/>
<text xml:space="preserve" text-anchor="middle" x="582" y="93" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#666666">Fail</text>
<!-- build_fix → build (curved left) -->
<path fill="none" stroke="#666666" d="M534,120 C508,105 508,72 530,58"/>
<polygon fill="#666666" stroke="#666666" points="526,62 530,52 534,62"/>
<!-- verify → triage [More work] (backward arc below) -->
<path fill="none" stroke="#666666" d="M655,68 C655,162 395,162 395,68"/>
<polygon fill="#666666" stroke="#666666" points="391,74 395,64 399,74"/>
<text xml:space="preserve" text-anchor="middle" x="525" y="162" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#666666">More work</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 6.4 KiB