@junghanacs/entwurf 0.39.0 → 0.41.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 (137) hide show
  1. package/AGENTS.md +4 -2
  2. package/BASELINE.md +60 -5
  3. package/CHANGELOG.md +557 -0
  4. package/CONTRIBUTING.md +1 -1
  5. package/DELIVERY.md +52 -2
  6. package/FAQ.md +8 -4
  7. package/README.md +37 -17
  8. package/VERIFY.md +23 -10
  9. package/docs/acp-backend-rail.md +5 -1
  10. package/docs/external-mcp-host.md +8 -6
  11. package/docs/fresh-cut-policy.md +9 -1
  12. package/docs/setup-clean-host.md +64 -9
  13. package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +100 -24
  14. package/mcp/entwurf-bridge/dist/pi-extensions/lib/cc-retire-pidfd.py +236 -0
  15. package/mcp/entwurf-bridge/dist/pi-extensions/lib/cc-retire-request.js +473 -0
  16. package/mcp/entwurf-bridge/dist/pi-extensions/lib/control-retire.js +125 -0
  17. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-retire-production.js +149 -0
  18. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-retire.js +158 -0
  19. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-surface.js +2 -1
  20. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-visible-resume.js +283 -20
  21. package/mcp/entwurf-bridge/dist/pi-extensions/lib/fresh-call-composition.js +7 -0
  22. package/mcp/entwurf-bridge/dist/pi-extensions/lib/fresh-call-dispatch.js +47 -3
  23. package/mcp/entwurf-bridge/dist/pi-extensions/lib/herdr-fresh-call.js +3 -1
  24. package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-mailbox-body.js +10 -4
  25. package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-session.js +96 -16
  26. package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +5 -29
  27. package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-launch.js +28 -0
  28. package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-resume-call.js +26 -11
  29. package/mcp/entwurf-bridge/dist/pi-extensions/lib/pi-retire-request.js +76 -0
  30. package/mcp/entwurf-bridge/dist/pi-extensions/lib/remote-pair-receive.js +123 -0
  31. package/mcp/entwurf-bridge/dist/pi-extensions/lib/remote-pair-send.js +259 -0
  32. package/mcp/entwurf-bridge/dist/pi-extensions/lib/remote-pair-transport.js +80 -0
  33. package/mcp/entwurf-bridge/dist/pi-extensions/lib/resume-launch-identity.js +67 -17
  34. package/mcp/entwurf-bridge/dist/pi-extensions/lib/retirement-event.js +211 -0
  35. package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-hook.js +124 -10
  36. package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-pi-durable.js +53 -2
  37. package/mcp/entwurf-bridge/dist/scripts/meta-facts.js +1 -1
  38. package/mcp/entwurf-bridge/dist/scripts/probe-bridge-command.js +1 -0
  39. package/mcp/entwurf-bridge/dist/scripts/remote-pair-receive.js +65 -0
  40. package/mcp/entwurf-bridge/src/index.ts +118 -24
  41. package/mcp/entwurf-bridge/test.sh +2 -2
  42. package/mcp/entwurf-bridge/tsconfig.build.json +17 -1
  43. package/package.json +5 -5
  44. package/pi/meta-bridge/entwurf-meta-receive/scripts/hook-launch.sh +3 -3
  45. package/pi-extensions/entwurf-control.ts +55 -20
  46. package/pi-extensions/lib/cc-retire-pidfd.py +236 -0
  47. package/pi-extensions/lib/cc-retire-request.ts +553 -0
  48. package/pi-extensions/lib/control-retire.ts +195 -0
  49. package/pi-extensions/lib/entwurf-control-rpc.ts +17 -1
  50. package/pi-extensions/lib/entwurf-v2-retire-production.ts +227 -0
  51. package/pi-extensions/lib/entwurf-v2-retire.ts +269 -0
  52. package/pi-extensions/lib/entwurf-v2-surface.ts +2 -1
  53. package/pi-extensions/lib/entwurf-v2-visible-resume.ts +390 -36
  54. package/pi-extensions/lib/fresh-call-composition.ts +15 -0
  55. package/pi-extensions/lib/fresh-call-dispatch.ts +45 -4
  56. package/pi-extensions/lib/herdr-fresh-call.ts +5 -0
  57. package/pi-extensions/lib/meta-mailbox-body.ts +13 -4
  58. package/pi-extensions/lib/meta-session.ts +140 -16
  59. package/pi-extensions/lib/mux-fresh-call.ts +7 -27
  60. package/pi-extensions/lib/mux-launch.ts +29 -0
  61. package/pi-extensions/lib/mux-resume-call.ts +33 -12
  62. package/pi-extensions/lib/pi-retire-request.ts +111 -0
  63. package/pi-extensions/lib/remote-pair-receive.ts +184 -0
  64. package/pi-extensions/lib/remote-pair-send.ts +325 -0
  65. package/pi-extensions/lib/remote-pair-transport.ts +81 -0
  66. package/pi-extensions/lib/resume-launch-identity.ts +104 -18
  67. package/pi-extensions/lib/retirement-event.ts +253 -0
  68. package/pi-extensions/meta-bridge-hook.ts +142 -13
  69. package/pi-extensions/meta-bridge-pi-durable.ts +56 -2
  70. package/run.sh +122 -7
  71. package/scripts/agy-bridge-config.py +4 -1
  72. package/scripts/build-bridge.sh +14 -0
  73. package/scripts/check-capability-bundle-reach.ts +135 -2
  74. package/scripts/check-entwurf-bridge-boot.ts +2 -1
  75. package/scripts/check-entwurf-fact-provider.ts +1 -0
  76. package/scripts/check-entwurf-v2-production.ts +2 -0
  77. package/scripts/check-entwurf-v2-surface.ts +7 -0
  78. package/scripts/check-gate-qualification.ts +20 -8
  79. package/scripts/check-herdr-activation.ts +2 -2
  80. package/scripts/check-herdr-placement.ts +1 -0
  81. package/scripts/check-hook-launch-topology.ts +6 -15
  82. package/scripts/check-install-container.sh +2 -2
  83. package/scripts/check-meta-doctor-oracle.sh +27 -1
  84. package/scripts/check-meta-facts.ts +8 -3
  85. package/scripts/check-meta-hook-session-switch.ts +230 -13
  86. package/scripts/check-meta-identity-consumers.ts +1 -0
  87. package/scripts/check-meta-listing.ts +1 -0
  88. package/scripts/check-meta-manifest-schema.py +1 -1
  89. package/scripts/check-meta-session.ts +31 -0
  90. package/scripts/check-meta-v3-record.ts +55 -1
  91. package/scripts/check-mux-resume-call.ts +35 -9
  92. package/scripts/check-peer-facts.ts +1 -0
  93. package/scripts/check-pi-durable-contact.ts +3 -1
  94. package/scripts/check-pi-durable-send.ts +4 -2
  95. package/scripts/check-pi-mcp-bridge.ts +1 -0
  96. package/scripts/check-remote-pair-installed.ts +242 -0
  97. package/scripts/check-resume-launch-identity.ts +62 -23
  98. package/scripts/lib/meta-hook-bundle.ts +72 -0
  99. package/scripts/lib/remote-pair-installed-audit.ts +25 -0
  100. package/scripts/meta-bridge-claude-floor.sh +13 -9
  101. package/scripts/meta-bridge-doctor.sh +17 -0
  102. package/scripts/meta-bridge-install.sh +6 -0
  103. package/scripts/meta-facts.ts +1 -1
  104. package/scripts/mutants/bridge-boot-resume.json +5 -2
  105. package/scripts/mutants/cc-retire-pidfd.json +129 -0
  106. package/scripts/mutants/cc-retire-request.json +171 -0
  107. package/scripts/mutants/control-retire.json +105 -0
  108. package/scripts/mutants/control-send-receipt.json +2 -2
  109. package/scripts/mutants/copilot-receive.json +2 -6
  110. package/scripts/mutants/entwurf-v2-retire.json +476 -0
  111. package/scripts/mutants/fresh-call-dispatch.json +37 -0
  112. package/scripts/mutants/meta-hook-session-switch.json +88 -0
  113. package/scripts/mutants/meta-identity.json +57 -0
  114. package/scripts/mutants/mux-fresh-call.json +1 -1
  115. package/scripts/mutants/mux-resume-call.json +40 -4
  116. package/scripts/mutants/pi-durable.json +79 -0
  117. package/scripts/mutants/pi-retire-request.json +72 -0
  118. package/scripts/mutants/remote-pair-display.json +53 -0
  119. package/scripts/mutants/remote-pair-installed-audit.json +28 -0
  120. package/scripts/mutants/remote-pair-receive.json +126 -0
  121. package/scripts/mutants/remote-pair-roundtrip.json +83 -0
  122. package/scripts/mutants/remote-pair-send.json +194 -0
  123. package/scripts/mutants/remote-pair-transport.json +66 -0
  124. package/scripts/mutants/resume-launch-identity.json +35 -8
  125. package/scripts/mutants/retirement-event.json +195 -0
  126. package/scripts/mutants/v2-surface.json +16 -0
  127. package/scripts/mutants/v2-visible-resume.json +283 -13
  128. package/scripts/probe-bridge-command.ts +1 -0
  129. package/scripts/remote-pair-receive.ts +73 -0
  130. package/scripts/smoke-agy-install-state.sh +2 -2
  131. package/scripts/smoke-copilot-mcp-state.sh +1 -1
  132. package/scripts/smoke-herdr-raw-install-live.sh +5 -4
  133. package/scripts/smoke-meta-install-state.sh +4 -4
  134. package/scripts/smoke-mux-lifecycle-live.ts +10 -10
  135. package/scripts/smoke-omp-mcp-state.sh +1 -1
  136. package/scripts/smoke-pi-durable-fresh-live.ts +5 -3
  137. package/scripts/smoke-pi-provider-state.sh +3 -3
