Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
1 change: 1 addition & 0 deletions src/loop/bridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,7 @@ const claudeChannelInstructions = (): string =>
[
`Messages from the Codex agent arrive as <channel source="${BRIDGE_SERVER}" chat_id="..." user="${CLAUDE_CHANNEL_USER}" ...>.`,
'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.",

Choose a reason for hiding this comment

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

medium

This line contains two distinct instructions. To improve clarity for the agent, it would be better to split them into separate lines.

Suggested change
"Never answer the human when the inbound message came from Codex. Send the response back through the bridge tools instead.",
"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");
Expand Down
6 changes: 4 additions & 2 deletions src/loop/paired-loop.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.',

Choose a reason for hiding this comment

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

medium

This line combines multiple instructions. For better clarity for the agent, consider splitting it into separate lines, each with a single instruction.

    'Do not ask the human to relay messages between agents.',
    'Do not 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");
};
Expand Down Expand Up @@ -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.",

Choose a reason for hiding this comment

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

medium

For better clarity and to ensure each instruction is distinct for the agent, consider splitting this line into two separate instructions. This aligns with the goal of breaking down complex instructions into simpler, more direct ones.

Suggested change
"Treat this as direct agent-to-agent coordination. Do not reply to the human.",
"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 => {
Expand Down
134 changes: 80 additions & 54 deletions src/loop/tmux.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -122,13 +124,21 @@ const appendProofPrompt = (parts: string[], proof: string): void => {
parts.push(`Proof requirements:\n${trimmed}`);
};

const pairedBridgeGuidance = (agent: Agent): string => {
const peer = agent === "claude" ? "Codex" : "Claude";
const pairedBridgeGuidance = (agent: Agent, runId: string): string => {
const serverName = buildClaudeChannelServerName(runId);

if (agent === "claude") {
return [
`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.',
].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.`,
"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.',
'Message Claude with "send_to_agent", not a human-facing message.',
'Use "bridge_status" or "receive_messages" only if delivery looks stuck.',
].join("\n");
};

Expand All @@ -138,104 +148,119 @@ 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");
}

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 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");
};

const buildPrimaryPrompt = (task: string, opts: Options): string => {
const buildPrimaryPrompt = (
task: string,
opts: Options,
runId: string
): 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}.`,
];
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} 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");
};

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 = [
`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.`,
];
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. 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.`
`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 = [
`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" when you want ${peer} to review work, answer questions, 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(
`${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 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. 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 = [
`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)}. 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)}. 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. I am waiting for your request." After that, wait for the human or ${primary} to provide a concrete task or review request.`
`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 => {
Expand Down Expand Up @@ -675,13 +700,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;
};

Expand All @@ -701,7 +732,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;
Expand Down Expand Up @@ -805,16 +836,17 @@ 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,
...buildClaudeCommand(
claudeSessionId,
resolveTmuxModel("claude", launch.opts),
claudeChannelServer,
hadClaudeSession
hadClaudeSession,
hadClaudeSession ? undefined : claudePrompt
),
]);
const codexCommand = buildShellCommand([
Expand Down Expand Up @@ -870,18 +902,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);
}
Expand Down
3 changes: 3 additions & 0 deletions tests/loop/bridge.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
8 changes: 8 additions & 0 deletions tests/loop/paired-loop.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -773,13 +777,17 @@ 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:"
);
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"'
);
});
});

Expand Down
Loading
Loading