From 8383ae15ffd35cc9e91fd06c4e5aa4c864af31aa Mon Sep 17 00:00:00 2001 From: Axel Delafosse Date: Tue, 24 Mar 2026 16:03:04 -0700 Subject: [PATCH 1/8] Clarify Claude reply routing in paired mode --- src/loop/bridge.ts | 1 + src/loop/paired-loop.ts | 6 ++++-- src/loop/tmux.ts | 18 ++++++++++++++---- tests/loop/bridge.test.ts | 3 +++ tests/loop/paired-loop.test.ts | 8 ++++++++ tests/loop/tmux.test.ts | 13 ++++++++++++- 6 files changed, 42 insertions(+), 7 deletions(-) diff --git a/src/loop/bridge.ts b/src/loop/bridge.ts index 2be31dc..79e98bd 100644 --- a/src/loop/bridge.ts +++ b/src/loop/bridge.ts @@ -362,6 +362,7 @@ const claudeChannelInstructions = (): string => [ `Messages from the Codex agent arrive as .`, 'When you are replying to an inbound channel message, use the "reply" tool and pass back the same chat_id.', + "Never answer the human when the inbound message came from Codex. Send the response back through the bridge tools instead.", 'Use the "send_to_agent" tool for proactive messages to Codex that are not direct replies to a channel message.', 'Use "bridge_status" only when direct delivery appears stuck.', ].join("\n"); diff --git a/src/loop/paired-loop.ts b/src/loop/paired-loop.ts index 3fc2323..54405e0 100644 --- a/src/loop/paired-loop.ts +++ b/src/loop/paired-loop.ts @@ -94,7 +94,7 @@ const bridgeGuidance = (agent: Agent): string => { return [ "Paired mode:", `You are in a persistent Claude/Codex pair. Use the MCP tool "send_to_agent" when you want ${peer} to act, review, or answer.`, - 'Do not ask the human to relay messages between agents. Use "bridge_status" if you need the current bridge state.', + 'Do not ask the human to relay messages between agents or answer the human on the other agent\'s behalf. Use "bridge_status" if you need the current bridge state.', 'If "bridge_status" shows pending messages addressed to you, call "receive_messages" to read them.', ].join("\n"); }; @@ -148,7 +148,9 @@ const forwardBridgePrompt = (source: Agent, message: string): string => [ `Message from ${capitalize(source)} via the loop bridge:`, message.trim(), - "Treat this as direct agent-to-agent coordination. Reply with send_to_agent only when you have something useful for the other agent to act on. Do not acknowledge receipt without new information.", + "Treat this as direct agent-to-agent coordination. Do not reply to the human.", + 'Reply to the other agent with "send_to_agent" only when you have something useful for them to act on.', + "Do not acknowledge receipt without new information.", ].join("\n\n"); const updateIds = (state: PairedState): void => { diff --git a/src/loop/tmux.ts b/src/loop/tmux.ts index 64e959e..720b45e 100644 --- a/src/loop/tmux.ts +++ b/src/loop/tmux.ts @@ -123,10 +123,20 @@ const appendProofPrompt = (parts: string[], proof: string): void => { }; const pairedBridgeGuidance = (agent: Agent): string => { - const peer = agent === "claude" ? "Codex" : "Claude"; + if (agent === "claude") { + return [ + "Paired mode:", + "You are in a persistent Claude/Codex pair.", + 'When Codex sends you an inbound channel message, reply to Codex with the MCP tool "reply" and reuse the same chat_id.', + 'Use "send_to_agent" only when you need to start a new proactive message to Codex.', + "Do not ask the human to relay messages between agents. Do not answer the human when Codex is waiting for the response.", + 'Use "bridge_status" only for diagnostics if direct delivery seems stuck. Use "receive_messages" only as a manual fallback.', + ].join("\n"); + } + return [ "Paired mode:", - `You are in a persistent Claude/Codex pair. Use the MCP tool "send_to_agent" when you want ${peer} to act, review, or answer.`, + 'You are in a persistent Claude/Codex pair. Use the MCP tool "send_to_agent" when you want Claude to act, review, or answer.', "Do not ask the human to relay messages between agents. Normal paired messages should arrive directly.", 'Use "bridge_status" only for diagnostics if direct delivery seems stuck. Use "receive_messages" only as a manual fallback.', ].join("\n"); @@ -215,13 +225,13 @@ const buildInteractivePeerPrompt = (opts: Options, agent: Agent): string => { const parts = [ `Paired tmux mode. ${primary} is the primary agent for this run.`, "No task has been assigned yet.", - `You are ${capitalize(agent)}. Your reviewer/support role is active, but do not start implementing or verifying anything until ${primary} or the human gives you a specific request.`, + `You are ${capitalize(agent)}. Your reviewer/support role is active, but do not start implementing or verifying anything until ${primary} sends a specific request or the human clearly assigns you separate work.`, ]; appendProofPrompt(parts, opts.proof); parts.push(pairedBridgeGuidance(agent)); parts.push(pairedWorkflowGuidance(opts, agent)); parts.push( - `Your first action is to use "send_to_agent" to tell ${primary}: "Reviewer ready. No task yet. I am waiting for your request." After that, wait for the human or ${primary} to provide a concrete task or review request.` + `Your first action is to use "send_to_agent" to tell ${primary}: "Reviewer ready. No task yet. I am waiting for your request." After that, wait for ${primary} to provide a concrete task or review request. If the human clearly assigns you separate work in this pane, treat that as a new task. If you are answering ${primary}, use the bridge tools instead of a human-facing reply.` ); return parts.join("\n\n"); }; diff --git a/tests/loop/bridge.test.ts b/tests/loop/bridge.test.ts index 22475a8..b8f0803 100644 --- a/tests/loop/bridge.test.ts +++ b/tests/loop/bridge.test.ts @@ -308,6 +308,9 @@ test("bridge MCP handles standard empty-list and ping requests through the CLI p expect(result.stdout).toContain( '\\"reply\\" tool and pass back the same chat_id' ); + expect(result.stdout).toContain( + "Never answer the human when the inbound message came from Codex" + ); expect(result.stdout).toContain('"id":2'); expect(result.stdout).toContain('"result":{}'); expect(result.stdout).toContain('"id":3'); diff --git a/tests/loop/paired-loop.test.ts b/tests/loop/paired-loop.test.ts index a5a0627..8e0a7c4 100644 --- a/tests/loop/paired-loop.test.ts +++ b/tests/loop/paired-loop.test.ts @@ -607,6 +607,10 @@ test("runPairedLoop delivers forwarded bridge messages to the target agent", asy "Message from Codex via the loop bridge:" ); expect(calls[0]?.prompt).toContain("Please review the Codex output."); + expect(calls[0]?.prompt).toContain("Do not reply to the human."); + expect(calls[0]?.prompt).toContain( + 'Reply to the other agent with "send_to_agent"' + ); const events = bridgeInternals.readBridgeEvents(runDir); expect(events.some((event) => event.kind === "delivered")).toBe(true); @@ -773,6 +777,7 @@ test("runPairedLoop delivers peer messages back to the primary agent", async () expect(calls[1]?.prompt).toContain( "Please verify the implementation details." ); + expect(calls[1]?.prompt).toContain("Do not reply to the human."); expect(calls[2]?.agent).toBe("claude"); expect(calls[2]?.prompt).toContain( "Message from Codex via the loop bridge:" @@ -780,6 +785,9 @@ test("runPairedLoop delivers peer messages back to the primary agent", async () expect(calls[2]?.prompt).toContain( "Found one change to make before landing this." ); + expect(calls[2]?.prompt).toContain( + 'Reply to the other agent with "send_to_agent"' + ); }); }); diff --git a/tests/loop/tmux.test.ts b/tests/loop/tmux.test.ts index 90b3850..a5f1465 100644 --- a/tests/loop/tmux.test.ts +++ b/tests/loop/tmux.test.ts @@ -495,6 +495,10 @@ test("tmux prompts keep the paired review workflow explicit", () => { expect(peerPrompt).toContain("You are the reviewer/support agent."); expect(peerPrompt).toContain("Do not take over the task or create the PR"); expect(peerPrompt).toContain("Reviewer ready."); + expect(peerPrompt).toContain('"reply"'); + expect(peerPrompt).toContain( + "Do not answer the human when Codex is waiting for the response." + ); }); test("interactive tmux prompts tell both agents to wait for the human", () => { @@ -509,7 +513,14 @@ test("interactive tmux prompts tell both agents to wait for the human", () => { expect(primaryPrompt).toContain("worktree isolation"); expect(peerPrompt).toContain("No task has been assigned yet."); expect(peerPrompt).toContain("Reviewer ready. No task yet."); - expect(peerPrompt).toContain("wait for the human"); + expect(peerPrompt).toContain("human clearly assigns you separate work"); + expect(peerPrompt).toContain('"reply"'); + expect(peerPrompt).toContain( + "Do not answer the human when Codex is waiting for the response." + ); + expect(peerPrompt).toContain( + "If you are answering Codex, use the bridge tools instead of a human-facing reply." + ); }); test("runInTmux auto-confirms Claude startup prompts in paired mode", async () => { From 37fbb0e8784488feba9c2319d3384182ce148d1f Mon Sep 17 00:00:00 2001 From: Axel Delafosse Date: Tue, 24 Mar 2026 20:27:34 -0700 Subject: [PATCH 2/8] Pass Claude tmux startup prompt at launch --- src/loop/tmux.ts | 9 ++------- tests/loop/tmux.test.ts | 44 +++++++++++++++++++++++++++++++---------- 2 files changed, 36 insertions(+), 17 deletions(-) diff --git a/src/loop/tmux.ts b/src/loop/tmux.ts index 720b45e..103b36f 100644 --- a/src/loop/tmux.ts +++ b/src/loop/tmux.ts @@ -824,7 +824,8 @@ const startPairedSession = async ( claudeSessionId, resolveTmuxModel("claude", launch.opts), claudeChannelServer, - hadClaudeSession + hadClaudeSession, + hadClaudeSession ? undefined : claudePrompt ), ]); const codexCommand = buildShellCommand([ @@ -880,18 +881,12 @@ const startPairedSession = async ( const primaryPrompt = launch.opts.agent === "claude" ? claudePrompt : codexPrompt; - if (!hadClaudeSession && peerPane.endsWith(":0.0")) { - await seedPanePrompt(peerPane, peerPrompt, deps); - } if (!hadCodexThread && peerPane.endsWith(":0.1")) { await submitCodexPrompt(session, peerPrompt, deps); } if (!(hadClaudeSession && hadCodexThread)) { await deps.sleep(REVIEWER_BOOT_DELAY_MS); } - if (!hadClaudeSession && primaryPane.endsWith(":0.0")) { - await seedPanePrompt(primaryPane, primaryPrompt, deps); - } if (!hadCodexThread && primaryPane.endsWith(":0.1")) { await submitCodexPrompt(session, primaryPrompt, deps); } diff --git a/tests/loop/tmux.test.ts b/tests/loop/tmux.test.ts index a5f1465..e1b4419 100644 --- a/tests/loop/tmux.test.ts +++ b/tests/loop/tmux.test.ts @@ -289,6 +289,11 @@ test("runInTmux starts paired tmux panes for Claude and Codex", async () => { ["bun", "/repo/src/cli.ts"], storage.runDir ); + const claudePrompt = tmuxInternals.buildPeerPrompt( + "Ship feature", + opts, + "claude" + ); const claudeCommand = tmuxInternals.buildShellCommand([ "env", ...env, @@ -296,7 +301,8 @@ test("runInTmux starts paired tmux panes for Claude and Codex", async () => { "claude-session-1", "opus", claudeChannelServer, - false + false, + claudePrompt ), ]); const codexCommand = tmuxInternals.buildShellCommand([ @@ -373,9 +379,7 @@ test("runInTmux starts paired tmux panes for Claude and Codex", async () => { values.push(entry.text); typedByPane.set(entry.pane, values); } - expect(typedByPane.get("repo-loop-1:0.0")?.join("\n")).toBe( - tmuxInternals.buildPeerPrompt("Ship feature", opts, "claude") - ); + expect(typedByPane.get("repo-loop-1:0.0")).toBeUndefined(); expect(typedByPane.get("repo-loop-1:0.1")?.join("\n")).toBe( tmuxInternals.buildPrimaryPrompt("Ship feature", opts) ); @@ -461,15 +465,37 @@ test("runInTmux starts paired interactive tmux panes without a task", async () = expect(delegated).toBe(true); expect(calls[0]).toEqual(["tmux", "has-session", "-t", "repo-loop-1"]); + const env = ["LOOP_RUN_BASE=repo", "LOOP_RUN_ID=1"]; + const claudeChannelServer = tmuxInternals.buildClaudeChannelServerName("1"); + const claudePrompt = tmuxInternals.buildInteractivePeerPrompt(opts, "claude"); + const claudeCommand = tmuxInternals.buildShellCommand([ + "env", + ...env, + ...tmuxInternals.buildClaudeCommand( + "claude-session-1", + "opus", + claudeChannelServer, + false, + claudePrompt + ), + ]); + expect(calls[2]).toEqual([ + "tmux", + "new-session", + "-d", + "-s", + "repo-loop-1", + "-c", + "/repo", + claudeCommand, + ]); const typedByPane = new Map(); for (const entry of typed) { const values = typedByPane.get(entry.pane) ?? []; values.push(entry.text); typedByPane.set(entry.pane, values); } - expect(typedByPane.get("repo-loop-1:0.0")?.join("\n")).toBe( - tmuxInternals.buildInteractivePeerPrompt(opts, "claude") - ); + expect(typedByPane.get("repo-loop-1:0.0")).toBeUndefined(); expect(typedByPane.get("repo-loop-1:0.1")?.join("\n")).toBe( tmuxInternals.buildInteractivePrimaryPrompt(opts) ); @@ -629,9 +655,7 @@ test("runInTmux auto-confirms Claude startup prompts in paired mode", async () = values.push(entry.text); typedByPane.set(entry.pane, values); } - expect(typedByPane.get("repo-loop-1:0.0")?.join("\n")).toBe( - tmuxInternals.buildPeerPrompt("Ship feature", opts, "claude") - ); + expect(typedByPane.get("repo-loop-1:0.0")).toBeUndefined(); expect(typedByPane.get("repo-loop-1:0.1")?.join("\n")).toBe( tmuxInternals.buildPrimaryPrompt("Ship feature", opts) ); From 9727516700a83d4f1cb41dec2fdcb9aac9a6307b Mon Sep 17 00:00:00 2001 From: Axel Delafosse Date: Tue, 24 Mar 2026 20:34:03 -0700 Subject: [PATCH 3/8] Simplify paired tmux startup prompts --- src/loop/tmux.ts | 41 +++++++++++++++++++---------------------- tests/loop/tmux.test.ts | 20 ++++++++------------ 2 files changed, 27 insertions(+), 34 deletions(-) diff --git a/src/loop/tmux.ts b/src/loop/tmux.ts index 103b36f..701bbb6 100644 --- a/src/loop/tmux.ts +++ b/src/loop/tmux.ts @@ -127,18 +127,18 @@ const pairedBridgeGuidance = (agent: Agent): string => { return [ "Paired mode:", "You are in a persistent Claude/Codex pair.", - 'When Codex sends you an inbound channel message, reply to Codex with the MCP tool "reply" and reuse the same chat_id.', - 'Use "send_to_agent" only when you need to start a new proactive message to Codex.', - "Do not ask the human to relay messages between agents. Do not answer the human when Codex is waiting for the response.", - 'Use "bridge_status" only for diagnostics if direct delivery seems stuck. Use "receive_messages" only as a manual fallback.', + 'Reply to inbound Codex channel messages with the MCP tool "reply" and the same chat_id.', + 'Use "send_to_agent" only for new proactive messages to Codex.', + "Do not route messages through the human or answer the human while Codex is waiting on you.", + 'Use "bridge_status" or "receive_messages" only if delivery looks stuck.', ].join("\n"); } return [ "Paired mode:", - 'You are in a persistent Claude/Codex pair. Use the MCP tool "send_to_agent" when you want Claude to act, review, or answer.', - "Do not ask the human to relay messages between agents. Normal paired messages should arrive directly.", - 'Use "bridge_status" only for diagnostics if direct delivery seems stuck. Use "receive_messages" only as a manual fallback.', + 'You are in a persistent Claude/Codex pair. Use "send_to_agent" when you want Claude to act, review, or answer.', + "Do not route messages through the human.", + 'Use "bridge_status" or "receive_messages" only if delivery looks stuck.', ].join("\n"); }; @@ -149,12 +149,10 @@ const pairedWorkflowGuidance = (opts: Options, agent: Agent): string => { if (agent === opts.agent) { return [ "Workflow:", - `You are the main worker. ${peer} is the peer reviewer/support agent.`, - "Do the implementation and verification work yourself first.", - `After your initial pass, ask ${peer} for review with "send_to_agent". Also do your own final review before closing out.`, - "If either your own review or the peer review finds an issue, keep working and repeat the review cycle until both reviews pass.", - "Do not stop after a single passing review.", - "Once both reviews pass, do the PR step yourself: create a draft PR for the current branch, or if a PR already exists, send a follow-up commit to it.", + `You are the main worker. ${peer} reviews and helps on request.`, + "Implement and verify first, then ask for review.", + "Keep iterating until your own review and the peer review both pass.", + "After both pass, handle the PR yourself: create a draft PR or send a follow-up commit to the existing PR.", ].join("\n"); } @@ -162,9 +160,8 @@ const pairedWorkflowGuidance = (opts: Options, agent: Agent): string => { "Workflow:", `${primary} is the main worker. You are the reviewer/support agent.`, "Do not take over the task or create the PR yourself.", - `When ${primary} asks for review, do a real review against the task, proof requirements, and current repo state.`, - "If you find an issue, send clear actionable feedback back to the main worker.", - "If the work looks good, send an explicit approval so the main worker can count your review as passed.", + `When ${primary} asks, do a real review against the task, proof requirements, and repo state.`, + "Send either clear actionable feedback or an explicit approval.", ].join("\n"); }; @@ -180,7 +177,7 @@ const buildPrimaryPrompt = (task: string, opts: Options): string => { parts.push(pairedBridgeGuidance(opts.agent)); parts.push(pairedWorkflowGuidance(opts, opts.agent)); parts.push( - `${peer} has already been prompted as the reviewer/support agent and should send you a short ready message. Wait briefly for that ready signal if it arrives quickly, then review the repo and begin the task. Ask ${peer} for review once you have concrete work or a specific question.` + `${peer} should send a short ready message. Wait briefly if it arrives, then inspect the repo and start. Ask ${peer} for review once you have concrete work or a specific question.` ); return parts.join("\n\n"); }; @@ -196,7 +193,7 @@ const buildPeerPrompt = (task: string, opts: Options, agent: Agent): string => { parts.push(pairedBridgeGuidance(agent)); parts.push(pairedWorkflowGuidance(opts, agent)); parts.push( - `Your first action is to use "send_to_agent" to tell ${primary}: "Reviewer ready. I have the task context and I am waiting for your request." After that, wait for ${primary} to send you a targeted request or review ask.` + `Your first action is to use "send_to_agent" to tell ${primary}: "Reviewer ready. Waiting for your request." After that, wait for ${primary} to send you a targeted request or review ask.` ); return parts.join("\n\n"); }; @@ -206,7 +203,7 @@ const buildInteractivePrimaryPrompt = (opts: Options): string => { const parts = [ `Paired tmux mode. You are the primary ${capitalize(opts.agent)} agent for this run.`, "No task has been assigned yet.", - `Your peer is ${peer}. Stay in paired mode and use "send_to_agent" when you want ${peer} to review work, answer questions, or help once the human gives you a task.`, + `Your peer is ${peer}. Stay in paired mode and use "send_to_agent" for review or help once the human gives you a task.`, ]; appendProofPrompt(parts, opts.proof); parts.push( @@ -215,7 +212,7 @@ const buildInteractivePrimaryPrompt = (opts: Options): string => { parts.push(pairedBridgeGuidance(opts.agent)); parts.push(pairedWorkflowGuidance(opts, opts.agent)); parts.push( - `Wait for the human to provide the first task. Do not start implementing anything until a task arrives. Once you have a concrete task, coordinate directly with ${peer} and keep the paired review workflow intact.` + `Wait for the first human task. Do not implement until one arrives. Once it does, coordinate directly with ${peer} and keep the paired review workflow intact.` ); return parts.join("\n\n"); }; @@ -225,13 +222,13 @@ const buildInteractivePeerPrompt = (opts: Options, agent: Agent): string => { const parts = [ `Paired tmux mode. ${primary} is the primary agent for this run.`, "No task has been assigned yet.", - `You are ${capitalize(agent)}. Your reviewer/support role is active, but do not start implementing or verifying anything until ${primary} sends a specific request or the human clearly assigns you separate work.`, + `You are ${capitalize(agent)}. Stay idle until ${primary} sends a specific request or the human clearly assigns you separate work.`, ]; appendProofPrompt(parts, opts.proof); parts.push(pairedBridgeGuidance(agent)); parts.push(pairedWorkflowGuidance(opts, agent)); parts.push( - `Your first action is to use "send_to_agent" to tell ${primary}: "Reviewer ready. No task yet. I am waiting for your request." After that, wait for ${primary} to provide a concrete task or review request. If the human clearly assigns you separate work in this pane, treat that as a new task. If you are answering ${primary}, use the bridge tools instead of a human-facing reply.` + `Your first action is to use "send_to_agent" to tell ${primary}: "Reviewer ready. No task yet. Waiting for your request." After that, wait for ${primary} to provide a concrete task or review request. If the human clearly assigns you separate work in this pane, treat that as a new task. If you are answering ${primary}, use the bridge tools instead of a human-facing reply.` ); return parts.join("\n\n"); }; diff --git a/tests/loop/tmux.test.ts b/tests/loop/tmux.test.ts index e1b4419..910d317 100644 --- a/tests/loop/tmux.test.ts +++ b/tests/loop/tmux.test.ts @@ -513,18 +513,18 @@ test("tmux prompts keep the paired review workflow explicit", () => { expect(primaryPrompt).toContain("You are the main worker."); expect(primaryPrompt).toContain( - "If either your own review or the peer review finds an issue" + "your own review and the peer review both pass" ); - expect(primaryPrompt).toContain("create a draft PR"); - expect(primaryPrompt).toContain("ready signal"); + expect(primaryPrompt).toContain( + "create a draft PR or send a follow-up commit to the existing PR" + ); + expect(primaryPrompt).toContain("Wait briefly if it arrives"); expect(primaryPrompt).toContain("worktree isolation"); expect(peerPrompt).toContain("You are the reviewer/support agent."); expect(peerPrompt).toContain("Do not take over the task or create the PR"); expect(peerPrompt).toContain("Reviewer ready."); expect(peerPrompt).toContain('"reply"'); - expect(peerPrompt).toContain( - "Do not answer the human when Codex is waiting for the response." - ); + expect(peerPrompt).toContain("Do not route messages through the human"); }); test("interactive tmux prompts tell both agents to wait for the human", () => { @@ -533,17 +533,13 @@ test("interactive tmux prompts tell both agents to wait for the human", () => { const peerPrompt = tmuxInternals.buildInteractivePeerPrompt(opts, "claude"); expect(primaryPrompt).toContain("No task has been assigned yet."); - expect(primaryPrompt).toContain( - "Wait for the human to provide the first task" - ); + expect(primaryPrompt).toContain("Wait for the first human task"); expect(primaryPrompt).toContain("worktree isolation"); expect(peerPrompt).toContain("No task has been assigned yet."); expect(peerPrompt).toContain("Reviewer ready. No task yet."); expect(peerPrompt).toContain("human clearly assigns you separate work"); expect(peerPrompt).toContain('"reply"'); - expect(peerPrompt).toContain( - "Do not answer the human when Codex is waiting for the response." - ); + expect(peerPrompt).toContain("Do not route messages through the human"); expect(peerPrompt).toContain( "If you are answering Codex, use the bridge tools instead of a human-facing reply." ); From 6f7d0146cc96f75a6a9d302995f625f38204c9ff Mon Sep 17 00:00:00 2001 From: Axel Delafosse Date: Tue, 24 Mar 2026 20:46:22 -0700 Subject: [PATCH 4/8] Clarify paired tmux bridge messaging --- src/loop/tmux.ts | 6 ++---- tests/loop/tmux.test.ts | 14 ++++++++++++-- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/loop/tmux.ts b/src/loop/tmux.ts index 701bbb6..6db4986 100644 --- a/src/loop/tmux.ts +++ b/src/loop/tmux.ts @@ -128,16 +128,14 @@ const pairedBridgeGuidance = (agent: Agent): string => { "Paired mode:", "You are in a persistent Claude/Codex pair.", 'Reply to inbound Codex channel messages with the MCP tool "reply" and the same chat_id.', - 'Use "send_to_agent" only for new proactive messages to Codex.', - "Do not route messages through the human or answer the human while Codex is waiting on you.", + 'Use "send_to_agent" only for new proactive messages to Codex; do not send Codex-facing responses as a human-facing message.', 'Use "bridge_status" or "receive_messages" only if delivery looks stuck.', ].join("\n"); } return [ "Paired mode:", - 'You are in a persistent Claude/Codex pair. Use "send_to_agent" when you want Claude to act, review, or answer.', - "Do not route messages through the human.", + 'You are in a persistent Claude/Codex pair. Message Claude with "send_to_agent", not a human-facing message.', 'Use "bridge_status" or "receive_messages" only if delivery looks stuck.', ].join("\n"); }; diff --git a/tests/loop/tmux.test.ts b/tests/loop/tmux.test.ts index 910d317..e93c67a 100644 --- a/tests/loop/tmux.test.ts +++ b/tests/loop/tmux.test.ts @@ -519,12 +519,17 @@ test("tmux prompts keep the paired review workflow explicit", () => { "create a draft PR or send a follow-up commit to the existing PR" ); expect(primaryPrompt).toContain("Wait briefly if it arrives"); + expect(primaryPrompt).toContain( + 'Message Claude with "send_to_agent", not a human-facing message' + ); expect(primaryPrompt).toContain("worktree isolation"); expect(peerPrompt).toContain("You are the reviewer/support agent."); expect(peerPrompt).toContain("Do not take over the task or create the PR"); expect(peerPrompt).toContain("Reviewer ready."); expect(peerPrompt).toContain('"reply"'); - expect(peerPrompt).toContain("Do not route messages through the human"); + expect(peerPrompt).toContain( + 'Use "send_to_agent" only for new proactive messages to Codex; do not send Codex-facing responses as a human-facing message.' + ); }); test("interactive tmux prompts tell both agents to wait for the human", () => { @@ -534,12 +539,17 @@ test("interactive tmux prompts tell both agents to wait for the human", () => { expect(primaryPrompt).toContain("No task has been assigned yet."); expect(primaryPrompt).toContain("Wait for the first human task"); + expect(primaryPrompt).toContain( + 'Message Claude with "send_to_agent", not a human-facing message' + ); expect(primaryPrompt).toContain("worktree isolation"); expect(peerPrompt).toContain("No task has been assigned yet."); expect(peerPrompt).toContain("Reviewer ready. No task yet."); expect(peerPrompt).toContain("human clearly assigns you separate work"); expect(peerPrompt).toContain('"reply"'); - expect(peerPrompt).toContain("Do not route messages through the human"); + expect(peerPrompt).toContain( + 'Use "send_to_agent" only for new proactive messages to Codex; do not send Codex-facing responses as a human-facing message.' + ); expect(peerPrompt).toContain( "If you are answering Codex, use the bridge tools instead of a human-facing reply." ); From 86b857be0fb6e3e2396c84a7de4e14337ebc26a2 Mon Sep 17 00:00:00 2001 From: Axel Delafosse Date: Tue, 24 Mar 2026 20:49:15 -0700 Subject: [PATCH 5/8] Rename paired tmux prompt labels --- src/loop/tmux.ts | 14 +++++++------- tests/loop/tmux.test.ts | 2 ++ 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/loop/tmux.ts b/src/loop/tmux.ts index 6db4986..6298ea3 100644 --- a/src/loop/tmux.ts +++ b/src/loop/tmux.ts @@ -125,7 +125,7 @@ const appendProofPrompt = (parts: string[], proof: string): void => { const pairedBridgeGuidance = (agent: Agent): string => { if (agent === "claude") { return [ - "Paired mode:", + "Agent-to-agent pair programming:", "You are in a persistent Claude/Codex pair.", 'Reply to inbound Codex channel messages with the MCP tool "reply" and the same chat_id.', 'Use "send_to_agent" only for new proactive messages to Codex; do not send Codex-facing responses as a human-facing message.', @@ -134,7 +134,7 @@ const pairedBridgeGuidance = (agent: Agent): string => { } return [ - "Paired mode:", + "Agent-to-agent pair programming:", 'You are in a persistent Claude/Codex pair. Message Claude with "send_to_agent", not a human-facing message.', 'Use "bridge_status" or "receive_messages" only if delivery looks stuck.', ].join("\n"); @@ -166,7 +166,7 @@ const pairedWorkflowGuidance = (opts: Options, agent: Agent): string => { const buildPrimaryPrompt = (task: string, opts: Options): string => { const peer = capitalize(peerAgent(opts.agent)); const parts = [ - `Paired tmux mode. You are the primary ${capitalize(opts.agent)} agent for this run.`, + `Agent-to-agent pair programming. You are the primary ${capitalize(opts.agent)} agent for this run.`, `Task:\n${task.trim()}`, `Your peer is ${peer}. Do the initial pass yourself, then use "send_to_agent" when you want review or targeted help from ${peer}.`, ]; @@ -183,7 +183,7 @@ const buildPrimaryPrompt = (task: string, opts: Options): string => { const buildPeerPrompt = (task: string, opts: Options, agent: Agent): string => { const primary = capitalize(opts.agent); const parts = [ - `Paired tmux mode. ${primary} is the primary agent for this run.`, + `Agent-to-agent pair programming. ${primary} is the primary agent for this run.`, `Task:\n${task.trim()}`, `You are ${capitalize(agent)}. Do not start implementing or verifying this task on your own.`, ]; @@ -199,9 +199,9 @@ const buildPeerPrompt = (task: string, opts: Options, agent: Agent): string => { const buildInteractivePrimaryPrompt = (opts: Options): string => { const peer = capitalize(peerAgent(opts.agent)); const parts = [ - `Paired tmux mode. You are the primary ${capitalize(opts.agent)} agent for this run.`, + `Agent-to-agent pair programming. You are the primary ${capitalize(opts.agent)} agent for this run.`, "No task has been assigned yet.", - `Your peer is ${peer}. Stay in paired mode and use "send_to_agent" for review or help once the human gives you a task.`, + `Your peer is ${peer}. Use "send_to_agent" for review or help once the human gives you a task.`, ]; appendProofPrompt(parts, opts.proof); parts.push( @@ -218,7 +218,7 @@ const buildInteractivePrimaryPrompt = (opts: Options): string => { const buildInteractivePeerPrompt = (opts: Options, agent: Agent): string => { const primary = capitalize(opts.agent); const parts = [ - `Paired tmux mode. ${primary} is the primary agent for this run.`, + `Agent-to-agent pair programming. ${primary} is the primary agent for this run.`, "No task has been assigned yet.", `You are ${capitalize(agent)}. Stay idle until ${primary} sends a specific request or the human clearly assigns you separate work.`, ]; diff --git a/tests/loop/tmux.test.ts b/tests/loop/tmux.test.ts index e93c67a..0949837 100644 --- a/tests/loop/tmux.test.ts +++ b/tests/loop/tmux.test.ts @@ -511,6 +511,7 @@ test("tmux prompts keep the paired review workflow explicit", () => { "claude" ); + expect(primaryPrompt).toContain("Agent-to-agent pair programming"); expect(primaryPrompt).toContain("You are the main worker."); expect(primaryPrompt).toContain( "your own review and the peer review both pass" @@ -537,6 +538,7 @@ test("interactive tmux prompts tell both agents to wait for the human", () => { const primaryPrompt = tmuxInternals.buildInteractivePrimaryPrompt(opts); const peerPrompt = tmuxInternals.buildInteractivePeerPrompt(opts, "claude"); + expect(primaryPrompt).toContain("Agent-to-agent pair programming"); expect(primaryPrompt).toContain("No task has been assigned yet."); expect(primaryPrompt).toContain("Wait for the first human task"); expect(primaryPrompt).toContain( From af73268eba358eea7b134e34910c1840f08614af Mon Sep 17 00:00:00 2001 From: Axel Delafosse Date: Tue, 24 Mar 2026 21:01:24 -0700 Subject: [PATCH 6/8] Clarify tmux bridge guidance and unblock Claude startup --- src/loop/tmux.ts | 23 ++++++----- tests/loop/tmux.test.ts | 89 ++++++++++++++++++++++++++++++++++++++++- 2 files changed, 101 insertions(+), 11 deletions(-) diff --git a/src/loop/tmux.ts b/src/loop/tmux.ts index 6298ea3..01ed299 100644 --- a/src/loop/tmux.ts +++ b/src/loop/tmux.ts @@ -36,6 +36,8 @@ const RUN_BASE_ENV = "LOOP_RUN_BASE"; const RUN_ID_ENV = "LOOP_RUN_ID"; const CLAUDE_TRUST_PROMPT = "Is this a project you created or one you trust?"; const CLAUDE_BYPASS_PROMPT = "running in Bypass Permissions mode"; +const CLAUDE_DEV_CHANNELS_PROMPT = "WARNING: Loading development channels"; +const CLAUDE_DEV_CHANNELS_CONFIRM = "I am using this for local development"; const CLAUDE_CHANNEL_SCOPE = "local"; const CLAUDE_PROMPT_INITIAL_POLLS = 8; const CLAUDE_PROMPT_POLL_DELAY_MS = 250; @@ -125,8 +127,6 @@ const appendProofPrompt = (parts: string[], proof: string): void => { const pairedBridgeGuidance = (agent: Agent): string => { if (agent === "claude") { return [ - "Agent-to-agent pair programming:", - "You are in a persistent Claude/Codex pair.", 'Reply to inbound Codex channel messages with the MCP tool "reply" and the same chat_id.', 'Use "send_to_agent" only for new proactive messages to Codex; do not send Codex-facing responses as a human-facing message.', 'Use "bridge_status" or "receive_messages" only if delivery looks stuck.', @@ -134,8 +134,7 @@ const pairedBridgeGuidance = (agent: Agent): string => { } return [ - "Agent-to-agent pair programming:", - 'You are in a persistent Claude/Codex pair. Message Claude with "send_to_agent", not a human-facing message.', + 'Message Claude with "send_to_agent", not a human-facing message.', 'Use "bridge_status" or "receive_messages" only if delivery looks stuck.', ].join("\n"); }; @@ -680,13 +679,19 @@ const runTmuxCommand = ( throw new Error(`${message}${suffix}`); }; -const detectClaudePrompt = (text: string): "bypass" | "trust" | undefined => { - if (text.includes(CLAUDE_TRUST_PROMPT)) { - return "trust"; - } +const detectClaudePrompt = (text: string): "bypass" | "confirm" | undefined => { if (text.includes(CLAUDE_BYPASS_PROMPT)) { return "bypass"; } + if (text.includes(CLAUDE_TRUST_PROMPT)) { + return "confirm"; + } + if ( + text.includes(CLAUDE_DEV_CHANNELS_PROMPT) && + text.includes(CLAUDE_DEV_CHANNELS_CONFIRM) + ) { + return "confirm"; + } return undefined; }; @@ -706,7 +711,7 @@ const unblockClaudePane = async ( attempt += 1 ) { const prompt = detectClaudePrompt(deps.capturePane(pane)); - if (prompt === "trust") { + if (prompt === "confirm") { deps.sendKeys(pane, ["Enter"]); handledPrompt = true; quietPolls = 0; diff --git a/tests/loop/tmux.test.ts b/tests/loop/tmux.test.ts index 0949837..c652553 100644 --- a/tests/loop/tmux.test.ts +++ b/tests/loop/tmux.test.ts @@ -563,6 +563,15 @@ test("runInTmux auto-confirms Claude startup prompts in paired mode", async () = const typed: Array<{ pane: string; text: string }> = []; let sessionStarted = false; let pollCount = 0; + const devChannelsPrompt = [ + "WARNING: Loading development channels", + "", + "--dangerously-load-development-channels is for local channel development only.", + "", + "1. I am using this for local development", + ].join("\n"); + const bypassPrompt = + "WARNING: Claude Code running in Bypass Permissions mode"; const opts = makePairedOptions(); const manifest = createRunManifest({ cwd: "/repo", @@ -587,10 +596,10 @@ test("runInTmux auto-confirms Claude startup prompts in paired mode", async () = capturePane: () => { pollCount += 1; if (pollCount === 1) { - return "Is this a project you created or one you trust?"; + return devChannelsPrompt; } if (pollCount === 2) { - return "WARNING: Claude Code running in Bypass Permissions mode"; + return `${devChannelsPrompt}\n\n${bypassPrompt}`; } return ""; }, @@ -669,6 +678,82 @@ test("runInTmux auto-confirms Claude startup prompts in paired mode", async () = ); }); +test("runInTmux still confirms Claude trust prompts in paired mode", async () => { + const keyCalls: Array<{ keys: string[]; pane: string }> = []; + let sessionStarted = false; + let pollCount = 0; + const manifest = createRunManifest({ + cwd: "/repo", + mode: "paired", + pid: 1234, + repoId: "repo-123", + runId: "1", + status: "running", + }); + const storage = { + manifestPath: "/repo/.loop/runs/1/manifest.json", + repoId: "repo-123", + runDir: "/repo/.loop/runs/1", + runId: "1", + storageRoot: "/repo/.loop/runs", + transcriptPath: "/repo/.loop/runs/1/transcript.jsonl", + }; + + await runInTmux( + ["--tmux", "--proof", "verify with tests"], + { + capturePane: () => { + pollCount += 1; + if (pollCount === 1) { + return "Is this a project you created or one you trust?"; + } + return ""; + }, + cwd: "/repo", + env: {}, + findBinary: () => true, + getCodexAppServerUrl: () => "ws://127.0.0.1:4500", + getLastCodexThreadId: () => "codex-thread-1", + isInteractive: () => false, + launchArgv: ["bun", "/repo/src/cli.ts"], + log: (): void => undefined, + makeClaudeSessionId: () => "claude-session-1", + preparePairedRun: (nextOpts) => { + nextOpts.codexMcpConfigArgs = [ + "-c", + 'mcp_servers.loop-bridge.command="loop"', + ]; + return { manifest, storage }; + }, + sendKeys: (pane: string, keys: string[]) => { + keyCalls.push({ keys, pane }); + }, + sendText: (): void => undefined, + sleep: () => Promise.resolve(), + startCodexProxy: () => Promise.resolve("ws://127.0.0.1:4600/"), + startPersistentAgentSession: () => Promise.resolve(undefined), + spawn: (args: string[]) => { + if (args[0] === "tmux" && args[1] === "has-session") { + return sessionStarted + ? { exitCode: 0, stderr: "" } + : { exitCode: 1, stderr: "" }; + } + if (args[0] === "tmux" && args[1] === "new-session") { + sessionStarted = true; + } + return { exitCode: 0, stderr: "" }; + }, + updateRunManifest: (_path, update) => update(manifest), + }, + { opts: makePairedOptions(), task: "Ship feature" } + ); + + expect(keyCalls).toContainEqual({ + keys: ["Enter"], + pane: "repo-loop-1:0.0", + }); +}); + test("runInTmux reopens paired tmux panes without replaying the task", async () => { const calls: string[][] = []; const typed: Array<{ pane: string; text: string }> = []; From bd2b1b45b068846466292f697f27342dde519979 Mon Sep 17 00:00:00 2001 From: Axel Delafosse Date: Tue, 24 Mar 2026 22:36:58 -0700 Subject: [PATCH 7/8] Fix paired tmux bridge reply routing --- src/loop/tmux.ts | 72 +++++++++++++++++++++++++++-------------- tests/loop/tmux.test.ts | 40 ++++++++++++++++------- 2 files changed, 76 insertions(+), 36 deletions(-) diff --git a/src/loop/tmux.ts b/src/loop/tmux.ts index 01ed299..9d2885f 100644 --- a/src/loop/tmux.ts +++ b/src/loop/tmux.ts @@ -124,9 +124,13 @@ const appendProofPrompt = (parts: string[], proof: string): void => { parts.push(`Proof requirements:\n${trimmed}`); }; -const pairedBridgeGuidance = (agent: Agent): string => { +const pairedBridgeGuidance = (agent: Agent, runId: string): string => { + const serverName = buildClaudeChannelServerName(runId); + const prefix = `Your bridge MCP server is "${serverName}". All bridge tool calls must use the mcp__${serverName}__ prefix.`; + if (agent === "claude") { return [ + prefix, 'Reply to inbound Codex channel messages with the MCP tool "reply" and the same chat_id.', 'Use "send_to_agent" only for new proactive messages to Codex; do not send Codex-facing responses as a human-facing message.', 'Use "bridge_status" or "receive_messages" only if delivery looks stuck.', @@ -145,7 +149,6 @@ const pairedWorkflowGuidance = (opts: Options, agent: Agent): string => { if (agent === opts.agent) { return [ - "Workflow:", `You are the main worker. ${peer} reviews and helps on request.`, "Implement and verify first, then ask for review.", "Keep iterating until your own review and the peer review both pass.", @@ -154,7 +157,6 @@ const pairedWorkflowGuidance = (opts: Options, agent: Agent): string => { } return [ - "Workflow:", `${primary} is the main worker. You are the reviewer/support agent.`, "Do not take over the task or create the PR yourself.", `When ${primary} asks, do a real review against the task, proof requirements, and repo state.`, @@ -162,16 +164,20 @@ const pairedWorkflowGuidance = (opts: Options, agent: Agent): string => { ].join("\n"); }; -const buildPrimaryPrompt = (task: string, opts: Options): string => { +const buildPrimaryPrompt = ( + task: string, + opts: Options, + runId: string +): string => { const peer = capitalize(peerAgent(opts.agent)); const parts = [ - `Agent-to-agent pair programming. You are the primary ${capitalize(opts.agent)} agent for this run.`, + `Agent-to-agent pair programming: you are the primary ${capitalize(opts.agent)} agent for this run.`, `Task:\n${task.trim()}`, `Your peer is ${peer}. Do the initial pass yourself, then use "send_to_agent" when you want review or targeted help from ${peer}.`, ]; appendProofPrompt(parts, opts.proof); parts.push(SPAWN_TEAM_WITH_WORKTREE_ISOLATION); - parts.push(pairedBridgeGuidance(opts.agent)); + parts.push(pairedBridgeGuidance(opts.agent, runId)); parts.push(pairedWorkflowGuidance(opts, opts.agent)); parts.push( `${peer} should send a short ready message. Wait briefly if it arrives, then inspect the repo and start. Ask ${peer} for review once you have concrete work or a specific question.` @@ -179,26 +185,34 @@ const buildPrimaryPrompt = (task: string, opts: Options): string => { return parts.join("\n\n"); }; -const buildPeerPrompt = (task: string, opts: Options, agent: Agent): string => { +const buildPeerPrompt = ( + task: string, + opts: Options, + agent: Agent, + runId: string +): string => { const primary = capitalize(opts.agent); const parts = [ - `Agent-to-agent pair programming. ${primary} is the primary agent for this run.`, + `Agent-to-agent pair programming: ${primary} is the primary agent for this run.`, `Task:\n${task.trim()}`, `You are ${capitalize(agent)}. Do not start implementing or verifying this task on your own.`, ]; appendProofPrompt(parts, opts.proof); - parts.push(pairedBridgeGuidance(agent)); + parts.push(pairedBridgeGuidance(agent, runId)); parts.push(pairedWorkflowGuidance(opts, agent)); parts.push( - `Your first action is to use "send_to_agent" to tell ${primary}: "Reviewer ready. Waiting for your request." After that, wait for ${primary} to send you a targeted request or review ask.` + `Wait for ${primary} to send you a targeted request or review ask.` ); return parts.join("\n\n"); }; -const buildInteractivePrimaryPrompt = (opts: Options): string => { +const buildInteractivePrimaryPrompt = ( + opts: Options, + runId: string +): string => { const peer = capitalize(peerAgent(opts.agent)); const parts = [ - `Agent-to-agent pair programming. You are the primary ${capitalize(opts.agent)} agent for this run.`, + `Agent-to-agent pair programming: you are the primary ${capitalize(opts.agent)} agent for this run.`, "No task has been assigned yet.", `Your peer is ${peer}. Use "send_to_agent" for review or help once the human gives you a task.`, ]; @@ -206,40 +220,48 @@ const buildInteractivePrimaryPrompt = (opts: Options): string => { parts.push( `${SPAWN_TEAM_WITH_WORKTREE_ISOLATION} Apply that once the human gives you a concrete task.` ); - parts.push(pairedBridgeGuidance(opts.agent)); + parts.push(pairedBridgeGuidance(opts.agent, runId)); parts.push(pairedWorkflowGuidance(opts, opts.agent)); parts.push( - `Wait for the first human task. Do not implement until one arrives. Once it does, coordinate directly with ${peer} and keep the paired review workflow intact.` + `Wait for the first human task. Do not implement until one arrives. Once it does, coordinate directly with ${peer} and keep the paired review workflow intact. Do not send a message to ${peer} until then.` ); return parts.join("\n\n"); }; -const buildInteractivePeerPrompt = (opts: Options, agent: Agent): string => { +const buildInteractivePeerPrompt = ( + opts: Options, + agent: Agent, + runId: string +): string => { const primary = capitalize(opts.agent); const parts = [ - `Agent-to-agent pair programming. ${primary} is the primary agent for this run.`, + `Agent-to-agent pair programming: ${primary} is the primary agent for this run.`, "No task has been assigned yet.", `You are ${capitalize(agent)}. Stay idle until ${primary} sends a specific request or the human clearly assigns you separate work.`, ]; appendProofPrompt(parts, opts.proof); - parts.push(pairedBridgeGuidance(agent)); + parts.push(pairedBridgeGuidance(agent, runId)); parts.push(pairedWorkflowGuidance(opts, agent)); parts.push( - `Your first action is to use "send_to_agent" to tell ${primary}: "Reviewer ready. No task yet. Waiting for your request." After that, wait for ${primary} to provide a concrete task or review request. If the human clearly assigns you separate work in this pane, treat that as a new task. If you are answering ${primary}, use the bridge tools instead of a human-facing reply.` + `Wait for ${primary} to provide a concrete task or review request. Do not send a message to ${primary} yet. If the human clearly assigns you separate work in this pane, treat that as a new task. If you are answering ${primary}, use the bridge tools instead of a human-facing reply.` ); return parts.join("\n\n"); }; -const buildLaunchPrompt = (launch: PairedTmuxLaunch, agent: Agent): string => { +const buildLaunchPrompt = ( + launch: PairedTmuxLaunch, + agent: Agent, + runId: string +): string => { const task = launch.task?.trim(); if (!task) { return launch.opts.agent === agent - ? buildInteractivePrimaryPrompt(launch.opts) - : buildInteractivePeerPrompt(launch.opts, agent); + ? buildInteractivePrimaryPrompt(launch.opts, runId) + : buildInteractivePeerPrompt(launch.opts, agent, runId); } return launch.opts.agent === agent - ? buildPrimaryPrompt(task, launch.opts) - : buildPeerPrompt(task, launch.opts, agent); + ? buildPrimaryPrompt(task, launch.opts, runId) + : buildPeerPrompt(task, launch.opts, agent, runId); }; const resolveTmuxModel = (agent: Agent, opts: Options): string => { @@ -815,8 +837,8 @@ const startPairedSession = async ( const claudeChannelServer = buildClaudeChannelServerName(storage.runId); registerClaudeChannelServer(deps, claudeChannelServer, storage.runDir); const env = [`${RUN_BASE_ENV}=${runBase}`, `${RUN_ID_ENV}=${storage.runId}`]; - const claudePrompt = buildLaunchPrompt(launch, "claude"); - const codexPrompt = buildLaunchPrompt(launch, "codex"); + const claudePrompt = buildLaunchPrompt(launch, "claude", storage.runId); + const codexPrompt = buildLaunchPrompt(launch, "codex", storage.runId); const claudeCommand = buildShellCommand([ "env", ...env, diff --git a/tests/loop/tmux.test.ts b/tests/loop/tmux.test.ts index c652553..939df2c 100644 --- a/tests/loop/tmux.test.ts +++ b/tests/loop/tmux.test.ts @@ -292,7 +292,8 @@ test("runInTmux starts paired tmux panes for Claude and Codex", async () => { const claudePrompt = tmuxInternals.buildPeerPrompt( "Ship feature", opts, - "claude" + "claude", + "1" ); const claudeCommand = tmuxInternals.buildShellCommand([ "env", @@ -381,7 +382,7 @@ test("runInTmux starts paired tmux panes for Claude and Codex", async () => { } expect(typedByPane.get("repo-loop-1:0.0")).toBeUndefined(); expect(typedByPane.get("repo-loop-1:0.1")?.join("\n")).toBe( - tmuxInternals.buildPrimaryPrompt("Ship feature", opts) + tmuxInternals.buildPrimaryPrompt("Ship feature", opts, "1") ); expect(logs[0]).toBe( "[loop] starting paired tmux workspace. This can take a few seconds..." @@ -467,7 +468,11 @@ test("runInTmux starts paired interactive tmux panes without a task", async () = expect(calls[0]).toEqual(["tmux", "has-session", "-t", "repo-loop-1"]); const env = ["LOOP_RUN_BASE=repo", "LOOP_RUN_ID=1"]; const claudeChannelServer = tmuxInternals.buildClaudeChannelServerName("1"); - const claudePrompt = tmuxInternals.buildInteractivePeerPrompt(opts, "claude"); + const claudePrompt = tmuxInternals.buildInteractivePeerPrompt( + opts, + "claude", + "1" + ); const claudeCommand = tmuxInternals.buildShellCommand([ "env", ...env, @@ -497,18 +502,23 @@ test("runInTmux starts paired interactive tmux panes without a task", async () = } expect(typedByPane.get("repo-loop-1:0.0")).toBeUndefined(); expect(typedByPane.get("repo-loop-1:0.1")?.join("\n")).toBe( - tmuxInternals.buildInteractivePrimaryPrompt(opts) + tmuxInternals.buildInteractivePrimaryPrompt(opts, "1") ); expect(manifest.tmuxSession).toBe("repo-loop-1"); }); test("tmux prompts keep the paired review workflow explicit", () => { const opts = makePairedOptions(); - const primaryPrompt = tmuxInternals.buildPrimaryPrompt("Ship feature", opts); + const primaryPrompt = tmuxInternals.buildPrimaryPrompt( + "Ship feature", + opts, + "1" + ); const peerPrompt = tmuxInternals.buildPeerPrompt( "Ship feature", opts, - "claude" + "claude", + "1" ); expect(primaryPrompt).toContain("Agent-to-agent pair programming"); @@ -526,17 +536,23 @@ test("tmux prompts keep the paired review workflow explicit", () => { expect(primaryPrompt).toContain("worktree isolation"); expect(peerPrompt).toContain("You are the reviewer/support agent."); expect(peerPrompt).toContain("Do not take over the task or create the PR"); - expect(peerPrompt).toContain("Reviewer ready."); + expect(peerPrompt).toContain("Wait for Codex to send you a targeted request"); expect(peerPrompt).toContain('"reply"'); expect(peerPrompt).toContain( 'Use "send_to_agent" only for new proactive messages to Codex; do not send Codex-facing responses as a human-facing message.' ); + expect(primaryPrompt).not.toContain("mcp__loop-bridge-1__ prefix"); + expect(peerPrompt).toContain("mcp__loop-bridge-1__ prefix"); }); test("interactive tmux prompts tell both agents to wait for the human", () => { const opts = makePairedOptions({ proof: "" }); - const primaryPrompt = tmuxInternals.buildInteractivePrimaryPrompt(opts); - const peerPrompt = tmuxInternals.buildInteractivePeerPrompt(opts, "claude"); + const primaryPrompt = tmuxInternals.buildInteractivePrimaryPrompt(opts, "1"); + const peerPrompt = tmuxInternals.buildInteractivePeerPrompt( + opts, + "claude", + "1" + ); expect(primaryPrompt).toContain("Agent-to-agent pair programming"); expect(primaryPrompt).toContain("No task has been assigned yet."); @@ -546,7 +562,7 @@ test("interactive tmux prompts tell both agents to wait for the human", () => { ); expect(primaryPrompt).toContain("worktree isolation"); expect(peerPrompt).toContain("No task has been assigned yet."); - expect(peerPrompt).toContain("Reviewer ready. No task yet."); + expect(peerPrompt).toContain("Wait for Codex to provide a concrete task"); expect(peerPrompt).toContain("human clearly assigns you separate work"); expect(peerPrompt).toContain('"reply"'); expect(peerPrompt).toContain( @@ -555,6 +571,8 @@ test("interactive tmux prompts tell both agents to wait for the human", () => { expect(peerPrompt).toContain( "If you are answering Codex, use the bridge tools instead of a human-facing reply." ); + expect(primaryPrompt).not.toContain("mcp__loop-bridge-1__ prefix"); + expect(peerPrompt).toContain("mcp__loop-bridge-1__ prefix"); }); test("runInTmux auto-confirms Claude startup prompts in paired mode", async () => { @@ -674,7 +692,7 @@ test("runInTmux auto-confirms Claude startup prompts in paired mode", async () = } expect(typedByPane.get("repo-loop-1:0.0")).toBeUndefined(); expect(typedByPane.get("repo-loop-1:0.1")?.join("\n")).toBe( - tmuxInternals.buildPrimaryPrompt("Ship feature", opts) + tmuxInternals.buildPrimaryPrompt("Ship feature", opts, "1") ); }); From 2d6ee98cd0a83930f57a5516c414e75723f3ad17 Mon Sep 17 00:00:00 2001 From: Axel Delafosse Date: Tue, 24 Mar 2026 22:39:29 -0700 Subject: [PATCH 8/8] Clean --- src/loop/tmux.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/loop/tmux.ts b/src/loop/tmux.ts index 9d2885f..0d81c7a 100644 --- a/src/loop/tmux.ts +++ b/src/loop/tmux.ts @@ -126,11 +126,10 @@ const appendProofPrompt = (parts: string[], proof: string): void => { const pairedBridgeGuidance = (agent: Agent, runId: string): string => { const serverName = buildClaudeChannelServerName(runId); - const prefix = `Your bridge MCP server is "${serverName}". All bridge tool calls must use the mcp__${serverName}__ prefix.`; if (agent === "claude") { return [ - prefix, + `Your bridge MCP server is "${serverName}". All bridge tool calls must use the mcp__${serverName}__ prefix.`, 'Reply to inbound Codex channel messages with the MCP tool "reply" and the same chat_id.', 'Use "send_to_agent" only for new proactive messages to Codex; do not send Codex-facing responses as a human-facing message.', 'Use "bridge_status" or "receive_messages" only if delivery looks stuck.',