@@ -28,8 +28,14 @@
28
28
  * - B4: the live load seam `metaCapabilityFor()` returns for every
29
29
  * META_CITIZEN_BACKENDS backend without throwing — the real `entwurf_v2` path.
30
30
  *
31
- * Deterministic: filesystem + import only. No backend, no network, no API, no
32
- * record write. Safe in the `pnpm check` static floor.
31
+ * - C (#147 L2 B1): the compiled Claude Code hook's static `from` closure equals what
32
+ * the installer copies, and the hook staged with only that closure runs.
33
+ * - D (#147 G3): the compiled Claude Code retire adapter resolves its helper beside itself,
34
+ * byte-identical to pi-extensions/lib/cc-retire-pidfd.py.
35
+ *
36
+ * Deterministic: filesystem, import, and ONE shipped-hook child process (C2) that writes a
37
+ * record into its own private store under a private HOME and XDG roots. No backend, vendor
38
+ * CLI, model, network or API. Safe in the `pnpm check` static floor.
33
39
  *
34
40
  * REQUIRES a built bridge (`pnpm run build-bridge`, which `prepare`/`prepack` run).
35
41
  * A missing dist is a FAILURE, never a skip — a gate that skips when the artifact
@@ -37,9 +43,13 @@
37
43
  */
38
44
 
39
45
  import assert from "node:assert/strict";
46
+ import { spawnSync } from "node:child_process";
40
47
  import fs from "node:fs";
48
+ import os from "node:os";
41
49
  import path from "node:path";
42
50
  import { pathToFileURL } from "node:url";
51
+ import { hookImportClosure, installerCopiedLibFiles, stageMetaHookBundle } from "./lib/meta-hook-bundle.ts";
52
+ import { reclaimOnExit } from "./lib/reclaim-on-exit.ts";
43
53
 
44
54
  const REPO = path.join(import.meta.dirname, "..");
45
55
 
@@ -127,4 +137,127 @@ for (const copy of copies) {
127
137
  }
128
138
  }
129
139
 
140
+ // C — the COMPILED Claude Code hook bundle reaches its whole closure (#147 L2 B1). Moved here from
141
+ // check-meta-hook-session-switch, whose source-only qualification snapshot has no dist: this gate
142
+ // already requires the built bundle, so the emission proof sits with the dist consumer. Scope is
143
+ // the static double-quoted `from` imports scripts/lib/meta-hook-bundle.ts follows; the staged RUN
144
+ // is the independent proof that nothing outside them is needed.
145
+ {
146
+ const DIST_HOOK = path.join(BRIDGE_DIST, "pi-extensions", "meta-bridge-hook.js");
147
+ ok(
148
+ "C0: the compiled hook entry exists (a missing dist is a failure, not a skip)",
149
+ fs.existsSync(DIST_HOOK),
150
+ `--- expected ---\n${DIST_HOOK}\nrun: pnpm run build-bridge`,
151
+ );
152
+ const installerCopies = installerCopiedLibFiles(
153
+ fs.readFileSync(path.join(REPO, "scripts", "meta-bridge-install.sh"), "utf8"),
154
+ );
155
+ const distClosure = hookImportClosure(DIST_HOOK)
156
+ .map((rel) => path.basename(rel))
157
+ .map((base) => (base === "session-id.js" ? base : base.replace(/\.(ts|js)$/, ".$LIB_EXT")))
158
+ .sort();
159
+ ok(
160
+ "C1: the installer copies exactly the compiled hook's static `from` import closure [QK:BUNDLE-REACH-COMPILED-HOOK-CLOSURE]",
161
+ distClosure.includes("entwurf-v2-lock.$LIB_EXT") && JSON.stringify(distClosure) === JSON.stringify(installerCopies),
162
+ `--- compiled closure ---\n${distClosure.join("\n")}\n--- installer copies ---\n${installerCopies.join("\n")}`,
163
+ );
164
+ // Stage the compiled entry with ONLY that closure and run it the way Claude does: the shipped
165
+ // manifest's exec argv through the shipped launcher, every meta root sandboxed, provenance never
166
+ // inherited. A record on disk means the compiled closure loaded end to end.
167
+ const PLUGIN_SRC = path.join(REPO, "pi", "meta-bridge", "entwurf-meta-receive");
168
+ const leaf =
169
+ (
170
+ JSON.parse(fs.readFileSync(path.join(PLUGIN_SRC, "hooks", "hooks.json"), "utf8")) as {
171
+ hooks: Record<string, Array<{ hooks: Array<{ command?: string; args?: string[] }> }>>;
172
+ }
173
+ ).hooks.SessionStart?.[0]?.hooks[0] ?? {};
174
+ const root = reclaimOnExit(fs.mkdtempSync(path.join(os.tmpdir(), "psa-bundle-reach-jshook-")));
175
+ const agent = reclaimOnExit(fs.mkdtempSync(path.join(os.tmpdir(), "psa-bundle-reach-jsagent-")));
176
+ stageMetaHookBundle(root, DIST_HOOK, path.join(PLUGIN_SRC, "scripts", "hook-launch.sh"));
177
+ const el = (v: string) =>
178
+ v
179
+ .replace("__NODE_BIN__", process.execPath)
180
+ .replaceAll("${CLAUDE_PLUGIN_ROOT}", root)
181
+ .replace("__HOOK_ENTRY__", "meta-bridge-hook.js");
182
+ // Hard Rule 12: the child's HOME and every writable XDG root are private too, not only the
183
+ // meta roots — a hook that resolved anything through HOME/XDG must land inside `agent`.
184
+ const priv = (name: string) => {
185
+ const dir = path.join(agent, name);
186
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
187
+ return dir;
188
+ };
189
+ const env: NodeJS.ProcessEnv = {
190
+ ...process.env,
191
+ HOME: priv("home"),
192
+ XDG_CONFIG_HOME: priv("xdg-config"),
193
+ XDG_CACHE_HOME: priv("xdg-cache"),
194
+ XDG_DATA_HOME: priv("xdg-data"),
195
+ XDG_STATE_HOME: priv("xdg-state"),
196
+ XDG_RUNTIME_DIR: priv("xdg-runtime"),
197
+ CLAUDE_PLUGIN_ROOT: root,
198
+ PI_CODING_AGENT_DIR: agent,
199
+ ENTWURF_META_SESSIONS_DIR: path.join(agent, "meta-sessions"),
200
+ ENTWURF_META_SENDERS_DIR: path.join(agent, "meta-senders"),
201
+ ENTWURF_META_RECEIVERS_DIR: path.join(agent, "meta-receivers"),
202
+ ENTWURF_META_MAILBOX_DIR: path.join(agent, "meta-mailbox"),
203
+ };
204
+ delete env.ENTWURF_META_HOOK_LAUNCH;
205
+ const run = spawnSync(el(leaf.command ?? ""), (leaf.args ?? []).map(el), {
206
+ encoding: "utf8",
207
+ input: JSON.stringify({
208
+ hook_event_name: "SessionStart",
209
+ session_id: "c0c0c0c0-compiled-closure",
210
+ transcript_path: path.join(agent, "c0c0c0c0.jsonl"),
211
+ cwd: agent,
212
+ source: "startup",
213
+ }),
214
+ env,
215
+ });
216
+ const sessions = path.join(agent, "meta-sessions");
217
+ ok(
218
+ "C2: the compiled hook, staged with only that closure, runs to a written record [QK:BUNDLE-REACH-COMPILED-HOOK-RUNS]",
219
+ run.status === 0 && fs.existsSync(sessions) && fs.readdirSync(sessions).some((n) => n.endsWith(".meta.json")),
220
+ `--- exit ${run.status} ---\n${run.stderr}`,
221
+ );
222
+ }
223
+
224
+ // D — the COMPILED Claude Code retire adapter reaches its helper from where it lives (#147 G3).
225
+ // `ccRetireHelperPath()` resolves a fixed basename beside its own module, which is a tautology from
226
+ // source; only the emitted copy can say whether build-bridge.sh put the bytes next to it. Import and
227
+ // path only — nothing is spawned or signalled.
228
+ {
229
+ const DIST_ADAPTER = path.join(BRIDGE_DIST, "pi-extensions", "lib", "cc-retire-request.js");
230
+ const HELPER_SOURCE = path.join(REPO, "pi-extensions", "lib", "cc-retire-pidfd.py");
231
+ ok(
232
+ "D0: the compiled retire adapter exists (a missing dist is a failure, not a skip)",
233
+ fs.existsSync(DIST_ADAPTER),
234
+ `--- expected ---\n${DIST_ADAPTER}\nrun: pnpm run build-bridge`,
235
+ );
236
+ const adapter = await import(pathToFileURL(DIST_ADAPTER).href);
237
+ const resolved: string | null = adapter.ccRetireHelperPath();
238
+ ok(
239
+ "D1: the compiled adapter resolves the helper beside itself, byte-identical to the canonical source [QK:BUNDLE-REACH-CC-RETIRE-HELPER]",
240
+ resolved === path.join(path.dirname(DIST_ADAPTER), "cc-retire-pidfd.py") &&
241
+ fs.readFileSync(resolved).equals(fs.readFileSync(HELPER_SOURCE)),
242
+ `--- resolved ---\n${resolved}\n--- canonical ---\n${HELPER_SOURCE}`,
243
+ );
244
+ }
245
+
246
+ // E — ask the actual emitted Pi request adapter, not its source-path test import.
247
+ // Pure parsing only: no socket, SDK lifecycle, model, or operator state is touched.
248
+ {
249
+ const entry = path.join(BRIDGE_DIST, "pi-extensions", "lib", "pi-retire-request.js");
250
+ ok("E0: the compiled Pi retire adapter exists (missing emission is not a skip)", fs.existsSync(entry));
251
+ const adapter = await import(pathToFileURL(entry).href);
252
+ const response = { type: "response", command: "retire", success: true, data: { boundary: "shutdown-requested" } };
253
+ const accepted = adapter.parsePiRetireResponse(response);
254
+ const rejected = adapter.parsePiRetireResponse({ ...response, command: "send" });
255
+ ok(
256
+ "E1: the shipped Pi adapter loads, accepts a requested reply, and rejects a foreign command as unknown [QK:BUNDLE-REACH-PI-RETIRE-ADAPTER]",
257
+ accepted.boundary === "shutdown-requested" &&
258
+ rejected.boundary === "unknown" &&
259
+ rejected.reason === "malformed-response",
260
+ );
261
+ }
262
+
130
263
  console.log(`\ncheck-capability-bundle-reach: PASS (${passed} assertions)`);
@@ -253,11 +253,12 @@ async function main(): Promise<void> {
253
253
  "entwurf_peers",
254
254
  "entwurf_register_native",
255
255
  "entwurf_resume_call",
256
+ "entwurf_retire_call",
256
257
  "entwurf_self",
257
258
  "entwurf_v2",
258
259
  ];
259
260
  ok(
260
- "[QK:BRIDGEBOOT-PUBLIC-SURFACE-EXACT-SET] G1f: the runtime tools/list surface is EXACTLY the eight shipped garden verbs — no missing verb, no undecided extra, no duplicate",
261
+ "[QK:BRIDGEBOOT-PUBLIC-SURFACE-EXACT-SET] G1f: the runtime tools/list surface is EXACTLY the nine garden verbs — no missing verb, no undecided extra, no duplicate",
261
262
  publicSurface.length === expectedSurface.length && expectedSurface.every((n, i) => publicSurface[i] === n),
262
263
  `--- want ---\n${expectedSurface.join(",")}\n--- got ---\n${publicSurface.join(",")}`,
263
264
  );
@@ -59,6 +59,7 @@ function rec(gardenId: string, backend: MetaCitizenBackend): string {
59
59
  transcriptPath: null,
60
60
  createdAt: "2026-06-11T00:00:00.000Z",
61
61
  recordUpdatedAt: "2026-06-11T00:00:00.000Z",
62
+ attachIncarnation: null,
62
63
  });
