@minhspark/codex-mcp-bridge 1.17.0 → 1.18.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,20 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and [SemVer](ht
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [1.18.1] - 2026-09-25
8
+
9
+ ### Fixed
10
+
11
+ - Report npm installation completion with an unchanged version instead of claiming the package is already up to date after a same-version reinstall.
12
+ - Discover Claude Desktop sessions when accumulated task configuration exceeds 32 MiB. Retain only bounded identity fields and SHA-256 fingerprints while preserving account isolation, duplicate detection, and concurrent-change checks.
13
+ - Report Desktop task verification failures separately from missing peer endpoints in session discovery and bridge status.
14
+
15
+ ## [1.18.0] - 2026-09-23
16
+
17
+ ### Changed
18
+
19
+ - Guide agents to write every cross-agent prompt in English with a fixed section order: Goal, Context, Task, Scope, Constraints, Done when, Reply format. The Codex, Claude and VS Code bridges share one definition in their server instructions, sending-tool descriptions and prompt fields, and text supplied by the user is still sent unchanged.
20
+
7
21
  ## [1.17.0] - 2026-09-21
8
22
 
9
23
  ### Added
package/README.md CHANGED
@@ -231,6 +231,22 @@ Example requests:
231
231
 
232
232
  Use the exact project directory and destination task. If several Claude sessions match, specify the task ID. A `reply_received` receipt confirms a reply; a timeout does not mean the task stopped, so inspect it before retrying.
233
233
 
234
+ The sending tools ask agents to write each prompt in English with these sections, dropping any that do not apply: `Goal`, `Context`, `Task`, `Scope`, `Constraints`, `Done when`, `Reply format`. The first line names the sender, the project and the purpose. Text the user supplied is sent unchanged.
235
+
236
+ ```text
237
+ [From Claude Code · my-app · edit coordination]
238
+
239
+ ## Goal
240
+ Avoid conflicting edits while Claude Code patches `src/export.ps1`.
241
+
242
+ ## Task
243
+ 1. Do not modify `src/export.ps1` or `README.txt` until told otherwise.
244
+ 2. Write any unsaved edits to those files to disk now.
245
+
246
+ ## Reply format
247
+ Exactly one line: `DONE — changed: <files>` or `DONE — no changes`.
248
+ ```
249
+
234
250
  ## Important behavior
235
251
 
236
252
  - Desktop mode uses the native relay and the apps' permissions; it does not fall back to an external app-server.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@minhspark/codex-mcp-bridge",
3
- "version": "1.17.0",
3
+ "version": "1.18.1",
4
4
  "description": "Two-way MCP bridge between Claude and Codex: prompts into a live Codex thread, messages into a running Claude Code session.",
5
5
  "keywords": [
6
6
  "mcp",
@@ -22,7 +22,7 @@ import { createReleaseSnapshot, snapshotRoot } from "../src/release-snapshot.mjs
22
22
 
23
23
  exitForVersionRequest(import.meta.url);
24
24
 
25
- const VERSION = "1.17.0";
25
+ const VERSION = "1.18.1";
26
26
  const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
27
27
  const entry = path.join(root, "src", "mcp-supervisor.mjs");
28
28
  const entryArgs = ["native-relay-companion.mjs"];
@@ -130,7 +130,7 @@ try {
130
130
  } elseif ($before.Status -eq 'missing') {
131
131
  Write-Host "Successfully installed: @minhspark/codex-mcp-bridge v$($after.Version)"
132
132
  } elseif ($before.Version -ceq $after.Version) {
133
- Write-Host "Already up to date: @minhspark/codex-mcp-bridge v$($after.Version)"
133
+ Write-Host "Installation completed: @minhspark/codex-mcp-bridge v$($after.Version) (version unchanged)"
134
134
  } else {
135
135
  Write-Host "Successfully updated: @minhspark/codex-mcp-bridge v$($before.Version) -> v$($after.Version)"
136
136
  }
@@ -70,7 +70,7 @@ npm() {
70
70
  if [ -z "$bridge_after" ] || [ "$bridge_before_verified" -ne 1 ] || [ "$bridge_after_root" != "$bridge_root" ]; then
71
71
  printf 'Warning: npm completed, but the installed @minhspark/codex-mcp-bridge version could not be verified.\n' >&2
72
72
  elif [ "$bridge_after_root" = "$bridge_root" ] && [ "$bridge_after" = "$bridge_before" ]; then
73
- printf 'Already up to date: @minhspark/codex-mcp-bridge v%s\n' "$bridge_after"
73
+ printf 'Installation completed: @minhspark/codex-mcp-bridge v%s (version unchanged)\n' "$bridge_after"
74
74
  elif [ "$bridge_after_root" = "$bridge_root" ] && [ -n "$bridge_before" ]; then
75
75
  printf 'Successfully updated: @minhspark/codex-mcp-bridge v%s -> v%s\n' "$bridge_before" "$bridge_after"
76
76
  else
@@ -22,10 +22,11 @@ import { readClaudeAccountContext } from "./desktop-account-context.mjs";
22
22
  import { assertAccountIdentity, bindUnsolicitedClaudeMessageAccount, publicAccountState, readBridgeAccounts, requireBridgeAccounts, sameAccountIdentity } from "./bridge-account-context.mjs";
23
23
  import { resolveClaudeDesktopSession, sameClaudeDesktopRecipient } from "./claude-session-router.mjs";
24
24
  import { createHardenedRootPolicy } from "./hardened-root-policy.mjs";
25
+ import { AGENT_PROMPT_GUIDANCE, PROMPT_FIELD_HINT } from "./prompt-guidance.mjs";
25
26
 
26
27
  exitForVersionRequest(import.meta.url);
27
28
 
28
- const VERSION = "1.17.0";
29
+ const VERSION = "1.18.1";
29
30
  void import("./telemetry.mjs").then(({ startUsageReporting }) => startUsageReporting({ version: VERSION })).catch(() => {});
30
31
  const FORWARD_MIN_INTERVAL_MS = 5000;
31
32
  const FORWARD_MAX_PER_SESSION = 50;
@@ -163,6 +164,40 @@ function withDesktopContext(session, account = readClaudeAccountContext()) {
163
164
  ? { ...session, desktop: readClaudeDesktopContext(session, { account }), inbound: readClaudeInboundPolicy(session.cwd) } : session;
164
165
  }
165
166
 
167
+ function inspectDiscovery(sessions, account, expectedCwd) {
168
+ const eligible = [];
169
+ const excluded = [];
170
+ let nonDesktopCount = 0;
171
+ let candidateCount = 0;
172
+ for (const session of sessions) {
173
+ if (rootPolicy.enabled && !rootPolicy.allows(session.cwd)) continue;
174
+ if (expectedCwd !== undefined) {
175
+ try { assertClaudeSessionCwd(session, expectedCwd); }
176
+ catch { continue; }
177
+ }
178
+ if (desktopOnly && session.entrypoint !== "claude-desktop") {
179
+ nonDesktopCount += 1;
180
+ continue;
181
+ }
182
+ candidateCount += 1;
183
+ const inspected = withDesktopContext(session, account);
184
+ if (!desktopOnly || inspected.desktop?.status === "matched") {
185
+ eligible.push(inspected);
186
+ continue;
187
+ }
188
+ const status = inspected.desktop?.status ?? "missing";
189
+ const reason = inspected.desktop?.reason ?? "The Desktop task identity could not be verified.";
190
+ const previous = excluded.find((entry) => entry.status === status && entry.reason === reason);
191
+ if (previous) previous.count += 1;
192
+ else excluded.push({ status, reason, count: 1 });
193
+ }
194
+ return { sessions: eligible, discovery: { candidateCount, matchedCount: eligible.length, nonDesktopCount, excluded } };
195
+ }
196
+
197
+ function discoveryDetails(discovery) {
198
+ return discovery.excluded.map((entry) => `Desktop task verification: ${entry.count} candidate(s) ${entry.status} - ${entry.reason}`);
199
+ }
200
+
166
201
  function assertSessionRoot(session, label = "Claude session") {
167
202
  return rootPolicy.assert(session?.cwd, `${label} working directory`);
168
203
  }
@@ -261,7 +296,8 @@ const server = new McpServer(
261
296
  "The sender class follows the verified approval policy; automated review flags are reported but never change it. " +
262
297
  "A Desktop recipient whose settings refuse or hold inbound messages, or whose task metadata shows a different permission class without an explicit accept, is refused before sending, because Claude Desktop cannot show the approval dialog. " +
263
298
  "Never launch a CLI session or an external app-server as a substitute. A receipt confirms a reply, not visual verification in the app. " +
264
- "A held receipt does not prove that Desktop exposes an approval button; verify the UI before asking the user to approve.",
299
+ "A held receipt does not prove that Desktop exposes an approval button; verify the UI before asking the user to approve. " +
300
+ AGENT_PROMPT_GUIDANCE,
265
301
  },
266
302
  );
267
303
 
@@ -280,7 +316,7 @@ registerTool(
280
316
  inputSchema: {
281
317
  requestId: z.string().describe("Stable UUID for this creation; reuse exactly when inspecting or retrying"),
282
318
  cwd: z.string().describe("Exact absolute directory of the existing Claude Desktop project"),
283
- prompt: z.string().describe("Initial prompt for the NEW conversation; shown for user confirmation"),
319
+ prompt: z.string().describe(`Initial prompt for the NEW conversation; shown for user confirmation. ${PROMPT_FIELD_HINT}`),
284
320
  },
285
321
  annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false },
286
322
  },
@@ -338,17 +374,14 @@ registerTool(
338
374
  if (expectedCwd !== undefined) assertClaudeSessionCwd({ cwd: expectedCwd }, expectedCwd);
339
375
  const account = readClaudeAccountContext();
340
376
  if (desktopOnly && account.status !== "verified") return { ...textResult(`Claude account ${account.status}: ${account.reason} Sign in and retry discovery.`, true), structuredContent: { account: publicAccountState(account), sessions: [] } };
341
- const sessions = listClaudeSessions({ includeDead: includeDead ?? false }).filter(
342
- (s) => s.pid !== process.pid && (!desktopOnly || s.entrypoint === "claude-desktop"),
343
- ).filter((session) => !rootPolicy.enabled || rootPolicy.allows(session.cwd)
344
- ).filter((session) => {
345
- if (expectedCwd === undefined) return true;
346
- try { assertClaudeSessionCwd(session, expectedCwd); return true; }
347
- catch { return false; }
348
- }).map((session) => withDesktopContext(session, account)).filter((session) => !desktopOnly || session.desktop?.status === "matched");
349
- if (!sessions.length) return textResult(desktopOnly ? missingDesktopSession : "No live Claude Code session found.");
350
- return { ...textResult(`${sessions.length} Claude${desktopOnly ? " Desktop" : ""} session(s):\n\n${sessions.map(formatSessionRow).join("\n")}`),
351
- structuredContent: { account: publicAccountState(account), sessions: sessions.map(({ socket, bridgeSessionId, ...session }) => session) } };
377
+ const { sessions, discovery } = inspectDiscovery(listClaudeSessions({ includeDead: includeDead ?? false }).filter((s) => s.pid !== process.pid), account, expectedCwd);
378
+ const summary = sessions.length
379
+ ? `${sessions.length} Claude${desktopOnly ? " Desktop" : ""} session(s):\n\n${sessions.map(formatSessionRow).join("\n")}`
380
+ : discovery.excluded.length
381
+ ? `Found ${discovery.candidateCount} registered Claude Desktop candidate(s) with messaging endpoints, but their Desktop task identities could not be verified. Delivery remains blocked.`
382
+ : desktopOnly ? missingDesktopSession : "No live Claude Code session found.";
383
+ return { ...textResult([summary, ...discoveryDetails(discovery)].join("\n")),
384
+ structuredContent: { account: publicAccountState(account), sessions: sessions.map(({ socket, bridgeSessionId, ...session }) => session), discovery } };
352
385
  } catch (err) {
353
386
  return failure(err);
354
387
  }
@@ -369,7 +402,7 @@ registerTool(
369
402
  "The sender's current permissions are verified per call; environment overrides and manual binding cannot bypass an unknown sender. It never creates a replacement session.",
370
403
  inputSchema: {
371
404
  target: z.string().describe("Use auto for current-account discovery in expectedCwd, an exact session identity, or new to prepare a separate Desktop conversation requiring user confirmation"),
372
- message: z.string().describe("The message text to deliver"),
405
+ message: z.string().describe(`The message text to deliver. ${PROMPT_FIELD_HINT}`),
373
406
  expectedCwd: z.string().optional().describe("Exact absolute project directory independently verified by the caller; required in Desktop-only mode"),
374
407
  expectedTaskId: z.string().optional().describe("Exact native Claude Desktop task ID from list_claude_sessions; verify its title in the app before sending"),
375
408
  waitSec: z
@@ -644,9 +677,7 @@ registerTool(
644
677
  await peer.start();
645
678
  const sessions = listClaudeSessions().filter((s) => s.pid !== process.pid);
646
679
  const accounts = readBridgeAccounts();
647
- const eligible = sessions.filter((session) => !desktopOnly || session.entrypoint === "claude-desktop")
648
- .filter((session) => !rootPolicy.enabled || rootPolicy.allows(session.cwd))
649
- .filter((session) => !desktopOnly || withDesktopContext(session, accounts.claude).desktop?.status === "matched");
680
+ const { sessions: eligible, discovery } = inspectDiscovery(sessions, accounts.claude);
650
681
  const visibleInbox = peer.inbox.filter((record) => recordVisible(record, accounts));
651
682
  const visiblePending = [...peer.pendingMessages.keys()].filter((id) => readReceipt(id));
652
683
  const visibleForwarding = replyForwarder.status((record) => recordVisible(record, accounts));
@@ -664,7 +695,8 @@ registerTool(
664
695
  `Claude account: ${accounts.claude.status}${accounts.claude.fingerprint ? ` (${accounts.claude.fingerprint.slice(0, 12)})` : ` - ${accounts.claude.reason}`}`,
665
696
  `Codex account: ${accounts.codex.status}${accounts.codex.fingerprint ? ` (${accounts.codex.fingerprint.slice(0, 12)})` : ` - ${accounts.codex.reason}`}`,
666
697
  `live sessions: ${eligible.length}`,
667
- ...(rootPolicy.enabled ? [] : [`excluded: ${sessions.length - eligible.length} non-Desktop or inactive-account session(s)`]),
698
+ `excluded: ${discovery.nonDesktopCount} non-Desktop session(s); ${discovery.candidateCount - eligible.length} Desktop candidate(s) failed task verification`,
699
+ ...discoveryDetails(discovery),
668
700
  `relay thread: ${forwarding.threadId ?? "(none - use bind_codex_thread)"}`,
669
701
  `delivery: ${delivery.describe()}`,
670
702
  `inbox: ${visibleInbox.length} pending message(s)`,
@@ -674,7 +706,7 @@ registerTool(
674
706
  ];
675
707
  const state = runtime.status();
676
708
  lines.push(`runtime pid: ${state.pid}`, `loaded source: ${state.revision}`, `disk source: ${state.diskRevision ?? "unreadable"}`, `runtime state: ${state.current ? "current" : `STALE - ${state.reason}; reconnect this MCP server in the existing task`}`);
677
- return { ...textResult(lines.join("\n"), !state.current), structuredContent: { runtime: state, accounts: { claude: publicAccountState(accounts.claude), codex: publicAccountState(accounts.codex) }, replyForwarding: visibleForwarding, ...(sender ? { sender } : {}) } };
709
+ return { ...textResult(lines.join("\n"), !state.current), structuredContent: { runtime: state, accounts: { claude: publicAccountState(accounts.claude), codex: publicAccountState(accounts.codex) }, discovery, replyForwarding: visibleForwarding, ...(sender ? { sender } : {}) } };
678
710
  } catch (err) {
679
711
  return failure(err);
680
712
  }
@@ -1,3 +1,4 @@
1
+ import { createHash } from "node:crypto";
1
2
  import fs from "node:fs";
2
3
  import os from "node:os";
3
4
  import path from "node:path";
@@ -7,7 +8,7 @@ const ACCOUNT_ID = new RegExp(`^${UUID}$`, "i");
7
8
  const TASK_FILE = new RegExp(`^(local_${UUID})\\.json$`, "i");
8
9
  const MAX_ENTRIES = 8192;
9
10
  const MAX_FILE_BYTES = 4 * 1024 * 1024;
10
- const MAX_TOTAL_BYTES = 32 * 1024 * 1024;
11
+ const MAX_IDENTITY_BYTES = 32 * 1024 * 1024;
11
12
  const VERSION_FIELDS = ["size", "mtimeMs", "ctimeMs", "ino", "dev"];
12
13
 
13
14
  const sameVersion = (left, right) => VERSION_FIELDS.every((field) => left[field] === right[field]);
@@ -69,18 +70,16 @@ function readMetadata(file, budget) {
69
70
  try {
70
71
  const before = fs.fstatSync(descriptor);
71
72
  if (!before.isFile() || before.size > MAX_FILE_BYTES) throw new Error("file limit");
72
- budget.bytes += before.size;
73
- if (budget.bytes > MAX_TOTAL_BYTES) throw new Error("total limit");
74
73
  const bytes = Buffer.alloc(before.size);
75
74
  if (fs.readSync(descriptor, bytes, 0, bytes.length, 0) !== bytes.length) throw new Error("short read");
76
75
  const after = fs.fstatSync(descriptor);
77
76
  const current = fs.lstatSync(file);
78
77
  if (!sameVersion(before, after) || current.isSymbolicLink() || !sameVersion(current, after)) throw new Error("metadata changed");
79
78
  budget.versions.set(file, current);
80
- budget.contents.set(file, bytes);
79
+ budget.digests.set(file, createHash("sha256").update(bytes).digest("hex"));
81
80
  const data = JSON.parse(bytes.toString("utf8"));
82
81
  if (!data || typeof data !== "object" || Array.isArray(data)) throw new Error("invalid metadata");
83
- return {
82
+ const record = {
84
83
  taskId: data.sessionId,
85
84
  fileTaskId: TASK_FILE.exec(path.basename(file))[1],
86
85
  cliSessionId: data.cliSessionId,
@@ -90,18 +89,14 @@ function readMetadata(file, budget) {
90
89
  isArchived: data.isArchived,
91
90
  permissionMode: data.permissionMode,
92
91
  };
92
+ budget.bytes += Buffer.byteLength(JSON.stringify(record));
93
+ if (budget.bytes > MAX_IDENTITY_BYTES) throw new Error("identity limit");
94
+ return record;
93
95
  } finally {
94
96
  fs.closeSync(descriptor);
95
97
  }
96
98
  }
97
99
 
98
- /**
99
- * Stat fields cannot prove a record is unchanged: a same-size rewrite inside
100
- * one filesystem timestamp tick (about 16 ms on Windows) keeps size, times
101
- * and inode identical. The final consistency pass therefore compares the
102
- * bytes themselves; the records are small JSON files, so the extra read is
103
- * cheap and the check is deterministic on every platform.
104
- */
105
100
  function rereadMetadata(file) {
106
101
  const descriptor = fs.openSync(file, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW ?? 0));
107
102
  try {
@@ -109,7 +104,7 @@ function rereadMetadata(file) {
109
104
  if (!stat.isFile() || stat.size > MAX_FILE_BYTES) throw new Error("file limit");
110
105
  const bytes = Buffer.alloc(stat.size);
111
106
  if (fs.readSync(descriptor, bytes, 0, bytes.length, 0) !== bytes.length) throw new Error("short read");
112
- return bytes;
107
+ return createHash("sha256").update(bytes).digest("hex");
113
108
  } finally {
114
109
  fs.closeSync(descriptor);
115
110
  }
@@ -138,8 +133,14 @@ export function readClaudeDesktopContext(session, { platform = process.platform,
138
133
  const directory = root ?? (account ? path.join(account.root, "claude-code-sessions") : sessionsRoot(platform, env));
139
134
  if (!fs.existsSync(directory)) return result("missing", "Claude Desktop task metadata is not available on this host.");
140
135
  records = readClaudeDesktopTasks({ platform, env, root, account });
141
- } catch {
142
- return result("mismatch", "Claude Desktop task metadata could not be read completely and consistently; no task identity is confirmed.");
136
+ } catch (error) {
137
+ const detail = {
138
+ "file limit": " A task metadata file exceeds the 4 MiB per-file limit.",
139
+ "identity limit": " Task identity fields exceed the 32 MiB retained-data limit.",
140
+ "scan limit": " The task directory exceeds the 8192-entry scan limit.",
141
+ "metadata changed": " Task metadata changed during inspection; retry discovery.",
142
+ }[error.message] ?? "";
143
+ return result("mismatch", `Claude Desktop task metadata could not be read completely and consistently; no task identity is confirmed.${detail}`);
143
144
  }
144
145
 
145
146
  const primary = records.filter((record) => record.cliSessionId === session.sessionId);
@@ -198,13 +199,13 @@ export function readClaudeDesktopTasks({ platform = process.platform, env = proc
198
199
  }
199
200
  const directory = root ?? (account ? path.join(account.root, "claude-code-sessions") : sessionsRoot(platform, env));
200
201
  const files = metadataFiles(directory, account?.accountId);
201
- const budget = { bytes: 0, versions: new Map(), contents: new Map() };
202
+ const budget = { bytes: 0, versions: new Map(), digests: new Map() };
202
203
  const records = files.map((file) => readMetadata(file, budget));
203
204
  const currentFiles = metadataFiles(directory, account?.accountId);
204
205
  if (files.length !== currentFiles.length || files.some((file, index) => file !== currentFiles[index])) throw new Error("metadata changed");
205
206
  for (const file of files) {
206
207
  const current = fs.lstatSync(file);
207
- if (!current.isFile() || !sameVersion(current, budget.versions.get(file)) || !rereadMetadata(file).equals(budget.contents.get(file))) throw new Error("metadata changed");
208
+ if (!current.isFile() || !sameVersion(current, budget.versions.get(file)) || rereadMetadata(file) !== budget.digests.get(file)) throw new Error("metadata changed");
208
209
  }
209
210
  return records;
210
211
  }
package/src/index.mjs CHANGED
@@ -30,10 +30,11 @@ import { createRuntimeState } from "./runtime-state.mjs";
30
30
  import { assertRoutingReload, clientReloadReason, createReloadControl } from "./reload-control.mjs";
31
31
  import { accountIdentity, assertAccountIdentity, publicAccountState, readBridgeAccounts, requireBridgeAccounts } from "./bridge-account-context.mjs";
32
32
  import { assertClaudeSenderContext, readClaudeSenderContext, requireClaudeSenderContext } from "./claude-sender-context.mjs";
33
+ import { AGENT_PROMPT_GUIDANCE, PROMPT_FIELD_HINT } from "./prompt-guidance.mjs";
33
34
 
34
35
  exitForVersionRequest(import.meta.url);
35
36
 
36
- const VERSION = "1.17.0";
37
+ const VERSION = "1.18.1";
37
38
  void import("./telemetry.mjs").then(({ startUsageReporting }) => startUsageReporting({ version: VERSION })).catch(() => {});
38
39
  const log = (msg) => process.stderr.write(`[codex-mcp-bridge] ${msg}\n`);
39
40
 
@@ -345,7 +346,8 @@ const server = new McpServer(
345
346
  "requested cwd. With Desktop tasks enabled it assigns the exact saved project and starts visibly in " +
346
347
  "Codex Desktop using Desktop permissions. Otherwise it releases the bridge writer lock and opens the exact thread in " +
347
348
  "Codex Desktop. Use send_to_codex_thread only when an existing threadId is intentional; use " +
348
- "list_codex_threads or read_codex_thread to inspect sessions and codex_bridge_status to inspect wiring.",
349
+ "list_codex_threads or read_codex_thread to inspect sessions and codex_bridge_status to inspect wiring. " +
350
+ AGENT_PROMPT_GUIDANCE,
349
351
  },
350
352
  );
351
353
 
@@ -400,7 +402,7 @@ registerTool(
400
402
  "return Codex's reply, and hand the session to Codex Desktop without leaving the bridge writer lock behind.",
401
403
  inputSchema: {
402
404
  cwd: z.string().describe("Absolute project directory where Codex must work"),
403
- prompt: z.string().describe("The complete task Claude is delegating to Codex"),
405
+ prompt: z.string().describe(`The complete task Claude is delegating to Codex. ${PROMPT_FIELD_HINT}`),
404
406
  name: z.string().min(1).max(200).optional().describe("Optional Codex session title; otherwise one is derived from the prompt"),
405
407
  timeoutSec: z
406
408
  .number()
@@ -494,7 +496,7 @@ registerTool(
494
496
  "Do not close the task, create a replacement, or ask the user to copy the message manually.",
495
497
  inputSchema: {
496
498
  threadId: z.string().describe("Codex thread id (UUID) - get it from list_codex_threads"),
497
- prompt: z.string().describe("The message to send to Codex, exactly as a user would type it"),
499
+ prompt: z.string().describe(`The message to send to Codex as a new user turn. ${PROMPT_FIELD_HINT}`),
498
500
  timeoutSec: z
499
501
  .number()
500
502
  .int()
@@ -703,7 +705,7 @@ registerTool(
703
705
  description: "Start a Codex task. In Desktop mode include the initial prompt to create and assign a visible task atomically; use delegate_to_codex to also wait for its reply.",
704
706
  inputSchema: {
705
707
  cwd: z.string().describe("Absolute working directory for the new Codex session"),
706
- prompt: z.string().min(1).optional().describe("Initial task; required with CODEX_BRIDGE_DESKTOP_TASKS=1, starts immediately"),
708
+ prompt: z.string().min(1).optional().describe(`Initial task; required with CODEX_BRIDGE_DESKTOP_TASKS=1, starts immediately. ${PROMPT_FIELD_HINT}`),
707
709
  model: z.string().optional().describe("Model override, e.g. gpt-5.6-luna"),
708
710
  name: z.string().min(1).max(200).optional().describe("Optional title to show for the new Codex session"),
709
711
  },
@@ -34,7 +34,7 @@ import { createHardenedRootPolicy } from "./hardened-root-policy.mjs";
34
34
 
35
35
  exitForVersionRequest(import.meta.url);
36
36
 
37
- const VERSION = "1.17.0";
37
+ const VERSION = "1.18.1";
38
38
  const log = (msg) => process.stderr.write(`[native-relay] ${msg}\n`);
39
39
 
40
40
  function errorResponse(code, message, sent) {
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Every message this bridge carries is a prompt one agent wrote for another.
3
+ * A single paragraph that runs the request into the reply format leaves the
4
+ * recipient guessing where the task ends, so the sending tools all describe
5
+ * one shape. It lives here so the Codex, Claude and VS Code bridges cannot
6
+ * drift apart.
7
+ */
8
+ export const PROMPT_SECTIONS = Object.freeze([
9
+ "Goal",
10
+ "Context",
11
+ "Task",
12
+ "Scope",
13
+ "Constraints",
14
+ "Done when",
15
+ "Reply format",
16
+ ]);
17
+
18
+ export const AGENT_PROMPT_GUIDANCE =
19
+ "Write every prompt you compose for another agent in English, in this order, omitting sections that do not apply: " +
20
+ "a first line naming the sender, project and purpose, e.g. [From Claude Code · <project> · <purpose>]; " +
21
+ "## Goal, the outcome in one sentence; ## Context, measured facts, paths with line numbers and existing code to reuse; " +
22
+ "## Task, numbered imperative steps; ## Scope, the files that may and must not change; " +
23
+ "## Constraints, conventions and tests, plus: if the current state differs from this description, stop and report instead of reconciling it; " +
24
+ "## Done when, checkable criteria; ## Reply format, the exact shape of the reply. " +
25
+ "A short coordination message needs only Goal, Context, Task and Reply format. " +
26
+ "Send text the user supplied verbatim, and keep file names, identifiers and user-facing strings in their original language.";
27
+
28
+ export const PROMPT_FIELD_HINT =
29
+ `The prompt for the recipient agent, in English with the sections ${PROMPT_SECTIONS.join(", ")} ` +
30
+ "(omit any that do not apply); text the user supplied is sent verbatim";
@@ -10,6 +10,7 @@ import { findRollout, readState, readCodexSenderContext } from "./codex-sender-c
10
10
  import { readProcessAncestry } from "./claude-sender-context.mjs";
11
11
  import { listClaudeSessions, assertClaudeSessionCwd, assertClaudeSessionProcess, PeerEndpoint } from "./peer-protocol.mjs";
12
12
  import { readClaudeInboundPolicy } from "./claude-inbound-policy.mjs";
13
+ import { AGENT_PROMPT_GUIDANCE, PROMPT_FIELD_HINT } from "./prompt-guidance.mjs";
13
14
 
14
15
  const host = process.argv[2];
15
16
  if (!["claude", "codex"].includes(host)) throw new Error("Specify the calling extension: claude or codex");
@@ -102,7 +103,7 @@ tool("vscode_bridge_status", "Verify this extension caller and list same-project
102
103
  });
103
104
 
104
105
  if (host === "claude") {
105
- tool("send_to_codex_vscode", "Send to an existing Codex extension task in this project, preserving its selected permissions. Never retries or opens another app. Use read_codex_vscode_reply after submission.", { threadId: z.string(), message: z.string().min(1).max(50000) }, async ({ threadId, message }, sender, meta) => {
106
+ tool("send_to_codex_vscode", `Send to an existing Codex extension task in this project, preserving its selected permissions. Never retries or opens another app. Use read_codex_vscode_reply after submission. ${AGENT_PROMPT_GUIDANCE}`, { threadId: z.string(), message: z.string().min(1).max(50000).describe(PROMPT_FIELD_HINT) }, async ({ threadId, message }, sender, meta) => {
106
107
  if (unresolved.has(threadId)) throw new Error("An earlier send is unresolved. Read its reply before sending again.");
107
108
  const inspect = () => {
108
109
  const state = codexTask(threadId, sender.cwd);
@@ -127,7 +128,7 @@ if (host === "claude") {
127
128
  return { threadId, ...completion };
128
129
  });
129
130
  } else {
130
- tool("send_to_claude_vscode", "Send to the exact Claude Code VS Code session in this project. Never changes recipient permissions. Do not ask the recipient to call back while this synchronous request waits.", { sessionId: z.string(), message: z.string().min(1).max(50000), waitSec: z.number().int().min(0).max(45).default(30) }, async ({ sessionId, message, waitSec }, sender, meta) => {
131
+ tool("send_to_claude_vscode", `Send to the exact Claude Code VS Code session in this project. Never changes recipient permissions. Do not ask the recipient to call back while this synchronous request waits. ${AGENT_PROMPT_GUIDANCE}`, { sessionId: z.string(), message: z.string().min(1).max(50000).describe(PROMPT_FIELD_HINT), waitSec: z.number().int().min(0).max(45).default(30) }, async ({ sessionId, message, waitSec }, sender, meta) => {
131
132
  const inspect = () => {
132
133
  const matches = listClaudeSessions().filter((session) => session.entrypoint === "claude-vscode" && session.sessionId === sessionId);
133
134
  if (matches.length !== 1) throw new Error("No unique live Claude Code VS Code session matches");