@junghanacs/entwurf 0.40.0 → 0.41.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.
Files changed (128) hide show
  1. package/AGENTS.md +4 -2
  2. package/BASELINE.md +55 -2
  3. package/CHANGELOG.md +562 -0
  4. package/CONTRIBUTING.md +1 -1
  5. package/DELIVERY.md +18 -2
  6. package/FAQ.md +8 -4
  7. package/README.md +60 -45
  8. package/VERIFY.md +14 -11
  9. package/demo/README.md +1 -1
  10. package/docs/acp-backend-rail.md +6 -2
  11. package/docs/external-mcp-host.md +8 -6
  12. package/docs/fresh-cut-policy.md +9 -1
  13. package/docs/setup-clean-host.md +76 -20
  14. package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +81 -19
  15. package/mcp/entwurf-bridge/dist/pi-extensions/lib/cc-retire-pidfd.py +236 -0
  16. package/mcp/entwurf-bridge/dist/pi-extensions/lib/cc-retire-request.js +473 -0
  17. package/mcp/entwurf-bridge/dist/pi-extensions/lib/control-retire.js +125 -0
  18. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-retire-production.js +149 -0
  19. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-retire.js +158 -0
  20. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-surface.js +2 -1
  21. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-visible-resume.js +283 -20
  22. package/mcp/entwurf-bridge/dist/pi-extensions/lib/fresh-call-composition.js +7 -0
  23. package/mcp/entwurf-bridge/dist/pi-extensions/lib/fresh-call-dispatch.js +47 -3
  24. package/mcp/entwurf-bridge/dist/pi-extensions/lib/herdr-fresh-call.js +3 -1
  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/resume-launch-identity.js +67 -17
  31. package/mcp/entwurf-bridge/dist/pi-extensions/lib/retirement-event.js +211 -0
  32. package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-hook.js +124 -10
  33. package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-pi-durable.js +53 -2
  34. package/mcp/entwurf-bridge/dist/scripts/meta-facts.js +1 -1
  35. package/mcp/entwurf-bridge/dist/scripts/probe-bridge-command.js +1 -0
  36. package/mcp/entwurf-bridge/src/index.ts +93 -19
  37. package/mcp/entwurf-bridge/test.sh +2 -2
  38. package/mcp/entwurf-bridge/tsconfig.build.json +10 -1
  39. package/package.json +15 -14
  40. package/pi/meta-bridge/entwurf-meta-receive/scripts/hook-launch.sh +3 -3
  41. package/pi/pi-durable/carrier/tui.js +18 -3
  42. package/pi/pi-durable/overlay/upstream-pin.json +12 -12
  43. package/pi-extensions/entwurf-control.ts +55 -20
  44. package/pi-extensions/lib/acp/backend-adapter.ts +5 -3
  45. package/pi-extensions/lib/acp/backend.ts +12 -9
  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-session.ts +140 -16
  58. package/pi-extensions/lib/mux-fresh-call.ts +7 -27
  59. package/pi-extensions/lib/mux-launch.ts +29 -0
  60. package/pi-extensions/lib/mux-resume-call.ts +33 -12
  61. package/pi-extensions/lib/pi-retire-request.ts +111 -0
  62. package/pi-extensions/lib/resume-launch-identity.ts +104 -18
  63. package/pi-extensions/lib/retirement-event.ts +253 -0
  64. package/pi-extensions/meta-bridge-hook.ts +142 -13
  65. package/pi-extensions/meta-bridge-pi-durable.ts +57 -3
  66. package/run.sh +123 -34
  67. package/scripts/agy-bridge-config.py +4 -1
  68. package/scripts/build-bridge.sh +14 -0
  69. package/scripts/check-capability-bundle-reach.ts +135 -2
  70. package/scripts/check-entwurf-bridge-boot.ts +2 -1
  71. package/scripts/check-entwurf-fact-provider.ts +1 -0
  72. package/scripts/check-entwurf-v2-production.ts +2 -0
  73. package/scripts/check-entwurf-v2-surface.ts +7 -0
  74. package/scripts/check-gate-qualification.ts +14 -8
  75. package/scripts/check-herdr-activation.ts +2 -2
  76. package/scripts/check-herdr-placement.ts +1 -0
  77. package/scripts/check-hook-launch-topology.ts +6 -15
  78. package/scripts/check-install-container.sh +2 -2
  79. package/scripts/check-meta-doctor-oracle.sh +27 -1
  80. package/scripts/check-meta-facts.ts +8 -3
  81. package/scripts/check-meta-hook-session-switch.ts +230 -13
  82. package/scripts/check-meta-identity-consumers.ts +1 -0
  83. package/scripts/check-meta-listing.ts +1 -0
  84. package/scripts/check-meta-manifest-schema.py +1 -1
  85. package/scripts/check-meta-session.ts +31 -0
  86. package/scripts/check-meta-v3-record.ts +55 -1
  87. package/scripts/check-mux-resume-call.ts +35 -9
  88. package/scripts/check-peer-facts.ts +1 -0
  89. package/scripts/check-pi-durable-contact.ts +3 -1
  90. package/scripts/check-pi-durable-send.ts +4 -2
  91. package/scripts/check-pi-mcp-bridge.ts +1 -0
  92. package/scripts/check-resume-launch-identity.ts +62 -23
  93. package/scripts/lib/codex-fresh-live-protocol.ts +3 -2
  94. package/scripts/lib/meta-hook-bundle.ts +72 -0
  95. package/scripts/meta-bridge-claude-floor.sh +13 -9
  96. package/scripts/meta-bridge-doctor.sh +17 -0
  97. package/scripts/meta-bridge-install.sh +6 -0
  98. package/scripts/meta-facts.ts +1 -1
  99. package/scripts/mutants/bridge-boot-resume.json +5 -2
  100. package/scripts/mutants/cc-retire-pidfd.json +129 -0
  101. package/scripts/mutants/cc-retire-request.json +171 -0
  102. package/scripts/mutants/control-retire.json +105 -0
  103. package/scripts/mutants/control-send-receipt.json +1 -1
  104. package/scripts/mutants/copilot-receive.json +2 -6
  105. package/scripts/mutants/entwurf-v2-retire.json +476 -0
  106. package/scripts/mutants/fresh-call-dispatch.json +37 -0
  107. package/scripts/mutants/meta-hook-session-switch.json +88 -0
  108. package/scripts/mutants/meta-identity.json +57 -0
  109. package/scripts/mutants/mux-fresh-call.json +1 -1
  110. package/scripts/mutants/mux-resume-call.json +40 -4
  111. package/scripts/mutants/pack-install.json +4 -4
  112. package/scripts/mutants/pi-durable.json +139 -0
  113. package/scripts/mutants/pi-floor.json +5 -5
  114. package/scripts/mutants/pi-retire-request.json +72 -0
  115. package/scripts/mutants/resume-launch-identity.json +35 -8
  116. package/scripts/mutants/retirement-event.json +195 -0
  117. package/scripts/mutants/v2-surface.json +16 -0
  118. package/scripts/mutants/v2-visible-resume.json +283 -13
  119. package/scripts/probe-bridge-command.ts +1 -0
  120. package/scripts/smoke-agy-install-state.sh +2 -2
  121. package/scripts/smoke-codex-fresh-live.ts +2 -1
  122. package/scripts/smoke-copilot-mcp-state.sh +1 -1
  123. package/scripts/smoke-herdr-raw-install-live.sh +15 -7
  124. package/scripts/smoke-meta-install-state.sh +4 -4
  125. package/scripts/smoke-mux-lifecycle-live.ts +10 -10
  126. package/scripts/smoke-omp-mcp-state.sh +1 -1
  127. package/scripts/smoke-pi-durable-fresh-live.ts +5 -3
  128. package/scripts/smoke-pi-provider-state.sh +3 -3
