Skip to content

Commit 63a8f23

Browse files
Fix Codex bridge tool guidance (#57)
1 parent 4b470f5 commit 63a8f23

6 files changed

Lines changed: 96 additions & 50 deletions

File tree

src/loop/bridge-guidance.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,24 @@
11
import { BRIDGE_SERVER, CLAUDE_CHANNEL_USER } from "./bridge-constants";
22
import type { Agent } from "./types";
33

4+
export type BridgeTool = "bridge_status" | "receive_messages" | "send_message";
5+
46
const bridgeTargetLiteral = (agent: Agent): string => `target: "${agent}"`;
7+
const codexBridgeToolName = (tool: BridgeTool): string =>
8+
`mcp__${BRIDGE_SERVER.replaceAll("-", "_")}__${tool}`;
9+
10+
export const bridgeToolName = (agent: Agent, tool: BridgeTool): string =>
11+
agent === "claude" ? tool : codexBridgeToolName(tool);
12+
13+
export const quotedBridgeTool = (agent: Agent, tool: BridgeTool): string =>
14+
`"${bridgeToolName(agent, tool)}"`;
515

616
export const bridgeStatusStuckGuidance =
717
'Use "bridge_status" only when direct delivery appears stuck.';
818

919
export const receiveMessagesStuckGuidance =
1020
'Use "bridge_status" or "receive_messages" only if delivery looks stuck.';
1121

12-
export const sendToClaudeGuidance = (): string =>
13-
`Use "send_message" with ${bridgeTargetLiteral("claude")} for Claude-facing messages, not a human-facing message.`;
14-
1522
export const sendProactiveCodexGuidance = (): string =>
1623
`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.`;
1724

src/loop/paired-loop.ts

Lines changed: 31 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {
33
acknowledgeBridgeDelivery,
44
readNextPendingBridgeMessage,
55
} from "./bridge-dispatch";
6+
import { quotedBridgeTool } from "./bridge-guidance";
67
import { formatCodexBridgeMessage } from "./bridge-message-format";
78
import { getLastClaudeSessionId } from "./claude-sdk-server";
89
import { getLastCodexThreadId } from "./codex-app-server";
@@ -100,30 +101,31 @@ const bridgeGuidance = (agent: Agent): string => {
100101
const target = agent === "claude" ? "codex" : "claude";
101102
return [
102103
"Paired mode:",
103-
`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.`,
104-
'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.',
105-
'Use "receive_messages" only if "bridge_status" shows pending messages addressed to you and direct delivery looks stuck.',
104+
`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.`,
105+
`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.`,
106+
`Use ${quotedBridgeTool(agent, "receive_messages")} only if ${quotedBridgeTool(agent, "bridge_status")} shows pending messages addressed to you and direct delivery looks stuck.`,
106107
].join("\n");
107108
};
108109

109-
const bridgeToolGuidance = [
110-
'You can use the MCP tools "send_message", "bridge_status", and "receive_messages" for direct Claude/Codex coordination.',
111-
'Only use "bridge_status" or "receive_messages" when delivery looks stuck.',
112-
"Do not ask the human to relay messages between agents.",
113-
].join("\n");
110+
const bridgeToolGuidance = (agent: Agent): string =>
111+
[
112+
`You can use the MCP tools ${quotedBridgeTool(agent, "send_message")}, ${quotedBridgeTool(agent, "bridge_status")}, and ${quotedBridgeTool(agent, "receive_messages")} for direct Claude/Codex coordination.`,
113+
`Only use ${quotedBridgeTool(agent, "bridge_status")} or ${quotedBridgeTool(agent, "receive_messages")} when delivery looks stuck.`,
114+
"Do not ask the human to relay messages between agents.",
115+
].join("\n");
114116

115117
const reviewDeliveryGuidance = (reviewer: Agent, opts: Options): string => {
116118
if (reviewer === opts.agent) {
117119
return "If review is needed, keep the actionable notes in your review body before the final review signal.";
118120
}
119121

120-
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.`;
122+
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.`;
121123
};
122124

123125
const reviewToolGuidance = (reviewer: Agent, opts: Options): string =>
124126
reviewer === opts.agent
125127
? "Use the review body itself for follow-up notes. No bridge message is needed for a self-review."
126-
: bridgeToolGuidance;
128+
: bridgeToolGuidance(reviewer);
127129

128130
const formatSelfReviewNotes = (
129131
failures: ReviewFailure[],
@@ -158,22 +160,26 @@ const forwardBridgePrompt = ({
158160
}: {
159161
message: string;
160162
source: Agent;
161-
}): string =>
162-
(source === "claude"
163-
? [
164-
formatCodexBridgeMessage(source, message),
165-
"Treat this as direct agent-to-agent coordination. Do not reply to the human.",
166-
'Send a message to the other agent with "send_message" only when you have something useful for them to act on.',
167-
"Do not acknowledge receipt without new information.",
168-
]
169-
: [
170-
`Message from ${capitalize(source)} via the loop bridge:`,
171-
message.trim(),
172-
"Treat this as direct agent-to-agent coordination. Do not reply to the human.",
173-
'Send a message to the other agent with "send_message" only when you have something useful for them to act on.',
174-
"Do not acknowledge receipt without new information.",
175-
]
163+
}): string => {
164+
const agent = source === "claude" ? "codex" : "claude";
165+
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.`;
166+
return (
167+
source === "claude"
168+
? [
169+
formatCodexBridgeMessage(source, message),
170+
"Treat this as direct agent-to-agent coordination. Do not reply to the human.",
171+
replyGuidance,
172+
"Do not acknowledge receipt without new information.",
173+
]
174+
: [
175+
`Message from ${capitalize(source)} via the loop bridge:`,
176+
message.trim(),
177+
"Treat this as direct agent-to-agent coordination. Do not reply to the human.",
178+
replyGuidance,
179+
"Do not acknowledge receipt without new information.",
180+
]
176181
).join("\n\n");
182+
};
177183

178184
const updateIds = (state: PairedState): void => {
179185
const next = touchRunManifest(

src/loop/tmux.ts

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,7 @@ import {
1212
legacyClaudeChannelServerName,
1313
resolveClaudeChannelServerName,
1414
} from "./bridge-config";
15-
import {
16-
receiveMessagesStuckGuidance,
17-
sendProactiveCodexGuidance,
18-
sendToClaudeGuidance,
19-
} from "./bridge-guidance";
15+
import { type BridgeTool, quotedBridgeTool } from "./bridge-guidance";
2016
import { getCodexAppServerUrl, getLastCodexThreadId } from "./codex-app-server";
2117
import {
2218
CODEX_TMUX_PROXY_SUBCOMMAND,
@@ -155,20 +151,27 @@ const appendProofPrompt = (parts: string[], proof: string): void => {
155151
parts.push(`Proof requirements:\n${trimmed}`);
156152
};
157153

154+
const quotedClaudeTmuxBridgeTool = (
155+
serverName: string,
156+
tool: BridgeTool
157+
): string => `"mcp__${serverName}__${tool}"`;
158+
158159
const pairedBridgeGuidance = (
159160
agent: Agent,
160161
_runId: string,
161162
serverName: string
162163
): string => {
163164
if (agent === "claude") {
164165
return [
165-
`Your bridge MCP server is "${serverName}". All bridge tool calls must use the mcp__${serverName}__ prefix.`,
166-
sendProactiveCodexGuidance(),
167-
receiveMessagesStuckGuidance,
166+
`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.`,
167+
`Use ${quotedClaudeTmuxBridgeTool(serverName, "bridge_status")} or ${quotedClaudeTmuxBridgeTool(serverName, "receive_messages")} only if delivery looks stuck.`,
168168
].join("\n");
169169
}
170170

171-
return [sendToClaudeGuidance(), receiveMessagesStuckGuidance].join("\n");
171+
return [
172+
`Use the MCP tool ${quotedBridgeTool(agent, "send_message")} with target: "claude" for Claude-facing messages, not a human-facing message.`,
173+
`Use ${quotedBridgeTool(agent, "bridge_status")} or ${quotedBridgeTool(agent, "receive_messages")} only if delivery looks stuck.`,
174+
].join("\n");
172175
};
173176

174177
const pairedWorkflowGuidance = (opts: Options, agent: Agent): string => {
@@ -202,7 +205,7 @@ const buildPrimaryPrompt = (
202205
const parts = [
203206
`Agent-to-agent pair programming: you are the primary ${capitalize(opts.agent)} agent for this run.`,
204207
`Task:\n${task.trim()}`,
205-
`Your peer is ${peer}. Do the initial pass yourself, then use "send_message" when you want review or targeted help from ${peer}.`,
208+
`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}.`,
206209
];
207210
appendProofPrompt(parts, opts.proof);
208211
parts.push(SPAWN_TEAM_WITH_WORKTREE_ISOLATION);
@@ -245,7 +248,7 @@ const buildInteractivePrimaryPrompt = (
245248
const parts = [
246249
`Agent-to-agent pair programming: you are the primary ${capitalize(opts.agent)} agent for this run.`,
247250
"No task has been assigned yet.",
248-
`Your peer is ${peer}. Use "send_message" for review or help once the human gives you a task.`,
251+
`Your peer is ${peer}. Use ${quotedBridgeTool(opts.agent, "send_message")} for review or help once the human gives you a task.`,
249252
];
250253
appendProofPrompt(parts, opts.proof);
251254
parts.push(

tests/loop/bridge-guidance.test.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { expect, test } from "bun:test";
2+
import {
3+
bridgeToolName,
4+
quotedBridgeTool,
5+
} from "../../src/loop/bridge-guidance";
6+
7+
test("bridgeToolName namespaces Codex bridge tools only", () => {
8+
expect(bridgeToolName("codex", "send_message")).toBe(
9+
"mcp__loop_bridge__send_message"
10+
);
11+
expect(bridgeToolName("codex", "bridge_status")).toBe(
12+
"mcp__loop_bridge__bridge_status"
13+
);
14+
expect(bridgeToolName("claude", "send_message")).toBe("send_message");
15+
});
16+
17+
test("quotedBridgeTool wraps the resolved bridge tool name", () => {
18+
expect(quotedBridgeTool("codex", "send_message")).toBe(
19+
'"mcp__loop_bridge__send_message"'
20+
);
21+
expect(quotedBridgeTool("claude", "receive_messages")).toBe(
22+
'"receive_messages"'
23+
);
24+
});

tests/loop/paired-loop.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -777,6 +777,7 @@ test("runPairedLoop delivers peer messages back to the primary agent", async ()
777777
"Please verify the implementation details."
778778
);
779779
expect(calls[1]?.prompt).toContain("Do not reply to the human.");
780+
expect(calls[1]?.prompt).toContain('"mcp__loop_bridge__send_message"');
780781
expect(calls[2]?.agent).toBe("claude");
781782
expect(calls[2]?.prompt).toContain(
782783
"Message from Codex via the loop bridge:"
@@ -862,7 +863,7 @@ test("runPairedLoop preserves claudex reviewers in paired mode", async () => {
862863
"concrete file paths, commands, and code locations that must change"
863864
);
864865
expect(reviewPrompts[1]?.prompt).toContain(
865-
'send the actionable notes to Claude with "send_message" using target: "claude"'
866+
'send the actionable notes to Claude with "mcp__loop_bridge__send_message" using target: "claude"'
866867
);
867868
});
868869
});

tests/loop/tmux.test.ts

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -792,19 +792,21 @@ test("tmux prompts keep the paired review workflow explicit", () => {
792792
"create a draft PR or send a follow-up commit to the existing PR"
793793
);
794794
expect(primaryPrompt).not.toContain("Wait briefly if it arrives");
795-
expect(primaryPrompt).toContain(
796-
'Use "send_message" with target: "claude" for Claude-facing messages'
797-
);
795+
expect(primaryPrompt).toContain('"mcp__loop_bridge__send_message"');
798796
expect(primaryPrompt).toContain("worktree isolation");
799797
expect(peerPrompt).toContain("You are the reviewer/support agent.");
800798
expect(peerPrompt).toContain("Do not take over the task or create the PR");
801799
expect(peerPrompt).toContain("Wait for Codex to send you a targeted request");
802800
expect(peerPrompt).not.toContain('"reply"');
803801
expect(peerPrompt).toContain(
804-
'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.'
802+
'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.'
805803
);
806804
expect(primaryPrompt).not.toContain("mcp__loop-bridge-repo-123-1__ prefix");
807-
expect(peerPrompt).toContain("mcp__loop-bridge-repo-123-1__ prefix");
805+
expect(peerPrompt).not.toContain("mcp__loop-bridge-repo-123-1__ prefix");
806+
expect(peerPrompt).toContain('"mcp__loop-bridge-repo-123-1__bridge_status"');
807+
expect(peerPrompt).toContain(
808+
'"mcp__loop-bridge-repo-123-1__receive_messages"'
809+
);
808810
});
809811

810812
test("interactive tmux prompts tell both agents to wait for the human", () => {
@@ -831,9 +833,7 @@ test("interactive tmux prompts tell both agents to wait for the human", () => {
831833
expect(primaryPrompt).toContain("If the human asks for plan mode");
832834
expect(primaryPrompt).toContain("ask Claude for a plan review");
833835
expect(primaryPrompt).toContain("ask the human to review the plan");
834-
expect(primaryPrompt).toContain(
835-
'Use "send_message" with target: "claude" for Claude-facing messages'
836-
);
836+
expect(primaryPrompt).toContain('"mcp__loop_bridge__send_message"');
837837
expect(primaryPrompt).toContain("worktree isolation");
838838
expect(peerPrompt).toContain("No task has been assigned yet.");
839839
expect(peerPrompt).toContain(
@@ -843,13 +843,18 @@ test("interactive tmux prompts tell both agents to wait for the human", () => {
843843
expect(peerPrompt).toContain("human clearly assigns you separate work");
844844
expect(peerPrompt).not.toContain('"reply"');
845845
expect(peerPrompt).toContain(
846-
'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.'
846+
'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.'
847+
);
848+
expect(peerPrompt).not.toContain("mcp__loop-bridge-repo-123-1__ prefix");
849+
expect(peerPrompt).toContain('"mcp__loop-bridge-repo-123-1__bridge_status"');
850+
expect(peerPrompt).toContain(
851+
'"mcp__loop-bridge-repo-123-1__receive_messages"'
847852
);
848853
expect(peerPrompt).toContain(
849854
"If you are answering Codex, use the bridge tools instead of a human-facing reply."
850855
);
851856
expect(primaryPrompt).not.toContain("mcp__loop-bridge-repo-123-1__ prefix");
852-
expect(peerPrompt).toContain("mcp__loop-bridge-repo-123-1__ prefix");
857+
expect(peerPrompt).not.toContain("mcp__loop-bridge-repo-123-1__ prefix");
853858
});
854859

855860
test("runInTmux auto-confirms Claude startup prompts in paired mode", async () => {

0 commit comments

Comments
 (0)