63
64
  }
64
65
 
@@ -80,6 +80,7 @@ function identity(backend: MetaIdentity["backend"], gardenId = GID): MetaIdentit
80
80
  transcriptPath: null,
81
81
  createdAt: "2026-06-13T01:00:00.000Z",
82
82
  recordUpdatedAt: "2026-06-13T01:00:00.000Z",
83
+ attachIncarnation: null,
83
84
  };
84
85
  }
85
86
 
@@ -97,6 +98,7 @@ function receiverMarker(gid: string, backend: string, nativeSessionId = "n"): Me
97
98
  ownerKind: "claude-code-cli",
98
99
  armProvenance: "session-start",
99
100
  updatedAt: "t",
101
+ attachIncarnation: null,
100
102
  };
101
103
  }
102
104
 
@@ -630,6 +630,13 @@ async function main(): Promise<void> {
630
630
  /visible-first/.test(dormantHint) &&
631
631
  /entwurf_resume_call/.test(dormantHint),
632
632
  );
633
+ ok(
634
+ "[QK:V2SURF-RESUME-HINT-SCOPE] dormant hint names Pi/Claude Code promptless resume only on explicit operator request, never automatic permission",
635
+ dormantHint.includes("Only on an explicit operator request") &&
636
+ dormantHint.includes("entwurf_resume_call {target} for Pi or Claude Code") &&
637
+ dormantHint.includes("it submits no prompt") &&
638
+ dormantHint.includes("not permission to resume automatically"),
639
+ );
633
640
  // The frozen wire id still spells "-no-spawn" although nothing spawns anymore. The id is