@@ -53,20 +53,38 @@
53
53
  * closure (build-bridge → dist), because Node REFUSES strip-types on a `.ts` below
54
54
  * node_modules (ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING). The installer picks
55
55
  * the artifact by layout (0.12.5), mirroring start.sh / store-doctor.
56
- * Imports `./lib/meta-session.ts` (+ `./lib/session-id.js`); `rewriteRelativeImportExtensions`
57
- * rewrites that to `./lib/meta-session.js` in the emitted closure. The installer
58
- * copies the lib dir alongside this file so `${CLAUDE_PLUGIN_ROOT}` self-locates it.
56
+ * Imports `./lib/meta-session.ts` and `./lib/entwurf-v2-lock.ts` (+ `./lib/session-id.js`);
57
+ * `rewriteRelativeImportExtensions` rewrites those to `.js` in the emitted closure. The
58
+ * installer copies exactly those lib files alongside this file so `${CLAUDE_PLUGIN_ROOT}`
59
+ * self-locates them.
60
+ *
61
+ * RECORD-ARM LOCK (#147 L2 B1). Every write this hook makes to an EXISTING record happens
62
+ * under a per-gid lock (`defaultMetaRecordArmLocksDir`, the shared `acquireLock` primitive in
63
+ * its own directory — never the v2 dispatch lock dir, which visible resume holds across its
64
+ * observation). The garden id is learned by a READ-ONLY certified lookup; a record found
65
+ * absent is published create-only. On an arm-capable event the lock is held from the upsert
66
+ * that commits a fresh `attachIncarnation` through the receiver marker that copies it, so two
67
+ * normal arms (or an arm and a UserPromptSubmit) always finish as a matching record/marker
68
+ * pair. An arm that cannot take the lock within its bound writes NOTHING and logs ERROR: that
69
+ * attach is not committed, and a live native process may then exist that no record names — a
70
+ * boundary, not a refusal. A UserPromptSubmit that finds the lock busy skips its backfill.
59
71
  */
60
72
 
61
73
  import * as fs from "node:fs";
62
74
  import * as path from "node:path";
75
+ import { acquireLock, type LockClaim, releaseLock } from "./lib/entwurf-v2-lock.ts";
63
76
  import {
64
77
  defaultMetaMailboxDir,
78
+ defaultMetaRecordArmLocksDir,
65
79
  defaultMetaSessionsDir,
80
+ findCertifiedIdentityByNativeSessionId,
66
81
  isPlausibleOwnerPid,
82
+ type MetaIdentityMintInput,
67
83
  type MetaReceiverArmProvenance,
84
+ mintAttachIncarnation,
68
85
  readMetaSenderMarker,
69
86
  removeMetaReceiverMarker,
87
+ UpsertActionMismatchError,
70
88
  upsertMetaSession,
71
89
  writeMetaReceiverMarker,
72
90
  writeMetaSenderMarker,
@@ -97,8 +115,62 @@ function logLine(level: LogLevel, message: string): void {
97
115
  }
98
116
  }
99
117
 
118
+ /** The record-arm lock this run holds, released on every exit path (see {@link emit}). */
119
+ let heldArmLock: LockClaim | null = null;
120
+
121
+ /** Arm-capable events wait this long for the record-arm lock; the critical section is a few file writes. */
122
+ const ARM_LOCK_WAIT_MS = 2_000;
123
+ const ARM_LOCK_POLL_MS = 25;
124
+
125
+ function sleepSync(ms: number): void {
126
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
127
+ }
128
+
129
+ /**
130
+ * Take the record-arm lock for `gardenId`: ONE attempt, or (arm-capable events) polled up to
131
+ * {@link ARM_LOCK_WAIT_MS}. A throw from the primitive (its claim could not be written) is
132
+ * the same answer as busy — the caller writes nothing.
133
+ */
134
+ function takeArmLock(gardenId: string, wait: boolean): { ok: true } | { ok: false; detail: string } {
135
+ const dir = defaultMetaRecordArmLocksDir();
136
+ const deadline = Date.now() + (wait ? ARM_LOCK_WAIT_MS : 0);
137
+ for (;;) {
138
+ let detail: string;
139
+ try {
140
+ const acquired = acquireLock(gardenId, { dir });
141
+ if (acquired.ok) {
142
+ heldArmLock = acquired.claim;
143
+ return { ok: true };
144
+ }
145
+ detail = acquired.conflict.detail;
146
+ } catch (err) {
147
+ detail = err instanceof Error ? err.message : String(err);
148
+ }
149
+ if (Date.now() >= deadline) return { ok: false, detail };
150
+ sleepSync(ARM_LOCK_POLL_MS);
151
+ }
152
+ }
153
+
154
+ function releaseArmLock(): void {
155
+ const claim = heldArmLock;
156
+ if (claim === null) return;
157
+ heldArmLock = null;
158
+ try {
159
+ const outcome = releaseLock(claim, { dir: defaultMetaRecordArmLocksDir() });
160
+ if (outcome !== "released") logLine("WARN", `record-arm lock release for ${claim.gardenId}: ${outcome}`);
161
+ } catch (err) {
162
+ // The holder pid is this short-lived hook, so a lock it failed to unlink is reclaimed
163
+ // by the next acquirer once this process is gone (same host + ESRCH).
164
+ logLine(
165
+ "WARN",
166
+ `record-arm lock release failed for ${claim.gardenId}: ${err instanceof Error ? err.message : String(err)}`,
167
+ );
168
+ }
169
+ }
170
+
100
171
  /** Emit a hook response on stdout and exit 0. `{}` means "did nothing, do not block startup". */
101
172
  function emit(payload: Record<string, unknown>): never {
173
+ releaseArmLock();
102
174
  process.stdout.write(`${JSON.stringify(payload)}\n`);
103
175
  process.exit(0);
104
176
  }
@@ -174,6 +246,59 @@ function resolveMetaHookOwnerPid(): number | null {
174
246
  return ownerPid;
175
247
  }
176
248
 
249
+ /**
250
+ * Commit this event's record write and return the garden id (#147 L2 B1). The garden id to
251
+ * lock is learned READ ONLY; a record found absent is published create-only (a create needs no
252
+ * lock: its exclusive publish cannot replace anything, and a concurrent publisher turns the
253
+ * create into a refusal here, never into an unlocked attach). Every write to an EXISTING
254
+ * record then happens under the record-arm lock, which the caller keeps through the receiver
255
+ * marker write (released by {@link emit}). An arm-capable event mints its binding in that same
256
+ * atomic upsert. Lock busy: an arm writes nothing and logs ERROR (admission failure — not a
257
+ * committed attach, not an armed watch); any other event skips its backfill with a WARN.
258
+ * Throws on a store defect, exactly as the upsert does.
259
+ */
260
+ function commitRecord(
261
+ input: MetaIdentityMintInput,
262
+ armsWatch: boolean,
263
+ logContext: string,
264
+ ): { gardenId: string; attachIncarnation: string | undefined } {
265
+ let existing = findCertifiedIdentityByNativeSessionId(input.nativeSessionId);
266
+ if (existing === null) {
267
+ try {
268
+ const created = upsertMetaSession({ input, expectAction: "create" });
269
+ logLine("INFO", `create record ${path.basename(created.path)} (${logContext})`);
270
+ // Nothing to bind and nothing older to preserve: the create is the whole write.
271
+ if (!armsWatch) return { gardenId: created.record.gardenId, attachIncarnation: undefined };
272
+ existing = created.record;
273
+ } catch (err) {
274
+ if (!(err instanceof UpsertActionMismatchError)) throw err;
275
+ existing = findCertifiedIdentityByNativeSessionId(input.nativeSessionId);
276
+ if (existing === null) throw err;
277
+ }
278
+ }
279
+ const locked = takeArmLock(existing.gardenId, armsWatch);
280
+ if (!locked.ok) {
281
+ if (armsWatch) {
282
+ // Admission failure: NOTHING is written, so the store keeps whatever pair it had. This
283
+ // attach is not committed, and a live process may now hold this session unseen.
284
+ logLine(
285
+ "ERROR",
286
+ `record-arm lock busy — attach NOT committed, watch not armed (garden=${existing.gardenId}, ${logContext}): ${locked.detail}`,
287
+ );
288
+ } else {
289
+ logLine(
290
+ "WARN",
291
+ `record-arm lock busy — backfill skipped (garden=${existing.gardenId}, ${logContext}): ${locked.detail}`,
292
+ );
293
+ }
294
+ emit({});
295
+ }
296
+ const attachIncarnation = armsWatch ? mintAttachIncarnation() : undefined;
297
+ const result = upsertMetaSession({ input: { ...input, attachIncarnation }, expectAction: "attach" });
298
+ logLine("INFO", `${result.action} record ${path.basename(result.path)} (${logContext})`);
299
+ return { gardenId: result.record.gardenId, attachIncarnation };
300
+ }
301
+
177
302
  function main(): void {
178
303
  let raw: string;
179
304
  try {
@@ -230,16 +355,16 @@ function main(): void {
230
355
  emit({});
231
356
  }
232
357
 
358
+ // Arm-capable events commit a fresh binding; every other event preserves it.
359
+ const armsWatch = armProvenanceFor(eventName) !== null;
233
360
  let gardenId: string;
361
+ let attachIncarnation: string | undefined;
234
362
  try {
235
- const result = upsertMetaSession({
236
- input: { backend: "claude-code", nativeSessionId: sessionId, transcriptPath, cwd, model },
237
- });
238
- gardenId = result.record.gardenId;
239
- logLine(
240
- "INFO",
241
- `${result.action} record ${path.basename(result.path)} (event=${eventName}, source=${source}, native=${sessionId})`,
242
- );
363
+ ({ gardenId, attachIncarnation } = commitRecord(
364
+ { backend: "claude-code", nativeSessionId: sessionId, transcriptPath, cwd, model },
365
+ armsWatch,
366
+ `event=${eventName}, source=${source}, native=${sessionId}`,
367
+ ));
243
368
  } catch (err) {
244
369
  // Best-effort: a broken record store must surface via the doctor, not by
245
370
  // breaking the user's session open. Log and continue with no arm. This is
@@ -362,12 +487,16 @@ function main(): void {
362
487
  nativeSessionId: sessionId,
363
488
  ownerPid,
364
489
  armProvenance,
490
+ attachIncarnation,
365
491
  });
366
492
  logLine("INFO", `receiver marker ${gardenId} owner=${ownerPid} arm=${eventName} source=${source}`);
367
493
  } catch (err) {
494
+ // ERROR, after `INFO armed watch` so the hook-log doctor does not read a later arm as
495
+ // recovery: the record already carries this arm's binding, the marker does not, so the
496
+ // garden is undeliverable AND a later resume refuses it as unbound.
368
497
  logLine(
369
- "WARN",
370
- `receiver marker write failed (event=${eventName}, garden=${gardenId}): ${err instanceof Error ? err.message : String(err)}`,
498
+ "ERROR",
499
+ `receiver marker write failed — record binding committed, marker not (event=${eventName}, garden=${gardenId}): ${err instanceof Error ? err.message : String(err)}`,
371
500
  );
372
501
  }
373
502
  } else {
@@ -98,6 +98,13 @@
98
98
  * operator host started outside tmux gets the bridge's named `no-tmux-context` refusal. Exposing the
99
99
  * tool is not a caller LIVE proof.
100
100
  *
101
+ * CALLER RESUME (the seventh tool, #147 L1). `entwurf_resume_call` lets this durable citizen reopen a
102
+ * DORMANT pi or Claude Code citizen under its own garden id (#147 L2), under the bridge's own
103
+ * single-`target` schema (pinned below). The bridge decides everything else from the target's record,
104
+ * exactly as for any other caller: other backends have no same-id resume, a live target is refused,
105
+ * and the window opens in the tmux session this host was launched in. Exposing the tool names no new authority and is not a caller LIVE
106
+ * proof; it never resumes this durable host itself.
107
+ *
101
108
  * NOT HERE: crash-safe receive. `entwurf_inbox_read` archives a body before the durable tool
102
109
  * result commits, so a crash between the two loses that body to the model; recovery settles the
103
110
  * call as interrupted and no doorbell rings again. A drain that fails part-way has archived the
@@ -156,6 +163,8 @@ export const PI_DURABLE_TOOL_NAMES = [
156
163
  "entwurf_inbox_read",
157
164
  "entwurf_callback",
158
165
  "entwurf_fresh_call",
166
+ "entwurf_resume_call",
167
+ "entwurf_retire_call",
159
168
  ] as const;
160
169
  export type PiDurableToolName = (typeof PI_DURABLE_TOOL_NAMES)[number];
161
170
 
@@ -164,7 +173,7 @@ const DRAFT_07 = "http://json-schema.org/draft-07/schema#";
164
173
  /**
165
174
  * The bridge's `tools/list` inputSchemas for the exposed tools (`mcp/entwurf-bridge/src/index.ts`
166
175
  * `entwurf_v2` 456-507, `entwurf_self`, `entwurf_peers`, `entwurf_inbox_read`, `entwurf_callback`,
167
- * `entwurf_fresh_call`),
176
+ * `entwurf_fresh_call`, `entwurf_resume_call`, `entwurf_retire_call`),
168
177
  * as the bridge serialises them.
169
178
  * `attach` compares the live list against these and refuses on any difference.
170
179
  */
@@ -235,7 +244,7 @@ export const PI_DURABLE_TOOL_SCHEMAS: Readonly<Record<PiDurableToolName, Readonl
235
244
  maxLength: 200,
236
245
  pattern: "^[A-Za-z0-9][A-Za-z0-9._/:\\[\\]-]*$",
237
246
  description:
238
- "Required runtime model: canonical provider/model for pi, provider/<exact catalog model id> for pi-durable, a Claude Code model id/alias, or a Copilot/OMP/Codex model name.",
247
+ "Required runtime model: canonical provider/model for pi (a bare alias is refused), provider/<exact catalog model id> for pi-durable, a Claude Code model id/alias, or a Copilot/OMP/Codex model name. Neither pi nor pi-durable may name the openrouter provider.",
239
248
  },
240
249
  task: {
241
250
  type: "string",
@@ -266,6 +275,33 @@ export const PI_DURABLE_TOOL_SCHEMAS: Readonly<Record<PiDurableToolName, Readonl
266
275
  required: ["backend", "model", "task"],
267
276
  $schema: DRAFT_07,
268
277
  },
278
+ entwurf_resume_call: {
279
+ type: "object",
280
+ properties: {
281
+ target: {
282
+ type: "string",
283
+ minLength: 1,
284
+ pattern: "^\\d{8}T\\d{6}-[0-9a-f]{6}$",
285
+ description: "Garden id of the DORMANT pi or Claude Code citizen to reopen (discover with entwurf_peers).",
286
+ },
287
+ },
288
+ required: ["target"],
289
+ $schema: DRAFT_07,
290
+ },
291
+ entwurf_retire_call: {
292
+ type: "object",
293
+ properties: {
294
+ target: {
295
+ type: "string",
296
+ minLength: 1,
297
+ pattern: "^\\d{8}T\\d{6}-[0-9a-f]{6}$",
298
+ description:
299
+ "Exact garden id of the Pi or Claude Code target the human operator explicitly requested to retire.",
300
+ },
301
+ },
302
+ required: ["target"],
303
+ $schema: DRAFT_07,
304
+ },
269
305
  };
270
306
 
271
307
  /** Key-order-insensitive JSON, for comparing a schema copy against the bridge's own. */
@@ -318,7 +354,7 @@ export interface PiDurableRoots {
318
354
  // ---------------------------------------------------------------------------
319
355
  // Durable surface, typed NARROWLY and locally — read at pi-durable 1.0.2 @cd32f77 (#129,
320
356
  // `harness/types.ts`; historical coordinates, not re-anchored here). The current supply is the
321
- // carrier emitted from Entwurf's owned source (pi/pi-durable/src, #141) against the 1.3.0 SDK set
357
+ // carrier emitted from Entwurf's owned source (pi/pi-durable/src, #141) against the 1.4.0 SDK set
322
358
  // `pi/pi-durable/overlay/upstream-pin.json` names (origin `abe508e`). `[read 2026-10-13]` at
323
359
  // v1.2.0 one shape typed below moved: `ToolExecutionResult.content` is now `output`, beside the new
324
360
  // `structuredOutput` (`harness/types.ts`, its CHANGELOG's breaking changes). An omitted `output`
@@ -748,6 +784,24 @@ const TOOL_DESCRIPTIONS: Record<PiDurableToolName, string> = {
748
784
  "(backend, model and task required; optional cwd and placement). Returns the bridge's LAUNCH receipt or its " +
749
785
  "named refusal — not a started runtime, a delivered task or a reply; the sibling's callback names it. Opening " +
750
786
  "beside this host needs the tmux pane this host itself was launched in.",
787
+ entwurf_resume_call:
788
+ "Reopen ONE dormant pi or Claude Code citizen under its own garden id in a visible tmux window through the " +
789
+ "entwurf-bridge (target required; the record supplies the session and cwd — no task, no prompt, no turn). " +
790
+ "Returns the bridge's LAUNCH and OBSERVATION receipts or its named refusal: a live target, another backend, " +
791
+ "or a Claude Code citizen not proven dormant is refused by name, and opening the window needs the tmux " +
792
+ "session this host itself was launched in.",
793
+ entwurf_retire_call:
794
+ "Only when the human operator explicitly requested this particular target/work: make ONE retirement attempt for that request " +
795
+ "through the bridge (target only, Pi or Claude Code; self retirement refuses). Development work, a role or " +
796
+ "a record-backed actor is NOT a grant. Use host approval where provided/configured; " +
797
+ "Pi and pi-durable have no built-in per-call approval prompt by default. An explicit operator request and " +
798
+ "agent operating policy are required; attribution and MCP annotations are hints, not grants or proof of approval. " +
799
+ "No autonomous cleanup, timers or automatic " +
800
+ "retry of unknown, cancelled or lost results. Do not manually /resume or /clear the target during the request. " +
801
+ "Pi shutdown-requested is only an ACK, not process exit, flush or model silence. Claude Code exited is that " +
802
+ "fd-pinned process only, not conversation end, children or flush. The JSON receipt keeps outcome, record and " +
803
+ "release separate; receipt return does not mean exit, and cancellation does not undo an action. Invoke once " +
804
+ "per explicitly named target, never infer a roster. This contact does not turn hints into authorization.",
751
805
  };
752
806
 
753
807
  /** Announce, never push: the notice names the garden and the tool, never the body. */
package/run.sh CHANGED
@@ -224,7 +224,7 @@ Usage:
224
224
  ./run.sh smoke-agy-native-push-live # 봉인 8 LIVE acceptance for the native-push (agy) rail — OUT of pnpm check, needs LIVE=1 + AGY_CONVERSATION_ID (a live agy conversation). Drives the REAL antigravity adapter + register core + runEntwurfV2 (production deps): doctor-static preflight (dangling→FAIL, the ③ gate), probe route, register create/attach idempotency, fire→native-push delivered, post-send re-probe (D7 partial), bogus-conv→native-push-probe-indeterminate. Meta-store isolated to a temp dir (only the agy round-trip is real; no real-store residue). COST FENCE: open that agy conversation on gemini-3.6-flash (free account) — never a Pro tier; entwurf never selects the agy model and no assertion reads it. LIVE=1 AGY_CONVERSATION_ID=<convId> ./run.sh smoke-agy-native-push-live
225
225
  ./run.sh smoke-codex-native-push-live # on-demand LIVE acceptance for Codex native-push — needs LIVE=1 + CODEX_LIVE_THREAD_ID loaded by a visible TUI attached to the default app-server; probes the real UDS, routes through production runEntwurfV2, queues one exact-token turn without retry, re-probes, and leaves the visible TUI to show the model result
226
226
  ./run.sh smoke-codex-fresh-live # #95 RELEASE MUST: record-backed receipt fixture -> PUBLIC initial Pi fresh/callback in a session S that is NOT the app-server's -> Pi PUBLIC omitted-placement Codex fresh/exact callback, also in S -> Pi-to-Codex addressed native-push v2 -> Codex PUBLIC omitted-placement AND omitted-cwd fresh Pi/exact callback IN S beside its own TUI (lane B claim: the app-server env names another session, so S can only come from the caller's pane title; lane C claim: no cwd was asked for, so the directory can only come from the Codex caller's own record — and the Codex thread's own rollout session_meta.cwd, its pane and its record must all be the requested scratch, not the app-server's repo) -> Codex final v2 evidence. Needs LIVE=1 + ENTWURF_CODEX_APP_SERVER_PID + ENTWURF_CODEX_FRESH_MODEL + ENTWURF_CODEX_FRESH_PI_MODEL, and its ONE stable launch directory answered 'Trust' once in a plain codex -C there: codex records its direct consent per exact directory, so a directory with no answer at all opens a consent screen instead of a first turn. Only THIS gate treats that as a precondition and fails up front as codex-launch-cwd-undecided rather than timing out on a callback — an ordinary entwurf_fresh_call only prints the note and opens the window, because a human can answer the screen. The smoke PRINTS that directory as 'launch-cwd <dir>' and repeats it in the failure as a ready-to-run repair command — do not retype it from memory, and do not write it as \$TMPDIR/... (os.tmpdir() falls back to /tmp when TMPDIR is unset, so that spelling names a different directory in the filesystem root). The PID names the operator-owned app-server, which must sit in a DIFFERENT session from S; the smoke checks/prints all four coordinates, never starts/stops/guesses the app-server/session, never reads screen text, interrupts only its exact still-running Codex turn, and cleans only receipt-named window ids
227
- ./run.sh smoke-mux-lifecycle-live # RELEASE MUST integrated LIVE lifecycle acceptance for mux, through the REAL MCP surface — OUT of pnpm check, needs LIVE=1 and spends model turns (two pi siblings: native + recorded-ACP provider, each resumed once; one Claude Code sibling). tools/call fresh_call -> nonce callback sender envelope -> v2 control send landing in the sibling's own transcript -> resume_call REFUSED while live (window count unchanged) -> stable-handle close (pane gone, socket dead, record kept) -> dormant delivery refused honestly -> public entwurf_resume_call with LAUNCH and OBSERVATION receipts kept apart, same-gid socket alive, zero new citizens, zero lock residue, resumed pane_start_path == RECORD cwd (separate tmux query), transcript byte-identical across the resume -> v2 recall of the pre-close fact. claude-code resume refused target-not-pi, no window opened and no lock residue. LIVE=1 ./run.sh smoke-mux-lifecycle-live
227
+ ./run.sh smoke-mux-lifecycle-live # RELEASE MUST integrated LIVE lifecycle acceptance for mux, through the REAL MCP surface — OUT of pnpm check, needs LIVE=1 and spends model turns (two pi siblings: native + recorded-ACP provider, each resumed once; one Claude Code sibling). tools/call fresh_call -> nonce callback sender envelope -> v2 control send landing in the sibling's own transcript -> resume_call REFUSED while live (window count unchanged) -> stable-handle close (pane gone, socket dead, record kept) -> dormant delivery refused honestly -> public entwurf_resume_call with LAUNCH and OBSERVATION receipts kept apart, same-gid socket alive, zero new citizens, zero lock residue, resumed pane_start_path == RECORD cwd (separate tmux query), transcript byte-identical across the resume -> v2 recall of the pre-close fact. claude-code resume of the still-live sibling refused target-live, no window opened and no lock residue (its dormant same-id resume is deterministic-only, #147 L2). LIVE=1 ./run.sh smoke-mux-lifecycle-live
228
228
  ./run.sh check-meta-facts # deterministic gate for the meta-facts projection (#65): drives the REAL CLI — full-record join, parse-before-uniqueness, no-winner duplicates, drift/symlink/invalid-UTF-8 defects in-band, deterministic bytes, exit contract 0/2/3, dispatch+emit reachability
229
229
  ./run.sh check-herdr-plugin # deterministic gate for the #116 M2-b herdr plugin (`plugins/herdr/`): STATIC manifest shape for herdr 0.9.0 + the forbidden sections ([[startup]]/[[events]]/[[actions]]/[[link_handlers]]) asserted absent ([[build]] is check-herdr-plugin-build's subject since #116 M3-b3); BEHAVIOURAL drive of the REAL pane entry against a stub `entwurf` and stub HERDR_BIN_PATH that LOG every call — exactly one `herdr integration status` + one `entwurf peer-facts` + one `herdr agent list` per open, in that order (counted, not claimed), through ONE spawn call site; the four blocks each naming their own owner/observation time with no aggregate verdict; the D3=C fallback reading the activation ledger from the path its WRITER owns (XDG state, never the runtime's data root) and only when the phase is `active`, with ENTWURF_BIN and PATH still winning outright; the citizen axis alone skipping by name when no binary is reachable; every broken-evidence state a named red rather than a crash or an empty table, and a selected herdr atom that is `outdated`/`needs repair` as block [1]'s OWN red (`not installed` stays a reported skip); ambiguous never first-wins, diagnostics shown, activity in its own column, and zero writes to the plugin state/config dirs. No herdr binary, no Entwurf install
230
230
  ./run.sh check-herdr-runtime-bootstrap # deterministic gate for the #116 M3-b1 runtime leaf (`plugins/herdr/lib/runtime-bootstrap.mjs`): the Entwurf-owned stable active root as a REAL directory (nothing is put on PATH — the scoped wiring above names ABSOLUTE commands under it), ownership decided BEFORE the first mkdir, disk facts from lstat (a dangling symlink is not 'absent' and a link into another tree is not a directory), exact name@version + compiled entry + all three required bins present AND executable + a real `check-bridge`, the plugin-owned artifact lock coherent with the checkout, the owned npm cache and --ignore-scripts, a CERTIFIED journal (non-object/array/scalar/blank-identity/phase-contradicting-digest all refused) as the only ownership proof, all EIGHT active/staging/previous combinations named, the last good runtime never lost (torn swap AND corrupt-active beside a good backup), prior provenance carried across an install that may not finish, a failed candidate leaving the running runtime byte-identical, idempotent same-spec reinstall, a journal believed only while the disk backs it, a preflight-then-mutate inverse taking runtime last, and the refusal to record Herdr's commit as ours. NETWORK ZERO, npm ZERO — it drives a FIXTURE package, so the ACTUAL package proof lives in check-pack-install
@@ -1343,6 +1343,44 @@ check_control_send_receipt() {
1343
1343
  run_vitest pi-extensions/lib/control-send-receipt.test.ts
1344
1344
  }
1345
1345
 
1346
+ check_control_retire() {
1347
+ # Mutant execution coordinate only; the existing beside glob owns test discovery.
1348
+ section "Pi idle-only shutdown boundary (mutant execution coordinate)"
1349
+ run_vitest pi-extensions/lib/control-retire.test.ts
1350
+ }
1351
+
1352
+ check_pi_retire_request() {
1353
+ # Mutant execution coordinate only; the existing beside glob owns test discovery.
1354
+ section "Pi retire request adapter (mutant execution coordinate)"
1355
+ run_vitest pi-extensions/lib/pi-retire-request.test.ts
1356
+ }
1357
+
1358
+ check_cc_retire_pidfd() {
1359
+ # MUTANT EXECUTION COORDINATE, not a discovery path — check-tests-beside-behavior finds the test
1360
+ # like every other beside lane. This case exists because the #147 G3 CCRETIRE-* mutants name it as
1361
+ # gate argv; it stays in check:contracts for the same reason as check-control-send-receipt.
1362
+ section "Linux pidfd retire helper (mutant execution coordinate)"
1363
+ run_vitest pi-extensions/lib/cc-retire-pidfd.test.ts
1364
+ }
1365
+
1366
+ check_cc_retire_request() {
1367
+ # Mutant execution coordinate only; the existing beside glob owns test discovery.
1368
+ section "Claude Code retire request adapter (mutant execution coordinate)"
1369
+ run_vitest pi-extensions/lib/cc-retire-request.test.ts
1370
+ }
1371
+
1372
+ check_retirement_event() {
1373
+ # Mutant execution coordinate only; the existing beside glob owns test discovery.
1374
+ section "Retirement append leaf (mutant execution coordinate)"
1375
+ run_vitest pi-extensions/lib/retirement-event.test.ts
1376
+ }
1377
+
1378
+ check_entwurf_v2_retire() {
1379
+ # Mutant execution coordinate only; the existing beside glob owns test discovery.
1380
+ section "Requested retirement dispatcher and production binding (mutant execution coordinate)"
1381
+ run_vitest pi-extensions/lib/entwurf-v2-retire.test.ts pi-extensions/lib/entwurf-v2-retire-production.test.ts
1382
+ }
1383
+
1346
1384
  check_win32_preview() {
1347
1385
  # MUTANT EXECUTION COORDINATE, not a discovery path — the Windows preview's (#78) loader and
1348
1386
  # platform wiring tests are found by check-tests-beside-behavior like every other beside-behaviour
@@ -1455,6 +1493,15 @@ check_acp_foreground_only() {
1455
1493
  run_vitest pi-extensions/lib/acp/backend-adapter.test.ts
1456
1494
  }
1457
1495
 
1496
+ check_fresh_call_provider() {
1497
+ # NOT a discovery path — check-tests-beside-behavior finds the test by glob. This case is the
1498
+ # MUTANT EXECUTION COORDINATE for the #76 provider claims in scripts/mutants/fresh-call-dispatch.json
1499
+ # (same reasoning as check_entwurf_resume_args above): pi and pi-durable refuse a model that names
1500
+ # the openrouter provider, and pi refuses one that names no provider, at the dispatch root.
1501
+ section "fresh-call provider contract (mutant execution coordinate)"
1502
+ run_vitest pi-extensions/lib/fresh-call-dispatch.test.ts
1503
+ }
1504
+
1458
1505
  check_acp_set_model_readback() {
1459
1506
  # NOT a discovery path — check-tests-beside-behavior finds the test by glob. This case is the
1460
1507
  # MUTANT EXECUTION COORDINATE for scripts/mutants/acp-set-model-readback.json (#128): the claude
@@ -1670,7 +1717,8 @@ smoke_mux_lifecycle_live() {
1670
1717
  # resumed pane_start_path == the RECORD's cwd (separate tmux query, never pane text), and the
1671
1718
  # transcript byte-identical across the resume itself -> entwurf_v2 recalling the fact from
1672
1719
  # BEFORE the close, which is what proves the same CONVERSATION returned. Claude Code runs the
1673
- # callback+mailbox half and its resume is refused as target-not-pi with no window opened and no lock residue.
1720
+ # callback+mailbox half and its resume, while that sibling is still live, is refused as target-live with no
1721
+ # window opened and no lock residue (#147 L2: Claude Code's dormant resume is certified deterministically).
1674
1722
  # Fixture-fenced writes (XDG, four meta roots, v2 lock dir, control socket via fixture HOME);
1675
1723
  # the runtime auth roots stay REAL and sibling transcripts remain in the real pi agent dir as
1676
1724
  # evidence. Private tmux server per cell, bounded teardown, operator real-root entry sets
@@ -2731,7 +2779,7 @@ EOF
2731
2779
  check_claude_floor_coherence() {
2732
2780
  # #51 policy A. The Claude Code floor is a THREE-PART version, unlike the Node
2733
2781
  # floor's major lane: the discriminator lives in the patch position (2.1.138 drops
2734
- # `args`, 2.1.139 introduces the exec form, 2.1.217 is the version actually proven
2782
+ # `args`, 2.1.139 introduces the exec form, 2.1.217 is the version the exec form was proven on
2735
2783
  # in a live session). So it cannot borrow check-node-floor-coherence's major-only
2736
2784
  # comparison and needs its own binding.
2737
2785
  #
@@ -3973,6 +4021,10 @@ check_pack() {
3973
4021
  # JS for installed entrypoints. The files array lists pi/pi-durable per FILE, so nothing admits these
3974
4022
  # today; this tripwire is what fails loud the day an allowlist entry would.
3975
4023
  local durable_source_pattern='^pi/pi-durable/(src/|tsconfig\.json$|overlay/runtime-contacts\.patch$)'
4024
+ # #148 S1b: the goldens and generator a beside test reads (pi-extensions/lib/fixtures/) are test input,
4025
+ # not product. `pi-extensions/` ships whole, so the `!pi-extensions/**/fixtures/**` negation is the only
4026
+ # thing keeping them out, and this is its tripwire (same 결합 규칙 as the `.test.ts` pattern above).
4027
+ local test_fixture_pattern='^pi-extensions/(.*/)?fixtures/'
3976
4028
 
3977
4029
  local pass=1 f pat hit
3978
4030
 
@@ -3999,6 +4051,13 @@ check_pack() {
3999
4051
  pass=0
4000
4052
  fi
4001
4053
 
4054
+ hit=$(grep -E "$test_fixture_pattern" <<<"$file_list" || true)
4055
+ if [ -n "$hit" ]; then
4056
+ fail "[check-pack] a beside test's fixtures (pi-extensions/**/fixtures/) ride the tarball — test input is not product; restore the files-array negation:"
4057
+ echo "$hit" | sed 's/^/ /' >&2
4058
+ pass=0
4059
+ fi
4060
+
4002
4061
  # #78 — a pack carries the Windows addon not at all, or as exactly a verified addon + manifest
4003
4062
  # pair (pack_win32_carrier_verdict, proven on owned fixtures by check-pack-win32-carrier).
4004
4063
  # "present" means the carrier verified, never that the build was approved for publication.
@@ -4109,7 +4168,7 @@ check_pack_win32_carrier() {
4109
4168
  # which is exactly what property (1)'s `(_|$)` absorbs; all three shapes are fixtures
4110
4169
  # below so a future suffix change cannot pass vacuously.)
4111
4170
  pack_install_leaked_pi() {
4112
- grep '^@earendil-works+' | grep -Ev '@1\.3\.0(_|$)' || true
4171
+ grep '^@earendil-works+' | grep -Ev '@1\.4\.0(_|$)' || true
4113
4172
  }
4114
4173
 
4115
4174
  # Matcher self-test on SYNTHETIC lookalikes: a healthy install tree cannot exercise either
@@ -4126,18 +4185,18 @@ check_pack_pin_matcher() {
4126
4185
  # Cell 1 — the version boundary. Every measured peer-hash shape is a fixture: the
4127
4186
  # 0.84.4-era `_@modelcontextprotocol+sdk@…`, the `_ws@8.21.3` observed on a real
4128
4187
  # 0.85.1 tree, and `[측정 2026-09-20]` the combined suffix a 0.86.0 install carries.
4129
- # None may leak; the two lookalikes must — a PREFIX-EXTENDED version (`0.86.0-beta.1`,
4188
+ # None may leak; the two lookalikes must — a PREFIX-EXTENDED version (`1.4.0-beta.1`,
4130
4189
  # the prerelease shape that an unanchored match would bless) and an off-pin version.
4131
4190
  matcher_probe=$(printf '%s\n' \
4132
- '@earendil-works+pi-ai@1.3.0' \
4133
- '@earendil-works+pi-ai@1.3.0_@modelcontextprotocol+sdk@1.29.0_zod@4.3.6' \
4134
- '@earendil-works+pi-ai@1.3.0_ws@8.21.3' \
4135
- '@earendil-works+pi-ai@1.3.0_@modelcontextprotocol+sdk@1.29.0_zod@4.3.6__ws@8.21.3_zod@4.3.6' \
4136
- '@earendil-works+pi-ai@1.3.0-beta.1' \
4191
+ '@earendil-works+pi-ai@1.4.0' \
4192
+ '@earendil-works+pi-ai@1.4.0_@modelcontextprotocol+sdk@1.29.0_zod@4.3.6' \
4193
+ '@earendil-works+pi-ai@1.4.0_ws@8.21.3' \
4194
+ '@earendil-works+pi-ai@1.4.0_@modelcontextprotocol+sdk@1.29.0_zod@4.3.6__ws@8.21.3_zod@4.3.6' \
4195
+ '@earendil-works+pi-ai@1.4.0-beta.1' \
4137
4196
  '@earendil-works+pi-agent-core@0.87.1' | pack_install_leaked_pi)
4138
- if [ "$matcher_probe" != '@earendil-works+pi-ai@1.3.0-beta.1
4197
+ if [ "$matcher_probe" != '@earendil-works+pi-ai@1.4.0-beta.1
4139
4198
  @earendil-works+pi-agent-core@0.87.1' ]; then
4140
- fail "[QK:PACK-INSTALL-PIN-MATCHER-BOUNDED] the pin-leak matcher must flag the prefix-extended 1.3.0-beta.1 and the off-pin 0.87.1 lookalikes, and pass 1.3.0 bare or with any measured peer-hash — got: ${matcher_probe:-<nothing leaked>}"
4199
+ fail "[QK:PACK-INSTALL-PIN-MATCHER-BOUNDED] the pin-leak matcher must flag the prefix-extended 1.4.0-beta.1 and the off-pin 0.87.1 lookalikes, and pass 1.4.0 bare or with any measured peer-hash — got: ${matcher_probe:-<nothing leaked>}"
4141
4200
  return 1
4142
4201
  fi
4143
4202
 
@@ -4145,9 +4204,9 @@ check_pack_pin_matcher() {
4145
4204
  # (0.85.0 onward, still true at 0.99.1). An off-pin chord MUST leak; the pinned one must not. A matcher narrowed
4146
4205
  # back to `^@earendil-works+pi-` sees nothing here and dies at this signature.
4147
4206
  matcher_probe=$(printf '%s\n' \
4148
- '@earendil-works+chord@1.3.0' \
4207
+ '@earendil-works+chord@1.4.0' \
4149
4208
  '@earendil-works+chord@0.87.1' \
4150
- '@earendil-works+pi-ai@1.3.0' | pack_install_leaked_pi)
4209
+ '@earendil-works+pi-ai@1.4.0' | pack_install_leaked_pi)
4151
4210
  if [ "$matcher_probe" != '@earendil-works+chord@0.87.1' ]; then
4152
4211
  fail "[QK:PACK-INSTALL-PIN-MATCHER-COVERS-CLOSURE] the pin-leak matcher must cover every @earendil-works closure member, not just the pi-* families — an off-pin @earendil-works/chord has to leak (it was a runtime dependency of pi-coding-agent, pi-agent-core, pi-client and pi-protocol — `[측정 2026-09-20]` at 0.86.0, re-measured `[측정 2026-09-22]` at 0.87.0, `[측정 2026-09-23]` at 0.87.1 and `[측정 2026-09-30]` at 0.99.1, where pi-coding-agent adds pi-codemode and pi-mcp beside them; `[측정 2026-10-13]` at 1.2.0 pi-durable alone declares it) — got: ${matcher_probe:-<nothing leaked>}"
4153
4212
  return 1
@@ -4161,7 +4220,7 @@ check_pack_pin_matcher() {
4161
4220
  # Cells: L1s (Entwurf alone, $3), L1 (project-local beside an aligned host pi-coding-agent at the
4162
4221
  # pinned SDK version), L2 (an isolated `npm -g --prefix`), each through (a) the INSTALLED verifier
4163
4222
  # (exit 0, prints its own carrier-resolver) and (b) an independent consumer oracle; then the L3
4164
- # NEGATIVE (an override binds pi-durable's pi-ai edge to an older published version: the installed verifier must
4223
+ # NEGATIVE (an override binds pi-durable's pi-codemode edge to an older published version: the installed verifier must
4165
4224
  # refuse it by name, while a bare import still succeeds); then (c) the public launch on L1s.
4166
4225
  # The oracle observes RESOLUTIONS (a resolve hook), not every filesystem read; it asserts each
4167
4226
  # resolved product file lies inside THIS consumer's node_modules — a sandbox ancestor is not the
@@ -4256,13 +4315,18 @@ ORACLE
4256
4315
  echo "[check-pack-install] pi-durable $cell: installed verifier PASS; consumer oracle $out"
4257
4316
  done
4258
4317
 
4259
- printf '%s\n' "{\"name\":\"pd-mismatch\",\"version\":\"1.0.0\",\"private\":true,\"overrides\":{\"@earendil-works/pi-durable\":{\"@earendil-works/pi-ai\":\"1.0.3\"}}}" > "$box/pd-mismatch/package.json"
4318
+ # The wrong edge must still IMPORT, or the control proves a crash rather than a refusal. `[측정 2026-10-22]`
4319
+ # SDK 1.4 pi-durable imports pi-ai's new `utils/identity`, absent from the older pi-ai control versions
4320
+ # checked here (1.0.3 and 1.3.0 both fail to load); pi-telemetry 1.0.3/1.3.0 and chord 1.3.0 lack exports
4321
+ # it imports. pi-codemode 1.3.0 loads, and depends on no SDK member, so this tree carries exactly one
4322
+ # depth-2 mismatch (L4 keeps depth 3).
4323
+ printf '%s\n' "{\"name\":\"pd-mismatch\",\"version\":\"1.0.0\",\"private\":true,\"overrides\":{\"@earendil-works/pi-durable\":{\"@earendil-works/pi-codemode\":\"1.3.0\"}}}" > "$box/pd-mismatch/package.json"
4260
4324
  out=$(cd "$box/pd-mismatch" && npm install "$tgz" --no-audit --no-fund 2>&1) || {
4261
4325
  fail "[check-pack-install] pi-durable L3 (mismatch control) install failed:"; echo "$out" | tail -10 | sed 's/^/ /' >&2; return 1; }
4262
4326
  pkg="$box/pd-mismatch/node_modules/@junghanacs/entwurf"
4263
4327
  rc=0; out=$(cd "$box" && node "$pkg/mcp/entwurf-bridge/dist/pi-extensions/lib/pi-durable-runtime.js" resolve 2>&1) || rc=$?
4264
- if [ "$rc" -ne 3 ] || ! grep -q "pi-durable-sdk-mismatch: @earendil-works/pi-durable → @earendil-works/pi-ai wants $pin_ver, binds 1.0.3" <<<"$out"; then
4265
- fail "[QK:PACK-INSTALL-DURABLE-MISMATCH-REFUSED] [check-pack-install] pi-durable L3: an installed tree whose pi-durable binds pi-ai 1.0.3 must be refused by name as pi-durable-sdk-mismatch (rc=$rc): $out"
4328
+ if [ "$rc" -ne 3 ] || ! grep -q "pi-durable-sdk-mismatch: @earendil-works/pi-durable → @earendil-works/pi-codemode wants $pin_ver, binds 1.3.0" <<<"$out"; then
4329
+ fail "[QK:PACK-INSTALL-DURABLE-MISMATCH-REFUSED] [check-pack-install] pi-durable L3: an installed tree whose pi-durable binds pi-codemode 1.3.0 must be refused by name as pi-durable-sdk-mismatch (rc=$rc): $out"
4266
4330
  return 1
4267
4331
  fi
4268
4332
  rc=0; out=$(cd "$box" && node --input-type=module -e "import { pathToFileURL } from 'node:url'; await import('$pkg/pi/pi-durable/carrier-resolver.mjs'); const m = await import(pathToFileURL('$pkg/pi/pi-durable/carrier/runtime.js').href); console.log(typeof m.openDurable);" 2>&1) || rc=$?
@@ -4787,19 +4851,21 @@ _check_pack_install_impl() {
4787
4851
  # `[측정 2026-10-16]` at 1.3.0 the constellation is unchanged: the installed manifests give the
4788
4852
  # same member edges as at 1.2.0, now every one exact 1.3.0 (upstream #10865), and the consumer
4789
4853
  # oracle walked 10 member edges with 0 mismatches (.tmp-verify/145-stage1/08-pack-install.log).
4790
- echo "[check-pack-install] pnpm add into $tmp (with the aligned 1.3.x SDK + chord + codemode + mcp + durable + typebox)"
4854
+ # `[측정 2026-10-22]` at 1.4.0 the nine members are again exact 1.4.0 edges (registry manifests; #148 S0),
4855
+ # and pi-tui 1.4.0 adds one third-party dependency (marked-cjk-friendly 0.1.2) outside the closure.
4856
+ echo "[check-pack-install] pnpm add into $tmp (with the aligned 1.4.x SDK + chord + codemode + mcp + durable + typebox)"
4791
4857
  local install_log
4792
4858
  install_log=$(cd "$tmp" && pnpm add \
4793
4859
  "$tgz_path" \
4794
- "@earendil-works/pi-ai@1.3.0" \
4795
- "@earendil-works/pi-coding-agent@1.3.0" \
4796
- "@earendil-works/pi-tui@1.3.0" \
4797
- "@earendil-works/pi-agent-core@1.3.0" \
4798
- "@earendil-works/pi-telemetry@1.3.0" \
4799
- "@earendil-works/chord@1.3.0" \
4800
- "@earendil-works/pi-codemode@1.3.0" \
4801
- "@earendil-works/pi-mcp@1.3.0" \
4802
- "@earendil-works/pi-durable@1.3.0" \
4860
+ "@earendil-works/pi-ai@1.4.0" \
4861
+ "@earendil-works/pi-coding-agent@1.4.0" \
4862
+ "@earendil-works/pi-tui@1.4.0" \
4863
+ "@earendil-works/pi-agent-core@1.4.0" \
4864
+ "@earendil-works/pi-telemetry@1.4.0" \
4865
+ "@earendil-works/chord@1.4.0" \
4866
+ "@earendil-works/pi-codemode@1.4.0" \
4867
+ "@earendil-works/pi-mcp@1.4.0" \
4868
+ "@earendil-works/pi-durable@1.4.0" \
4803
4869
  "typebox@latest" \
4804
4870
  --ignore-workspace --ignore-scripts 2>&1) || {
4805
4871
  fail "[check-pack-install] pnpm add failed:"
@@ -4815,11 +4881,11 @@ _check_pack_install_impl() {
4815
4881
  local leaked_pi
4816
4882
  leaked_pi=$(ls "$tmp/node_modules/.pnpm" 2>/dev/null | pack_install_leaked_pi)
4817
4883
  if [ -n "$leaked_pi" ]; then
4818
- fail "[check-pack-install] UNVERIFIED pi runtime resolved into the install tree (expected only 1.3.0):"
4884
+ fail "[check-pack-install] UNVERIFIED pi runtime resolved into the install tree (expected only 1.4.0):"
4819
4885
  printf '%s\n' "$leaked_pi" | sed 's/^/ /' >&2
4820
4886
  return 1
4821
4887
  fi
4822
- echo "[check-pack-install] pi runtime tree pin verified: every @earendil-works package is 1.3.0 (chord and pi-durable included)"
4888
+ echo "[check-pack-install] pi runtime tree pin verified: every @earendil-works package is 1.4.0 (chord and pi-durable included)"
4823
4889
 
4824
4890
  # Resolve the installed package.json and confirm pi.extensions
4825
4891
  # arrived intact. If pi.extensions is empty or missing, the
@@ -5381,7 +5447,7 @@ sys.exit(0 if any(isinstance(s,str) and s.rstrip('/')== '$npm2_pkg' for s in src
5381
5447
  #!/usr/bin/env bash
5382
5448
  printf '%s\n' "$*" >> "${FAKE_CLAUDE_LOG:?}"
5383
5449
  case "$1${2:+ $2}" in
5384
- "--version") echo "2.1.217 (Claude Code)" ;;
5450
+ "--version") echo "2.1.306 (Claude Code)" ;;
5385
5451
  "plugin validate") : ;;
5386
5452
  "plugin uninstall") : ;;
5387
5453
  "plugin marketplace") : ;;
@@ -5457,6 +5523,8 @@ SH
5457
5523
  mkdir -p "$nm_probe/lib"
5458
5524
  cp "$installed_hook" "$nm_probe/meta-bridge-hook.js"
5459
5525
  cp "$stable_asm/entwurf-meta-receive/lib/meta-session.js" "$nm_probe/lib/meta-session.js"
5526
+ # The real installer already supplies this hook import; the node_modules probe must too.
5527
+ cp "$stable_asm/entwurf-meta-receive/lib/entwurf-v2-lock.js" "$nm_probe/lib/entwurf-v2-lock.js"
5460
5528
  cp "$stable_asm/entwurf-meta-receive/lib/session-id.js" "$nm_probe/lib/session-id.js"
5461
5529
  cp "$stable_asm/entwurf-meta-receive/entwurf-capabilities.json" "$nm_probe/entwurf-capabilities.json"
5462
5530
  local probe_env='{"session_id":"pack-probe","transcript_path":"/tmp/x.jsonl","cwd":"/tmp","hook_event_name":"SessionStart","model":{"id":"probe"}}'
@@ -5695,7 +5763,7 @@ function finish(trimmed) {
5695
5763
  // gate that exists to catch "green clone, dead consumer". Equality also catches the
5696
5764
  // other direction the narrow-surface rule cares about: an extra or duplicated verb
5697
5765
  // reaching hosts without a decision. Written sorted; `names` is sorted above.
5698
- const EXPECT_TOOLS = ['entwurf_callback', 'entwurf_fresh_call', 'entwurf_inbox_read', 'entwurf_peers', 'entwurf_register_native', 'entwurf_resume_call', 'entwurf_self', 'entwurf_v2'];
5766
+ 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'];
5699
5767
  if (names.length !== EXPECT_TOOLS.length || EXPECT_TOOLS.some((n, i) => names[i] !== n)) {
5700
5768
  console.error('installed MCP tool set MISMATCH — want exactly [' + EXPECT_TOOLS.join(',') + '] got [' + names.join(',') + ']');
5701
5769
  process.exit(1);
@@ -6257,12 +6325,12 @@ function finishOk(trimmed) {
6257
6325
  }
6258
6326
  const names = tools.map((t) => t?.name).sort();
6259
6327
  // EXACT set, not a floor. This is the assertion behind the operator-facing claim that
6260
- // `entwurf check-bridge` proves the installed bytes list the eight garden tools — and
6328
+ // `entwurf check-bridge` proves the installed bytes list the nine garden tools — and
6261
6329
  // under an installed package this launcher IS the dist branch of start.sh, so a subset
6262
6330
  // check here let an artifact missing entwurf_fresh_call / entwurf_resume_call read as a
6263
6331
  // green bridge. Equality also refuses an undecided extra verb. Written sorted; `names`
6264
6332
  // is sorted above.
6265
- const expected = ['entwurf_callback', 'entwurf_fresh_call', 'entwurf_inbox_read', 'entwurf_peers', 'entwurf_register_native', 'entwurf_resume_call', 'entwurf_self', 'entwurf_v2'];
6333
+ const expected = ['entwurf_callback', 'entwurf_fresh_call', 'entwurf_inbox_read', 'entwurf_peers', 'entwurf_register_native', 'entwurf_resume_call', 'entwurf_retire_call', 'entwurf_self', 'entwurf_v2'];
6266
6334
  if (names.length !== expected.length || expected.some((n, i) => names[i] !== n)) {
6267
6335
  console.error(`MCP tool set MISMATCH — want exactly [${expected.join(',')}] got [${names.join(',')}]`);
6268
6336
  process.exit(1);
@@ -7502,6 +7570,24 @@ case "$cmd" in
7502
7570
  check-control-send-receipt)
7503
7571
  check_control_send_receipt
7504
7572
  ;;
7573
+ check-control-retire)
7574
+ check_control_retire
7575
+ ;;
7576
+ check-pi-retire-request)
7577
+ check_pi_retire_request
7578
+ ;;
7579
+ check-cc-retire-pidfd)
7580
+ check_cc_retire_pidfd
7581
+ ;;
7582
+ check-cc-retire-request)
7583
+ check_cc_retire_request
7584
+ ;;
7585
+ check-retirement-event)
7586
+ check_retirement_event
7587
+ ;;
7588
+ check-entwurf-v2-retire)
7589
+ check_entwurf_v2_retire
7590
+ ;;
7505
7591
  check-win32-preview)
7506
7592
  check_win32_preview
7507
7593
  ;;
@@ -7532,6 +7618,9 @@ case "$cmd" in
7532
7618
  check-acp-foreground-only)
7533
7619
  check_acp_foreground_only
7534
7620
  ;;
7621
+ check-fresh-call-provider)
7622
+ check_fresh_call_provider
7623
+ ;;
7535
7624
  check-acp-set-model-readback)
7536
7625
  check_acp_set_model_readback
7537
7626
  ;;