Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions src/loop/bridge-guidance.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
import { BRIDGE_SERVER, CLAUDE_CHANNEL_USER } from "./bridge-constants";
import type { Agent } from "./types";

type BridgeTool = "bridge_status" | "receive_messages" | "send_message";
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The BridgeTool type is used in multiple files (paired-loop.ts, tmux.ts). Exporting it from here centralizes the definition and avoids duplication.

Suggested change
type BridgeTool = "bridge_status" | "receive_messages" | "send_message";
export type BridgeTool = "bridge_status" | "receive_messages" | "send_message";
References
  1. When communicating between agents in a paired tmux runtime, use newline-delimited JSON on stdout for agent CLIs, as Content-Length framing can break registration/connection.


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);
Comment on lines +10 to +11
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The quotedBridgeTool helper is currently duplicated in paired-loop.ts and tmux.ts. It should be defined once here and exported to improve maintainability. Ensure that the resulting strings are used within newline-delimited JSON for agent communication.

Suggested change
export const bridgeToolName = (agent: Agent, tool: BridgeTool): string =>
agent === "claude" ? tool : codexBridgeToolName(tool);
export const bridgeToolName = (agent: Agent, tool: BridgeTool): string =>
agent === "claude" ? tool : codexBridgeToolName(tool);
export const quotedBridgeTool = (agent: Agent, tool: BridgeTool): string =>
'"' + bridgeToolName(agent, tool) + '"';
References
  1. When communicating between agents in a paired tmux runtime, use newline-delimited JSON on stdout for agent CLIs, as Content-Length framing can break registration/connection.


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

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.`;

Expand Down
60 changes: 35 additions & 25 deletions src/loop/paired-loop.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
acknowledgeBridgeDelivery,
readNextPendingBridgeMessage,
} from "./bridge-dispatch";
import { bridgeToolName } from "./bridge-guidance";
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Import the shared quotedBridgeTool helper from bridge-guidance.ts to reduce local duplication.

Suggested change
import { bridgeToolName } from "./bridge-guidance";
import { bridgeToolName, quotedBridgeTool } from "./bridge-guidance";
References
  1. When communicating between agents in a paired tmux runtime, use newline-delimited JSON on stdout for agent CLIs, as Content-Length framing can break registration/connection.

import { formatCodexBridgeMessage } from "./bridge-message-format";
import { getLastClaudeSessionId } from "./claude-sdk-server";
import { getLastCodexThreadId } from "./codex-app-server";
Expand Down Expand Up @@ -47,6 +48,7 @@ import type {
import { hasSignal } from "./utils";

const MAX_BRIDGE_HOPS = 12;
type BridgeTool = "bridge_status" | "receive_messages" | "send_message";
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This type is now exported from bridge-guidance.ts. Removing the local definition avoids redundancy.

References
  1. When communicating between agents in a paired tmux runtime, use newline-delimited JSON on stdout for agent CLIs, as Content-Length framing can break registration/connection.


interface PairedState {
manifest: RunManifest;
Expand Down Expand Up @@ -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)}"`;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This helper has been moved to bridge-guidance.ts and should be removed from here.

References
  1. When communicating between agents in a paired tmux runtime, use newline-delimited JSON on stdout for agent CLIs, as Content-Length framing can break registration/connection.


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[],
Expand Down Expand Up @@ -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(
Expand Down
16 changes: 12 additions & 4 deletions src/loop/tmux.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ import {
resolveClaudeChannelServerName,
} from "./bridge-config";
import {
bridgeToolName,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Import the shared quotedBridgeTool helper from bridge-guidance.ts instead of defining it locally.

Suggested change
bridgeToolName,
bridgeToolName,
quotedBridgeTool,
References
  1. When communicating between agents in a paired tmux runtime, use newline-delimited JSON on stdout for agent CLIs, as Content-Length framing can break registration/connection.

receiveMessagesStuckGuidance,
sendProactiveCodexGuidance,
sendToClaudeGuidance,
} from "./bridge-guidance";
import { getCodexAppServerUrl, getLastCodexThreadId } from "./codex-app-server";
import {
Expand Down Expand Up @@ -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)}"`;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This helper is duplicated and should be removed in favor of the shared implementation in bridge-guidance.ts.

References
  1. When communicating between agents in a paired tmux runtime, use newline-delimited JSON on stdout for agent CLIs, as Content-Length framing can break registration/connection.


const pairedBridgeGuidance = (
agent: Agent,
_runId: string,
Expand All @@ -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 => {
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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(
Expand Down
12 changes: 12 additions & 0 deletions tests/loop/bridge-guidance.test.ts
Original file line number Diff line number Diff line change
@@ -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");
});
3 changes: 2 additions & 1 deletion tests/loop/paired-loop.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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:"
Expand Down Expand Up @@ -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"'
);
});
});
Expand Down
8 changes: 2 additions & 6 deletions tests/loop/tmux.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down Expand Up @@ -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(
Expand Down
Loading