634
641
  // deliberately NOT renamed (renaming a public reject is its own contract cut), so the
635
642
  // hint carries the whole correction: probe inconclusive, and nothing ran.
@@ -3449,9 +3449,15 @@ let manifestCount: number;
3449
3449
  "agy-permission": 6,
3450
3450
  "bridge-boot-resume": 5,
3451
3451
  "bridge-command-boot": 12,
3452
+ "cc-retire-pidfd": 11,
3453
+ "cc-retire-request": 15,
3454
+ "entwurf-v2-retire": 32,
3452
3455
  "capability-cache": 3,
3453
3456
  "codex-app-server-launch": 10,
3454
3457
  "codex-caller-seat": 28,
3458
+ "pi-retire-request": 6,
3459
+ "retirement-event": 16,
3460
+ "control-retire": 9,
3455
3461
  "control-send-receipt": 19,
3456
3462
  "codex-native": 87,
3457
3463
  "compaction-send-guard": 7,
@@ -3464,10 +3470,10 @@ let manifestCount: number;
3464
3470
  "pi-bridge-sender": 4,
3465
3471
  "pi-mcp-bridge": 2,
3466
3472
  "pi-mcp-register": 3,
3467
- "pi-durable": 84,
3473
+ "pi-durable": 89,
3468
3474
  "copilot-receive": 20,
