From 803d95a866df2e15080230ef7147e8f4e4a13dd6 Mon Sep 17 00:00:00 2001 From: Ryder Freeman Date: Thu, 26 Feb 2026 22:34:03 -0800 Subject: [PATCH 1/4] fix(cli): inject anti-recursion guard into review and debate prompts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When claude-code-acp is used as the review/debate tool, it reads CLAUDE.md/AGENTS.md rules containing "MUST use csa review" directives, causing it to invoke csa commands inside Bash — creating an infinite recursion loop that ends in SIGTERM (exit 143). Fix: prepend an explicit anti-recursion instruction to the prompt telling the tool it is running inside a CSA subprocess and must NOT invoke any csa commands. This matches the role detection pattern already documented in the csa-review and debate SKILL.md files. Closes #272 Co-Authored-By: Claude Opus 4.6 --- crates/cli-sub-agent/src/debate_cmd.rs | 8 ++++++-- crates/cli-sub-agent/src/review_cmd.rs | 12 ++++++++++-- 2 files changed, 16 insertions(+), 4 deletions(-) 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}")); } From a4ad2838f6dfeb71f27e9e7b293eadd69c01d2b6 Mon Sep 17 00:00:00 2001 From: Ryder Freeman Date: Thu, 26 Feb 2026 22:35:13 -0800 Subject: [PATCH 2/4] fix(acp): include captured stderr in ProcessExited error When an ACP subprocess crashes before initialization, the ProcessExited error only reported the exit code with zero context. The stderr pipe contents (containing crash details like Node.js EPIPE stack traces) were silently lost. Change ProcessExited from a tuple variant to a struct with both code and stderr fields. The ensure_process_running() method now captures the last 10 lines of stderr and includes them in the error message for post-mortem diagnosis. Closes #273 Co-Authored-By: Claude Opus 4.6 --- crates/csa-acp/src/connection.rs | 4 +++- crates/csa-acp/src/error.rs | 14 ++++++++++++-- 2 files changed, 15 insertions(+), 3 deletions(-) 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}")] From c4801fa53d7139ca14addf0355c58d31cb0bbe62 Mon Sep 17 00:00:00 2001 From: Ryder Freeman Date: Thu, 26 Feb 2026 22:35:53 -0800 Subject: [PATCH 3/4] fix(patterns): enforce review-before-PR ordering in pr-codex-bot Add condition guard to workflow.toml Step 4 (Push and Create PR) that blocks PR creation when LOCAL_REVIEW_HAS_ISSUES is set. This prevents an orchestrator from creating a PR before the local review passes. Also add explicit precondition documentation to PATTERN.md Step 4 explaining the two-layer review architecture dependency. Closes #274 Co-Authored-By: Claude Opus 4.6 --- patterns/pr-codex-bot/PATTERN.md | 6 ++++++ patterns/pr-codex-bot/workflow.toml | 1 + 2 files changed, 7 insertions(+) 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}" From 59121eaf2cdbb80cc8fb131da0f8c51b7619a6da Mon Sep 17 00:00:00 2001 From: Ryder Freeman Date: Thu, 26 Feb 2026 22:36:10 -0800 Subject: [PATCH 4/4] chore: bump version to 0.1.44 Co-Authored-By: Claude Opus 4.6 --- Cargo.lock | 30 +++++++++++++++--------------- Cargo.toml | 2 +- 2 files changed, 16 insertions(+), 16 deletions(-) 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"