From ff82259e219a1b292db861ec83cc53ef8499f532 Mon Sep 17 00:00:00 2001 From: Axel Delafosse Date: Tue, 31 Mar 2026 21:25:41 -0700 Subject: [PATCH 1/3] Fix Codex bridge tool guidance --- src/loop/bridge-guidance.ts | 10 +++-- src/loop/paired-loop.ts | 60 +++++++++++++++++------------- src/loop/tmux.ts | 16 ++++++-- tests/loop/bridge-guidance.test.ts | 12 ++++++ tests/loop/paired-loop.test.ts | 3 +- tests/loop/tmux.test.ts | 8 +--- 6 files changed, 70 insertions(+), 39 deletions(-) create mode 100644 tests/loop/bridge-guidance.test.ts diff --git a/src/loop/bridge-guidance.ts b/src/loop/bridge-guidance.ts index 3c82c3d..f539c52 100644 --- a/src/loop/bridge-guidance.ts +++ b/src/loop/bridge-guidance.ts @@ -1,7 +1,14 @@ import { BRIDGE_SERVER, CLAUDE_CHANNEL_USER } from "./bridge-constants"; import type { Agent } from "./types"; +type BridgeTool = "bridge_status" | "receive_messages" | "send_message"; + const bridgeTargetLiteral = (agent: Agent): string => `target: "${agent}"`; +const codexBridgeToolName = (tool: BridgeTool): string => + `mcp__${BRIDGE_SERVER.replaceAll("-", "_")}__${tool}`; + +export const bridgeToolName = (agent: Agent, tool: BridgeTool): string => + agent === "claude" ? tool : codexBridgeToolName(tool); export const bridgeStatusStuckGuidance = 'Use "bridge_status" only when direct delivery appears stuck.'; @@ -9,9 +16,6 @@ export const bridgeStatusStuckGuidance = export const receiveMessagesStuckGuidance = 'Use "bridge_status" or "receive_messages" only if delivery looks stuck.'; -export const sendToClaudeGuidance = (): string => - `Use "send_message" with ${bridgeTargetLiteral("claude")} for Claude-facing messages, not a human-facing message.`; - export const sendProactiveCodexGuidance = (): string => `Use "send_message" with ${bridgeTargetLiteral("codex")} for Codex-facing messages, including replies to inbound Codex channel messages; do not send Codex-facing responses as a human-facing message.`; diff --git a/src/loop/paired-loop.ts b/src/loop/paired-loop.ts index e0f9bf4..cd9be0c 100644 --- a/src/loop/paired-loop.ts +++ b/src/loop/paired-loop.ts @@ -3,6 +3,7 @@ import { acknowledgeBridgeDelivery, readNextPendingBridgeMessage, } from "./bridge-dispatch"; +import { bridgeToolName } from "./bridge-guidance"; import { formatCodexBridgeMessage } from "./bridge-message-format"; import { getLastClaudeSessionId } from "./claude-sdk-server"; import { getLastCodexThreadId } from "./codex-app-server"; @@ -47,6 +48,7 @@ import type { import { hasSignal } from "./utils"; const MAX_BRIDGE_HOPS = 12; +type BridgeTool = "bridge_status" | "receive_messages" | "send_message"; interface PairedState { manifest: RunManifest; @@ -100,30 +102,34 @@ const bridgeGuidance = (agent: Agent): string => { const target = agent === "claude" ? "codex" : "claude"; return [ "Paired mode:", - `You are in a persistent Claude/Codex pair. Use the MCP tool "send_message" with ${bridgeTargetLiteral(target)} when you want ${peer} to act, review, or answer.`, - 'Do not ask the human to relay messages between agents or answer the human on the other agent\'s behalf. Use "bridge_status" only if delivery looks stuck.', - 'Use "receive_messages" only if "bridge_status" shows pending messages addressed to you and direct delivery looks stuck.', + `You are in a persistent Claude/Codex pair. Use the MCP tool ${quotedBridgeTool(agent, "send_message")} with ${bridgeTargetLiteral(target)} when you want ${peer} to act, review, or answer.`, + `Do not ask the human to relay messages between agents or answer the human on the other agent's behalf. Use ${quotedBridgeTool(agent, "bridge_status")} only if delivery looks stuck.`, + `Use ${quotedBridgeTool(agent, "receive_messages")} only if ${quotedBridgeTool(agent, "bridge_status")} shows pending messages addressed to you and direct delivery looks stuck.`, ].join("\n"); }; -const bridgeToolGuidance = [ - 'You can use the MCP tools "send_message", "bridge_status", and "receive_messages" for direct Claude/Codex coordination.', - 'Only use "bridge_status" or "receive_messages" when delivery looks stuck.', - "Do not ask the human to relay messages between agents.", -].join("\n"); +const quotedBridgeTool = (agent: Agent, tool: BridgeTool): string => + `"${bridgeToolName(agent, tool)}"`; + +const bridgeToolGuidance = (agent: Agent): string => + [ + `You can use the MCP tools ${quotedBridgeTool(agent, "send_message")}, ${quotedBridgeTool(agent, "bridge_status")}, and ${quotedBridgeTool(agent, "receive_messages")} for direct Claude/Codex coordination.`, + `Only use ${quotedBridgeTool(agent, "bridge_status")} or ${quotedBridgeTool(agent, "receive_messages")} when delivery looks stuck.`, + "Do not ask the human to relay messages between agents.", + ].join("\n"); const reviewDeliveryGuidance = (reviewer: Agent, opts: Options): string => { if (reviewer === opts.agent) { return "If review is needed, keep the actionable notes in your review body before the final review signal."; } - return `If review is needed, send the actionable notes to ${capitalize(opts.agent)} with "send_message" using ${bridgeTargetLiteral(opts.agent)} before returning your final review signal.`; + return `If review is needed, send the actionable notes to ${capitalize(opts.agent)} with ${quotedBridgeTool(reviewer, "send_message")} using ${bridgeTargetLiteral(opts.agent)} before returning your final review signal.`; }; const reviewToolGuidance = (reviewer: Agent, opts: Options): string => reviewer === opts.agent ? "Use the review body itself for follow-up notes. No bridge message is needed for a self-review." - : bridgeToolGuidance; + : bridgeToolGuidance(reviewer); const formatSelfReviewNotes = ( failures: ReviewFailure[], @@ -158,22 +164,26 @@ const forwardBridgePrompt = ({ }: { message: string; source: Agent; -}): string => - (source === "claude" - ? [ - formatCodexBridgeMessage(source, message), - "Treat this as direct agent-to-agent coordination. Do not reply to the human.", - 'Send a message to the other agent with "send_message" only when you have something useful for them to act on.', - "Do not acknowledge receipt without new information.", - ] - : [ - `Message from ${capitalize(source)} via the loop bridge:`, - message.trim(), - "Treat this as direct agent-to-agent coordination. Do not reply to the human.", - 'Send a message to the other agent with "send_message" only when you have something useful for them to act on.', - "Do not acknowledge receipt without new information.", - ] +}): string => { + const agent = source === "claude" ? "codex" : "claude"; + const replyGuidance = `Send a message to the other agent with ${quotedBridgeTool(agent, "send_message")} only when you have something useful for them to act on.`; + return ( + source === "claude" + ? [ + formatCodexBridgeMessage(source, message), + "Treat this as direct agent-to-agent coordination. Do not reply to the human.", + replyGuidance, + "Do not acknowledge receipt without new information.", + ] + : [ + `Message from ${capitalize(source)} via the loop bridge:`, + message.trim(), + "Treat this as direct agent-to-agent coordination. Do not reply to the human.", + replyGuidance, + "Do not acknowledge receipt without new information.", + ] ).join("\n\n"); +}; const updateIds = (state: PairedState): void => { const next = touchRunManifest( diff --git a/src/loop/tmux.ts b/src/loop/tmux.ts index 7df54a4..7946895 100644 --- a/src/loop/tmux.ts +++ b/src/loop/tmux.ts @@ -13,9 +13,9 @@ import { resolveClaudeChannelServerName, } from "./bridge-config"; import { + bridgeToolName, receiveMessagesStuckGuidance, sendProactiveCodexGuidance, - sendToClaudeGuidance, } from "./bridge-guidance"; import { getCodexAppServerUrl, getLastCodexThreadId } from "./codex-app-server"; import { @@ -155,6 +155,11 @@ const appendProofPrompt = (parts: string[], proof: string): void => { parts.push(`Proof requirements:\n${trimmed}`); }; +const quotedBridgeTool = ( + agent: Agent, + tool: "bridge_status" | "receive_messages" | "send_message" +): string => `"${bridgeToolName(agent, tool)}"`; + const pairedBridgeGuidance = ( agent: Agent, _runId: string, @@ -168,7 +173,10 @@ const pairedBridgeGuidance = ( ].join("\n"); } - return [sendToClaudeGuidance(), receiveMessagesStuckGuidance].join("\n"); + return [ + `Use the MCP tool ${quotedBridgeTool(agent, "send_message")} with target: "claude" for Claude-facing messages, not a human-facing message.`, + `Use ${quotedBridgeTool(agent, "bridge_status")} or ${quotedBridgeTool(agent, "receive_messages")} only if delivery looks stuck.`, + ].join("\n"); }; const pairedWorkflowGuidance = (opts: Options, agent: Agent): string => { @@ -202,7 +210,7 @@ const buildPrimaryPrompt = ( const parts = [ `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_message" when you want review or targeted help from ${peer}.`, + `Your peer is ${peer}. Do the initial pass yourself, then use ${quotedBridgeTool(opts.agent, "send_message")} when you want review or targeted help from ${peer}.`, ]; appendProofPrompt(parts, opts.proof); parts.push(SPAWN_TEAM_WITH_WORKTREE_ISOLATION); @@ -245,7 +253,7 @@ const buildInteractivePrimaryPrompt = ( const parts = [ `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_message" for review or help once the human gives you a task.`, + `Your peer is ${peer}. Use ${quotedBridgeTool(opts.agent, "send_message")} for review or help once the human gives you a task.`, ]; appendProofPrompt(parts, opts.proof); parts.push( diff --git a/tests/loop/bridge-guidance.test.ts b/tests/loop/bridge-guidance.test.ts new file mode 100644 index 0000000..26fedce --- /dev/null +++ b/tests/loop/bridge-guidance.test.ts @@ -0,0 +1,12 @@ +import { expect, test } from "bun:test"; +import { bridgeToolName } from "../../src/loop/bridge-guidance"; + +test("bridgeToolName namespaces Codex bridge tools only", () => { + expect(bridgeToolName("codex", "send_message")).toBe( + "mcp__loop_bridge__send_message" + ); + expect(bridgeToolName("codex", "bridge_status")).toBe( + "mcp__loop_bridge__bridge_status" + ); + expect(bridgeToolName("claude", "send_message")).toBe("send_message"); +}); diff --git a/tests/loop/paired-loop.test.ts b/tests/loop/paired-loop.test.ts index a0a13ab..60ab211 100644 --- a/tests/loop/paired-loop.test.ts +++ b/tests/loop/paired-loop.test.ts @@ -777,6 +777,7 @@ test("runPairedLoop delivers peer messages back to the primary agent", async () "Please verify the implementation details." ); expect(calls[1]?.prompt).toContain("Do not reply to the human."); + expect(calls[1]?.prompt).toContain('"mcp__loop_bridge__send_message"'); expect(calls[2]?.agent).toBe("claude"); expect(calls[2]?.prompt).toContain( "Message from Codex via the loop bridge:" @@ -862,7 +863,7 @@ test("runPairedLoop preserves claudex reviewers in paired mode", async () => { "concrete file paths, commands, and code locations that must change" ); expect(reviewPrompts[1]?.prompt).toContain( - 'send the actionable notes to Claude with "send_message" using target: "claude"' + 'send the actionable notes to Claude with "mcp__loop_bridge__send_message" using target: "claude"' ); }); }); diff --git a/tests/loop/tmux.test.ts b/tests/loop/tmux.test.ts index 697d17e..83e66ad 100644 --- a/tests/loop/tmux.test.ts +++ b/tests/loop/tmux.test.ts @@ -792,9 +792,7 @@ 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).not.toContain("Wait briefly if it arrives"); - expect(primaryPrompt).toContain( - 'Use "send_message" with target: "claude" for Claude-facing messages' - ); + expect(primaryPrompt).toContain('"mcp__loop_bridge__send_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"); @@ -831,9 +829,7 @@ test("interactive tmux prompts tell both agents to wait for the human", () => { expect(primaryPrompt).toContain("If the human asks for plan mode"); expect(primaryPrompt).toContain("ask Claude for a plan review"); expect(primaryPrompt).toContain("ask the human to review the plan"); - expect(primaryPrompt).toContain( - 'Use "send_message" with target: "claude" for Claude-facing messages' - ); + expect(primaryPrompt).toContain('"mcp__loop_bridge__send_message"'); expect(primaryPrompt).toContain("worktree isolation"); expect(peerPrompt).toContain("No task has been assigned yet."); expect(peerPrompt).toContain( From ce2f183b905fd7ef50471316606e687ae2ed5c41 Mon Sep 17 00:00:00 2001 From: Axel Delafosse Date: Tue, 31 Mar 2026 21:49:08 -0700 Subject: [PATCH 2/3] Clarify Claude tmux bridge tool guidance --- src/loop/tmux.ts | 16 ++++++++-------- tests/loop/tmux.test.ts | 17 +++++++++++++---- 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/src/loop/tmux.ts b/src/loop/tmux.ts index 7946895..ac0b76b 100644 --- a/src/loop/tmux.ts +++ b/src/loop/tmux.ts @@ -12,11 +12,7 @@ import { legacyClaudeChannelServerName, resolveClaudeChannelServerName, } from "./bridge-config"; -import { - bridgeToolName, - receiveMessagesStuckGuidance, - sendProactiveCodexGuidance, -} from "./bridge-guidance"; +import { bridgeToolName } from "./bridge-guidance"; import { getCodexAppServerUrl, getLastCodexThreadId } from "./codex-app-server"; import { CODEX_TMUX_PROXY_SUBCOMMAND, @@ -160,6 +156,11 @@ const quotedBridgeTool = ( tool: "bridge_status" | "receive_messages" | "send_message" ): string => `"${bridgeToolName(agent, tool)}"`; +const quotedClaudeTmuxBridgeTool = ( + serverName: string, + tool: "bridge_status" | "receive_messages" | "send_message" +): string => `"mcp__${serverName}__${tool}"`; + const pairedBridgeGuidance = ( agent: Agent, _runId: string, @@ -167,9 +168,8 @@ const pairedBridgeGuidance = ( ): string => { if (agent === "claude") { return [ - `Your bridge MCP server is "${serverName}". All bridge tool calls must use the mcp__${serverName}__ prefix.`, - sendProactiveCodexGuidance(), - receiveMessagesStuckGuidance, + `Your bridge MCP server is "${serverName}". Use ${quotedClaudeTmuxBridgeTool(serverName, "send_message")} with target: "codex" for Codex-facing messages, including replies to inbound Codex channel messages; do not send Codex-facing responses as a human-facing message.`, + `Use ${quotedClaudeTmuxBridgeTool(serverName, "bridge_status")} or ${quotedClaudeTmuxBridgeTool(serverName, "receive_messages")} only if delivery looks stuck.`, ].join("\n"); } diff --git a/tests/loop/tmux.test.ts b/tests/loop/tmux.test.ts index 83e66ad..e592db7 100644 --- a/tests/loop/tmux.test.ts +++ b/tests/loop/tmux.test.ts @@ -799,10 +799,14 @@ test("tmux prompts keep the paired review workflow explicit", () => { expect(peerPrompt).toContain("Wait for Codex to send you a targeted request"); expect(peerPrompt).not.toContain('"reply"'); expect(peerPrompt).toContain( - 'Use "send_message" with target: "codex" for Codex-facing messages, including replies to inbound Codex channel messages; do not send Codex-facing responses as a human-facing message.' + 'Use "mcp__loop-bridge-repo-123-1__send_message" with target: "codex" for Codex-facing messages, including replies to inbound Codex channel messages; do not send Codex-facing responses as a human-facing message.' ); expect(primaryPrompt).not.toContain("mcp__loop-bridge-repo-123-1__ prefix"); - expect(peerPrompt).toContain("mcp__loop-bridge-repo-123-1__ prefix"); + expect(peerPrompt).not.toContain("mcp__loop-bridge-repo-123-1__ prefix"); + expect(peerPrompt).toContain('"mcp__loop-bridge-repo-123-1__bridge_status"'); + expect(peerPrompt).toContain( + '"mcp__loop-bridge-repo-123-1__receive_messages"' + ); }); test("interactive tmux prompts tell both agents to wait for the human", () => { @@ -839,13 +843,18 @@ test("interactive tmux prompts tell both agents to wait for the human", () => { expect(peerPrompt).toContain("human clearly assigns you separate work"); expect(peerPrompt).not.toContain('"reply"'); expect(peerPrompt).toContain( - 'Use "send_message" with target: "codex" for Codex-facing messages, including replies to inbound Codex channel messages; do not send Codex-facing responses as a human-facing message.' + 'Use "mcp__loop-bridge-repo-123-1__send_message" with target: "codex" for Codex-facing messages, including replies to inbound Codex channel messages; do not send Codex-facing responses as a human-facing message.' + ); + expect(peerPrompt).not.toContain("mcp__loop-bridge-repo-123-1__ prefix"); + expect(peerPrompt).toContain('"mcp__loop-bridge-repo-123-1__bridge_status"'); + expect(peerPrompt).toContain( + '"mcp__loop-bridge-repo-123-1__receive_messages"' ); 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-repo-123-1__ prefix"); - expect(peerPrompt).toContain("mcp__loop-bridge-repo-123-1__ prefix"); + expect(peerPrompt).not.toContain("mcp__loop-bridge-repo-123-1__ prefix"); }); test("runInTmux auto-confirms Claude startup prompts in paired mode", async () => { From 6c2b3d15ac627a784d755dda0c2feda185625dde Mon Sep 17 00:00:00 2001 From: Axel Delafosse Date: Tue, 31 Mar 2026 22:02:27 -0700 Subject: [PATCH 3/3] Deduplicate bridge tool helpers --- src/loop/bridge-guidance.ts | 5 ++++- src/loop/paired-loop.ts | 6 +----- src/loop/tmux.ts | 9 ++------- tests/loop/bridge-guidance.test.ts | 14 +++++++++++++- 4 files changed, 20 insertions(+), 14 deletions(-) diff --git a/src/loop/bridge-guidance.ts b/src/loop/bridge-guidance.ts index f539c52..4a3bed1 100644 --- a/src/loop/bridge-guidance.ts +++ b/src/loop/bridge-guidance.ts @@ -1,7 +1,7 @@ import { BRIDGE_SERVER, CLAUDE_CHANNEL_USER } from "./bridge-constants"; import type { Agent } from "./types"; -type BridgeTool = "bridge_status" | "receive_messages" | "send_message"; +export type BridgeTool = "bridge_status" | "receive_messages" | "send_message"; const bridgeTargetLiteral = (agent: Agent): string => `target: "${agent}"`; const codexBridgeToolName = (tool: BridgeTool): string => @@ -10,6 +10,9 @@ const codexBridgeToolName = (tool: BridgeTool): string => export const bridgeToolName = (agent: Agent, tool: BridgeTool): string => agent === "claude" ? tool : codexBridgeToolName(tool); +export const quotedBridgeTool = (agent: Agent, tool: BridgeTool): string => + `"${bridgeToolName(agent, tool)}"`; + export const bridgeStatusStuckGuidance = 'Use "bridge_status" only when direct delivery appears stuck.'; diff --git a/src/loop/paired-loop.ts b/src/loop/paired-loop.ts index cd9be0c..1b1bfbd 100644 --- a/src/loop/paired-loop.ts +++ b/src/loop/paired-loop.ts @@ -3,7 +3,7 @@ import { acknowledgeBridgeDelivery, readNextPendingBridgeMessage, } from "./bridge-dispatch"; -import { bridgeToolName } from "./bridge-guidance"; +import { quotedBridgeTool } from "./bridge-guidance"; import { formatCodexBridgeMessage } from "./bridge-message-format"; import { getLastClaudeSessionId } from "./claude-sdk-server"; import { getLastCodexThreadId } from "./codex-app-server"; @@ -48,7 +48,6 @@ import type { import { hasSignal } from "./utils"; const MAX_BRIDGE_HOPS = 12; -type BridgeTool = "bridge_status" | "receive_messages" | "send_message"; interface PairedState { manifest: RunManifest; @@ -108,9 +107,6 @@ const bridgeGuidance = (agent: Agent): string => { ].join("\n"); }; -const quotedBridgeTool = (agent: Agent, tool: BridgeTool): string => - `"${bridgeToolName(agent, tool)}"`; - const bridgeToolGuidance = (agent: Agent): string => [ `You can use the MCP tools ${quotedBridgeTool(agent, "send_message")}, ${quotedBridgeTool(agent, "bridge_status")}, and ${quotedBridgeTool(agent, "receive_messages")} for direct Claude/Codex coordination.`, diff --git a/src/loop/tmux.ts b/src/loop/tmux.ts index ac0b76b..fb73716 100644 --- a/src/loop/tmux.ts +++ b/src/loop/tmux.ts @@ -12,7 +12,7 @@ import { legacyClaudeChannelServerName, resolveClaudeChannelServerName, } from "./bridge-config"; -import { bridgeToolName } from "./bridge-guidance"; +import { type BridgeTool, quotedBridgeTool } from "./bridge-guidance"; import { getCodexAppServerUrl, getLastCodexThreadId } from "./codex-app-server"; import { CODEX_TMUX_PROXY_SUBCOMMAND, @@ -151,14 +151,9 @@ const appendProofPrompt = (parts: string[], proof: string): void => { parts.push(`Proof requirements:\n${trimmed}`); }; -const quotedBridgeTool = ( - agent: Agent, - tool: "bridge_status" | "receive_messages" | "send_message" -): string => `"${bridgeToolName(agent, tool)}"`; - const quotedClaudeTmuxBridgeTool = ( serverName: string, - tool: "bridge_status" | "receive_messages" | "send_message" + tool: BridgeTool ): string => `"mcp__${serverName}__${tool}"`; const pairedBridgeGuidance = ( diff --git a/tests/loop/bridge-guidance.test.ts b/tests/loop/bridge-guidance.test.ts index 26fedce..53632f9 100644 --- a/tests/loop/bridge-guidance.test.ts +++ b/tests/loop/bridge-guidance.test.ts @@ -1,5 +1,8 @@ import { expect, test } from "bun:test"; -import { bridgeToolName } from "../../src/loop/bridge-guidance"; +import { + bridgeToolName, + quotedBridgeTool, +} from "../../src/loop/bridge-guidance"; test("bridgeToolName namespaces Codex bridge tools only", () => { expect(bridgeToolName("codex", "send_message")).toBe( @@ -10,3 +13,12 @@ test("bridgeToolName namespaces Codex bridge tools only", () => { ); expect(bridgeToolName("claude", "send_message")).toBe("send_message"); }); + +test("quotedBridgeTool wraps the resolved bridge tool name", () => { + expect(quotedBridgeTool("codex", "send_message")).toBe( + '"mcp__loop_bridge__send_message"' + ); + expect(quotedBridgeTool("claude", "receive_messages")).toBe( + '"receive_messages"' + ); +});