3469
3475
  "entwurf-peers": 1,
3470
- "fresh-call-dispatch": 12,
3476
+ "fresh-call-dispatch": 15,
3471
3477
  "fresh-cut": 3,
3472
3478
  "gate-qualification": 10,
3473
3479
  "herdr-placement": 13,
@@ -3481,8 +3487,8 @@ let manifestCount: number;
3481
3487
  "herdr-supply": 11,
3482
3488
  "meta-facts": 4,
3483
3489
  "peer-facts": 8,
3484
- "meta-hook-session-switch": 17,
3485
- "meta-identity": 4,
3490
+ "meta-hook-session-switch": 25,
3491
+ "meta-identity": 9,
3486
3492
  "meta-retire": 4,
3487
3493
  "mux-boundary": 16,
3488
3494
  "mux-fresh-call": 65,
@@ -3491,21 +3497,27 @@ let manifestCount: number;
3491
3497
  "pack-install": 5,
3492
3498
  "pi-package-ownership": 8,
3493
3499
  "qualification-ledger": 42,
3494
- "mux-resume-call": 10,
3500
+ "mux-resume-call": 13,
3495
3501
  "omp-birth": 13,
3496
3502
  "omp-fresh": 24,
3497
3503
  "omp-receive": 11,
3498
3504
  "probe-ordering": 1,
