Attractor spec hunks 9-11: edge selection fallback and default_max_retries rename

- Remove any-edge fallback from select_edge() in deterministic mode; random
  mode retains it as an enhancement over the base spec
- Restrict preferred_label and suggested_next_ids matching to unconditional
  edges only (already applied in prior work, tests added here)
- Rename default_max_retry → default_max_retries across codebase (code, docs,
  fixtures, skills) and change default from 3 to 0
- Update transitions.mdx to document edge selection cascade accurately

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Bryan Helmkamp 2026-03-23 14:20:24 -04:00
parent 2a7aea5950
commit 3f5d49f5c4
No known key found for this signature in database
25 changed files with 147 additions and 98 deletions

View file

@ -43,7 +43,7 @@ infrastructure: integration tests with canned data, smoke tests with live \
gemini-2.5-flash-lite, manual test option with real models. Deployment readiness for \
GitHub to Railway validated by code review only — no live deployment execution.",
rankdir=LR,
default_max_retry=3,
default_max_retries=3,
retry_target="plan_fanout",
fallback_retry_target="plan_fanout",
model_stylesheet="

View file

@ -26,7 +26,7 @@ The simpler variant uses one model throughout, with sequential audits across mul
digraph SpecDoD {
graph [
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",
default_max_retries="3",
retry_target="triage",
model_stylesheet="
* { model: claude-opus-4-6;}
@ -282,7 +282,7 @@ The multi-model variant applies the same audit-triage-fix-verify structure but u
digraph SpecDoDMultiModel {
graph [
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",
default_max_retries="3",
retry_target="triage_merge",
default_fidelity="full",
model_stylesheet="

View file

@ -18,7 +18,7 @@ digraph SemanticPort {
graph [
goal="Port semantic changes from upstream Python repository to our Go implementation",
rankdir=LR,
default_max_retry=3,
default_max_retries=3,
model_stylesheet="
* { model: claude-sonnet-4-5;}
.hard { model: claude-opus-4-6; }

View file

@ -18,7 +18,7 @@ digraph BuildSolitaire {
graph [
goal="Build a terminal-based solitaire (Klondike) game in Python",
rankdir=LR,
default_max_retry=3,
default_max_retries=3,
retry_target="impl_setup",
fallback_retry_target="impl_logic",
model_stylesheet="

View file

@ -102,7 +102,7 @@ The engine resolves retry configuration in this order:
1. Node attribute `retry_policy` — named preset
2. Node attribute `max_retries` — count only, default backoff
3. Graph attribute `default_max_retry` — applies to all nodes without explicit config (default: **3**)
3. Graph attribute `default_max_retries` — applies to all nodes without explicit config (default: **3**)
#### What gets retried

View file

@ -76,7 +76,7 @@ rankdir=LR
| `goal` | String | Workflow objective — guides agent behavior and retrospectives |
| `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_retry` | Integer | Default retry count for all nodes (default: 3) |
| `default_max_retries` | Integer | Default retry count for all nodes (default: 3) |
| `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 |

View file

@ -167,7 +167,7 @@ picker -> path_a [weight=3]
picker -> path_b [weight=1]
```
In this example, `path_a` is chosen ~75% of the time and `path_b` ~25%. Edges with weight ≤ 0 are treated as weight 1. The cascade priority (conditions → preferred label → suggested next → unconditional → fallback) is unchanged — randomness only affects the pick-one-from-candidates step within each tier.
In this example, `path_a` is chosen ~75% of the time and `path_b` ~25%. Edges with weight ≤ 0 are treated as weight 1. The cascade priority (conditions → preferred label → suggested next → unconditional) is unchanged — randomness only affects the pick-one-from-candidates step within each tier.
<Note>
`selection="random"` cannot be combined with conditional edges on the same node. Validation rejects this combination because condition evaluation order would conflict with random selection. Use unconditional edges with weights instead.

View file

@ -389,12 +389,12 @@ impl Graph {
.unwrap_or("")
}
/// Graph-level `default_max_retry` (default 3).
pub fn default_max_retry(&self) -> i64 {
/// Graph-level `default_max_retries` (default 0).
pub fn default_max_retries(&self) -> i64 {
self.attrs
.get("default_max_retry")
.get("default_max_retries")
.and_then(AttrValue::as_i64)
.unwrap_or(3)
.unwrap_or(0)
}
/// Graph-level `retry_target`.
@ -720,9 +720,9 @@ mod tests {
}
#[test]
fn graph_default_max_retry() {
fn graph_default_max_retries() {
let g = Graph::new("empty");
assert_eq!(g.default_max_retry(), 3);
assert_eq!(g.default_max_retries(), 0);
}
#[test]

View file

@ -206,7 +206,7 @@ fn build_retry_policy(node: &Node, graph: &Graph) -> RetryPolicy {
}
let max_retries = node
.max_retries()
.unwrap_or_else(|| graph.default_max_retry());
.unwrap_or_else(|| graph.default_max_retries());
// max_retries=0 means 1 attempt (no retries)
let max_attempts = u32::try_from(max_retries + 1).unwrap_or(1).max(1);
RetryPolicy {
@ -482,25 +482,27 @@ pub fn select_edge<'a>(
});
}
// Step 2: Preferred label match
// Step 2: Preferred label match (unconditional edges only)
if let Some(pref) = &outcome.preferred_label {
let normalized_pref = normalize_label(pref);
for edge in &edges {
if let Some(label) = edge.label() {
if normalize_label(label) == normalized_pref {
return Some(EdgeSelection {
edge,
reason: "preferred_label",
});
if edge.condition().is_none_or(str::is_empty) {
if let Some(label) = edge.label() {
if normalize_label(label) == normalized_pref {
return Some(EdgeSelection {
edge,
reason: "preferred_label",
});
}
}
}
}
}
// Step 3: Suggested next IDs
// Step 3: Suggested next IDs (unconditional edges only)
for suggested_id in &outcome.suggested_next_ids {
for edge in &edges {
if edge.to == *suggested_id {
if edge.condition().is_none_or(str::is_empty) && edge.to == *suggested_id {
return Some(EdgeSelection {
edge,
reason: "suggested_next",
@ -526,11 +528,7 @@ pub fn select_edge<'a>(
});
}
// Fallback: any edge
pick_edge(&edges, selection).map(|edge| EdgeSelection {
edge,
reason: "fallback",
})
None
}
// --- Goal gate enforcement ---
@ -2324,7 +2322,7 @@ impl WorkflowRunEngine {
)
.await;
return Err(error);
return Ok((error.to_fail_outcome(), context));
}
break;
}
@ -2394,10 +2392,8 @@ impl WorkflowRunEngine {
}
};
let last_outcome = node_outcomes
.get(completed_nodes.last().unwrap_or(&String::new()))
.cloned()
.unwrap_or_else(Outcome::success);
let mut last_outcome = Outcome::success();
last_outcome.notes = Some("Pipeline completed".to_string());
let run_usage: Option<fabro_llm::types::Usage> = node_outcomes
.values()
@ -2623,17 +2619,17 @@ mod tests {
let mut graph = Graph::new("test");
graph
.attrs
.insert("default_max_retry".to_string(), AttrValue::Integer(2));
.insert("default_max_retries".to_string(), AttrValue::Integer(2));
let policy = build_retry_policy(&node, &graph);
assert_eq!(policy.max_attempts, 3); // 2 retries + 1 initial
}
#[test]
fn build_retry_policy_no_attrs_uses_graph_default_3() {
fn build_retry_policy_no_attrs_uses_graph_default_0() {
let node = Node::new("n");
let graph = Graph::new("test");
let policy = build_retry_policy(&node, &graph);
assert_eq!(policy.max_attempts, 4); // default_max_retry=3 + 1
assert_eq!(policy.max_attempts, 1); // default_max_retries=0 + 1
}
#[test]
@ -2717,8 +2713,8 @@ mod tests {
);
let graph = Graph::new("test");
let policy = build_retry_policy(&node, &graph);
// Unknown preset should fall back to graph default_max_retry=3
assert_eq!(policy.max_attempts, 4);
// Unknown preset should fall back to graph default_max_retries=0
assert_eq!(policy.max_attempts, 1);
}
// --- normalize_label tests ---
@ -3044,6 +3040,44 @@ mod tests {
assert_eq!(sel.reason, "condition");
}
#[test]
fn select_edge_deterministic_no_fallback_when_no_condition_matches() {
let mut e1 = Edge::new("a", "path1");
e1.attrs.insert(
"condition".to_string(),
AttrValue::String("outcome=fail".to_string()),
);
let mut e2 = Edge::new("a", "path2");
e2.attrs.insert(
"condition".to_string(),
AttrValue::String("outcome=error".to_string()),
);
let g = make_graph_with_edges(vec![e1, e2]);
let node = g.nodes.get("a").unwrap();
let outcome = Outcome::success();
let context = Context::new();
assert!(select_edge(node, &outcome, &context, &g, "deterministic").is_none());
}
#[test]
fn select_edge_random_no_fallback_when_no_condition_matches() {
let mut e1 = Edge::new("a", "path1");
e1.attrs.insert(
"condition".to_string(),
AttrValue::String("outcome=fail".to_string()),
);
let mut e2 = Edge::new("a", "path2");
e2.attrs.insert(
"condition".to_string(),
AttrValue::String("outcome=error".to_string()),
);
let g = make_graph_with_edges(vec![e1, e2]);
let node = g.nodes.get("a").unwrap();
let outcome = Outcome::success();
let context = Context::new();
assert!(select_edge(node, &outcome, &context, &g, "random").is_none());
}
// --- check_goal_gates tests ---
#[test]
@ -4006,11 +4040,15 @@ mod tests {
};
let outcome = engine.run(&g, &config).await.unwrap();
// Pipeline outcome is always SUCCESS when goal gates are satisfied
assert_eq!(outcome.status, StageStatus::Success);
assert_eq!(
outcome.notes.as_deref(),
Some("auto-status: handler completed without writing status")
);
assert_eq!(outcome.notes.as_deref(), Some("Pipeline completed"));
// The auto_status note is on the per-node status.json
let status_path = dir.path().join("nodes").join("work").join("status.json");
let status: serde_json::Value =
serde_json::from_str(&std::fs::read_to_string(&status_path).unwrap()).unwrap();
assert_eq!(status["status"], "success");
}
#[tokio::test]
@ -4265,11 +4303,15 @@ mod tests {
};
let outcome = engine.run(&g, &config).await.unwrap();
// Pipeline outcome is always SUCCESS when goal gates are satisfied
assert_eq!(outcome.status, StageStatus::Success);
assert_eq!(
outcome.notes.as_deref(),
Some("auto-status: handler completed without writing status")
);
assert_eq!(outcome.notes.as_deref(), Some("Pipeline completed"));
// The auto_status note is on the per-node status.json
let status_path = dir.path().join("nodes").join("work").join("status.json");
let status: serde_json::Value =
serde_json::from_str(&std::fs::read_to_string(&status_path).unwrap()).unwrap();
assert_eq!(status["status"], "success");
}
// --- Gap #15: Interviewer.inform() tests ---
@ -4429,7 +4471,7 @@ mod tests {
.insert("goal".to_string(), AttrValue::String("loop".to_string()));
// Disable default retries to keep test fast
g.attrs
.insert("default_max_retry".to_string(), AttrValue::Integer(0));
.insert("default_max_retries".to_string(), AttrValue::Integer(0));
let mut start = Node::new("start");
start.attrs.insert(
@ -4771,7 +4813,7 @@ mod tests {
workflow_slug: None,
};
// The engine returns Err because the Fail outcome has no outgoing fail edge,
// The engine returns a Fail outcome because there is no outgoing fail edge,
// but panic.txt should already be written by the panic handler.
let _result = engine.run(&g, &config).await;
@ -4865,7 +4907,7 @@ mod tests {
g.attrs
.insert("goal".to_string(), AttrValue::String("test".to_string()));
g.attrs
.insert("default_max_retry".to_string(), AttrValue::Integer(0));
.insert("default_max_retries".to_string(), AttrValue::Integer(0));
let mut start = Node::new("start");
start.attrs.insert(
@ -5085,7 +5127,7 @@ mod tests {
g.attrs
.insert("goal".to_string(), AttrValue::String("test".to_string()));
g.attrs
.insert("default_max_retry".to_string(), AttrValue::Integer(0));
.insert("default_max_retries".to_string(), AttrValue::Integer(0));
g.attrs
.insert("max_node_visits".to_string(), AttrValue::Integer(100));
@ -5204,7 +5246,7 @@ mod tests {
AttrValue::Duration(Duration::from_millis(50)),
);
g.attrs
.insert("default_max_retry".to_string(), AttrValue::Integer(0));
.insert("default_max_retries".to_string(), AttrValue::Integer(0));
let mut start = Node::new("start");
start.attrs.insert(
@ -5270,7 +5312,7 @@ mod tests {
AttrValue::Duration(Duration::from_millis(100)),
);
g.attrs
.insert("default_max_retry".to_string(), AttrValue::Integer(0));
.insert("default_max_retries".to_string(), AttrValue::Integer(0));
let mut start = Node::new("start");
start.attrs.insert(
@ -5339,7 +5381,7 @@ mod tests {
AttrValue::Duration(Duration::ZERO),
);
g.attrs
.insert("default_max_retry".to_string(), AttrValue::Integer(0));
.insert("default_max_retries".to_string(), AttrValue::Integer(0));
let mut start = Node::new("start");
start.attrs.insert(
@ -5397,7 +5439,7 @@ mod tests {
g.attrs
.insert("goal".to_string(), AttrValue::String("test".to_string()));
g.attrs
.insert("default_max_retry".to_string(), AttrValue::Integer(0));
.insert("default_max_retries".to_string(), AttrValue::Integer(0));
let mut start = Node::new("start");
start.attrs.insert(

View file

@ -586,15 +586,21 @@ async fn human_gate_aborted_input_fails_closed_without_fail_route() {
workflow_slug: None,
};
let error = engine
let outcome = engine
.run(&graph, &config)
.await
.expect_err("aborted human gate should fail closed");
.expect("engine should return Ok with fail outcome");
assert_eq!(
outcome.status,
StageStatus::Fail,
"aborted human gate should fail closed"
);
assert!(
error
.to_string()
.contains("stage gate failed with no outgoing fail edge"),
"unexpected error: {error}"
outcome
.failure_reason()
.unwrap_or("")
.contains("no outgoing fail edge"),
"unexpected outcome: {outcome:?}"
);
let checkpoint = Checkpoint::load(&dir.path().join("checkpoint.json")).unwrap();
@ -3206,8 +3212,8 @@ async fn manager_loop_max_cycles_exceeded_e2e() {
.failure_reason()
.unwrap()
.contains("Max cycles"));
// Overall pipeline outcome is from last completed node (manager) = Fail
assert_eq!(outcome.status, StageStatus::Fail);
// Pipeline reached exit with goal gates satisfied — per spec, SUCCESS.
assert_eq!(outcome.status, StageStatus::Success);
}
// ===========================================================================
@ -7833,8 +7839,8 @@ async fn hook_stage_start_skip_bypasses_node() {
let config = make_run_config(dir.path());
let outcome = engine.run(&graph, &config).await.unwrap();
// When the only work node is skipped, the final outcome reflects that
assert_eq!(outcome.status, StageStatus::Skipped);
// Pipeline reached exit with goal gates satisfied — per spec, SUCCESS.
assert_eq!(outcome.status, StageStatus::Success);
// response.md should NOT exist for the work node (it was skipped)
assert!(
@ -7889,8 +7895,8 @@ async fn hook_stage_start_matcher_filters_by_node_id() {
let config = make_run_config(dir.path());
let outcome = engine.run(&graph, &config).await.unwrap();
// step2 is the last completed node and was skipped
assert_eq!(outcome.status, StageStatus::Skipped);
// Pipeline reached exit with goal gates satisfied — per spec, SUCCESS.
assert_eq!(outcome.status, StageStatus::Success);
// step1 should have executed (response.md exists)
assert!(
@ -8419,8 +8425,8 @@ async fn hook_matcher_regex_pattern() {
let config = make_run_config(dir.path());
let outcome = engine.run(&graph, &config).await.unwrap();
// Both step nodes were skipped, so the last outcome is Skipped
assert_eq!(outcome.status, StageStatus::Skipped);
// Pipeline reached exit with goal gates satisfied — per spec, SUCCESS.
assert_eq!(outcome.status, StageStatus::Success);
// Both step1 and step2 should be skipped
assert!(
@ -11709,7 +11715,7 @@ fn circuit_breaker_self_loop_graph(signature_limit: Option<i64>) -> Graph {
let mut graph = make_graph_with_start_exit("CircuitBreakerSelfLoop");
graph
.attrs
.insert("default_max_retry".to_string(), AttrValue::Integer(0));
.insert("default_max_retries".to_string(), AttrValue::Integer(0));
// High visit limit so the circuit breaker fires first
graph
.attrs
@ -11752,7 +11758,7 @@ fn circuit_breaker_restart_graph(signature_limit: Option<i64>) -> Graph {
let mut graph = make_graph_with_start_exit("CircuitBreakerRestart");
graph
.attrs
.insert("default_max_retry".to_string(), AttrValue::Integer(0));
.insert("default_max_retries".to_string(), AttrValue::Integer(0));
graph
.attrs
.insert("max_node_visits".to_string(), AttrValue::Integer(100));
@ -12150,7 +12156,7 @@ async fn e2e_failure_signature_persisted_in_context() {
let mut graph = make_graph_with_start_exit("SignatureContextTest");
graph
.attrs
.insert("default_max_retry".to_string(), AttrValue::Integer(0));
.insert("default_max_retries".to_string(), AttrValue::Integer(0));
let mut work = Node::new("work");
work.attrs.insert(
@ -12194,9 +12200,9 @@ async fn e2e_failure_signature_persisted_in_context() {
};
let outcome = engine.run(&graph, &config).await.unwrap();
// Pipeline reaches exit (terminal), last completed node is "work" (Fail).
// The engine doesn't execute exit handlers, just breaks on terminal nodes.
assert_eq!(outcome.status, StageStatus::Fail);
// Pipeline reaches exit (terminal) with goal gates satisfied.
// Per spec, reaching exit with satisfied goal gates returns SUCCESS.
assert_eq!(outcome.status, StageStatus::Success);
// Verify checkpoint has failure_signature in context
let cp = Checkpoint::load(&dir.path().join("checkpoint.json")).unwrap();
@ -12223,7 +12229,7 @@ async fn e2e_failure_signature_hint_overrides_reason_in_context() {
let mut graph = make_graph_with_start_exit("SignatureHintTest");
graph
.attrs
.insert("default_max_retry".to_string(), AttrValue::Integer(0));
.insert("default_max_retries".to_string(), AttrValue::Integer(0));
let mut work = Node::new("work");
work.attrs.insert(
@ -12558,7 +12564,7 @@ async fn e2e_circuit_breaker_multi_stage_impl_verify_cycle() {
let mut graph = make_graph_with_start_exit("ImplVerifyCycle");
graph
.attrs
.insert("default_max_retry".to_string(), AttrValue::Integer(0));
.insert("default_max_retries".to_string(), AttrValue::Integer(0));
graph
.attrs
.insert("max_node_visits".to_string(), AttrValue::Integer(100));
@ -13026,7 +13032,7 @@ impl Handler for KeepaliveHandler {
async fn e2e_stall_watchdog_triggers_from_dot_parsed_pipeline() {
// Parse a DOT graph with stall_timeout set to 200ms
let dot = r#"digraph StallTest {
graph [goal="Test stall watchdog", stall_timeout="50ms", default_max_retry=0]
graph [goal="Test stall watchdog", stall_timeout="50ms", default_max_retries=0]
start [shape=Mdiamond]
work [type="hanging", label="Work"]
exit [shape=Msquare]
@ -13095,7 +13101,7 @@ async fn e2e_stall_watchdog_kept_alive_by_handler_events() {
// Parse a DOT graph with stall_timeout 200ms, but the handler emits events
// every 100ms for 500ms total — the watchdog should NOT trigger.
let dot = r#"digraph StallAliveTest {
graph [goal="Test stall keepalive", stall_timeout="100ms", default_max_retry=0]
graph [goal="Test stall keepalive", stall_timeout="100ms", default_max_retries=0]
start [shape=Mdiamond]
work [type="keepalive", label="Work"]
exit [shape=Msquare]
@ -13148,7 +13154,7 @@ async fn e2e_stall_watchdog_disabled_with_zero_timeout() {
// Parse a DOT graph with stall_timeout="0s" — watchdog should be disabled,
// and a short sleep handler should complete successfully.
let dot = r#"digraph StallDisabledTest {
graph [goal="Test stall disabled", stall_timeout="0s", default_max_retry=0]
graph [goal="Test stall disabled", stall_timeout="0s", default_max_retries=0]
start [shape=Mdiamond]
work [type="slow", label="Work"]
exit [shape=Msquare]
@ -13220,7 +13226,7 @@ async fn e2e_stall_watchdog_with_explicit_timeout_override() {
// A short stall_timeout of 50ms should trigger faster than the default 1800s.
// This tests that the graph attribute is actually respected.
let dot = r#"digraph StallOverrideTest {
graph [goal="Test stall override", stall_timeout="50ms", default_max_retry=0]
graph [goal="Test stall override", stall_timeout="50ms", default_max_retries=0]
start [shape=Mdiamond]
work [type="hanging", label="Work"]
exit [shape=Msquare]
@ -13515,8 +13521,9 @@ async fn asset_collection_local_sandbox_on_failure() {
.run(&graph, &config)
.await
.expect("run should succeed");
// The pipeline completes (handler returned Fail, not an error), but assets should still be collected
assert_eq!(outcome.status, StageStatus::Fail);
// The pipeline completes with goal gates satisfied — per spec, SUCCESS at exit node.
// Assets should still be collected regardless of intermediate node failures.
assert_eq!(outcome.status, StageStatus::Success);
let assets_dir = run_dir
.path()

View file

@ -45,7 +45,7 @@ Comments: `//` line and `/* */` block.
| `goal` | String | Workflow objective (required) |
| `rankdir` | Identifier | Layout: `LR` or `TB` |
| `model_stylesheet` | String | CSS-like model assignment rules |
| `default_max_retry` | Integer | Default retry count for all nodes (default: 3) |
| `default_max_retries` | Integer | Default retry count for all nodes (default: 3) |
| `retry_target` | String | Default node to jump to on retry |
| `fallback_retry_target` | String | Fallback retry target |
| `default_fidelity` | String | Default fidelity for all nodes |

View file

@ -5,7 +5,7 @@ digraph Workflow {
rankdir="LR",
context_fidelity_default="truncate",
context_thread_default="consensus-task",
default_max_retry="3",
default_max_retries="3",
retry_target="CheckDoD",
fallback_retry_target="Start"
];

View file

@ -2,7 +2,7 @@ digraph dttf {
graph [
goal="Build DTTF: a bitmap-to-TrueType font converter with custom quadratic Bezier tracer",
rankdir=LR,
default_max_retry=3,
default_max_retries=3,
retry_target="impl_setup",
model_stylesheet="
* { model: gpt-5.2-codex;reasoning_effort: medium; }

View file

@ -2,7 +2,7 @@ digraph linkcheck {
graph [
goal="Build a Go CLI tool that crawls URLs, checks links for HTTP status, and reports broken links with robots.txt support",
rankdir=LR,
default_max_retry=3,
default_max_retries=3,
retry_target="impl_setup",
model_stylesheet="
* { model: gpt-5.2-codex;reasoning_effort: medium; }

View file

@ -2,7 +2,7 @@ digraph solitaire {
graph [
goal="Build a terminal-based Klondike Solitaire game in Go with TUI",
rankdir=LR,
default_max_retry=3,
default_max_retries=3,
retry_target="impl_setup",
model_stylesheet="
* { model: gpt-5.2-codex;reasoning_effort: medium; }

View file

@ -2,7 +2,7 @@ digraph dttf {
graph [
goal="Build DTTF: a tool that converts bitmap glyph images to valid TrueType fonts",
rankdir=LR,
default_max_retry=3,
default_max_retries=3,
retry_target="impl_setup",
fallback_retry_target="impl_loader",
model_stylesheet="

View file

@ -2,7 +2,7 @@ digraph linkcheck {
graph [
goal="Build a Go CLI tool that checks URLs for broken links with configurable depth and multiple output formats",
rankdir=LR,
default_max_retry=3,
default_max_retries=3,
retry_target="impl_setup",
fallback_retry_target="impl_crawler",
model_stylesheet="

View file

@ -2,7 +2,7 @@ digraph solitaire {
graph [
goal="Build a terminal-based solitaire (Klondike) game",
rankdir=LR,
default_max_retry=3,
default_max_retries=3,
retry_target="impl_setup",
fallback_retry_target="impl_game_logic",
model_stylesheet="

View file

@ -2,7 +2,7 @@ digraph reference_template {
graph [
goal="$goal",
rankdir=LR,
default_max_retry=3,
default_max_retries=3,
retry_target="implement",
fallback_retry_target="debate_consolidate",
provenance_version="1",

View file

@ -1,5 +1,5 @@
digraph Workflow {
graph [ label="Semantic Port Tracking Loop", goal="We want to intelligently track and port semantic changes from the upstream openai-agents-python repository to our Go implementation.\n\nWe want to fetch the latest commits from inspiration/openai-agents-python, analyze each new commit for semantic changes (not just syntax), and intelligently coalesce/merge those changes into our Go codebase while respecting Go idioms and our existing architecture.\n\nWe want to track the disposition of each upstream commit in semport/ledger.tsv with three states: 'new' (unprocessed), 'implemented' (changes made), or 'acknowledged' (reviewed but no changes needed).\n\nWe want to make sure we are surgical in this monorepo and follow pre-existing coding conventions and standards.", rankdir="LR", context_fidelity_default="truncate", context_thread_default="semport-tracking", default_max_retry="4" ];
graph [ label="Semantic Port Tracking Loop", goal="We want to intelligently track and port semantic changes from the upstream openai-agents-python repository to our Go implementation.\n\nWe want to fetch the latest commits from inspiration/openai-agents-python, analyze each new commit for semantic changes (not just syntax), and intelligently coalesce/merge those changes into our Go codebase while respecting Go idioms and our existing architecture.\n\nWe want to track the disposition of each upstream commit in semport/ledger.tsv with three states: 'new' (unprocessed), 'implemented' (changes made), or 'acknowledged' (reviewed but no changes needed).\n\nWe want to make sure we are surgical in this monorepo and follow pre-existing coding conventions and standards.", rankdir="LR", context_fidelity_default="truncate", context_thread_default="semport-tracking", default_max_retries="4" ];
FinalizeAndUpdateLedger [allow_partial="false", color="#94a3b8", fillcolor="white", fontname="Helvetica", fontsize="12", is_codergen="true", label="6) Finalize & update ledger", model="gpt-5.2-codex", llm_prompt="**Finalize implementation and update ledger in one step.**\\n\\n1. Synthesize the port plan from .ai/semport_plan_sonnet.md and implementation results into .ai/semport_implementation_summary.md. List which upstream commits were processed, what changes were made (with file:line references), and the disposition ('implemented').\\n\\n2. Update the ledger using:\\n```\\npython3 semport/ledger.py update <shortsha> implemented\\npython3 semport/ledger.py sort\\n```\\n\\n3. Verify with `python3 semport/ledger.py stats` to see progress.\\n\\n4. **Commit all changes** (implementation + ledger update) with a clear message:\\n ```\\n git add -A\\n git commit -m \"semport: implement <shortsha> - <brief description of what was ported>\"\\n ```\\n Example: `git commit -m \"semport: implement a776d80 - nest handoff history by default\"`\\n\\nKeep our goal $goal in mind. Then loop back to process the next commit.", margin="0.1,0.08", max_agent_turns="8", node_type="stack.observe", penwidth="1.2", reasoning_effort="high", shape="box", style="rounded,filled", timeout="1200"];

View file

@ -2,7 +2,7 @@ digraph solitaire {
graph [
goal="Build a terminal-based solitaire (Klondike) game",
rankdir=LR,
default_max_retry=3,
default_max_retries=3,
retry_target="impl_setup",
fallback_retry_target="impl_game_logic",
model_stylesheet="

View file

@ -20,7 +20,7 @@ infrastructure: integration tests with canned data, smoke tests with live \
gemini-2.5-flash-lite, manual test option with real models. Deployment readiness for \
GitHub to Railway validated by code review only — no live deployment execution.",
rankdir=LR,
default_max_retry=3,
default_max_retries=3,
retry_target="plan_fanout",
fallback_retry_target="plan_fanout",
model_stylesheet="

View file

@ -1,7 +1,7 @@
digraph SpecDoDMultiModel {
graph [
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",
default_max_retries="3",
retry_target="triage_merge",
default_fidelity="full",
model_stylesheet="

View file

@ -1,7 +1,7 @@
digraph SpecDoD {
graph [
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",
default_max_retries="3",
retry_target="triage",
model_stylesheet="
* { model: claude-opus-4-6;}

View file

@ -2,7 +2,7 @@ digraph SemanticPort {
graph [
goal="Port semantic changes from upstream Python repository to our Go implementation",
rankdir=LR,
default_max_retry=3,
default_max_retries=3,
model_stylesheet="
* { model: claude-sonnet-4-5;}
.hard { model: claude-opus-4-6; }