diff --git a/src/loop/bridge-config.ts b/src/loop/bridge-config.ts index 193a7c0..1d8bde1 100644 --- a/src/loop/bridge-config.ts +++ b/src/loop/bridge-config.ts @@ -6,7 +6,7 @@ import { buildLaunchArgv } from "./launch"; import type { Agent } from "./types"; const CODEX_AUTO_APPROVED_BRIDGE_TOOLS = [ - "send_to_agent", + "send_message", "bridge_status", "receive_messages", ] as const; diff --git a/src/loop/bridge-guidance.ts b/src/loop/bridge-guidance.ts index 23b2cf6..3c82c3d 100644 --- a/src/loop/bridge-guidance.ts +++ b/src/loop/bridge-guidance.ts @@ -10,10 +10,10 @@ export const receiveMessagesStuckGuidance = 'Use "bridge_status" or "receive_messages" only if delivery looks stuck.'; export const sendToClaudeGuidance = (): string => - `Use "send_to_agent" with ${bridgeTargetLiteral("claude")} for Claude-facing messages, not a human-facing message.`; + `Use "send_message" with ${bridgeTargetLiteral("claude")} for Claude-facing messages, not a human-facing message.`; export const sendProactiveCodexGuidance = (): string => - `Use "send_to_agent" 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.`; + `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.`; export const claudeChannelInstructions = (): string => [ diff --git a/src/loop/bridge-message-format.ts b/src/loop/bridge-message-format.ts index 35a6408..00ef6cc 100644 --- a/src/loop/bridge-message-format.ts +++ b/src/loop/bridge-message-format.ts @@ -15,4 +15,4 @@ export const formatCodexBridgeMessage = ( }; export const normalizeBridgeMessage = (message: string): string => - message.trim().replace(BRIDGE_PREFIX_RE, "").replace(/\s+/g, " "); + message.trim().replace(BRIDGE_PREFIX_RE, "").replace(/\s+/g, " ").trim(); diff --git a/src/loop/bridge.ts b/src/loop/bridge.ts index af3dcf2..2372742 100644 --- a/src/loop/bridge.ts +++ b/src/loop/bridge.ts @@ -1,3 +1,5 @@ +import { mkdirSync, watch } from "node:fs"; +import { basename } from "node:path"; import { claudeChannelServerName } from "./bridge-config"; import { BRIDGE_SERVER as BRIDGE_SERVER_VALUE } from "./bridge-constants"; import { @@ -28,8 +30,8 @@ import { import { LOOP_VERSION } from "./constants"; import type { Agent } from "./types"; -const CHANNEL_POLL_DELAY_MS = 500; const CLAUDE_CHANNEL_CAPABILITY = "claude/channel"; +const CLAUDE_CHANNEL_FALLBACK_SWEEP_MS = 2000; const CONTENT_LENGTH_RE = /Content-Length:\s*(\d+)/i; const CONTENT_LENGTH_PREFIX = "content-length:"; const DEFAULT_PROTOCOL_VERSION = "2024-11-05"; @@ -142,7 +144,7 @@ const handleReceiveMessagesTool = ( }); }; -const handleSendToAgentTool = async ( +const handleSendMessageTool = async ( id: JsonRpcRequest["id"], runDir: string, source: Agent, @@ -154,7 +156,7 @@ const handleSendToAgentTool = async ( writeError( id, MCP_INVALID_PARAMS, - "send_to_agent requires a non-empty target" + "send_message requires a non-empty target" ); return; } @@ -171,7 +173,7 @@ const handleSendToAgentTool = async ( writeError( id, MCP_INVALID_PARAMS, - "send_to_agent requires a non-empty message" + "send_message requires a non-empty message" ); return; } @@ -179,7 +181,7 @@ const handleSendToAgentTool = async ( writeError( id, MCP_INVALID_PARAMS, - "send_to_agent cannot target the current agent" + "send_message cannot target the current agent" ); return; } @@ -244,12 +246,21 @@ const handleToolCall = async ( return; } - if (name !== "send_to_agent") { + if (name === "send_to_agent") { + writeError( + id, + MCP_INVALID_PARAMS, + 'Unknown tool: send_to_agent. Use "send_message" instead.' + ); + return; + } + + if (name !== "send_message") { writeError(id, MCP_INVALID_PARAMS, `Unknown tool: ${name}`); return; } - await handleSendToAgentTool(id, runDir, source, args); + await handleSendMessageTool(id, runDir, source, args); }; const requestedProtocolVersion = (request: JsonRpcRequest): string => @@ -312,7 +323,7 @@ const handleBridgeRequest = async ( tools: [ { annotations: MUTATING_TOOL_ANNOTATIONS, - description: "Send an explicit message to the paired agent.", + description: "Send a direct message to the paired agent.", inputSchema: { additionalProperties: false, properties: { @@ -325,12 +336,12 @@ const handleBridgeRequest = async ( required: ["target", "message"], type: "object", }, - name: "send_to_agent", + name: "send_message", }, { annotations: READ_ONLY_TOOL_ANNOTATIONS, description: - "Inspect the current paired run and pending bridge messages.", + "Inspect the current paired run and pending bridge state when delivery looks stuck.", inputSchema: { additionalProperties: false, properties: {}, @@ -341,7 +352,7 @@ const handleBridgeRequest = async ( { annotations: RECEIVE_MESSAGES_TOOL_ANNOTATIONS, description: - "Read and clear pending bridge messages addressed to you.", + "Read and clear pending bridge messages addressed to you when delivery looks stuck.", inputSchema: { additionalProperties: false, properties: {}, @@ -481,12 +492,24 @@ const consumeFrames = ( process.stdin.on("error", reject); }); +const isBridgeWatchEvent = ( + runDir: string, + filename: string | Buffer | null +): boolean => { + if (!filename) { + return true; + } + return filename.toString() === basename(bridgePath(runDir)); +}; + export const runBridgeMcpServer = async ( runDir: string, source: Agent ): Promise => { let channelReady = false; + let bridgeWatcher: { close: () => void } | undefined; let closed = false; + let fallbackSweep: ReturnType | undefined; let flushQueue: Promise = Promise.resolve(); let requestQueue: Promise = Promise.resolve(); const queueClaudeFlush = (): Promise => { @@ -499,39 +522,74 @@ export const runBridgeMcpServer = async ( flushQueue = flushQueue.then(next, next); return flushQueue; }; - const pollClaudeChannel = async (): Promise => { - while (!closed) { - await queueClaudeFlush(); + + const triggerClaudeFlush = (): void => { + queueClaudeFlush().catch(() => undefined); + }; + + const clearClaudeSweep = (): void => { + if (!fallbackSweep) { + return; + } + clearTimeout(fallbackSweep); + fallbackSweep = undefined; + }; + + const scheduleClaudeSweep = (): void => { + if (!(source === "claude" && channelReady) || closed || fallbackSweep) { + return; + } + fallbackSweep = setTimeout(() => { + fallbackSweep = undefined; if (closed) { return; } - await new Promise((resolve) => { - setTimeout(resolve, CHANNEL_POLL_DELAY_MS); - }); - } + triggerClaudeFlush(); + scheduleClaudeSweep(); + }, CLAUDE_CHANNEL_FALLBACK_SWEEP_MS); + fallbackSweep.unref?.(); }; - process.stdin.resume(); - const poller = source === "claude" ? pollClaudeChannel() : Promise.resolve(); - await consumeFrames( - (request) => { - const handleRequest = async (): Promise => { - if (request.method === "notifications/initialized") { - channelReady = true; + if (source === "claude") { + mkdirSync(runDir, { recursive: true }); + try { + bridgeWatcher = watch(runDir, (_eventType, filename) => { + if (!isBridgeWatchEvent(runDir, filename)) { + return; } - await handleBridgeRequest(runDir, source, request); - await queueClaudeFlush(); - }; - requestQueue = requestQueue.then(handleRequest, handleRequest); - }, - () => { - closed = true; + triggerClaudeFlush(); + }); + } catch { + bridgeWatcher = undefined; } - ); - closed = true; + } + + process.stdin.resume(); + try { + await consumeFrames( + (request) => { + const handleRequest = async (): Promise => { + if (request.method === "notifications/initialized") { + channelReady = true; + scheduleClaudeSweep(); + } + await handleBridgeRequest(runDir, source, request); + await queueClaudeFlush(); + }; + requestQueue = requestQueue.then(handleRequest, handleRequest); + }, + () => { + closed = true; + } + ); + } finally { + closed = true; + bridgeWatcher?.close(); + clearClaudeSweep(); + } + await requestQueue; await queueClaudeFlush(); - await poller; }; export const bridgeInternals = { diff --git a/src/loop/paired-loop.ts b/src/loop/paired-loop.ts index 0c5a414..e0f9bf4 100644 --- a/src/loop/paired-loop.ts +++ b/src/loop/paired-loop.ts @@ -100,14 +100,15 @@ 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_to_agent" 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" if you need the current bridge state.', - 'If "bridge_status" shows pending messages addressed to you, call "receive_messages" to read them.', + `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.', ].join("\n"); }; const bridgeToolGuidance = [ - 'You can use the MCP tools "send_to_agent", "bridge_status", and "receive_messages" for direct Claude/Codex coordination.', + '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"); @@ -116,7 +117,7 @@ const reviewDeliveryGuidance = (reviewer: Agent, opts: Options): string => { 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_to_agent" using ${bridgeTargetLiteral(opts.agent)} before returning your 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.`; }; const reviewToolGuidance = (reviewer: Agent, opts: Options): string => @@ -151,19 +152,25 @@ const reviewBridgePrompt = ( .filter(Boolean) .join("\n\n"); -const forwardBridgePrompt = (source: Agent, message: string): string => +const forwardBridgePrompt = ({ + message, + source, +}: { + 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_to_agent" only when you have something useful for them to act on.', + '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_to_agent" only when you have something useful for them to act on.', + '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.", ] ).join("\n\n"); @@ -284,7 +291,7 @@ const drainBridge = async ( const result = await tryRunPairedAgent( state, message.target, - forwardBridgePrompt(message.source, message.message) + forwardBridgePrompt(message) ); if (!result) { return { deliveredToPrimary }; diff --git a/src/loop/tmux.ts b/src/loop/tmux.ts index a456be0..7df54a4 100644 --- a/src/loop/tmux.ts +++ b/src/loop/tmux.ts @@ -202,14 +202,14 @@ 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_to_agent" when you want review or targeted help from ${peer}.`, + `Your peer is ${peer}. Do the initial pass yourself, then use "send_message" 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, runId, serverName)); 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.` + `Inspect the repo and start. Ask ${peer} for review once you have concrete work or a specific question.` ); return parts.join("\n\n"); }; @@ -245,7 +245,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_to_agent" for review or help once the human gives you a task.`, + `Your peer is ${peer}. Use "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.test.ts b/tests/loop/bridge.test.ts index c26ad88..f716b79 100644 --- a/tests/loop/bridge.test.ts +++ b/tests/loop/bridge.test.ts @@ -107,6 +107,59 @@ const runBridgeProcess = async ( return { code, stderr, stdout }; }; +const startLiveBridgeProcess = ( + runDir: string, + source: "claude" | "codex", + env?: NodeJS.ProcessEnv +): { + close: () => Promise<{ code: number | null; stderr: string; stdout: string }>; + write: (frame: string) => void; + waitForStdout: (pattern: string, timeoutMs?: number) => Promise; +} => { + const cli = join(process.cwd(), "src", "cli.ts"); + const child = spawn(process.execPath, [cli, "__bridge-mcp", runDir, source], { + cwd: process.cwd(), + env, + stdio: ["pipe", "pipe", "pipe"], + }); + let stdout = ""; + let stderr = ""; + + child.stdout.setEncoding("utf8"); + child.stdout.on("data", (chunk: string) => { + stdout += chunk; + }); + child.stderr.setEncoding("utf8"); + child.stderr.on("data", (chunk: string) => { + stderr += chunk; + }); + + return { + close: async () => { + child.stdin.end(); + const code = await new Promise((resolve) => { + child.on("close", resolve); + }); + return { code, stderr, stdout }; + }, + write: (frame) => { + child.stdin.write(frame); + }, + waitForStdout: async (pattern, timeoutMs = 5000) => { + const deadline = Date.now() + timeoutMs; + while (Date.now() < deadline) { + if (stdout.includes(pattern)) { + return; + } + await new Promise((resolve) => { + setTimeout(resolve, 25); + }); + } + throw new Error(`timed out waiting for stdout to contain: ${pattern}`); + }, + }; +}; + afterEach(() => { mock.restore(); }); @@ -418,11 +471,10 @@ test("bridge normalization treats short and legacy Claude prefixes as equivalent "Claude: Please verify the final diff." ) ).toBe(true); - rmSync(root, { recursive: true, force: true }); }); -test("bridge MCP send_to_agent queues a direct message through the CLI path", async () => { +test("bridge MCP send_message queues a direct message through the CLI path", async () => { const bridge = await loadBridge(); const root = makeTempDir(); const runDir = join(root, "run"); @@ -441,7 +493,7 @@ test("bridge MCP send_to_agent queues a direct message through the CLI path", as message: "ship it", target: "codex", }, - name: "send_to_agent", + name: "send_message", }, }), "\n", @@ -466,7 +518,7 @@ test("bridge MCP send_to_agent queues a direct message through the CLI path", as rmSync(root, { recursive: true, force: true }); }); -test("bridge MCP send_to_agent normalizes target case and whitespace", async () => { +test("bridge MCP send_message normalizes target case and whitespace", async () => { const bridge = await loadBridge(); const root = makeTempDir(); const runDir = join(root, "run"); @@ -485,7 +537,7 @@ test("bridge MCP send_to_agent normalizes target case and whitespace", async () message: "ship it", target: " CLAUDE ", }, - name: "send_to_agent", + name: "send_message", }, }), "\n", @@ -505,7 +557,7 @@ test("bridge MCP send_to_agent normalizes target case and whitespace", async () rmSync(root, { recursive: true, force: true }); }); -test("bridge MCP send_to_agent rejects an empty target after trimming", async () => { +test("bridge MCP send_message rejects an empty target after trimming", async () => { const root = makeTempDir(); const runDir = join(root, "run"); mkdirSync(runDir, { recursive: true }); @@ -523,7 +575,7 @@ test("bridge MCP send_to_agent rejects an empty target after trimming", async () message: "ship it", target: " ", }, - name: "send_to_agent", + name: "send_message", }, }), "\n", @@ -534,7 +586,7 @@ test("bridge MCP send_to_agent rejects an empty target after trimming", async () expect(JSON.parse(result.stdout)).toMatchObject({ error: { code: -32_602, - message: "send_to_agent requires a non-empty target", + message: "send_message requires a non-empty target", }, id: 1, jsonrpc: "2.0", @@ -542,7 +594,7 @@ test("bridge MCP send_to_agent rejects an empty target after trimming", async () rmSync(root, { recursive: true, force: true }); }); -test("bridge MCP send_to_agent rejects an unknown normalized target", async () => { +test("bridge MCP send_message rejects an unknown normalized target", async () => { const root = makeTempDir(); const runDir = join(root, "run"); mkdirSync(runDir, { recursive: true }); @@ -560,7 +612,7 @@ test("bridge MCP send_to_agent rejects an unknown normalized target", async () = message: "ship it", target: " FOO ", }, - name: "send_to_agent", + name: "send_message", }, }), "\n", @@ -579,7 +631,7 @@ test("bridge MCP send_to_agent rejects an unknown normalized target", async () = rmSync(root, { recursive: true, force: true }); }); -test("bridge MCP send_to_agent rejects targeting the current agent", async () => { +test("bridge MCP send_message rejects targeting the current agent", async () => { const root = makeTempDir(); const runDir = join(root, "run"); mkdirSync(runDir, { recursive: true }); @@ -597,6 +649,43 @@ test("bridge MCP send_to_agent rejects targeting the current agent", async () => message: "ship it", target: "claude", }, + name: "send_message", + }, + }), + "\n", + ].join("") + ); + + expect(result.code).toBe(0); + expect(JSON.parse(result.stdout)).toMatchObject({ + error: { + code: -32_602, + message: "send_message cannot target the current agent", + }, + id: 1, + jsonrpc: "2.0", + }); + rmSync(root, { recursive: true, force: true }); +}); + +test("bridge MCP rejects the old send_to_agent name with rename guidance", async () => { + const root = makeTempDir(); + const runDir = join(root, "run"); + mkdirSync(runDir, { recursive: true }); + + const result = await runBridgeProcess( + runDir, + "claude", + [ + encodeFrame({ + id: 1, + jsonrpc: "2.0", + method: "tools/call", + params: { + arguments: { + message: "ship it", + target: "codex", + }, name: "send_to_agent", }, }), @@ -608,7 +697,7 @@ test("bridge MCP send_to_agent rejects targeting the current agent", async () => expect(JSON.parse(result.stdout)).toMatchObject({ error: { code: -32_602, - message: "send_to_agent cannot target the current agent", + message: 'Unknown tool: send_to_agent. Use "send_message" instead.', }, id: 1, jsonrpc: "2.0", @@ -670,7 +759,7 @@ test("bridge MCP handles standard empty-list and ping requests through the Claud expect(result.stderr).toBe(""); expect(result.stdout).toContain('"claude/channel":{}'); expect(result.stdout).toContain( - '\\"send_to_agent\\" with target: \\"codex\\" for Codex-facing messages' + '\\"send_message\\" with target: \\"codex\\" for Codex-facing messages' ); expect(result.stdout).toContain( "Never answer the human when the inbound message came from Codex" @@ -693,7 +782,7 @@ test("bridge MCP handles standard empty-list and ping requests through the Claud openWorldHint: false, readOnlyHint: false, }, - name: "send_to_agent", + name: "send_message", }), expect.objectContaining({ annotations: { @@ -757,7 +846,7 @@ test("bridge MCP advertises only the Codex-visible bridge tools", async () => { openWorldHint: false, readOnlyHint: false, }, - name: "send_to_agent", + name: "send_message", }), expect.objectContaining({ annotations: { @@ -1776,6 +1865,61 @@ test("bridge MCP delivers pending codex messages to Claude as channel notificati rmSync(root, { recursive: true, force: true }); }); +test("bridge MCP flushes new Claude channel messages after bridge file changes", async () => { + const bridge = await loadBridge(); + const root = makeTempDir(); + const runDir = join(root, "run"); + mkdirSync(runDir, { recursive: true }); + const process = startLiveBridgeProcess(runDir, "claude"); + + process.write( + encodeLine({ + id: 1, + jsonrpc: "2.0", + method: "initialize", + params: { + protocolVersion: "2024-11-05", + }, + }) + ); + process.write( + encodeLine({ + jsonrpc: "2.0", + method: "notifications/initialized", + params: {}, + }) + ); + await process.waitForStdout('"id":1'); + + writeFileSync( + bridge.bridgeInternals.bridgePath(runDir), + `${JSON.stringify({ + at: "2026-03-23T10:00:00.000Z", + id: "msg-watch-1", + kind: "message", + message: "Please review the follow-up change.", + source: "codex", + target: "claude", + })}\n`, + "utf8" + ); + + await process.waitForStdout("Please review the follow-up change."); + const result = await process.close(); + + expect(result.code).toBe(0); + expect(result.stderr).toBe(""); + expect(result.stdout).toContain('"method":"notifications/claude/channel"'); + expect(bridge.readPendingBridgeMessages(runDir)).toEqual([]); + expect( + bridge.bridgeInternals + .readBridgeEvents(runDir) + .filter((event) => event.kind === "delivered") + ).toHaveLength(1); + + rmSync(root, { recursive: true, force: true }); +}); + test("bridge MCP blocks an immediate bounce from the paired agent", async () => { const bridge = await loadBridge(); const root = makeTempDir(); @@ -1819,7 +1963,7 @@ test("bridge MCP blocks an immediate bounce from the paired agent", async () => message: "ship it", target: "claude", }, - name: "send_to_agent", + name: "send_message", }, }), "\n", @@ -1905,7 +2049,7 @@ test("bridge MCP allows the same message later after unrelated traffic", async ( message: "ship it", target: "claude", }, - name: "send_to_agent", + name: "send_message", }, }), "\n", @@ -1971,7 +2115,7 @@ test("bridge MCP allows repeating the same message in the original direction", a message: "ship it", target: "codex", }, - name: "send_to_agent", + name: "send_message", }, }), "\n", @@ -2017,7 +2161,7 @@ test("bridge config helper builds the bridge MCP entry point for Codex", async ( "codex", ])}`, "-c", - 'mcp_servers.loop-bridge.tools.send_to_agent.approval_mode="approve"', + 'mcp_servers.loop-bridge.tools.send_message.approval_mode="approve"', "-c", 'mcp_servers.loop-bridge.tools.bridge_status.approval_mode="approve"', "-c", @@ -2117,7 +2261,7 @@ test("dispatchBridgeMessage reports delivered when direct codex delivery succeed expect(injectCodexMessage).toHaveBeenCalledWith( "ws://127.0.0.1:4500", "codex-thread-1", - "Claude: Please review the final diff." + expect.stringContaining("Claude: Please review the final diff.") ); expect(bridge.readPendingBridgeMessages(runDir)).toEqual([]); diff --git a/tests/loop/codex-app-server.test.ts b/tests/loop/codex-app-server.test.ts index d990dd1..808437e 100644 --- a/tests/loop/codex-app-server.test.ts +++ b/tests/loop/codex-app-server.test.ts @@ -357,7 +357,7 @@ test("startAppServer normalizes codex bridge config args before spawning", async "-c", expect.stringContaining("mcp_servers.loop-bridge.args="), "-c", - 'mcp_servers.loop-bridge.tools.send_to_agent.approval_mode="approve"', + 'mcp_servers.loop-bridge.tools.send_message.approval_mode="approve"', "-c", 'mcp_servers.loop-bridge.tools.bridge_status.approval_mode="approve"', "-c", diff --git a/tests/loop/paired-loop.test.ts b/tests/loop/paired-loop.test.ts index 550b8bf..a0a13ab 100644 --- a/tests/loop/paired-loop.test.ts +++ b/tests/loop/paired-loop.test.ts @@ -422,7 +422,7 @@ test("preparePairedOptions loads stored pair ids before planning", async () => { expect.arrayContaining([ expect.stringContaining("mcp_servers.loop-bridge.command="), expect.stringContaining("mcp_servers.loop-bridge.args="), - 'mcp_servers.loop-bridge.tools.send_to_agent.approval_mode="approve"', + 'mcp_servers.loop-bridge.tools.send_message.approval_mode="approve"', 'mcp_servers.loop-bridge.tools.bridge_status.approval_mode="approve"', 'mcp_servers.loop-bridge.tools.receive_messages.approval_mode="approve"', ]) @@ -610,7 +610,7 @@ test("runPairedLoop delivers forwarded bridge messages to the target agent", asy 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( - 'Send a message to the other agent with "send_to_agent"' + 'Send a message to the other agent with "send_message"' ); const events = bridgeInternals.readBridgeEvents(runDir); @@ -785,7 +785,7 @@ test("runPairedLoop delivers peer messages back to the primary agent", async () "Found one change to make before landing this." ); expect(calls[2]?.prompt).toContain( - 'Send a message to the other agent with "send_to_agent"' + 'Send a message to the other agent with "send_message"' ); }); }); @@ -862,7 +862,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_to_agent" using target: "claude"' + 'send the actionable notes to Claude with "send_message" using target: "claude"' ); }); }); diff --git a/tests/loop/tmux.test.ts b/tests/loop/tmux.test.ts index 6987ada..697d17e 100644 --- a/tests/loop/tmux.test.ts +++ b/tests/loop/tmux.test.ts @@ -791,9 +791,9 @@ test("tmux prompts keep the paired review workflow explicit", () => { 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).not.toContain("Wait briefly if it arrives"); expect(primaryPrompt).toContain( - 'Use "send_to_agent" with target: "claude" for Claude-facing messages' + 'Use "send_message" with target: "claude" for Claude-facing messages' ); expect(primaryPrompt).toContain("worktree isolation"); expect(peerPrompt).toContain("You are the reviewer/support agent."); @@ -801,7 +801,7 @@ 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_to_agent" 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 "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"); @@ -832,7 +832,7 @@ test("interactive tmux prompts tell both agents to wait for the human", () => { expect(primaryPrompt).toContain("ask Claude for a plan review"); expect(primaryPrompt).toContain("ask the human to review the plan"); expect(primaryPrompt).toContain( - 'Use "send_to_agent" with target: "claude" for Claude-facing messages' + 'Use "send_message" with target: "claude" for Claude-facing messages' ); expect(primaryPrompt).toContain("worktree isolation"); expect(peerPrompt).toContain("No task has been assigned yet."); @@ -843,7 +843,7 @@ 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_to_agent" 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 "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).toContain( "If you are answering Codex, use the bridge tools instead of a human-facing reply."