3499
3505
  "release-gate": 21,
3506
+ "remote-pair-display": 3,
3507
+ "remote-pair-installed-audit": 2,
3508
+ "remote-pair-receive": 8,
3509
+ "remote-pair-roundtrip": 5,
3510
+ "remote-pair-send": 12,
3511
+ "remote-pair-transport": 4,
3500
3512
  "retired-sent-surface": 2,
3501
3513
  "resume-args": 6,
3502
- "resume-launch-identity": 7,
3514
+ "resume-launch-identity": 10,
3503
3515
  "self-address": 8,
3504
3516
  "setup-verdict": 14,
3505
3517
  "source-install": 2,
3506
3518
  "typing-call-fence": 1,
3507
- "v2-surface": 13,
3508
- "v2-visible-resume": 18,
3519
+ "v2-surface": 14,
3520
+ "v2-visible-resume": 40,
3509
3521
  "win32-preview": 19,
3510
3522
  };
3511
3523
  const laneTally: Record<string, number> = {};
@@ -113,7 +113,7 @@ function fakeClaude(env: NodeJS.ProcessEnv): string {
113
113
  "#!/usr/bin/env bash",
114
114
  'printf \'%s\\n\' "$*" >> "${FAKE_CLAUDE_LOG:?}"',
115
115
  'case "$1${2:+ $2}" in',
116
- ' "--version") echo "2.1.217 (Claude Code)" ;;',
116
+ ' "--version") echo "2.1.306 (Claude Code)" ;;',
117
117
  ' "plugin validate") : ;;',
118
118
  ' "plugin uninstall") : ;;',
119
119
  ' "plugin marketplace") : ;;',
@@ -1317,7 +1317,7 @@ function ledgerWith(
1317
1317
  [
1318
1318
  "#!/usr/bin/env bash",
1319
1319
  'case "$1 $2" in',
1320
- ' "--version "*|"--version") echo "2.1.217 (Claude Code)" ;;',
1320
+ ' "--version "*|"--version") echo "2.1.306 (Claude Code)" ;;',
1321
1321
  ' *) echo "broken fixture" >&2; exit 1 ;;',
1322
1322
  "esac",
1323
1323
  ].join("\n"),
@@ -96,6 +96,7 @@ function identity(nativeSessionId: string, backend: MetaIdentity["backend"] = "c
96
96
  transcriptPath: null,
97
97
  createdAt: "2026-09-14T00:00:00.000Z",
98
98
  recordUpdatedAt: "2026-09-14T00:00:00.000Z",
99
+ attachIncarnation: null,
99
100
  };
100
101
  }
101
102
 
@@ -41,10 +41,11 @@
41
41
 
42
42
  import assert from "node:assert/strict";
43
43
  import { spawnSync } from "node:child_process";
44
- import { chmodSync, copyFileSync, existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync } from "node:fs";
44
+ import { existsSync, mkdtempSync, readdirSync, readFileSync } from "node:fs";
45
45
  import { tmpdir } from "node:os";
46
46
  import * as path from "node:path";
47
47
  import { fileURLToPath } from "node:url";
48
+ import { stageMetaHookBundle } from "./lib/meta-hook-bundle.ts";
48
49
  import { reclaimOnExit } from "./lib/reclaim-on-exit.ts";
49
50
 
50
51
  let passed = 0;
@@ -191,7 +192,7 @@ for (const event of OWNER_EVENTS) {
191
192
  const bare = spawnSync(LAUNCHER, [], { encoding: "utf8", input: "" });
192
193
  ok("launcher with EMPTY argv exits non-zero", bare.status !== null && bare.status !== 0);
193
194
  ok("launcher with empty argv names the args drop on stderr", /discarded the hook's `args`/.test(bare.stderr));
194
- ok("launcher with empty argv names the required floor", /2\.1\.217/.test(bare.stderr));
195
+ ok("launcher with empty argv names the required floor", /2\.1\.306/.test(bare.stderr));
195
196
  ok("launcher with empty argv refuses a fallback", /no shell-form fallback/i.test(bare.stderr));
196
197
  ok("launcher writes nothing to stdout on refusal (never a hook response)", bare.stdout === "");
197
198
  }
@@ -232,19 +233,9 @@ for (const event of OWNER_EVENTS) {
232
233
  // exactly what the metacharacter cell below silently failed to prove in its first
233
234
  // version (cross-review, 2026-07-22).
234
235
  function makeBundle(root: string): void {
235
- mkdirSync(path.join(root, "lib"), { recursive: true });
236
- mkdirSync(path.join(root, "scripts"), { recursive: true });
237
- copyFileSync(path.join(REPO_DIR, "pi-extensions", "meta-bridge-hook.ts"), path.join(root, "meta-bridge-hook.ts"));
238
- copyFileSync(
239
- path.join(REPO_DIR, "pi-extensions", "lib", "meta-session.ts"),
240
- path.join(root, "lib", "meta-session.ts"),
241
- );
242
- copyFileSync(path.join(REPO_DIR, "pi-extensions", "lib", "session-id.js"), path.join(root, "lib", "session-id.js"));
243
- const launcher = path.join(root, "scripts", "hook-launch.sh");
244
- copyFileSync(LAUNCHER, launcher);
245
- // The exec form names the launcher as the executable, so the +x bit is load-bearing:
246
- // without it Claude gets ENOEXEC, not a degraded path. The installer chmods it too.
247
- chmodSync(launcher, 0o755);
236
+ // Entry + its OWN import closure + the launcher (scripts/lib/meta-hook-bundle.ts): a hand-kept
237
+ // list here could stay green while the installer missed a file the hook now imports.
238
+ stageMetaHookBundle(root, path.join(REPO_DIR, "pi-extensions", "meta-bridge-hook.ts"), LAUNCHER);
248
239
  }
249
240
 
250
241
  /** Resolve one manifest element the way Claude does: plain-string substitution, no shell. */
@@ -321,7 +321,7 @@ function finish(t) {
321
321
  // artifact ship" is precisely the question it owns. The subset form named only the five
322
322
  // pre-0.14 verbs and would have accepted a candidate with no entwurf_fresh_call /
323
323
  // entwurf_resume_call; equality also refuses an extra or duplicated verb.
324
- const EXPECT_TOOLS = ['entwurf_callback', 'entwurf_fresh_call', 'entwurf_inbox_read', 'entwurf_peers', 'entwurf_register_native', 'entwurf_resume_call', 'entwurf_self', 'entwurf_v2'];
324
+ const EXPECT_TOOLS = ['entwurf_callback', 'entwurf_fresh_call', 'entwurf_inbox_read', 'entwurf_peers', 'entwurf_register_native', 'entwurf_resume_call', 'entwurf_retire_call', 'entwurf_self', 'entwurf_v2'];
325
325
  if (names.length !== EXPECT_TOOLS.length || EXPECT_TOOLS.some((n, i) => names[i] !== n)) {
326
326
  console.error('MCP tool set MISMATCH — want exactly [' + EXPECT_TOOLS.join(',') + '] got [' + names.join(',') + ']');
327
327
  process.exit(1);
@@ -357,7 +357,7 @@ cat > "$FAKEBIN/claude" <<'SH'
357
357
  #!/usr/bin/env bash
358
358
  printf '%s\n' "$*" >> "$FAKE_CLAUDE_LOG"
359
359
  case "$1${2:+ $2}${3:+ $3}" in
360
- "--version") echo "2.1.217 (Claude Code)" ;;
360
+ "--version") echo "2.1.306 (Claude Code)" ;;
361
361
  "plugin list --json")
362
362
  printf '[{"id":"entwurf-meta-receive@meta-bridge-local","version":"0.1.0","enabled":true,"installPath":"%s"}]\n' "$FAKE_INSTALL_PATH" ;;
363
363
  "plugin list"*) printf '%s\n' "entwurf-meta-receive@meta-bridge-local" " Status: enabled" ;;
@@ -91,7 +91,7 @@ cat > "$BIN/claude" <<'SH'
91
91
  printf '%s\n' "$*" >> "$FAKE_CLAUDE_LOG"
92
92
  case "$1${2:+ $2}${3:+ $3}" in
93
93
  "--version")
