mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-10-10 03:30:59 +00:00
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:
parent
c7a6467640
commit
b9fe1282d3
25 changed files with 147 additions and 98 deletions
|
|
@ -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="
|
||||
|
|
|
|||
|
|
@ -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="
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
|
|
|
|||
|
|
@ -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="
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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 |
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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 |
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
];
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
|
|
|
|||
|
|
@ -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="
|
||||
|
|
|
|||
|
|
@ -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="
|
||||
|
|
|
|||
|
|
@ -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="
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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"];
|
||||
|
||||
|
|
|
|||
|
|
@ -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="
|
||||
|
|
|
|||
|
|
@ -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="
|
||||
|
|
|
|||
|
|
@ -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="
|
||||
|
|
|
|||
|
|
@ -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;}
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue