@junghanacs/entwurf 0.20.1 → 0.22.0

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.
Files changed (111) hide show
  1. package/AGENTS.md +85 -190
  2. package/BASELINE.md +6 -3
  3. package/CHANGELOG.md +473 -14
  4. package/CONTRIBUTING.md +1 -1
  5. package/DELIVERY.md +332 -60
  6. package/README.md +100 -22
  7. package/VERIFY.md +93 -7
  8. package/docs/acp-backend-rail.md +0 -1
  9. package/docs/external-mcp-host.md +64 -33
  10. package/docs/setup-clean-host.md +151 -17
  11. package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +130 -114
  12. package/mcp/entwurf-bridge/dist/pi-extensions/lib/codex-caller-seat.js +174 -0
  13. package/mcp/entwurf-bridge/dist/pi-extensions/lib/codex-fresh-preflight.js +519 -0
  14. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-fact-provider.js +22 -2
  15. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-peers-render.js +3 -1
  16. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-contract.js +6 -7
  17. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-native-push.js +30 -17
  18. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-production.js +7 -1
  19. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-runner.js +3 -2
  20. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-send.js +8 -4
  21. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-surface.js +7 -4
  22. package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-mailbox-body.js +20 -11
  23. package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-sender-identity.js +227 -1
  24. package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +189 -19
  25. package/mcp/entwurf-bridge/dist/pi-extensions/lib/native-push/adapter.js +10 -7
  26. package/mcp/entwurf-bridge/dist/pi-extensions/lib/native-push/codex-ws-client.js +403 -0
  27. package/mcp/entwurf-bridge/dist/pi-extensions/lib/native-push/register.js +4 -4
  28. package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-hook-codex.js +325 -0
  29. package/mcp/entwurf-bridge/dist/scripts/codex-socket-path.js +30 -0
  30. package/mcp/entwurf-bridge/dist/scripts/meta-bridge-fresh-cut.js +6 -1
  31. package/mcp/entwurf-bridge/src/index.ts +154 -116
  32. package/mcp/entwurf-bridge/tsconfig.build.json +10 -0
  33. package/package.json +13 -4
  34. package/pi-extensions/entwurf-control.ts +71 -19
  35. package/pi-extensions/lib/codex-caller-seat.ts +204 -0
  36. package/pi-extensions/lib/codex-fresh-preflight.ts +580 -0
  37. package/pi-extensions/lib/compaction-send-guard.ts +80 -0
  38. package/pi-extensions/lib/entwurf-fact-provider.ts +29 -3
  39. package/pi-extensions/lib/entwurf-peers-render.ts +4 -1
  40. package/pi-extensions/lib/entwurf-v2-contract.ts +6 -7
  41. package/pi-extensions/lib/entwurf-v2-native-push.ts +35 -18
  42. package/pi-extensions/lib/entwurf-v2-production.ts +10 -3
  43. package/pi-extensions/lib/entwurf-v2-runner.ts +3 -2
  44. package/pi-extensions/lib/entwurf-v2-send.ts +16 -11
  45. package/pi-extensions/lib/entwurf-v2-surface.ts +7 -4
  46. package/pi-extensions/lib/meta-mailbox-body.ts +22 -13
  47. package/pi-extensions/lib/meta-sender-identity.ts +305 -0
  48. package/pi-extensions/lib/mux-fresh-call.ts +233 -29
  49. package/pi-extensions/lib/native-push/adapter.ts +21 -24
  50. package/pi-extensions/lib/native-push/codex-ws-client.ts +506 -0
  51. package/pi-extensions/lib/native-push/register.ts +7 -9
  52. package/pi-extensions/meta-bridge-hook-codex.ts +371 -0
  53. package/run.sh +342 -28
  54. package/scripts/check-agy-sender-identity.ts +1 -1
  55. package/scripts/check-codex-app-server-launch.ts +445 -0
  56. package/scripts/check-codex-birth-hook.ts +264 -0
  57. package/scripts/check-codex-bridge-identity.ts +179 -0
  58. package/scripts/check-codex-native-push.ts +386 -0
  59. package/scripts/check-codex-sender-identity.ts +495 -0
  60. package/scripts/check-compaction-send-guard.ts +130 -0
  61. package/scripts/check-copilot-receive-arm.ts +4 -1
  62. package/scripts/check-entwurf-fact-provider.ts +38 -0
  63. package/scripts/check-entwurf-peers-surface.ts +13 -1
  64. package/scripts/check-entwurf-self-address.ts +15 -16
  65. package/scripts/check-entwurf-v2-contract.ts +4 -3
  66. package/scripts/check-entwurf-v2-decider.ts +7 -5
  67. package/scripts/check-entwurf-v2-native-push.ts +35 -7
  68. package/scripts/check-entwurf-v2-production.ts +245 -12
  69. package/scripts/check-entwurf-v2-runner.ts +1 -1
  70. package/scripts/check-entwurf-v2-send.ts +26 -7
  71. package/scripts/check-entwurf-v2-surface.ts +1 -1
  72. package/scripts/check-gate-qualification.ts +8 -3
  73. package/scripts/check-harness-admission-parity.ts +0 -1
  74. package/scripts/check-mux-launch-tmux.ts +345 -4
  75. package/scripts/check-native-push-adapter.ts +20 -16
  76. package/scripts/check-native-push-register.ts +5 -1
  77. package/scripts/check-release-gate-outcomes.ts +47 -1
  78. package/scripts/check-setup-qualification.sh +3 -1
  79. package/scripts/codex-app-server-launch.sh +275 -0
  80. package/scripts/codex-birth-doctor.sh +276 -0
  81. package/scripts/codex-birth-install.sh +414 -0
  82. package/scripts/codex-birth-uninstall.sh +170 -0
  83. package/scripts/codex-mcp-config.py +435 -0
  84. package/scripts/codex-socket-path.ts +33 -0
  85. package/scripts/codex-statusline-config.py +434 -0
  86. package/scripts/codex-terminal-title-config.py +500 -0
  87. package/scripts/codex_toml_io.py +653 -0
  88. package/scripts/lib/codex-fresh-live-protocol.ts +121 -0
  89. package/scripts/lib/codex-fresh-source-receipts.ts +426 -0
  90. package/scripts/lib/launch-receipt-windows.ts +46 -0
  91. package/scripts/lib/tmux-coordinate-row.ts +58 -0
  92. package/scripts/meta-bridge-fresh-cut.ts +6 -1
  93. package/scripts/mutants/codex-app-server-launch.json +157 -0
  94. package/scripts/mutants/codex-caller-seat.json +336 -0
  95. package/scripts/mutants/codex-native.json +838 -0
  96. package/scripts/mutants/compaction-send-guard.json +103 -0
  97. package/scripts/mutants/entwurf-peers.json +19 -0
  98. package/scripts/mutants/mux-fresh-call.json +93 -10
  99. package/scripts/mutants/omp-fresh.json +6 -4
  100. package/scripts/mutants/release-gate.json +13 -0
  101. package/scripts/mutants/v2-surface.json +75 -1
  102. package/scripts/raw-async-delivery/README.md +2 -1
  103. package/scripts/raw-codex-measure/README.md +114 -46
  104. package/scripts/smoke-agy-native-push-live.ts +3 -1
  105. package/scripts/smoke-codex-birth.sh +347 -0
  106. package/scripts/smoke-codex-config-state.sh +700 -0
  107. package/scripts/smoke-codex-fresh-live.ts +1426 -0
  108. package/scripts/smoke-codex-native-push-live.ts +75 -0
  109. package/scripts/smoke-entwurf-chain-live.ts +50 -0
  110. package/scripts/smoke-setup-verdict.sh +125 -10
  111. package/scripts/tsconfig.json +1 -0
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Pure message builders and final mailbox selection for Codex first-admission LIVE.
3
+ *
4
+ * Historical model launch/address reports are deliberately gone. The fixture now observes
5
+ * Pi toolCall↔toolResult rows, Codex thread/read MCP items, and raw callback envelopes directly.
6
+ * The one remaining mailbox message is Codex's final source-validated payload; token and sender
7
+ * are selected together, and duplicate exact candidates fail closed.
8
+ *
9
+ * The initial Pi receives its own outer sender_info after the user payload. The forwarded Codex
10
+ * instruction therefore rides between explicit BEGIN/END lines and the prompt says that neither
11
+ * marker nor the generated sender_info after END belongs to the forwarded bytes. Exact source-call
12
+ * arguments enforce that boundary; no suffix stripping repairs model drift after the fact.
13
+ */
14
+
15
+ /** Sender envelope line of a delivered meta-mailbox body: ` session: <garden id>`. */
16
+ const SENDER_LINE = /^\s*session:\s+(\S+)/m;
17
+
18
+ export function reportSender(body: string): string | null {
19
+ return SENDER_LINE.exec(body)?.[1] ?? null;
20
+ }
21
+
22
+ /**
23
+ * The report whose token AND sender both match. Both halves are decided BEFORE the wait ends:
24
+ * a matching token from somebody else is skipped, not returned and then asserted on.
25
+ */
26
+ export function selectReport(bodies: readonly string[], token: string, expectedSender: string): string | null {
27
+ const matching: string[] = [];
28
+ for (const body of bodies) {
29
+ if (!body.includes(token)) continue;
30
+ if (reportSender(body) !== expectedSender) continue;
31
+ matching.push(body);
32
+ }
33
+ if (matching.length > 1) throw new Error(`duplicate exact mailbox reports (${matching.length})`);
34
+ return matching[0] ?? null;
35
+ }
36
+
37
+ export interface CodexInstructionInput {
38
+ waitToken: string;
39
+ finalToken: string;
40
+ callerGid: string;
41
+ piModel: string;
42
+ }
43
+
44
+ /** The payload the initial Pi forwards verbatim. It names only Codex's own steps. */
45
+ export function buildCodexInstruction(input: CodexInstructionInput): string {
46
+ return (
47
+ `${input.waitToken}\nThis is the addressed instruction from the initial Pi caller. Execute once, with no shell, ` +
48
+ "terminal, screen inspection, or retry:\n" +
49
+ `1. Call public mcp__entwurf_bridge__entwurf_fresh_call once with backend pi, model ${input.piModel}, ` +
50
+ // NO placement AND NO cwd are both CLAIMS rather than conveniences, and they are two
51
+ // different ones. The seat must come from this Codex's own pane title (#95 lane B); the
52
+ // DIRECTORY must come from this Codex's own record (#95 lane C), which is a fact only
53
+ // because its thread now opens where its pane is rather than where the app-server sits.
54
+ // Omitting the parameter is the whole point — passing one would prove neither.
55
+ 'NO cwd parameter and NO placement parameter, and task "After your automatic callback succeeds, answer ACK and ' +
56
+ 'stop. Do not open another sibling."\n' +
57
+ "2. Stop and wait for the Pi callback delivered into this Codex thread. Accept only a callback whose body " +
58
+ "repeats the exact nonce from your LAUNCH receipt and whose sender envelope names a garden id.\n" +
59
+ `3. Only after that match, call public mcp__entwurf_bridge__entwurf_v2 once with target ${input.callerGid}, ` +
60
+ "intent fire-and-forget, wants_reply false, and message:\n" +
61
+ `${input.finalToken}\nPI_LAUNCH_NONCE=<exact launch nonce>\nPI_CALLBACK_NONCE=<same exact nonce>\n` +
62
+ "PI_CALLBACK_FROM=<sender-envelope garden id>\nPI_SESSION_ID=<exact $session id from the LAUNCH receipt>\n" +
63
+ "PI_WINDOW_ID=<exact @window id from the LAUNCH receipt>\nDo not claim completion in prose."
64
+ );
65
+ }
66
+
67
+ export interface InitialPiPhaseOneInput {
68
+ initialPiWaitToken: string;
69
+ codexWaitToken: string;
70
+ codexModel: string;
71
+ scratch: string;
72
+ codexInstruction: string;
73
+ }
74
+
75
+ /** One bounded forwarded payload; source arguments, not model prose, are its oracle. */
76
+ export function buildInitialPiPhaseOne(input: InitialPiPhaseOneInput): string {
77
+ return (
78
+ `${input.initialPiWaitToken}\nExecute this public-tool chain exactly once; do not use shell, terminal, screen text, or retries:\n` +
79
+ // NO placement on BOTH legs since #95 D1: an omitted seat is the caller's own session for
80
+ // every backend, so this Codex opens in S beside the Pi that called it — which is what
81
+ // puts its TUI somewhere the app-server's inherited `TMUX` does not point.
82
+ `1. Call entwurf_fresh_call with backend codex, model ${input.codexModel}, cwd ${input.scratch}, NO placement, and task ` +
83
+ `"After your required callback receipt, end the turn and wait passively for the addressed instruction containing ${input.codexWaitToken}. ` +
84
+ `Do not call shell, sleep, terminal, or any tool to wait."\n` +
85
+ "2. Stop and wait for the Codex callback. Accept only a callback carrying the exact nonce from the LAUNCH " +
86
+ "receipt; take the Codex garden id only from its sender envelope.\n" +
87
+ "3. Call entwurf_v2 once to that callback-derived Codex garden id with intent fire-and-forget, wants_reply false, " +
88
+ "and message set to the exact bytes strictly between the BEGIN/END marker lines below; the marker lines and " +
89
+ "the generated sender_info after END are not part of the message. Then STOP. Send nothing else and contact " +
90
+ "nobody else.\n" +
91
+ "----- BEGIN MESSAGE FOR THE CODEX SIBLING -----\n" +
92
+ input.codexInstruction +
93
+ "\n----- END MESSAGE FOR THE CODEX SIBLING -----"
94
+ );
95
+ }
96
+
97
+ export interface CleanupStage {
98
+ label: string;
99
+ run: () => unknown | Promise<unknown>;
100
+ }
101
+
102
+ /**
103
+ * Run every teardown stage even when an earlier recovery or cleanup stage throws. A LIVE must
104
+ * report cleanup trouble, but that trouble must never prevent the later owned resources from
105
+ * being closed. Returned stage failures are labelled so the caller can fold them into its one
106
+ * final verdict without losing the original run failure.
107
+ */
108
+ export async function runCleanupStages(stages: readonly CleanupStage[]): Promise<string[]> {
109
+ const failures: string[] = [];
110
+ for (const stage of stages) {
111
+ try {
112
+ const reported = await stage.run();
113
+ if (Array.isArray(reported)) {
114
+ for (const failure of reported) failures.push(`${stage.label}: ${failure}`);
115
+ }
116
+ } catch (error) {
117
+ failures.push(`${stage.label}: threw: ${error instanceof Error ? error.message : String(error)}`);
118
+ }
119
+ }
120
+ return failures;
121
+ }
@@ -0,0 +1,426 @@
1
+ import { isDeepStrictEqual } from "node:util";
2
+ import { launchReceiptWindows } from "./launch-receipt-windows.ts";
3
+
4
+ export type SourceToolStatus = "pending" | "completed" | "failed";
5
+
6
+ export interface SourceToolReceipt {
7
+ toolName: string;
8
+ arguments: Record<string, unknown>;
9
+ text: string;
10
+ status: SourceToolStatus;
11
+ isError: boolean;
12
+ }
13
+
14
+ export interface ExpectedSourceToolCall {
15
+ toolName: string;
16
+ arguments: Record<string, unknown>;
17
+ }
18
+
19
+ /**
20
+ * Compare source-call arguments by MEANING for the one key whose absence already means something,
21
+ * and byte-exactly for every other.
22
+ *
23
+ * `entwurf_v2`'s `wants_reply` is optional and absent IS false — the schema says so
24
+ * (`mcp/entwurf-bridge/src/index.ts:474`, `z.boolean().optional().describe("… (default false)")`)
25
+ * and the decider enforces it (`pi-extensions/lib/entwurf-v2-decider.ts:270`,
26
+ * `input.wantsReply ?? false`). So a model that reads "wants_reply false" in its instruction and
27
+ * simply omits the parameter has obeyed the instruction; nothing about the dispatch differs.
28
+ *
29
+ * `[측정 2026-09-16]` this cost a LIVE run at assertion 43: the initial Pi called `entwurf_v2`
30
+ * with byte-identical `target`, `intent` and `message` and no `wants_reply` key, and the
31
+ * `isDeepStrictEqual` oracle called it drift. The gate was reading a serialization shape where
32
+ * its claim is about the dispatch. Only THIS key is normalized — a `wants_reply: true` still
33
+ * mismatches an expected `false`, and `target`/`intent`/`message`/`placement`/`cwd` stay exact,
34
+ * because for those a difference is a difference.
35
+ */
36
+ function normalizeSourceArguments(args: Record<string, unknown>): Record<string, unknown> {
37
+ if (!("wants_reply" in args)) return { ...args, wants_reply: false };
38
+ return args;
39
+ }
40
+
41
+ /** Arguments agree once the schema default above is filled in. */
42
+ export function sourceArgumentsMatch(expected: Record<string, unknown>, actual: Record<string, unknown>): boolean {
43
+ return isDeepStrictEqual(normalizeSourceArguments(expected), normalizeSourceArguments(actual));
44
+ }
45
+
46
+ /** Select one source call in its backend/target scope, rejecting drift, errors, and duplicates. */
47
+ export function selectExactSourceToolReceipt(
48
+ receipts: readonly SourceToolReceipt[],
49
+ toolName: string,
50
+ expectedArguments: Record<string, unknown>,
51
+ label: string,
52
+ ): SourceToolReceipt | null {
53
+ const scopeKey = toolName === "entwurf_v2" ? "target" : null;
54
+ const matchingScope = receipts.filter(
55
+ (receipt) =>
56
+ receipt.toolName === toolName &&
57
+ (scopeKey === null || receipt.arguments[scopeKey] === expectedArguments[scopeKey]),
58
+ );
59
+ if (matchingScope.length > 1) throw new Error(`${label}: duplicate source tool calls (${matchingScope.length})`);
60
+ const receipt = matchingScope[0] ?? null;
61
+ if (receipt === null) return null;
62
+ if (!sourceArgumentsMatch(expectedArguments, receipt.arguments))
63
+ throw new Error(
64
+ `${label}: source tool arguments differ\nexpected=${JSON.stringify(expectedArguments)}\nactual=${JSON.stringify(receipt.arguments)}`,
65
+ );
66
+ if (receipt.status === "pending") return null;
67
+ if (receipt.status === "failed") throw new Error(`${label}: exact source tool result failed: ${receipt.text}`);
68
+ return receipt;
69
+ }
70
+
71
+ export function assertSourceCallScopes(
72
+ receipts: readonly SourceToolReceipt[],
73
+ toolName: string,
74
+ scopeKey: string,
75
+ allowed: readonly unknown[],
76
+ label: string,
77
+ ): void {
78
+ for (const receipt of receipts) {
79
+ if (receipt.toolName !== toolName) continue;
80
+ if (!allowed.includes(receipt.arguments[scopeKey]))
81
+ throw new Error(`${label}: unexpected ${scopeKey} ${JSON.stringify(receipt.arguments[scopeKey])}`);
82
+ }
83
+ }
84
+
85
+ /** Final exact-once audit over every source-observed Entwurf role in one smoke citizen. */
86
+ export function assertExactSourceToolCalls(
87
+ receipts: readonly SourceToolReceipt[],
88
+ expected: readonly ExpectedSourceToolCall[],
89
+ label: string,
90
+ ): void {
91
+ if (receipts.length !== expected.length)
92
+ throw new Error(`${label}: source call count differs: expected=${expected.length} actual=${receipts.length}`);
93
+ const remaining = [...expected];
94
+ for (const receipt of receipts) {
95
+ if (receipt.status !== "completed") throw new Error(`${label}: ${receipt.toolName} remained ${receipt.status}`);
96
+ const index = remaining.findIndex(
97
+ (candidate) =>
98
+ candidate.toolName === receipt.toolName && sourceArgumentsMatch(candidate.arguments, receipt.arguments),
99
+ );
100
+ if (index < 0)
101
+ throw new Error(`${label}: unexpected source call ${receipt.toolName} ${JSON.stringify(receipt.arguments)}`);
102
+ remaining.splice(index, 1);
103
+ }
104
+ if (remaining.length > 0) throw new Error(`${label}: missing source calls ${JSON.stringify(remaining)}`);
105
+ }
106
+
107
+ /** The only production leaf that turns joined source receipts into cleanup window authority. */
108
+ export function sourceCleanupWindows(receipts: readonly SourceToolReceipt[]): string[] {
109
+ const windows: string[] = [];
110
+ for (const receipt of receipts) {
111
+ if (receipt.toolName !== "entwurf_fresh_call" || receipt.status !== "completed" || receipt.isError) continue;
112
+ const backend = receipt.arguments.backend;
113
+ if (backend !== "pi" && backend !== "codex") continue;
114
+ windows.push(...launchReceiptWindows(receipt.text, backend));
115
+ }
116
+ return windows;
117
+ }
118
+
119
+ function textContent(value: unknown): string {
120
+ if (!Array.isArray(value)) return "";
121
+ return value
122
+ .map((block) => {
123
+ if (typeof block !== "object" || block === null) return "";
124
+ const text = (block as { text?: unknown }).text;
125
+ return typeof text === "string" ? text : "";
126
+ })
127
+ .join("\n");
128
+ }
129
+
130
+ function looksLikeIncompleteJsonAppend(source: string): boolean {
131
+ let depth = 0;
132
+ let inString = false;
133
+ let escaped = false;
134
+ for (const character of source) {
135
+ if (inString) {
136
+ if (escaped) escaped = false;
137
+ else if (character === "\\") escaped = true;
138
+ else if (character === '"') inString = false;
139
+ continue;
140
+ }
141
+ if (character === '"') inString = true;
142
+ else if (character === "{" || character === "[") depth++;
143
+ else if (character === "}" || character === "]") depth--;
144
+ if (depth < 0) return false;
145
+ }
146
+ return inString || depth > 0;
147
+ }
148
+
149
+ /**
150
+ * Resolve a source transcript path whose own record may not carry it yet. The record stays the
151
+ * sole authority: an empty cache re-reads it, so a path the runtime writes AFTER callback
152
+ * correlation is still observed instead of being frozen out of the run. Measured — a pi record
153
+ * gained its `transcriptPath` 34s after correlation, and a one-shot read made every later
154
+ * source observation permanently empty. A reader that throws is fail-loud; only "the record has
155
+ * no path yet" is pending. Model prose and tmux listings are never a substitute source.
156
+ */
157
+ export function resolveSourceTranscriptPath(cached: string, readRecordPath: () => string | null | undefined): string {
158
+ if (cached !== "") return cached;
159
+ return readRecordPath() ?? "";
160
+ }
161
+
162
+ /** Parse complete JSONL rows; only one structurally incomplete final append is pending. */
163
+ export function parseJsonLines(source: string): unknown[] {
164
+ const lines = source.split("\n");
165
+ const entries: unknown[] = [];
166
+ for (const [index, line] of lines.entries()) {
167
+ if (line.trim() === "") continue;
168
+ try {
169
+ entries.push(JSON.parse(line));
170
+ } catch (error) {
171
+ const isIncompleteLastAppend =
172
+ index === lines.length - 1 && !source.endsWith("\n") && looksLikeIncompleteJsonAppend(line);
173
+ if (isIncompleteLastAppend) return entries;
174
+ throw new Error(
175
+ `invalid complete JSONL row ${index + 1}: ${error instanceof Error ? error.message : String(error)}`,
176
+ );
177
+ }
178
+ }
179
+ return entries;
180
+ }
181
+
182
+ /** Join Pi's own toolCall and toolResult rows by native id, retaining pending calls. */
183
+ export function piSourceToolReceipts(entries: readonly unknown[]): SourceToolReceipt[] {
184
+ const calls = new Map<string, { toolName: string; arguments: Record<string, unknown> }>();
185
+ const results = new Map<string, { toolName: string; text: string; isError: boolean }>();
186
+ for (const raw of entries) {
187
+ if (typeof raw !== "object" || raw === null) continue;
188
+ const entry = raw as { type?: unknown; message?: unknown };
189
+ if (entry.type !== "message" || typeof entry.message !== "object" || entry.message === null) continue;
190
+ const message = entry.message as {
191
+ role?: unknown;
192
+ content?: unknown;
193
+ toolCallId?: unknown;
194
+ toolName?: unknown;
195
+ isError?: unknown;
196
+ };
197
+ if (message.role === "assistant" && Array.isArray(message.content)) {
198
+ for (const rawBlock of message.content) {
199
+ if (typeof rawBlock !== "object" || rawBlock === null) continue;
200
+ const block = rawBlock as { type?: unknown; id?: unknown; name?: unknown; arguments?: unknown };
201
+ if (block.type !== "toolCall") continue;
202
+ if (
203
+ typeof block.id !== "string" ||
204
+ typeof block.name !== "string" ||
205
+ typeof block.arguments !== "object" ||
206
+ block.arguments === null ||
207
+ Array.isArray(block.arguments)
208
+ )
209
+ throw new Error("malformed Pi toolCall row");
210
+ if (calls.has(block.id)) throw new Error(`duplicate Pi toolCall id ${block.id}`);
211
+ calls.set(block.id, {
212
+ toolName: block.name,
213
+ arguments: block.arguments as Record<string, unknown>,
214
+ });
215
+ }
216
+ }
217
+ if (message.role === "toolResult") {
218
+ if (typeof message.toolCallId !== "string" || typeof message.toolName !== "string")
219
+ throw new Error("malformed Pi toolResult row");
220
+ if (typeof message.isError !== "boolean")
221
+ throw new Error(`Pi toolResult ${message.toolCallId} lacks explicit boolean isError`);
222
+ if (results.has(message.toolCallId)) throw new Error(`duplicate Pi toolResult id ${message.toolCallId}`);
223
+ results.set(message.toolCallId, {
224
+ toolName: message.toolName,
225
+ text: textContent(message.content),
226
+ isError: message.isError,
227
+ });
228
+ }
229
+ }
230
+ for (const id of results.keys()) {
231
+ if (!calls.has(id)) throw new Error(`unmatched Pi toolResult id ${id}`);
232
+ }
233
+ const joined: SourceToolReceipt[] = [];
234
+ for (const [id, call] of calls) {
235
+ const result = results.get(id);
236
+ if (result === undefined) {
237
+ joined.push({ ...call, text: "", status: "pending", isError: false });
238
+ continue;
239
+ }
240
+ if (result.toolName !== call.toolName)
241
+ throw new Error(`Pi tool name drift for ${id}: call=${call.toolName} result=${result.toolName}`);
242
+ joined.push({
243
+ ...call,
244
+ text: result.text,
245
+ status: result.isError ? "failed" : "completed",
246
+ isError: result.isError,
247
+ });
248
+ }
249
+ return joined;
250
+ }
251
+
252
+ export function codexThreadIsTerminal(thread: unknown): boolean {
253
+ if (typeof thread !== "object" || thread === null || !Array.isArray((thread as { turns?: unknown }).turns))
254
+ throw new Error("malformed Codex thread/read turns");
255
+ const turns = (thread as { turns: unknown[] }).turns;
256
+ if (turns.length === 0) return false;
257
+ for (const rawTurn of turns) {
258
+ if (typeof rawTurn !== "object" || rawTurn === null) throw new Error("malformed Codex thread/read turn");
259
+ if (!Array.isArray((rawTurn as { items?: unknown }).items)) throw new Error("malformed Codex thread/read items");
260
+ const status = (rawTurn as { status?: unknown }).status;
261
+ if (status === "inProgress") return false;
262
+ if (status === "failed" || status === "interrupted") throw new Error(`Codex turn ended ${status}, not completed`);
263
+ if (status !== "completed") throw new Error(`unknown Codex turn status ${String(status)}`);
264
+ }
265
+ return true;
266
+ }
267
+
268
+ /** Read Entwurf MCP calls from Codex thread/read, retaining vendor inProgress state. */
269
+ export function codexSourceToolReceipts(thread: unknown): SourceToolReceipt[] {
270
+ if (typeof thread !== "object" || thread === null) return [];
271
+ const turns = (thread as { turns?: unknown }).turns;
272
+ if (!Array.isArray(turns)) return [];
273
+ const found: SourceToolReceipt[] = [];
274
+ const ids = new Set<string>();
275
+ for (const rawTurn of turns) {
276
+ if (typeof rawTurn !== "object" || rawTurn === null) continue;
277
+ const items = (rawTurn as { items?: unknown }).items;
278
+ if (!Array.isArray(items)) continue;
279
+ for (const rawItem of items) {
280
+ if (typeof rawItem !== "object" || rawItem === null) continue;
281
+ const item = rawItem as {
282
+ type?: unknown;
283
+ id?: unknown;
284
+ server?: unknown;
285
+ tool?: unknown;
286
+ status?: unknown;
287
+ arguments?: unknown;
288
+ result?: unknown;
289
+ error?: unknown;
290
+ };
291
+ if (item.type !== "mcpToolCall" || item.server !== "entwurf-bridge") continue;
292
+ if (typeof item.id !== "string" || typeof item.tool !== "string")
293
+ throw new Error("malformed Codex entwurf MCP call identity");
294
+ if (ids.has(item.id)) throw new Error(`duplicate Codex MCP tool-call id ${item.id}`);
295
+ ids.add(item.id);
296
+ if (typeof item.arguments !== "object" || item.arguments === null || Array.isArray(item.arguments))
297
+ throw new Error(`malformed Codex MCP arguments for ${item.id}`);
298
+ const result = typeof item.result === "object" && item.result !== null ? item.result : null;
299
+ if (item.status === "inProgress") {
300
+ if (result !== null || item.error !== null)
301
+ throw new Error(`malformed inProgress Codex MCP state for ${item.id}`);
302
+ found.push({
303
+ toolName: item.tool,
304
+ arguments: item.arguments as Record<string, unknown>,
305
+ text: "",
306
+ status: "pending",
307
+ isError: false,
308
+ });
309
+ continue;
310
+ }
311
+ if (item.status !== "completed" && item.status !== "failed")
312
+ throw new Error(`unknown Codex MCP status ${String(item.status)} for ${item.id}`);
313
+ const completed = item.status === "completed" && item.error === null && result !== null;
314
+ found.push({
315
+ toolName: item.tool,
316
+ arguments: item.arguments as Record<string, unknown>,
317
+ text: textContent((result as { content?: unknown } | null)?.content),
318
+ status: completed ? "completed" : "failed",
319
+ isError: !completed,
320
+ });
321
+ }
322
+ }
323
+ return found;
324
+ }
325
+
326
+ function exactSenderInfoSuffix(text: string, nonce: string): string | null {
327
+ const prefix = `${nonce}\n\n<sender_info>`;
328
+ const suffix = "</sender_info>";
329
+ if (!text.startsWith(prefix) || !text.endsWith(suffix)) return null;
330
+ const json = text.slice(prefix.length, -suffix.length);
331
+ if (json.includes("\n") || json.includes("<sender_info>")) return null;
332
+ try {
333
+ const sender = JSON.parse(json) as { sessionId?: unknown };
334
+ return typeof sender.sessionId === "string" ? sender.sessionId : null;
335
+ } catch {
336
+ return null;
337
+ }
338
+ }
339
+
340
+ /** Exact callback in Pi's own transcript: nonce plus one terminal generated sender envelope. */
341
+ export function piCallbackSenders(entries: readonly unknown[], nonce: string): string[] {
342
+ const found: string[] = [];
343
+ for (const raw of entries) {
344
+ if (typeof raw !== "object" || raw === null) continue;
345
+ const entry = raw as { type?: unknown; customType?: unknown; content?: unknown };
346
+ if (entry.type !== "custom_message" || entry.customType !== "entwurf-message" || typeof entry.content !== "string")
347
+ continue;
348
+ const sender = exactSenderInfoSuffix(entry.content, nonce);
349
+ if (sender !== null) found.push(sender);
350
+ }
351
+ return found;
352
+ }
353
+
354
+ export interface MetaMailboxBody {
355
+ sender: string;
356
+ payload: string;
357
+ }
358
+
359
+ /** Strip exactly formatMetaMailboxBody's anchored outer frame and one terminal newline. */
360
+ export function parseMetaMailboxBody(text: string): MetaMailboxBody | null {
361
+ const frame =
362
+ /^\[entwurf received ⟵\]\n {2}from: {8}[^\n]*\n {2}session: {5}(\S+)(?: [^\n]*)?\n {2}at: {10}[^\n]+\n {2}wants reply: (?:yes|no)\n────────────────────────────────────────\n/;
363
+ const match = frame.exec(text);
364
+ if (match === null || !text.endsWith("\n")) return null;
365
+ return { sender: match[1] as string, payload: text.slice(match[0].length, -1) };
366
+ }
367
+
368
+ export interface FinalMailboxValidation {
369
+ accepted: boolean;
370
+ reason: string;
371
+ parsed: MetaMailboxBody | null;
372
+ }
373
+
374
+ export function validateFinalMailboxBody(
375
+ body: string,
376
+ expectedSender: string,
377
+ expectedPayload: string,
378
+ ): FinalMailboxValidation {
379
+ const parsed = parseMetaMailboxBody(body);
380
+ if (parsed === null) return { accepted: false, reason: "rejected: invalid outer frame", parsed: null };
381
+ if (parsed.sender !== expectedSender)
382
+ return { accepted: false, reason: `rejected: sender differs from ${expectedSender}`, parsed };
383
+ if (parsed.payload !== expectedPayload)
384
+ return { accepted: false, reason: "rejected: payload differs from source-call argument", parsed };
385
+ return { accepted: true, reason: "accepted: exact outer frame, sender, and source-call payload", parsed };
386
+ }
387
+
388
+ export interface MailboxCallback {
389
+ body: string;
390
+ sender: string;
391
+ }
392
+
393
+ /** Exact callback bodies read by the record-backed fixture's mailbox. */
394
+ export function mailboxCallbacks(bodies: readonly string[], nonce: string): MailboxCallback[] {
395
+ const found: MailboxCallback[] = [];
396
+ for (const body of bodies) {
397
+ const parsed = parseMetaMailboxBody(body);
398
+ if (parsed !== null && parsed.payload === nonce) found.push({ body, sender: parsed.sender });
399
+ }
400
+ return found;
401
+ }
402
+
403
+ /** Exact callback in a Codex thread/read userMessage item. */
404
+ export function codexCallbackSenders(thread: unknown, nonce: string): string[] {
405
+ if (typeof thread !== "object" || thread === null) return [];
406
+ const turns = (thread as { turns?: unknown }).turns;
407
+ if (!Array.isArray(turns)) return [];
408
+ const found: string[] = [];
409
+ for (const rawTurn of turns) {
410
+ if (typeof rawTurn !== "object" || rawTurn === null) continue;
411
+ const items = (rawTurn as { items?: unknown }).items;
412
+ if (!Array.isArray(items)) continue;
413
+ for (const rawItem of items) {
414
+ if (typeof rawItem !== "object" || rawItem === null || (rawItem as { type?: unknown }).type !== "userMessage")
415
+ continue;
416
+ const content = (rawItem as { content?: unknown }).content;
417
+ if (!Array.isArray(content) || content.length !== 1) continue;
418
+ const block = content[0];
419
+ if (typeof block !== "object" || block === null || typeof (block as { text?: unknown }).text !== "string")
420
+ continue;
421
+ const parsed = parseMetaMailboxBody((block as { text: string }).text);
422
+ if (parsed !== null && parsed.payload === nonce) found.push(parsed.sender);
423
+ }
424
+ }
425
+ return found;
426
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * The one parser that turns a LAUNCH receipt back into the tmux window handle it named.
3
+ *
4
+ * It exists for ONE job: when a travelling receipt never arrives, the visible window a
5
+ * sibling opened would be orphaned, because cleanup only ever kills handles the smoke
6
+ * recorded. The handle is still written down — in the caller's own transcript and in the
7
+ * Codex thread's own tool-call result — so recovery reads it from there instead of
8
+ * scanning tmux inventory or guessing.
9
+ *
10
+ * WHY THIS IS LINE-ORIENTED AND NOT A CHARACTER WINDOW. `renderFreshCall` writes one block
11
+ * whose `backend:` and `window:` lines are separated by a VARIABLE number of optional lines
12
+ * (cwd, seat). A character bound between them is a guess about how long those lines are, and
13
+ * the guess was already nearly spent: `[측정]` a rendered receipt carrying both cwd and seat
14
+ * spans 344 escaped characters, against the 400-character bound this replaced. A longer
15
+ * project path would have silently recovered NOTHING. The boundary here is the receipt's own
16
+ * line structure, which does not have a length.
17
+ *
18
+ * Transcripts and tool results carry the receipt JSON-escaped, so a line ends at a real
19
+ * newline OR at a literal backslash-n. Both are separators.
20
+ *
21
+ * It stays anchored on the receipt: a `window:` line with no `backend:` line before it is
22
+ * not a window this run opened, and a receipt for the OTHER backend is never mistaken for
23
+ * this one's.
24
+ */
25
+ const LINE = /\r?\n|\\n/;
26
+ const BACKEND_LINE = /^\s*backend:\s+([a-z-]+)\b/;
27
+ const WINDOW_LINE = /^\s*window:\s+(@\d+)\b/;
28
+
29
+ export function launchReceiptWindows(text: string, backend: string): string[] {
30
+ const found: string[] = [];
31
+ let open: string | null = null;
32
+ for (const line of text.split(LINE)) {
33
+ const backendLine = BACKEND_LINE.exec(line);
34
+ if (backendLine !== null) {
35
+ open = backendLine[1] ?? null;
36
+ continue;
37
+ }
38
+ const windowLine = WINDOW_LINE.exec(line);
39
+ if (windowLine === null) continue;
40
+ if (open === backend) found.push(windowLine[1] as string);
41
+ // One receipt names one window: close the block so a later stray line cannot inherit
42
+ // this backend.
43
+ open = null;
44
+ }
45
+ return found;
46
+ }
@@ -0,0 +1,58 @@
1
+ /**
2
+ * tmux-coordinate-row — parse ONE positional TSV row out of `tmux display-message -p`.
3
+ *
4
+ * WHY THIS EXISTS, measured. The Codex first-admission LIVE asked tmux for four coordinates in
5
+ * one row and split the result with `.trim().split("\t")`. On tmux 3.6a `#{server_pid}` answers
6
+ * with an EMPTY string, so the row arrived as `"\t$150\t@342\t%342\n"` — and `.trim()` ate the
7
+ * leading tab along with the newline. Every field then shifted one place left: the session id
8
+ * was read as the server pid, the window id as the session id, and the pane id vanished. The
9
+ * gate failed, which was right, but it failed at the wrong coordinate and blamed the host for a
10
+ * field the format never filled.
11
+ *
12
+ * So two rules, and the second one is the reason this is a separate leaf:
13
+ *
14
+ * 1. Exactly ONE terminal newline is removed — `\n` or `\r\n`, and only at the end. Anything
15
+ * else (an embedded newline, a second trailing one) means the row is not the single line
16
+ * the caller asked for, and is refused rather than silently reshaped.
17
+ * 2. EMPTY FIELDS ARE KEPT, leading and trailing alike. A format key that answers with
18
+ * nothing must fail at its own position — "the server pid is empty" is a fact the caller
19
+ * can act on; "everything after it moved" is a fact nobody can.
20
+ *
21
+ * The field COUNT is the caller's contract, so it is passed in and enforced here: a row with
22
+ * more or fewer columns than the format asked for is malformed, never truncated to fit.
23
+ */
24
+
25
+ export class TmuxRowError extends Error {}
26
+
27
+ /**
28
+ * @param raw the exact stdout of a single `display-message -p` invocation
29
+ * @param fieldCount how many `\t`-separated fields the caller's format string asked for
30
+ */
31
+ export function parseTmuxRow(raw: string, fieldCount: number): string[] {
32
+ if (!Number.isInteger(fieldCount) || fieldCount < 1) {
33
+ throw new TmuxRowError(`fieldCount must be a positive integer, got ${String(fieldCount)}`);
34
+ }
35
+ let line = raw;
36
+ if (line.endsWith("\r\n")) line = line.slice(0, -2);
37
+ else if (line.endsWith("\n")) line = line.slice(0, -1);
38
+ if (line.includes("\n") || line.includes("\r")) {
39
+ throw new TmuxRowError(
40
+ `tmux answered with more than one line (${JSON.stringify(raw)}); a positional row must be exactly one`,
41
+ );
42
+ }
43
+ const fields = line.split("\t");
44
+ if (fields.length !== fieldCount) {
45
+ throw new TmuxRowError(
46
+ `tmux answered ${fields.length} field(s), the format asked for ${fieldCount} (${JSON.stringify(raw)})`,
47
+ );
48
+ }
49
+ return fields;
50
+ }
51
+
52
+ /**
53
+ * The server pid key that actually answers. `[host]` measured on tmux 3.6a: `#{pid}` returns
54
+ * 5772, the same pid the `$TMUX` tuple carries, while `#{server_pid}` returns an empty string.
55
+ * Pinned as one literal so the LIVE inspector and its deterministic oracle cannot drift apart.
56
+ */
57
+ export const TMUX_COORDINATE_FORMAT = "#{pid}\t#{session_id}\t#{window_id}\t#{pane_id}";
58
+ export const TMUX_COORDINATE_FIELDS = 4;
@@ -442,7 +442,12 @@ async function inspectNativePushCitizens(storeDir: string): Promise<QuiesceViola
442
442
  try {
443
443
  const probe = await resolveNativePushAdapter(identity.backend).probe(identity.nativeSessionId);
444
444
  status = probe.status;
445
- reason = probe.status === "alive" ? `route ${probe.route.lsAddress}` : probe.reason;
445
+ reason =
446
+ probe.status === "alive"
447
+ ? probe.route.backend === "antigravity"
448
+ ? `route ${probe.route.lsAddress}`
449
+ : `socket ${probe.route.socketPath}`
450
+ : probe.reason;
446
451
  } catch (err) {
447
452
  // A probe that cannot run is not a dead conversation.
448
453
  violations.push({