94
- echo "2.1.217 (Claude Code)" ;;
94
+ echo "2.1.306 (Claude Code)" ;;
95
95
  "plugin list --json")
96
96
  # FAKE_NO_JSON stands in for a Claude floor with no --json surface at all.
97
97
  [ "${FAKE_NO_JSON:-0}" = 1 ] && exit 0
@@ -214,6 +214,23 @@ if env CLAUDE_PLUGIN_ROOT="$PLANTED" "${HOOK_ARGV[@]}" < "$TMP/hook-input.json"
214
214
  else
215
215
  bad "owner fixture drive failed: $(tr '\n' ' ' < "$TMP/hook-run.txt" | cut -c1-300)"
216
216
  fi
217
+ # The INSTALLED closure, not the checkout, committed the arm binding (#147 L2 B1): the record
218
+ # and the receiver marker the planted bundle wrote carry the same fresh value. A bundle missing
219
+ # the lock module its hook imports could not have loaded far enough to write either.
220
+ if python3 - "$AGENT" <<'PY'
221
+ import glob, json, os, re, sys
222
+ agent = sys.argv[1]
223
+ recs = [json.load(open(f)) for f in glob.glob(os.path.join(agent, "meta-sessions", "*.meta.json"))]
224
+ rec = next(r for r in recs if r.get("nativeSessionId") == "oracle-native-1")
225
+ marker = json.load(open(os.path.join(agent, "meta-receivers", f"{rec['gardenId']}.json")))
226
+ value = rec.get("attachIncarnation")
227
+ sys.exit(0 if isinstance(value, str) and re.fullmatch(r"[0-9a-f]{32}", value) and marker.get("attachIncarnation") == value else 1)
228
+ PY
229
+ then
230
+ ok "the installed bundle committed a paired arm binding (record == receiver marker)"
231
+ else
232
+ bad "[QK:DOCTOR-ORACLE-INSTALLED-ARM-BINDING] the installed bundle's arm did not leave a record and receiver marker carrying the same attachIncarnation"
233
+ fi
217
234
 
