Enrich tool call display, emit exit node events, add --no-retro and demo

- Show tool args in dim parenthetical: read_file(CLAUDE.md)
- Shorten CWD-relative paths in tool call display
- Show elapsed time on completed tool calls
- Emit StageStarted/StageCompleted for terminal (exit) nodes
- Add --no-retro flag to skip retro generation
- Add demo/02-tool-use.dot, renumber existing demos 03-07

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Bryan Helmkamp 2026-03-04 01:25:45 -05:00
parent 23a943efca
commit 4eea33b797
11 changed files with 190 additions and 16 deletions

View file

@ -123,6 +123,10 @@ pub struct RunArgs {
/// Attach a label to this run (repeatable, format: KEY=VALUE)
#[arg(long = "label", value_name = "KEY=VALUE")]
pub label: Vec<String>,
/// Skip retro generation after the run
#[arg(long)]
pub no_retro: bool,
}
#[derive(Args)]

View file

@ -41,7 +41,7 @@ cached_style!(
style_tool_running,
" {spinner:.dim} {wide_msg} {elapsed:.dim}"
);
cached_style!(style_tool_done, " {wide_msg}");
cached_style!(style_tool_done, " {wide_msg} {prefix:.dim}");
cached_style!(style_static_dim, " {wide_msg:.dim}");
cached_style!(style_empty, " ");
@ -76,20 +76,44 @@ fn format_duration_ms(ms: u64) -> String {
// ── Tool call display name ──────────────────────────────────────────────
fn tool_display_name(tool_name: &str, arguments: &serde_json::Value) -> String {
if tool_name == "bash" || tool_name == "execute_command" {
if let Some(cmd) = arguments.get("command").and_then(|v| v.as_str()) {
let truncated: String = if cmd.len() > 60 {
let mut s: String = cmd.chars().take(57).collect();
s.push_str("...");
s
} else {
cmd.to_string()
};
return format!("bash: {truncated}");
fn truncate(s: &str, max: usize) -> String {
if s.len() > max {
let mut t: String = s.chars().take(max - 3).collect();
t.push_str("...");
t
} else {
s.to_string()
}
}
fn shorten_path(path: &str) -> String {
if let Ok(cwd) = std::env::current_dir() {
if let Ok(rel) = std::path::Path::new(path).strip_prefix(&cwd) {
return rel.display().to_string();
}
}
tool_name.to_string()
path.to_string()
}
fn tool_display_name(tool_name: &str, arguments: &serde_json::Value) -> String {
let dim = Style::new().dim();
let arg = |key: &str| arguments.get(key).and_then(|v| v.as_str());
let path_arg = || arg("path").or_else(|| arg("file_path")).map(|p| truncate(&shorten_path(p), 60));
let detail = match tool_name {
"bash" | "execute_command" => arg("command").map(|c| truncate(c, 60)),
"glob" => arg("pattern").map(String::from),
"grep" | "ripgrep" => arg("pattern").map(|p| truncate(p, 40)),
"read_file" | "read" => path_arg(),
"write_file" | "write" | "create_file" => path_arg(),
"edit_file" | "edit" => path_arg(),
_ => None,
};
match detail {
Some(d) => format!("{tool_name}{}", dim.apply_to(format!("({d})"))),
None => tool_name.to_string(),
}
}
// ── Tool call entry ─────────────────────────────────────────────────────
@ -448,7 +472,9 @@ impl ProgressUI {
} else {
ToolCallStatus::Succeeded
};
let elapsed = format_duration_short(entry.bar.elapsed());
entry.bar.set_style(style_tool_done());
entry.bar.set_prefix(elapsed);
entry
.bar
.finish_with_message(format!("{glyph} {}", entry.display_name));

View file

@ -661,7 +661,7 @@ pub async fn run_command(
}
// Auto-derive retro (always, cheap) and optionally run retro agent
{
if !args.no_retro {
let (failed, failure_reason) = match &engine_result {
Ok(o) => (
o.status == StageStatus::Fail,
@ -972,7 +972,7 @@ async fn run_from_branch(
let _ = crate::git::remove_worktree(&original_cwd, &worktree_path);
// Auto-derive retro
{
if !args.no_retro {
let (failed, failure_reason) = match &engine_result {
Ok(o) => (
o.status == StageStatus::Fail,

View file

@ -1246,7 +1246,36 @@ impl WorkflowRunEngine {
// Step 1: Check for terminal node
if is_terminal(node) {
match check_goal_gates(graph, &node_outcomes) {
Ok(()) => break,
Ok(()) => {
self.services
.emitter
.emit(&WorkflowRunEvent::StageStarted {
node_id: node.id.clone(),
name: node.label().to_string(),
index: stage_index,
handler_type: node.handler_type().map(String::from),
attempt: 1,
max_attempts: 1,
});
self.services
.emitter
.emit(&WorkflowRunEvent::StageCompleted {
node_id: node.id.clone(),
name: node.label().to_string(),
index: stage_index,
duration_ms: 0,
status: StageStatus::Success.to_string(),
preferred_label: None,
suggested_next_ids: vec![],
usage: None,
failure: None,
notes: None,
files_touched: vec![],
attempt: 1,
max_attempts: 1,
});
break;
}
Err(failed_node_id) => {
if let Some(retry_target) = get_retry_target(&failed_node_id, graph) {
current_node_id = retry_target;

11
demo/01-hello.dot Normal file
View file

@ -0,0 +1,11 @@
digraph Hello {
graph [goal="Write a haiku about software workflows"]
rankdir=LR
start [shape=Mdiamond, label="Start"]
exit [shape=Msquare, label="Exit"]
compose [label="Compose", prompt="Write a haiku (5-7-5 syllable) about software workflows. Output only the haiku, nothing else.", codergen_mode="one_shot", reasoning_effort="low"]
start -> compose -> exit
}

11
demo/02-tool-use.dot Normal file
View file

@ -0,0 +1,11 @@
digraph ToolUse {
graph [goal="Explore the current directory using shell tools"]
rankdir=LR
start [shape=Mdiamond, label="Start"]
exit [shape=Msquare, label="Exit"]
explore [label="Explore", prompt="Use bash to list the files in the current directory, then read the first 5 lines of any README or CLAUDE.md file you find. Summarize what this project is about in 2-3 sentences."]
start -> explore -> exit
}

13
demo/03-pipeline.dot Normal file
View file

@ -0,0 +1,13 @@
digraph Pipeline {
graph [goal="Analyze the current directory and suggest improvements"]
rankdir=LR
start [shape=Mdiamond, label="Start"]
exit [shape=Msquare, label="Exit"]
scan [label="Scan Files", shape=parallelogram, script="find . -maxdepth 2 -type f | head -30"]
analyze [label="Analyze", prompt="Review the file listing from the previous step. Identify what kind of project this is and summarize its structure in 3-4 bullet points.", codergen_mode="one_shot", reasoning_effort="low"]
suggest [label="Suggest", prompt="Based on the analysis, suggest 3 concrete improvements to the project structure. Be specific and actionable.", codergen_mode="one_shot", reasoning_effort="low"]
start -> scan -> analyze -> suggest -> exit
}

16
demo/04-branch-loop.dot Normal file
View file

@ -0,0 +1,16 @@
digraph BranchLoop {
graph [goal="Create a Python script that passes its test suite"]
rankdir=LR
start [shape=Mdiamond, label="Start"]
exit [shape=Msquare, label="Exit"]
plan [label="Plan", prompt="Plan a small Python script (fizzbuzz.py) and a test file (test_fizzbuzz.py) using pytest. Describe what you will create.", codergen_mode="one_shot", reasoning_effort="low"]
implement [label="Implement", prompt="Create fizzbuzz.py and test_fizzbuzz.py as planned. Write the files to disk."]
validate [label="Validate", shape=parallelogram, script="python -m pytest test_fizzbuzz.py -v 2>&1 || true"]
gate [shape=diamond, label="Tests passing?"]
start -> plan -> implement -> validate -> gate
gate -> exit [label="Pass", condition="outcome=success"]
gate -> implement [label="Fix"]
}

25
demo/05-parallel.dot Normal file
View file

@ -0,0 +1,25 @@
digraph Parallel {
graph [goal="Perform a multi-perspective code review"]
rankdir=LR
start [shape=Mdiamond, label="Start"]
exit [shape=Msquare, label="Exit"]
fork [label="Fork Analysis", shape=component, join_policy="wait_all", error_policy="continue"]
security [label="Security Audit", prompt="Examine the codebase for security concerns: hardcoded secrets, injection risks, unsafe dependencies. List findings as bullet points.", codergen_mode="one_shot", reasoning_effort="low"]
architecture [label="Architecture Review", prompt="Assess the codebase architecture: separation of concerns, dependency structure, modularity. List findings as bullet points.", codergen_mode="one_shot", reasoning_effort="low"]
quality [label="Code Quality", prompt="Check code quality: naming conventions, dead code, test coverage gaps, error handling. List findings as bullet points.", codergen_mode="one_shot", reasoning_effort="low"]
merge [label="Merge Findings", shape=tripleoctagon]
report [label="Final Report", prompt="Synthesize the security, architecture, and code quality findings into a prioritized summary report with top 5 action items.", codergen_mode="one_shot"]
start -> fork
fork -> security
fork -> architecture
fork -> quality
security -> merge
architecture -> merge
quality -> merge
merge -> report -> exit
}

18
demo/06-human-gate.dot Normal file
View file

@ -0,0 +1,18 @@
digraph HumanGate {
graph [goal="Propose and implement a README improvement"]
rankdir=LR
start [shape=Mdiamond, label="Start"]
exit [shape=Msquare, label="Exit"]
draft [label="Draft Proposal", prompt="Read the README.md (or note its absence). Propose a specific improvement: either create one or enhance the existing one. Describe your proposed changes clearly but do NOT make any changes yet.", codergen_mode="one_shot"]
approve [label="Approve Changes?", shape=hexagon]
apply [label="Apply Changes", prompt="Apply the proposed README changes that were approved."]
skip [label="Skip", prompt="Acknowledged. No changes made.", codergen_mode="one_shot", reasoning_effort="low"]
start -> draft -> approve
approve -> apply [label="[A] Approve"]
approve -> skip [label="[S] Skip"]
apply -> exit
skip -> exit
}

21
demo/07-multi-model.dot Normal file
View file

@ -0,0 +1,21 @@
digraph MultiModel {
graph [
goal="Build and review a utility function using multiple models",
model_stylesheet="
* { llm_model: claude-haiku-4-5; llm_provider: anthropic; reasoning_effort: low; }
.coding { llm_model: claude-sonnet-4-5; llm_provider: anthropic; reasoning_effort: high; }
#review { llm_model: claude-sonnet-4-5; llm_provider: anthropic; reasoning_effort: high; }
"
]
rankdir=LR
start [shape=Mdiamond, label="Start"]
exit [shape=Msquare, label="Exit"]
spec [label="Write Spec", prompt="Write a brief spec for a TypeScript string utility module with 3 functions: slugify, truncate, and capitalize. Output the spec only.", codergen_mode="one_shot"]
implement [label="Implement", prompt="Implement the TypeScript string utility module from the spec. Write it to string-utils.ts.", class="coding"]
test [label="Write Tests", prompt="Write tests for the string utility module using Bun's test runner. Write to string-utils.test.ts.", class="coding"]
review [label="Code Review", prompt="Review the implementation and tests. Check for edge cases, type safety, and correctness. Provide a brief verdict.", codergen_mode="one_shot"]
start -> spec -> implement -> test -> review -> exit
}