diff --git a/Cargo.lock b/Cargo.lock index 4e4d2bf3..567f4ec9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -515,7 +515,7 @@ checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" [[package]] name = "cli-sub-agent" -version = "0.1.198" +version = "0.1.199" dependencies = [ "anyhow", "chrono", @@ -541,6 +541,7 @@ dependencies = [ "regex", "serde", "serde_json", + "serial_test", "sha2", "sysinfo", "tempfile", @@ -703,7 +704,7 @@ dependencies = [ [[package]] name = "csa-acp" -version = "0.1.198" +version = "0.1.199" dependencies = [ "agent-client-protocol", "anyhow", @@ -723,7 +724,7 @@ dependencies = [ [[package]] name = "csa-config" -version = "0.1.198" +version = "0.1.199" dependencies = [ "anyhow", "chrono", @@ -739,7 +740,7 @@ dependencies = [ [[package]] name = "csa-core" -version = "0.1.198" +version = "0.1.199" dependencies = [ "agent-teams", "chrono", @@ -754,7 +755,7 @@ dependencies = [ [[package]] name = "csa-eval" -version = "0.1.198" +version = "0.1.199" dependencies = [ "anyhow", "chrono", @@ -768,7 +769,7 @@ dependencies = [ [[package]] name = "csa-executor" -version = "0.1.198" +version = "0.1.199" dependencies = [ "agent-teams", "anyhow", @@ -794,7 +795,7 @@ dependencies = [ [[package]] name = "csa-hooks" -version = "0.1.198" +version = "0.1.199" dependencies = [ "anyhow", "chrono", @@ -811,7 +812,7 @@ dependencies = [ [[package]] name = "csa-lock" -version = "0.1.198" +version = "0.1.199" dependencies = [ "anyhow", "chrono", @@ -823,7 +824,7 @@ dependencies = [ [[package]] name = "csa-mcp-hub" -version = "0.1.198" +version = "0.1.199" dependencies = [ "anyhow", "axum", @@ -845,7 +846,7 @@ dependencies = [ [[package]] name = "csa-memory" -version = "0.1.198" +version = "0.1.199" dependencies = [ "anyhow", "async-trait", @@ -863,7 +864,7 @@ dependencies = [ [[package]] name = "csa-process" -version = "0.1.198" +version = "0.1.199" dependencies = [ "anyhow", "chrono", @@ -881,7 +882,7 @@ dependencies = [ [[package]] name = "csa-resource" -version = "0.1.198" +version = "0.1.199" dependencies = [ "anyhow", "csa-core", @@ -897,7 +898,7 @@ dependencies = [ [[package]] name = "csa-scheduler" -version = "0.1.198" +version = "0.1.199" dependencies = [ "anyhow", "chrono", @@ -915,7 +916,7 @@ dependencies = [ [[package]] name = "csa-session" -version = "0.1.198" +version = "0.1.199" dependencies = [ "anyhow", "chrono", @@ -936,7 +937,7 @@ dependencies = [ [[package]] name = "csa-todo" -version = "0.1.198" +version = "0.1.199" dependencies = [ "anyhow", "chrono", @@ -3069,6 +3070,15 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "scc" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46e6f046b7fef48e2660c57ed794263155d713de679057f2d0c169bfc6e756cc" +dependencies = [ + "sdd", +] + [[package]] name = "schannel" version = "0.1.29" @@ -3120,6 +3130,12 @@ dependencies = [ "untrusted", ] +[[package]] +name = "sdd" +version = "3.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "490dcfcbfef26be6800d11870ff2df8774fa6e86d047e3e8c8a76b25655e41ca" + [[package]] name = "security-framework" version = "3.7.0" @@ -3244,6 +3260,32 @@ dependencies = [ "serde", ] +[[package]] +name = "serial_test" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "911bd979bf1070a3f3aa7b691a3b3e9968f339ceeec89e08c280a8a22207a32f" +dependencies = [ + "futures-executor", + "futures-util", + "log", + "once_cell", + "parking_lot", + "scc", + "serial_test_derive", +] + +[[package]] +name = "serial_test_derive" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a7d91949b85b0d2fb687445e448b40d322b6b3e4af6b44a29b21d9a5f33e6d9" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "sha2" version = "0.10.9" @@ -4325,7 +4367,7 @@ dependencies = [ [[package]] name = "weave" -version = "0.1.198" +version = "0.1.199" dependencies = [ "anyhow", "clap", diff --git a/Cargo.toml b/Cargo.toml index 927c402e..3349af02 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ members = ["crates/*"] resolver = "2" [workspace.package] -version = "0.1.198" +version = "0.1.199" edition = "2024" rust-version = "1.88" license = "Apache-2.0" diff --git a/crates/cli-sub-agent/Cargo.toml b/crates/cli-sub-agent/Cargo.toml index 2711b8f3..accef8c2 100644 --- a/crates/cli-sub-agent/Cargo.toml +++ b/crates/cli-sub-agent/Cargo.toml @@ -47,5 +47,8 @@ sha2.workspace = true tokuin.workspace = true xurl-core.workspace = true +[dev-dependencies] +serial_test = "3.4" + [features] codex-pty-fork = ["csa-executor/codex-pty-fork"] diff --git a/crates/cli-sub-agent/src/cli_review.rs b/crates/cli-sub-agent/src/cli_review.rs index 3ccf24be..b0cc25a8 100644 --- a/crates/cli-sub-agent/src/cli_review.rs +++ b/crates/cli-sub-agent/src/cli_review.rs @@ -166,6 +166,22 @@ pub struct ReviewArgs { /// Read supplementary prompt/context from a file (bypasses shell quoting issues) #[arg(long, value_name = "PATH")] pub prompt_file: Option, + + /// [DEPRECATED] Daemon mode is now the default. This flag is a no-op. + #[arg(long, hide = true)] + pub daemon: bool, + + /// Run in foreground blocking mode instead of the default daemon mode. + #[arg(long)] + pub no_daemon: bool, + + /// Internal flag: this process IS the daemon child. Skip re-spawning. + #[arg(long, hide = true)] + pub daemon_child: bool, + + /// Internal: pre-assigned session ID from daemon parent + #[arg(long, hide = true)] + pub session_id: Option, } impl ReviewArgs { @@ -342,4 +358,20 @@ pub struct DebateArgs { /// Read the debate question from a file (bypasses shell quoting issues) #[arg(long, value_name = "PATH", conflicts_with_all = ["question", "topic"])] pub prompt_file: Option, + + /// [DEPRECATED] Daemon mode is now the default. This flag is a no-op. + #[arg(long, hide = true)] + pub daemon: bool, + + /// Run in foreground blocking mode instead of the default daemon mode. + #[arg(long)] + pub no_daemon: bool, + + /// Internal flag: this process IS the daemon child. Skip re-spawning. + #[arg(long, hide = true)] + pub daemon_child: bool, + + /// Internal: pre-assigned session ID from daemon parent + #[arg(long, hide = true)] + pub session_id: Option, } diff --git a/crates/cli-sub-agent/src/main.rs b/crates/cli-sub-agent/src/main.rs index 8662af18..4f6e4375 100644 --- a/crates/cli-sub-agent/src/main.rs +++ b/crates/cli-sub-agent/src/main.rs @@ -412,22 +412,13 @@ async fn run() -> Result<()> { daemon_child, session_id, } => { - // Daemon spawn: daemon mode is the default; --no-daemon opts out. - if !no_daemon && !daemon_child { - if let Some(ref _id) = session_id { - anyhow::bail!("--session-id is an internal flag and must not be used directly"); - } - // spawn_and_exit() calls process::exit(0) on success — never returns. - run_cmd_daemon::spawn_and_exit(cd.as_deref())?; - } - - // Daemon child: propagate pre-assigned session ID via env so the - // pipeline's create_session reuses it (same directory as spool files). - if let Some(ref sid) = session_id { - // SAFETY: This runs in the daemon child before tokio spawns worker - // threads (we are still in the synchronous dispatch path of main). - unsafe { std::env::set_var("CSA_DAEMON_SESSION_ID", sid) }; - } + run_cmd_daemon::check_daemon_flags( + "run", + no_daemon, + daemon_child, + &session_id, + cd.as_deref(), + )?; // Daemon child path: continue with normal run logic. // --stream-stdout forces streaming; --no-stream-stdout forces buffering; @@ -535,6 +526,13 @@ async fn run() -> Result<()> { memory_cmd::handle_memory_command(command).await?; } Commands::Review(args) => { + run_cmd_daemon::check_daemon_flags( + "review", + args.no_daemon, + args.daemon_child, + &args.session_id, + args.cd.as_deref(), + )?; let exit_code = review_cmd::handle_review(args, current_depth).await?; crate::pipeline::prompt_guard::emit_sa_mode_caller_guard( sa_mode_active, @@ -546,6 +544,13 @@ async fn run() -> Result<()> { std::process::exit(exit_code); } Commands::Debate(args) => { + run_cmd_daemon::check_daemon_flags( + "debate", + args.no_daemon, + args.daemon_child, + &args.session_id, + args.cd.as_deref(), + )?; let exit_code = debate_cmd::handle_debate(args, current_depth, output_format).await?; crate::pipeline::prompt_guard::emit_sa_mode_caller_guard( sa_mode_active, diff --git a/crates/cli-sub-agent/src/pipeline_gate_tests.rs b/crates/cli-sub-agent/src/pipeline_gate_tests.rs index b8c13c92..2b6a26d9 100644 --- a/crates/cli-sub-agent/src/pipeline_gate_tests.rs +++ b/crates/cli-sub-agent/src/pipeline_gate_tests.rs @@ -1,9 +1,10 @@ use super::*; +use serial_test::serial; /// Set CSA_DEPTH env var for test isolation. /// /// # Safety -/// Tests using this must be run with `--test-threads=1` or accept +/// Tests using this must be run with `#[serial]` or accept /// that env mutations are process-global. All gate tests restore the /// env var after use. unsafe fn set_depth(val: &str) { @@ -83,6 +84,7 @@ fn test_gate_result_no_exit_code_is_not_passed() { // --------------------------------------------------------------------------- #[tokio::test] +#[serial] async fn test_gate_skipped_when_csa_depth_set() { // SAFETY: Test-only env mutation; gate tests are not parallel-safe by nature. unsafe { set_depth("1") }; @@ -106,6 +108,7 @@ async fn test_gate_skipped_when_csa_depth_set() { } #[tokio::test] +#[serial] async fn test_gate_skipped_when_no_command_and_no_hooks_path() { // SAFETY: Test-only env mutation. unsafe { set_depth("0") }; @@ -131,6 +134,7 @@ async fn test_gate_skipped_when_no_command_and_no_hooks_path() { } #[tokio::test] +#[serial] async fn test_gate_runs_explicit_command_success() { // SAFETY: Test-only env mutation. unsafe { set_depth("0") }; @@ -151,6 +155,7 @@ async fn test_gate_runs_explicit_command_success() { } #[tokio::test] +#[serial] async fn test_gate_runs_explicit_command_failure() { // SAFETY: Test-only env mutation. unsafe { set_depth("0") }; @@ -175,6 +180,7 @@ async fn test_gate_runs_explicit_command_failure() { } #[tokio::test] +#[serial] async fn test_gate_timeout() { // SAFETY: Test-only env mutation. unsafe { set_depth("0") }; @@ -198,6 +204,7 @@ async fn test_gate_timeout() { } #[tokio::test] +#[serial] async fn test_gate_monitor_mode_still_runs() { // SAFETY: Test-only env mutation. unsafe { set_depth("0") }; @@ -218,6 +225,7 @@ async fn test_gate_monitor_mode_still_runs() { } #[tokio::test] +#[serial] async fn test_gate_captures_stdout_and_stderr() { // SAFETY: Test-only env mutation. unsafe { set_depth("0") }; @@ -245,6 +253,7 @@ async fn test_gate_captures_stdout_and_stderr() { // --------------------------------------------------------------------------- #[tokio::test] +#[serial] async fn test_detect_no_hooks_path() { let dir = tempfile::tempdir().unwrap(); tokio::process::Command::new("git") @@ -259,6 +268,7 @@ async fn test_detect_no_hooks_path() { } #[tokio::test] +#[serial] async fn test_detect_hooks_path_with_pre_commit() { let dir = tempfile::tempdir().unwrap(); tokio::process::Command::new("git") @@ -288,6 +298,7 @@ async fn test_detect_hooks_path_with_pre_commit() { } #[tokio::test] +#[serial] async fn test_detect_hooks_path_without_pre_commit() { let dir = tempfile::tempdir().unwrap(); tokio::process::Command::new("git") @@ -313,6 +324,7 @@ async fn test_detect_hooks_path_without_pre_commit() { } #[tokio::test] +#[serial] async fn test_detect_hooks_path_relative() { let dir = tempfile::tempdir().unwrap(); tokio::process::Command::new("git") @@ -344,6 +356,7 @@ async fn test_detect_hooks_path_relative() { // --------------------------------------------------------------------------- #[tokio::test] +#[serial] async fn test_pipeline_skipped_when_csa_depth_set() { // SAFETY: Test-only env mutation. unsafe { set_depth("1") }; @@ -368,6 +381,7 @@ async fn test_pipeline_skipped_when_csa_depth_set() { } #[tokio::test] +#[serial] async fn test_pipeline_empty_steps_skipped() { // SAFETY: Test-only env mutation. unsafe { set_depth("0") }; @@ -385,6 +399,7 @@ async fn test_pipeline_empty_steps_skipped() { } #[tokio::test] +#[serial] async fn test_pipeline_sequential_all_pass() { // SAFETY: Test-only env mutation. unsafe { set_depth("0") }; @@ -428,6 +443,7 @@ async fn test_pipeline_sequential_all_pass() { } #[tokio::test] +#[serial] async fn test_pipeline_fail_fast_on_first_failure() { // SAFETY: Test-only env mutation. unsafe { set_depth("0") }; @@ -460,6 +476,7 @@ async fn test_pipeline_fail_fast_on_first_failure() { } #[tokio::test] +#[serial] async fn test_pipeline_summary_for_review() { // SAFETY: Test-only env mutation. unsafe { set_depth("0") }; diff --git a/crates/cli-sub-agent/src/review_cmd_tests.rs b/crates/cli-sub-agent/src/review_cmd_tests.rs index 8dbfd003..ad99d6b9 100644 --- a/crates/cli-sub-agent/src/review_cmd_tests.rs +++ b/crates/cli-sub-agent/src/review_cmd_tests.rs @@ -330,15 +330,14 @@ fn resolve_review_tool_unknown_priority_still_uses_auto_heterogeneous_selection( // --- derive_scope tests --- -#[test] -fn derive_scope_uncommitted() { - let args = ReviewArgs { +fn default_review_args() -> ReviewArgs { + ReviewArgs { tool: None, sa_mode: None, session: None, model: None, thinking: None, - diff: true, + diff: false, branch: None, commit: None, range: None, @@ -365,6 +364,18 @@ fn derive_scope_uncommitted() { no_fs_sandbox: false, extra_writable: vec![], prompt_file: None, + daemon: false, + no_daemon: true, + daemon_child: false, + session_id: None, + } +} + +#[test] +fn derive_scope_uncommitted() { + let args = ReviewArgs { + diff: true, + ..default_review_args() }; assert_eq!(derive_scope(&args), "uncommitted"); } @@ -372,38 +383,8 @@ fn derive_scope_uncommitted() { #[test] fn derive_scope_commit() { let args = ReviewArgs { - tool: None, - sa_mode: None, - session: None, - model: None, - thinking: None, - diff: false, - branch: None, commit: Some("abc123".to_string()), - range: None, - files: None, - fix: false, - max_rounds: 3, - review_mode: None, - red_team: false, - security_mode: "auto".to_string(), - context: None, - reviewers: 1, - consensus: "majority".to_string(), - cd: None, - timeout: None, - idle_timeout: None, - initial_response_timeout: None, - stream_stdout: false, - no_stream_stdout: false, - allow_fallback: false, - force_override_user_config: false, - spec: None, - tier: None, - force_ignore_tier_setting: false, - no_fs_sandbox: false, - extra_writable: vec![], - prompt_file: None, + ..default_review_args() }; assert_eq!(derive_scope(&args), "commit:abc123"); } @@ -411,38 +392,8 @@ fn derive_scope_commit() { #[test] fn derive_scope_range() { let args = ReviewArgs { - tool: None, - sa_mode: None, - session: None, - model: None, - thinking: None, - diff: false, - branch: None, - commit: None, range: Some("main...HEAD".to_string()), - files: None, - fix: false, - max_rounds: 3, - review_mode: None, - red_team: false, - security_mode: "auto".to_string(), - context: None, - reviewers: 1, - consensus: "majority".to_string(), - cd: None, - timeout: None, - idle_timeout: None, - initial_response_timeout: None, - stream_stdout: false, - no_stream_stdout: false, - allow_fallback: false, - force_override_user_config: false, - spec: None, - tier: None, - force_ignore_tier_setting: false, - no_fs_sandbox: false, - extra_writable: vec![], - prompt_file: None, + ..default_review_args() }; assert_eq!(derive_scope(&args), "range:main...HEAD"); } @@ -450,38 +401,8 @@ fn derive_scope_range() { #[test] fn derive_scope_files() { let args = ReviewArgs { - tool: None, - sa_mode: None, - session: None, - model: None, - thinking: None, - diff: false, - branch: None, - commit: None, - range: None, files: Some("src/**/*.rs".to_string()), - fix: false, - max_rounds: 3, - review_mode: None, - red_team: false, - security_mode: "auto".to_string(), - context: None, - reviewers: 1, - consensus: "majority".to_string(), - cd: None, - timeout: None, - idle_timeout: None, - initial_response_timeout: None, - stream_stdout: false, - no_stream_stdout: false, - allow_fallback: false, - force_override_user_config: false, - spec: None, - tier: None, - force_ignore_tier_setting: false, - no_fs_sandbox: false, - extra_writable: vec![], - prompt_file: None, + ..default_review_args() }; assert_eq!(derive_scope(&args), "files:src/**/*.rs"); } @@ -489,38 +410,8 @@ fn derive_scope_files() { #[test] fn derive_scope_default_branch() { let args = ReviewArgs { - tool: None, - sa_mode: None, - session: None, - model: None, - thinking: None, - diff: false, branch: Some("develop".to_string()), - commit: None, - range: None, - files: None, - fix: false, - max_rounds: 3, - review_mode: None, - red_team: false, - security_mode: "auto".to_string(), - context: None, - reviewers: 1, - consensus: "majority".to_string(), - cd: None, - timeout: None, - idle_timeout: None, - initial_response_timeout: None, - stream_stdout: false, - no_stream_stdout: false, - allow_fallback: false, - force_override_user_config: false, - spec: None, - tier: None, - force_ignore_tier_setting: false, - no_fs_sandbox: false, - extra_writable: vec![], - prompt_file: None, + ..default_review_args() }; assert_eq!(derive_scope(&args), "base:develop"); } diff --git a/crates/cli-sub-agent/src/run_cmd_daemon.rs b/crates/cli-sub-agent/src/run_cmd_daemon.rs index a32591da..a6a10fb5 100644 --- a/crates/cli-sub-agent/src/run_cmd_daemon.rs +++ b/crates/cli-sub-agent/src/run_cmd_daemon.rs @@ -1,12 +1,34 @@ -//! Daemon spawn logic for `csa run` (daemon mode is the default). -//! -//! Extracted from main.rs to keep the dispatch function under the -//! monolith file limit. +//! Daemon spawn logic for execution commands (daemon mode is the default). +//! Shared by `csa run`, `csa review`, and `csa debate`. use std::io::Write; use anyhow::Result; +/// Check daemon flags and either spawn+exit or propagate session ID. +/// +/// Returns `Ok(())` when the caller should proceed with the child path. +/// **Never returns** when daemon spawn succeeds (calls `process::exit(0)`). +pub(crate) fn check_daemon_flags( + subcommand: &str, + no_daemon: bool, + daemon_child: bool, + session_id: &Option, + cd: Option<&str>, +) -> Result<()> { + if !no_daemon && !daemon_child { + if session_id.is_some() { + anyhow::bail!("--session-id is an internal flag and must not be used directly"); + } + spawn_and_exit(subcommand, cd)?; + } + if let Some(sid) = session_id { + // SAFETY: Runs in the daemon child before tokio spawns worker threads. + unsafe { std::env::set_var("CSA_DAEMON_SESSION_ID", sid) }; + } + Ok(()) +} + /// Fork a daemon child and exit the parent process. /// /// The daemon child will re-exec with `--daemon-child --session-id ` @@ -14,18 +36,18 @@ use anyhow::Result; /// captured in the session directory. /// /// This function **never returns on success** — it calls `process::exit(0)`. -pub(crate) fn spawn_and_exit(cd: Option<&str>) -> Result<()> { +pub(crate) fn spawn_and_exit(subcommand: &str, cd: Option<&str>) -> Result<()> { let sid = csa_session::new_session_id(); let project_root = crate::pipeline::determine_project_root(cd)?; let session_root = csa_session::get_session_root(&project_root)?; let session_dir = session_root.join("sessions").join(&sid); - // Collect args to forward: everything after the 'run' subcommand verb. - // spawn_daemon() injects 'run --daemon-child --session-id ' itself. - // We find "run" by position (not substring) to handle global flags - // that may appear before the subcommand (e.g. `csa --format json run ...`). + // Collect args to forward: everything after the subcommand verb. + // spawn_daemon() injects ' --daemon-child --session-id ' itself. + // We find the subcommand by position (not substring) to handle global flags + // that may appear before the subcommand (e.g. `csa --format json review ...`). let all_args: Vec = std::env::args().collect(); - let run_pos = all_args.iter().position(|a| a == "run").unwrap_or(1); + let run_pos = all_args.iter().position(|a| a == subcommand).unwrap_or(1); let forwarded_args: Vec = all_args .iter() .skip(run_pos + 1) @@ -39,6 +61,7 @@ pub(crate) fn spawn_and_exit(cd: Option<&str>) -> Result<()> { session_id: sid.clone(), session_dir: session_dir.clone(), csa_binary, + subcommand: subcommand.to_string(), args: forwarded_args, env: std::collections::HashMap::new(), }; diff --git a/crates/cli-sub-agent/src/session_cmds_daemon.rs b/crates/cli-sub-agent/src/session_cmds_daemon.rs index 63be4e35..89eb77d9 100644 --- a/crates/cli-sub-agent/src/session_cmds_daemon.rs +++ b/crates/cli-sub-agent/src/session_cmds_daemon.rs @@ -64,7 +64,13 @@ pub(crate) fn handle_session_wait(session: String, cd: Option) -> Result let mut f = std::fs::File::open(&stdout_log)?; std::io::copy(&mut f, &mut std::io::stdout().lock())?; } - return Ok(0); + // Propagate the session's exit code from result.toml. + let exit_code = fs::read_to_string(&result_path) + .ok() + .and_then(|s| toml::from_str::(&s).ok()) + .map(|r| r.exit_code) + .unwrap_or(0); + return Ok(exit_code); } // Detect dead daemon: PID gone but no result.toml. diff --git a/crates/csa-process/src/daemon.rs b/crates/csa-process/src/daemon.rs index 135664f9..e840031e 100644 --- a/crates/csa-process/src/daemon.rs +++ b/crates/csa-process/src/daemon.rs @@ -17,6 +17,8 @@ pub struct DaemonSpawnConfig { pub session_id: String, pub session_dir: PathBuf, pub csa_binary: PathBuf, + /// Subcommand verb for the child process (e.g. "run", "review", "debate"). + pub subcommand: String, pub args: Vec, pub env: HashMap, } @@ -52,7 +54,12 @@ pub fn spawn_daemon(config: DaemonSpawnConfig) -> Result { let stderr_file = open_log_file(&config.session_dir, "stderr.log")?; let mut cmd = Command::new(&config.csa_binary); - cmd.args(["run", "--daemon-child", "--session-id", &config.session_id]); + cmd.args([ + config.subcommand.as_str(), + "--daemon-child", + "--session-id", + &config.session_id, + ]); cmd.args(&config.args); for (k, v) in &config.env { @@ -132,6 +139,7 @@ mod tests { session_id: "TEST001".to_string(), session_dir: session_dir.clone(), csa_binary: wrapper, + subcommand: "run".to_string(), // After the injected flags, pass '--' then the real command. args: vec!["--".to_string(), "echo hello".to_string()], env: HashMap::new(), @@ -170,6 +178,7 @@ mod tests { session_id: "TEST002".to_string(), session_dir: session_dir.clone(), csa_binary: wrapper, + subcommand: "run".to_string(), args: vec![ "--".to_string(), "echo pid=$$ sid=$(ps -o sid= -p $$)".to_string(), diff --git a/patterns/ai-reviewed-commit/PATTERN.md b/patterns/ai-reviewed-commit/PATTERN.md index 17dd6e58..318adecd 100644 --- a/patterns/ai-reviewed-commit/PATTERN.md +++ b/patterns/ai-reviewed-commit/PATTERN.md @@ -44,7 +44,8 @@ Determine who authored the staged code: Tool: bash ```bash -csa debate "Review my staged changes for correctness, security, and test gaps. Run 'git diff --staged' yourself to see the full patch." +SID=$(csa debate "Review my staged changes for correctness, security, and test gaps. Run 'git diff --staged' yourself to see the full patch.") +csa session wait --session "$SID" ``` ## ELSE @@ -54,7 +55,8 @@ csa debate "Review my staged changes for correctness, security, and test gaps. R Tool: bash ```bash -csa review --diff --allow-fallback +SID=$(csa review --diff --allow-fallback) +csa session wait --session "$SID" ``` ## ENDIF @@ -85,7 +87,8 @@ Tool: bash Loop back to review. Maximum 3 review-fix cycles. ```bash -csa review --diff --allow-fallback +SID=$(csa review --diff --allow-fallback) +csa session wait --session "$SID" ``` ## ENDIF diff --git a/patterns/ai-reviewed-commit/workflow.toml b/patterns/ai-reviewed-commit/workflow.toml index b120373c..029a4c5f 100644 --- a/patterns/ai-reviewed-commit/workflow.toml +++ b/patterns/ai-reviewed-commit/workflow.toml @@ -17,6 +17,9 @@ name = "REVIEW_HAS_ISSUES" [[workflow.variables]] name = "SELF_AUTHORED" +[[workflow.variables]] +name = "SID" + [[workflow.steps]] id = 1 title = "Stage Changes" @@ -54,7 +57,8 @@ title = "Step 4a: Run Debate Review" tool = "bash" prompt = """ ```bash -csa debate "Review my staged changes for correctness, security, and test gaps. Run 'git diff --staged' yourself to see the full patch." +SID=$(csa debate "Review my staged changes for correctness, security, and test gaps. Run 'git diff --staged' yourself to see the full patch.") +csa session wait --session "$SID" ```""" on_fail = "abort" condition = "${SELF_AUTHORED}" @@ -65,7 +69,8 @@ title = "Step 4b: Run CSA Review" tool = "bash" prompt = """ ```bash -csa review --diff --allow-fallback +SID=$(csa review --diff --allow-fallback) +csa session wait --session "$SID" ```""" on_fail = "abort" condition = "!(${SELF_AUTHORED})" @@ -102,7 +107,8 @@ prompt = """ Loop back to review. Maximum 3 review-fix cycles. ```bash -csa review --diff --allow-fallback +SID=$(csa review --diff --allow-fallback) +csa session wait --session "$SID" ```""" on_fail = "abort" condition = "${REVIEW_HAS_ISSUES}" diff --git a/patterns/code-review/PATTERN.md b/patterns/code-review/PATTERN.md index 278bb765..4c39cd26 100644 --- a/patterns/code-review/PATTERN.md +++ b/patterns/code-review/PATTERN.md @@ -47,7 +47,8 @@ Do NOT pre-read diff into main agent context. ```bash gh pr checkout ${PR_NUM} -csa review --branch $(gh pr view --json baseRefName -q .baseRefName) +SID=$(csa review --branch $(gh pr view --json baseRefName -q .baseRefName)) +csa session wait --session "$SID" ``` ## ELSE diff --git a/patterns/code-review/workflow.toml b/patterns/code-review/workflow.toml index 47cbd00c..7239044d 100644 --- a/patterns/code-review/workflow.toml +++ b/patterns/code-review/workflow.toml @@ -11,6 +11,9 @@ name = "PR_NUM" [[workflow.variables]] name = "REVIEW_BODY" +[[workflow.variables]] +name = "SID" + [[workflow.variables]] name = "USER_REQUESTS_POSTING" @@ -56,7 +59,8 @@ Do NOT pre-read diff into main agent context. ```bash gh pr checkout ${PR_NUM} -csa review --branch $(gh pr view --json baseRefName -q .baseRefName) +SID=$(csa review --branch $(gh pr view --json baseRefName -q .baseRefName)) +csa session wait --session "$SID" ```""" on_fail = "abort" condition = "${PR_IS_LARGE}" diff --git a/patterns/commit/PATTERN.md b/patterns/commit/PATTERN.md index a5903cc0..add94842 100644 --- a/patterns/commit/PATTERN.md +++ b/patterns/commit/PATTERN.md @@ -330,7 +330,8 @@ Perform a cumulative review of the entire feature branch before pushing. This catches cross-commit issues that per-commit reviews might miss. ```bash -csa review --range main...HEAD +SID=$(csa review --range main...HEAD) +csa session wait --session "$SID" ``` ## Step 19: Auto PR Transaction diff --git a/patterns/commit/workflow.toml b/patterns/commit/workflow.toml index 3f97ee1c..abdaf46c 100644 --- a/patterns/commit/workflow.toml +++ b/patterns/commit/workflow.toml @@ -74,6 +74,9 @@ name = "SKIP_PUBLISH" [[workflow.variables]] name = "SCOPE" +[[workflow.variables]] +name = "SID" + [[workflow.variables]] name = "SPEC_OUTPUT" @@ -411,7 +414,8 @@ Perform a cumulative review of the entire feature branch before pushing. This catches cross-commit issues that per-commit reviews might miss. ```bash -csa review --range main...HEAD +SID=$(csa review --range main...HEAD) +csa session wait --session "$SID" ```""" tier = "tier-2-standard" on_fail = "abort" diff --git a/patterns/debate/PATTERN.md b/patterns/debate/PATTERN.md index 15944c1e..9931039e 100644 --- a/patterns/debate/PATTERN.md +++ b/patterns/debate/PATTERN.md @@ -56,7 +56,8 @@ the research context into the debate prompt: # Gather context via forked session, then feed into debate SID=$(csa run --fork-from "Summarize findings for debate context") csa session wait --session "$SID" -csa debate "question (with research context above)" +DEBATE_SID=$(csa debate "question (with research context above)") +csa session wait --session "$DEBATE_SID" ``` **Benefits**: Debaters inherit the research session's context (files read, diff --git a/patterns/dev2merge/PATTERN.md b/patterns/dev2merge/PATTERN.md index 66067881..f4065bf3 100644 --- a/patterns/dev2merge/PATTERN.md +++ b/patterns/dev2merge/PATTERN.md @@ -121,7 +121,8 @@ Even FAST_PATH runs cumulative review before push. ```bash set -euo pipefail -REVIEW_OUTPUT="$(csa review --sa-mode true --timeout 1800 --range "${DEFAULT_BRANCH}...HEAD" 2>&1)" || true +SID=$(csa review --sa-mode true --range "${DEFAULT_BRANCH}...HEAD") || true +REVIEW_OUTPUT="$(csa session wait --session "$SID" 2>&1)" || true printf '%s\n' "${REVIEW_OUTPUT}" echo "CSA_VAR:REVIEW_COMPLETED=true" ``` @@ -204,7 +205,8 @@ Sets REVIEW_COMPLETED=true as gate for push step. set -euo pipefail REVIEW_OUTPUT_FILE="$(mktemp)" set +e -csa review --sa-mode true --timeout 1800 --range "${DEFAULT_BRANCH}...HEAD" 2>&1 | tee "${REVIEW_OUTPUT_FILE}" +SID=$(csa review --sa-mode true --range "${DEFAULT_BRANCH}...HEAD") +csa session wait --session "$SID" 2>&1 | tee "${REVIEW_OUTPUT_FILE}" REVIEW_STATUS=${PIPESTATUS[0]} set -e REVIEW_OUTPUT="$(cat "${REVIEW_OUTPUT_FILE}")" diff --git a/patterns/dev2merge/workflow.toml b/patterns/dev2merge/workflow.toml index 4800acf3..51b01761 100644 --- a/patterns/dev2merge/workflow.toml +++ b/patterns/dev2merge/workflow.toml @@ -68,6 +68,9 @@ name = "REMOTE_SHA" [[workflow.variables]] name = "REVIEW_OUTPUT" +[[workflow.variables]] +name = "SID" + [[workflow.variables]] name = "REVIEW_OUTPUT_FILE" @@ -198,7 +201,8 @@ Even FAST_PATH runs cumulative review before push. ```bash set -euo pipefail -REVIEW_OUTPUT="$(csa review --sa-mode true --timeout 1800 --range "${DEFAULT_BRANCH}...HEAD" 2>&1)" || true +SID=$(csa review --sa-mode true --range "${DEFAULT_BRANCH}...HEAD") || true +REVIEW_OUTPUT="$(csa session wait --session "$SID" 2>&1)" || true printf '%s\n' "${REVIEW_OUTPUT}" echo "CSA_VAR:REVIEW_COMPLETED=true" ```''' @@ -288,7 +292,8 @@ Sets REVIEW_COMPLETED=true as gate for push step. set -euo pipefail REVIEW_OUTPUT_FILE="$(mktemp)" set +e -csa review --sa-mode true --timeout 1800 --range "${DEFAULT_BRANCH}...HEAD" 2>&1 | tee "${REVIEW_OUTPUT_FILE}" +SID=$(csa review --sa-mode true --range "${DEFAULT_BRANCH}...HEAD") +csa session wait --session "$SID" 2>&1 | tee "${REVIEW_OUTPUT_FILE}" REVIEW_STATUS=${PIPESTATUS[0]} set -e REVIEW_OUTPUT="$(cat "${REVIEW_OUTPUT_FILE}")" diff --git a/patterns/mktd/PATTERN.md b/patterns/mktd/PATTERN.md index f65b34c0..bc9846e5 100644 --- a/patterns/mktd/PATTERN.md +++ b/patterns/mktd/PATTERN.md @@ -326,7 +326,8 @@ DEBATE_PROMPT="$(printf '%s\n' \ "" \ "## Output Requirements" \ "Provide explicit verdict and confidence in your conclusion." )" -DEBATE_JSON="$(printf '%s\n' "${DEBATE_PROMPT}" | csa debate --rounds 3 --format json --timeout 240 --idle-timeout 120 --no-stream-stdout)" || { echo "csa debate failed" >&2; exit 1; } +SID=$(printf '%s\n' "${DEBATE_PROMPT}" | csa debate --rounds 3 --format json --idle-timeout 120 --no-stream-stdout) || { echo "csa debate failed" >&2; exit 1; } +DEBATE_JSON="$(csa session wait --session "$SID" 2>&1)" || { echo "csa debate failed" >&2; exit 1; } [[ -n "${DEBATE_JSON:-}" ]] || { echo "empty debate json output" >&2; exit 1; } RAW_VERDICT="$(printf '%s\n' "${DEBATE_JSON}" | jq -r '.verdict // "UNKNOWN"' | tr '[:lower:]' '[:upper:]')" case "${RAW_VERDICT}" in diff --git a/patterns/mktd/workflow.toml b/patterns/mktd/workflow.toml index 9650f684..60b38734 100644 --- a/patterns/mktd/workflow.toml +++ b/patterns/mktd/workflow.toml @@ -41,6 +41,9 @@ name = "RAW_VERDICT" [[workflow.variables]] name = "RESOLVED_LANGUAGE" +[[workflow.variables]] +name = "SID" + [[workflow.variables]] name = "SPEC_CONTENT" @@ -375,7 +378,8 @@ DEBATE_PROMPT="$(printf '%s\n' \ "" \ "## Output Requirements" \ "Provide explicit verdict and confidence in your conclusion." )" -DEBATE_JSON="$(printf '%s\n' "${DEBATE_PROMPT}" | csa debate --rounds 3 --format json --timeout 1800 --idle-timeout 600 --no-stream-stdout)" || { echo "csa debate failed" >&2; exit 1; } +SID=$(printf '%s\n' "${DEBATE_PROMPT}" | csa debate --rounds 3 --format json --idle-timeout 600 --no-stream-stdout) || { echo "csa debate failed" >&2; exit 1; } +DEBATE_JSON="$(csa session wait --session "$SID" 2>&1)" || { echo "csa debate failed" >&2; exit 1; } [[ -n "${DEBATE_JSON:-}" ]] || { echo "empty debate json output" >&2; exit 1; } RAW_VERDICT="$(printf '%s\n' "${DEBATE_JSON}" | jq -r '.verdict // "UNKNOWN"' | tr '[:lower:]' '[:upper:]')" case "${RAW_VERDICT}" in diff --git a/patterns/mktsk/workflow.toml b/patterns/mktsk/workflow.toml index cf24aafd..3ab09ab8 100644 --- a/patterns/mktsk/workflow.toml +++ b/patterns/mktsk/workflow.toml @@ -5,6 +5,9 @@ description = "Execute TODO plans as deterministic, resumable serial checklists [[workflow.variables]] name = "CONTEXT_ABOVE_80_PERCENT" +[[workflow.variables]] +name = "SID" + [[workflow.steps]] id = 1 title = "Parse TODO Plan" @@ -58,7 +61,7 @@ After each item: 1) just fmt 2) just clippy 3) just test -4) csa review --diff +4) SID=$(csa review --diff) && csa session wait --session "$SID" 5) mark TODO checkbox as completed ## Execution Checklist @@ -129,7 +132,8 @@ if command -v just >/dev/null 2>&1; then fi # Pre-PR cumulative review (hard gate — must pass before push) -if ! csa review --sa-mode true --range main...HEAD; then +SID=$(csa review --sa-mode true --range main...HEAD) +if ! csa session wait --session "$SID"; then echo "ERROR: Cumulative review failed. Fix issues before publishing." >&2 exit 1 fi diff --git a/patterns/pr-bot/PATTERN.md b/patterns/pr-bot/PATTERN.md index 6b5b36d9..07fe57da 100644 --- a/patterns/pr-bot/PATTERN.md +++ b/patterns/pr-bot/PATTERN.md @@ -80,7 +80,8 @@ REVIEW_HEAD="$(csa session list --recent-review 2>/dev/null | parse_head_sha || if [ -n "${REVIEW_HEAD}" ] && [ "${CURRENT_HEAD}" = "${REVIEW_HEAD}" ]; then echo "Fast-path: local review already covers current HEAD." else - csa review --branch main + SID=$(csa review --branch main) + csa session wait --session "$SID" fi REVIEW_COMPLETED=true echo "CSA_VAR:REVIEW_COMPLETED=$REVIEW_COMPLETED" @@ -265,7 +266,8 @@ if [ "${CLOUD_BOT}" = "false" ]; then echo "Cloud bot disabled, fast-path active: local review already covers HEAD ${CURRENT_HEAD}." else echo "Cloud bot disabled and fast-path invalid. Running full local review." - csa review --branch main + SID=$(csa review --branch main) + csa session wait --session "$SID" fi fi BOT_UNAVAILABLE="${BOT_UNAVAILABLE:-false}" diff --git a/patterns/pr-bot/workflow.toml b/patterns/pr-bot/workflow.toml index 80b75aa1..30c538e5 100644 --- a/patterns/pr-bot/workflow.toml +++ b/patterns/pr-bot/workflow.toml @@ -68,6 +68,9 @@ name = "PR_BODY" [[workflow.variables]] name = "REVIEW_COMPLETED" +[[workflow.variables]] +name = "SID" + [[workflow.variables]] name = "PR_NUM" @@ -127,7 +130,8 @@ if [ -n "${REVIEW_HEAD}" ] && [ "${CURRENT_HEAD}" = "${REVIEW_HEAD}" ]; then echo "Fast-path: latest local review already covers HEAD ${CURRENT_HEAD}; skip Step 2 review." else echo "No matching review HEAD found (or HEAD drift detected). Running full local review." - csa review --branch main + SID=$(csa review --branch main) + csa session wait --session "$SID" fi REVIEW_COMPLETED=true echo "CSA_VAR:REVIEW_COMPLETED=$REVIEW_COMPLETED" @@ -297,7 +301,8 @@ if [ "${CLOUD_BOT}" = "false" ]; then echo "Cloud bot disabled, fast-path active: local review already covers HEAD ${CURRENT_HEAD}." else echo "Cloud bot disabled and fast-path invalid. Running full local review." - csa review --branch main + SID=$(csa review --branch main) + csa session wait --session "$SID" fi fi BOT_UNAVAILABLE="${BOT_UNAVAILABLE:-false}" @@ -1148,7 +1153,8 @@ else echo "WARN: No positive signal (review event or inline comments) for HEAD ${CURRENT_SHA} after ${RETRIGGER_TS}." >&2 _check_setup_message echo "Falling back to local review." >&2 - if ! csa review --sa-mode true --range main...HEAD --timeout 1800; then + SID=$(csa review --sa-mode true --range main...HEAD) + if ! csa session wait --session "$SID"; then echo "ERROR: Local fallback review found issues after fix. Cannot merge." >&2 exit 1 fi @@ -1157,7 +1163,8 @@ else BOT_CLEAN=true else echo "WARN: Post-fix bot wait returned timeout/no-marker. Falling back to local review." - if ! csa review --sa-mode true --range main...HEAD --timeout 1800; then + SID=$(csa review --sa-mode true --range main...HEAD) + if ! csa session wait --session "$SID"; then echo "ERROR: Local fallback review found issues after fix. Cannot merge." >&2 exit 1 fi diff --git a/patterns/review-loop/PATTERN.md b/patterns/review-loop/PATTERN.md index 451254d6..58754976 100644 --- a/patterns/review-loop/PATTERN.md +++ b/patterns/review-loop/PATTERN.md @@ -36,7 +36,8 @@ OnFail: skip Run heterogeneous code review on current diff. ```bash -csa review --diff +SID=$(csa review --diff) +csa session wait --session "$SID" ``` Parse the output to determine if issues were found. diff --git a/patterns/review-loop/workflow.toml b/patterns/review-loop/workflow.toml index 90da3b47..c7ec29a0 100644 --- a/patterns/review-loop/workflow.toml +++ b/patterns/review-loop/workflow.toml @@ -11,6 +11,9 @@ name = "REMAINING_ISSUES" [[workflow.variables]] name = "REVIEW_HAS_ISSUES" +[[workflow.variables]] +name = "SID" + [[workflow.variables]] name = "ROUND" @@ -41,7 +44,8 @@ prompt = """ Run heterogeneous code review on current diff. ```bash -csa review --diff +SID=$(csa review --diff) +csa session wait --session "$SID" ``` Parse the output to determine if issues were found. diff --git a/weave.lock b/weave.lock index df910d19..523c9a45 100644 --- a/weave.lock +++ b/weave.lock @@ -1,9 +1,9 @@ package = [] [versions] -csa = "0.1.197" +csa = "0.1.199" last_migrated_at = "2026-03-08T12:08:01.820964091Z" -weave = "0.1.197" +weave = "0.1.199" [migrations] applied = [