218
235
  # Fake live entwurf MCP child of this same stand-in Claude pid. The doctor discovers it
219
236
  # by env + agent dir, so a UNIQUE sandbox agent dir keeps it from colliding with the
@@ -553,6 +570,15 @@ mv "$PLANTED/lib/meta-session.ts" "$TMP/meta-session.bak"
553
570
  expect_red "installed writer bundle missing" "deployed writer version is UNKNOWN"
554
571
  mv "$TMP/meta-session.bak" "$PLANTED/lib/meta-session.ts"
555
572
 
573
+ # M10b/M10c — the hook's record-arm lock module (#147 L2 B1) is part of the deployed writer
574
+ # closure: missing and drifted bytes are each a named red, not a runtime-only surprise.
575
+ mv "$PLANTED/lib/entwurf-v2-lock.ts" "$TMP/entwurf-v2-lock.bak"
576
+ expect_red "installed hook lock module missing" "deployed hook lock module MISSING"
577
+ cp "$TMP/entwurf-v2-lock.bak" "$PLANTED/lib/entwurf-v2-lock.ts"
578
+ printf '\n// drift\n' >> "$PLANTED/lib/entwurf-v2-lock.ts"
579
+ expect_red "installed hook lock module drifted" "deployed hook lock module STALE"
580
+ mv "$TMP/entwurf-v2-lock.bak" "$PLANTED/lib/entwurf-v2-lock.ts"
581
+
556
582
  # M11 — a FAILING `claude mcp get` whose output still matches. `|| true` would have
557
583
  # read this as a healthy USER-scope reach; rc must survive the capture.
558
584
  export FAKE_MCP_RC=3
@@ -5,7 +5,7 @@
5
5
  * Drives the REAL CLI as a subprocess against sandboxed fixture stores (never the
6
6
  * live store; every path is an explicit mkdtemp dir). What it proves:
7
7
  *
8
- * - the join is the FULL verbatim strict 9-field v3 record — gardenId,
8
+ * - the join is the FULL verbatim strict v3 record — gardenId,
9
9
  * nativeSessionId AND transcriptPath cross together, sorted by gardenId;
10
10
  * - drift / symlink / invalid-UTF-8 entries become in-band defects with exit 0,
11
11
  * and a symlinked record's target bytes are never followed into the join;
@@ -58,6 +58,7 @@ function record(gardenId: string, over: Partial<MetaIdentity> = {}): string {
58
58
  transcriptPath: `/t/${gardenId}.jsonl`,
59
59
  createdAt: "2026-06-11T00:00:00.000Z",
60
60
  recordUpdatedAt: "2026-06-11T00:00:00.000Z",
61
+ attachIncarnation: null,
61
62
  ...over,
62
63
  });
63
64
  }
@@ -119,6 +120,7 @@ function parseProjection(run: CliRun, label: string): Projection {
119
120
  transcriptPath: null,
120
121
  createdAt: "2026-06-11T00:00:00.000Z",
121
122
  recordUpdatedAt: "2026-06-11T00:00:00.000Z",
123
+ attachIncarnation: null,
122
124
  },
123
125
  {
124
126
  schemaVersion: 3,
@@ -130,11 +132,14 @@ function parseProjection(run: CliRun, label: string): Projection {
130
132
  transcriptPath: `/t/${GID_B}.jsonl`,
131
133
  createdAt: "2026-06-11T00:00:00.000Z",
132
134
  recordUpdatedAt: "2026-06-11T00:00:00.000Z",
135
+ attachIncarnation: null,
133
136
  },
134
137
  ],
135
- "[QK:MF-FULL-V3-JOIN] citizens are the full verbatim 9-field v3 records, sorted by gardenId",
138
+ "[QK:MF-FULL-V3-JOIN] citizens are the full verbatim v3 records (10 fields, attachIncarnation included), sorted by gardenId",
139
+ );
140
+ console.log(
141
+ " ok citizens are the full verbatim v3 records (10 fields, attachIncarnation included), sorted by gardenId",
136
142
  );
137
- console.log(" ok citizens are the full verbatim 9-field v3 records, sorted by gardenId");
138
143
  passed++;
139
144
  ok("valid store has no defects", facts.defects.length === 0);
140
145
  const again = cli([dir]);