diff --git a/crates/arc-workflows/src/cli/mod.rs b/crates/arc-workflows/src/cli/mod.rs index 10a4d4c28..9e33bb8c1 100644 --- a/crates/arc-workflows/src/cli/mod.rs +++ b/crates/arc-workflows/src/cli/mod.rs @@ -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, + + /// Skip retro generation after the run + #[arg(long)] + pub no_retro: bool, } #[derive(Args)] diff --git a/crates/arc-workflows/src/cli/progress.rs b/crates/arc-workflows/src/cli/progress.rs index 7f47e9690..e80ad77ab 100644 --- a/crates/arc-workflows/src/cli/progress.rs +++ b/crates/arc-workflows/src/cli/progress.rs @@ -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)); diff --git a/crates/arc-workflows/src/cli/run.rs b/crates/arc-workflows/src/cli/run.rs index 80f9dd642..052a62c28 100644 --- a/crates/arc-workflows/src/cli/run.rs +++ b/crates/arc-workflows/src/cli/run.rs @@ -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, diff --git a/crates/arc-workflows/src/engine.rs b/crates/arc-workflows/src/engine.rs index ed1b1c537..75bdddba6 100644 --- a/crates/arc-workflows/src/engine.rs +++ b/crates/arc-workflows/src/engine.rs @@ -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; diff --git a/demo/01-hello.dot b/demo/01-hello.dot new file mode 100644 index 000000000..4b0a905b5 --- /dev/null +++ b/demo/01-hello.dot @@ -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 +} diff --git a/demo/02-tool-use.dot b/demo/02-tool-use.dot new file mode 100644 index 000000000..a199ba49d --- /dev/null +++ b/demo/02-tool-use.dot @@ -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 +} diff --git a/demo/03-pipeline.dot b/demo/03-pipeline.dot new file mode 100644 index 000000000..55c180ca8 --- /dev/null +++ b/demo/03-pipeline.dot @@ -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 +} diff --git a/demo/04-branch-loop.dot b/demo/04-branch-loop.dot new file mode 100644 index 000000000..df066bd16 --- /dev/null +++ b/demo/04-branch-loop.dot @@ -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"] +} diff --git a/demo/05-parallel.dot b/demo/05-parallel.dot new file mode 100644 index 000000000..1c9335268 --- /dev/null +++ b/demo/05-parallel.dot @@ -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 +} diff --git a/demo/06-human-gate.dot b/demo/06-human-gate.dot new file mode 100644 index 000000000..f537a3fb9 --- /dev/null +++ b/demo/06-human-gate.dot @@ -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 +} diff --git a/demo/07-multi-model.dot b/demo/07-multi-model.dot new file mode 100644 index 000000000..302c92b02 --- /dev/null +++ b/demo/07-multi-model.dot @@ -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 +}