diff --git a/Cargo.lock b/Cargo.lock index 63ce8085..325b9a0a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -447,7 +447,7 @@ checksum = "c3e64b0cc0439b12df2fa678eae89a1c56a529fd067a9115f7827f1fffd22b32" [[package]] name = "cli-sub-agent" -version = "0.1.43" +version = "0.1.44" dependencies = [ "anyhow", "chrono", @@ -604,7 +604,7 @@ dependencies = [ [[package]] name = "csa-acp" -version = "0.1.43" +version = "0.1.44" dependencies = [ "agent-client-protocol", "anyhow", @@ -623,7 +623,7 @@ dependencies = [ [[package]] name = "csa-config" -version = "0.1.43" +version = "0.1.44" dependencies = [ "anyhow", "chrono", @@ -638,7 +638,7 @@ dependencies = [ [[package]] name = "csa-core" -version = "0.1.43" +version = "0.1.44" dependencies = [ "agent-teams", "chrono", @@ -652,7 +652,7 @@ dependencies = [ [[package]] name = "csa-executor" -version = "0.1.43" +version = "0.1.44" dependencies = [ "agent-teams", "anyhow", @@ -676,7 +676,7 @@ dependencies = [ [[package]] name = "csa-hooks" -version = "0.1.43" +version = "0.1.44" dependencies = [ "anyhow", "chrono", @@ -691,7 +691,7 @@ dependencies = [ [[package]] name = "csa-lock" -version = "0.1.43" +version = "0.1.44" dependencies = [ "anyhow", "chrono", @@ -703,7 +703,7 @@ dependencies = [ [[package]] name = "csa-mcp-hub" -version = "0.1.43" +version = "0.1.44" dependencies = [ "anyhow", "axum", @@ -725,7 +725,7 @@ dependencies = [ [[package]] name = "csa-memory" -version = "0.1.43" +version = "0.1.44" dependencies = [ "anyhow", "async-trait", @@ -743,7 +743,7 @@ dependencies = [ [[package]] name = "csa-process" -version = "0.1.43" +version = "0.1.44" dependencies = [ "anyhow", "csa-core", @@ -760,7 +760,7 @@ dependencies = [ [[package]] name = "csa-resource" -version = "0.1.43" +version = "0.1.44" dependencies = [ "anyhow", "csa-core", @@ -775,7 +775,7 @@ dependencies = [ [[package]] name = "csa-scheduler" -version = "0.1.43" +version = "0.1.44" dependencies = [ "anyhow", "chrono", @@ -793,7 +793,7 @@ dependencies = [ [[package]] name = "csa-session" -version = "0.1.43" +version = "0.1.44" dependencies = [ "anyhow", "chrono", @@ -814,7 +814,7 @@ dependencies = [ [[package]] name = "csa-todo" -version = "0.1.43" +version = "0.1.44" dependencies = [ "anyhow", "chrono", @@ -3902,7 +3902,7 @@ dependencies = [ [[package]] name = "weave" -version = "0.1.43" +version = "0.1.44" dependencies = [ "anyhow", "clap", diff --git a/Cargo.toml b/Cargo.toml index f43bd21c..77fa0dd0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ members = ["crates/*"] resolver = "2" [workspace.package] -version = "0.1.43" +version = "0.1.44" edition = "2024" rust-version = "1.85" license = "Apache-2.0" diff --git a/crates/cli-sub-agent/src/debate_cmd.rs b/crates/cli-sub-agent/src/debate_cmd.rs index 8825847c..087d8242 100644 --- a/crates/cli-sub-agent/src/debate_cmd.rs +++ b/crates/cli-sub-agent/src/debate_cmd.rs @@ -893,10 +893,14 @@ async fn wait_for_still_working_backoff() { /// The debate tool loads the debate skill from the project's `.claude/skills/` /// directory and follows its instructions autonomously. We only pass parameters. fn build_debate_instruction(question: &str, is_continuation: bool, rounds: u32) -> String { + // Anti-recursion guard (see GitHub issue #272). + let prefix = "CRITICAL: You are running INSIDE a CSA subprocess as the debate agent. \ + Do NOT run `csa run`, `csa review`, `csa debate`, or ANY `csa` command — \ + this would cause infinite recursion. Read files and run git commands directly.\n\n"; if is_continuation { - format!("Use the debate skill. continuation=true. rounds={rounds}. question={question}") + format!("{prefix}Use the debate skill. continuation=true. rounds={rounds}. question={question}") } else { - format!("Use the debate skill. rounds={rounds}. question={question}") + format!("{prefix}Use the debate skill. rounds={rounds}. question={question}") } } diff --git a/crates/cli-sub-agent/src/review_cmd.rs b/crates/cli-sub-agent/src/review_cmd.rs index 1d8d641e..495109ad 100644 --- a/crates/cli-sub-agent/src/review_cmd.rs +++ b/crates/cli-sub-agent/src/review_cmd.rs @@ -636,9 +636,17 @@ fn build_review_instruction( security_mode: &str, context: Option<&str>, ) -> String { - let mut instruction = format!( - "Use the csa-review skill. scope={scope}, mode={mode}, security_mode={security_mode}." + // Anti-recursion guard: tell the review tool it is running INSIDE a CSA + // subprocess so it must NOT invoke `csa run/review/debate` (which would + // cause infinite recursion — see GitHub issue #272). + let mut instruction = String::from( + "CRITICAL: You are running INSIDE a CSA subprocess as the review agent. \ + Do NOT run `csa run`, `csa review`, `csa debate`, or ANY `csa` command — \ + this would cause infinite recursion. Read files and run git commands directly.\n\n", ); + instruction.push_str(&format!( + "Use the csa-review skill. scope={scope}, mode={mode}, security_mode={security_mode}." + )); if let Some(ctx) = context { instruction.push_str(&format!(" context={ctx}")); } diff --git a/crates/csa-acp/src/connection.rs b/crates/csa-acp/src/connection.rs index 6a4f6072..04b1a3a3 100644 --- a/crates/csa-acp/src/connection.rs +++ b/crates/csa-acp/src/connection.rs @@ -391,7 +391,9 @@ impl AcpConnection { .try_wait() .map_err(|err| AcpError::ConnectionFailed(err.to_string()))? { - return Err(AcpError::ProcessExited(status.code().unwrap_or(-1))); + let code = status.code().unwrap_or(-1); + let stderr = self.stderr(); + return Err(AcpError::ProcessExited { code, stderr }); } Ok(()) } diff --git a/crates/csa-acp/src/error.rs b/crates/csa-acp/src/error.rs index f767dd1e..a2d516fa 100644 --- a/crates/csa-acp/src/error.rs +++ b/crates/csa-acp/src/error.rs @@ -1,5 +1,15 @@ use thiserror::Error; +fn format_stderr(stderr: &str) -> String { + if stderr.is_empty() { + String::new() + } else { + let last_lines: Vec<&str> = stderr.lines().rev().take(10).collect(); + let summary: String = last_lines.into_iter().rev().collect::>().join("\n"); + format!("; stderr:\n{summary}") + } +} + #[derive(Error, Debug)] pub enum AcpError { #[error("ACP connection failed: {0}")] @@ -10,8 +20,8 @@ pub enum AcpError { SessionFailed(String), #[error("ACP prompt failed: {0}")] PromptFailed(String), - #[error("ACP process exited unexpectedly: code {0}")] - ProcessExited(i32), + #[error("ACP process exited unexpectedly: code {code}{}", format_stderr(.stderr))] + ProcessExited { code: i32, stderr: String }, #[error("Session fork failed: {0}")] ForkFailed(String), #[error("ACP subprocess spawn failed: {0}")] diff --git a/patterns/pr-codex-bot/PATTERN.md b/patterns/pr-codex-bot/PATTERN.md index 1fa6edf9..06c8873e 100644 --- a/patterns/pr-codex-bot/PATTERN.md +++ b/patterns/pr-codex-bot/PATTERN.md @@ -89,6 +89,12 @@ Fix issues found by local review. Loop until clean (max 3 rounds). > **Layer**: 0 (Orchestrator) -- shell commands only, no code reading/writing. +**Precondition**: Step 2 (Local Pre-PR Review) MUST have completed successfully. +If `LOCAL_REVIEW_HAS_ISSUES` is set, this step is blocked until Step 3 resolves +all issues. The orchestrator MUST NOT create a PR with unresolved local review +issues — this enforces the two-layer review architecture. + +Condition: `!(${LOCAL_REVIEW_HAS_ISSUES})` Tool: bash OnFail: abort diff --git a/patterns/pr-codex-bot/workflow.toml b/patterns/pr-codex-bot/workflow.toml index e6a7fd2b..d215adda 100644 --- a/patterns/pr-codex-bot/workflow.toml +++ b/patterns/pr-codex-bot/workflow.toml @@ -111,6 +111,7 @@ retry = 3 id = 4 title = "Push and Create PR" tool = "bash" +condition = "!(${LOCAL_REVIEW_HAS_ISSUES})" prompt = """ ```bash git push -u origin "${WORKFLOW_BRANCH}"