@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
@@ -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. */
@@ -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
@@ -328,6 +328,9 @@ Usage:
328
328
  ./run.sh meta-bridge-prune # 1.0.0 meta-bridge Phase 4: LISTING-ONLY store hygiene — classify orphan/stale/ambiguous/keep, print manual rm commands, delete NOTHING ([dir] [--ttl-days N])
329
329
  ./run.sh meta-bridge-fresh-cut # the ONE generation verb (the verb every v3-only rejection names): quiesce-check live sockets/markers/native-push conversations (refusing any surface it cannot inspect), archive meta-sessions/ + meta-mailbox/ to `<dir>.archive-<ts>`, clear dead transport residue, open an empty v3 generation. No migration, no restore — the archive is forensic only. EXIT CONTRACT (#54, `--help` prints it): 0 complete / 1 NOTHING MOVED (re-run, do not setup) / 2 usage / 3 cut transition incomplete (inspect) / 4 cut complete but residue cleanup failed (`setup` may run; re-run only before new citizen birth, otherwise remove residue manually)
330
330
  ./run.sh meta-facts # #65 owner-normalized READ-ONLY store projection: deterministic JSON {schemaVersion:1, storeDir, citizens: full v3 records sorted by gardenId, defects: {filename,message}} — THE listing contract emitted by the owner so consumers stop copying the certification. No liveness/sockets/transcript contents. EXIT: 0 readable (defects in-band; missing store = empty), 2 usage, 3 unreadable ([dir])
331
+ ./run.sh check-remote-pair-installed # #107 rail 3, CHECKOUT-ONLY: install a caller-supplied candidate tarball (--tgz <abs>) into a sandboxed consumer prefix, check its compiled remote-pair surface, run the installed `entwurf remote-pair-receive` (usage exit 2 = twin reached), run the T2 roundtrip cells against the installed bridge+entry, and audit that the subject processes resolve every module inside the consumer prefix (with a checkout control). Never repacks; no real ssh, no model
332
+ ./run.sh check-remote-pair-installed-audit # #107 rail 3: the deterministic counterpart of check-remote-pair-installed — its checkout-invisibility acceptance primitive proven on synthetic logs (real path, root boundary). In check:vitest
333
+ ./run.sh remote-pair-receive # #107 rail 1: the RECEIVING half of one operator-declared remote pair. Reads ONE JSON claim {from,fromAgentId,fromCwd,target,message,wantsReply} on stdin, refuses a from/target outside the declared pair before any mutation (a scope limit under the same-operator SSH account trust model, not authentication), then hands a match to the existing v2 dispatch. Ambient sender carriers are never read. Installed, run_ts runs its compiled twin (dist/scripts/remote-pair-receive.js). EXIT: 0 dispatched / 4 dispatched, the dispatch reported an error (delivery not inferred) / 3 pair refused / 2 invalid claim or declaration (JSON receipt) or wrong argv (usage on stderr, no receipt) / 1 uncaught dispatch or store throw (no receipt; the sender reads it as indeterminate) (--pair <absolute declaration file>)
331
334
  ./run.sh peer-facts # #116 M2-a owner-normalized READ-ONLY peer projection: the same listEntwurfFacts provider through the same renderer entwurf_peers uses, emitted as deterministic JSON {schemaVersion:1, storeDir, peers, diagnostics} so a consumer never re-implements the placement join (whose pi half is a measured vendor floor). NOT the same bytes as an entwurf_peers call and deliberately so: that surface is a human listing (observationLimit, text over the wire), this is a machine projection with UNBOUNDED observation, because a rationed row says `unobserved` and a machine cannot tell that from "nobody could look". `placement` stays a STRUCTURED tagged union ({kind:"herdr-pane",paneId} / unobserved / none / ambiguous) — never the human `herdr <pane>` string. Publishes NO socket coordinate (ENTWURF_DIR selects what is PROBED, #50 C4) and NO herdr agent_status / interactive_ready / screen text. One placement read per listing, no watcher. EXIT: 0 readable (diagnostics in-band; missing store = empty), 2 usage, 3 unreadable ([dir])
332
335
  ./run.sh meta-bridge-managed-keys # 0.10.0 meta-bridge: print the SSOT of settings keys entwurf OWNS (consumers read this to stay disjoint — keyset-owner invariant)
333
336
  ./run.sh check-keyset-overlap <fragment.json...> # 0.10.0 meta-bridge: PREVENTIVE keyset guard — fail if a consumer fragment collides with any pi-owned key (cross-repo; not in pnpm check)
@@ -1340,6 +1343,44 @@ check_control_send_receipt() {
1340
1343
  run_vitest pi-extensions/lib/control-send-receipt.test.ts
1341
1344
  }
1342
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
+
1343
1384
  check_win32_preview() {
1344
1385
  # MUTANT EXECUTION COORDINATE, not a discovery path — the Windows preview's (#78) loader and
1345
1386
  # platform wiring tests are found by check-tests-beside-behavior like every other beside-behaviour
@@ -1452,6 +1493,15 @@ check_acp_foreground_only() {
1452
1493
  run_vitest pi-extensions/lib/acp/backend-adapter.test.ts
1453
1494
  }
1454
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
+
1455
1505
  check_acp_set_model_readback() {
1456
1506
  # NOT a discovery path — check-tests-beside-behavior finds the test by glob. This case is the
1457
1507
  # MUTANT EXECUTION COORDINATE for scripts/mutants/acp-set-model-readback.json (#128): the claude
@@ -1667,7 +1717,8 @@ smoke_mux_lifecycle_live() {
1667
1717
  # resumed pane_start_path == the RECORD's cwd (separate tmux query, never pane text), and the
1668
1718
  # transcript byte-identical across the resume itself -> entwurf_v2 recalling the fact from
1669
1719
  # BEFORE the close, which is what proves the same CONVERSATION returned. Claude Code runs the
1670
- # 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).
1671
1722
  # Fixture-fenced writes (XDG, four meta roots, v2 lock dir, control socket via fixture HOME);
1672
1723
  # the runtime auth roots stay REAL and sibling transcripts remain in the real pi agent dir as
1673
1724
  # evidence. Private tmux server per cell, bounded teardown, operator real-root entry sets
@@ -2728,7 +2779,7 @@ EOF
2728
2779
  check_claude_floor_coherence() {
2729
2780
  # #51 policy A. The Claude Code floor is a THREE-PART version, unlike the Node
2730
2781
  # floor's major lane: the discriminator lives in the patch position (2.1.138 drops
2731
- # `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
2732
2783
  # in a live session). So it cannot borrow check-node-floor-coherence's major-only
2733
2784
  # comparison and needs its own binding.
2734
2785
  #
@@ -3387,6 +3438,17 @@ check_acp_sdk_surface() {
3387
3438
  run_vitest test/acp-sdk-surface.contract.test.ts
3388
3439
  }
3389
3440
 
3441
+ check_remote_pair_installed_audit() {
3442
+ # #107 rail 3. The deterministic counterpart of `check-remote-pair-installed`: that gate needs a
3443
+ # caller-supplied tarball, so it cannot run in the floor or carry mutants, and its
3444
+ # checkout-invisibility verdict rests on one acceptance primitive
3445
+ # (scripts/lib/remote-pair-installed-audit.ts). This proves that primitive on tiny synthetic
3446
+ # logs and files. `test/**` is not discovered by check-tests-beside-behavior, so it needs this
3447
+ # public coordinate. Zero network, zero child process.
3448
+ section "remote-pair installed-closure audit (synthetic logs)"
3449
+ run_vitest test/remote-pair-installed-audit.test.ts
3450
+ }
3451
+
3390
3452
  check_pi_queue_oracle() {
3391
3453
  # #120 P2. The PUBLIC execution coordinate for `test/pi-queue.oracle.test.ts`, and the reason it
3392
3454
  # needs one: the discovery door below selects `pi-extensions/**/*.test.ts` and
@@ -3829,6 +3891,12 @@ check_pack() {
3829
3891
  # out: the placement join it emits is the copy a herdr-side consumer would
3830
3892
  # otherwise fork, and such a consumer runs against an installed host.
3831
3893
  "mcp/entwurf-bridge/dist/scripts/peer-facts.js"
3894
+ # #107 — the receiving host's entry the sender's ssh runs (run_ts twin), plus the three
3895
+ # remote-pair libs the bridge's entwurf_v2 handler and that entry import.
3896
+ "mcp/entwurf-bridge/dist/scripts/remote-pair-receive.js"
3897
+ "mcp/entwurf-bridge/dist/pi-extensions/lib/remote-pair-receive.js"
3898
+ "mcp/entwurf-bridge/dist/pi-extensions/lib/remote-pair-send.js"
3899
+ "mcp/entwurf-bridge/dist/pi-extensions/lib/remote-pair-transport.js"
3832
3900
  # #87 Bundle B — omp-receive-doctor drives this operator projection through
3833
3901
  # run.sh, so installed node_modules must have the compiled twin too.
3834
3902
  "mcp/entwurf-bridge/dist/scripts/omp-receive-facts.js"
@@ -4570,6 +4638,12 @@ _check_pack_install_impl() {
4570
4638
  "mcp/entwurf-bridge/dist/scripts/meta-facts.js"
4571
4639
  # #116 M2-a — the owner-normalized peer projection (see check-pack).
4572
4640
  "mcp/entwurf-bridge/dist/scripts/peer-facts.js"
4641
+ # #107 — the receiving host's entry the sender's ssh runs (run_ts twin), plus the three
4642
+ # remote-pair libs the bridge's entwurf_v2 handler and that entry import.
4643
+ "mcp/entwurf-bridge/dist/scripts/remote-pair-receive.js"
4644
+ "mcp/entwurf-bridge/dist/pi-extensions/lib/remote-pair-receive.js"
4645
+ "mcp/entwurf-bridge/dist/pi-extensions/lib/remote-pair-send.js"
4646
+ "mcp/entwurf-bridge/dist/pi-extensions/lib/remote-pair-transport.js"
4573
4647
  # #87 Bundle B — omp-receive-doctor reaches this through the installed dispatcher.
4574
4648
  "mcp/entwurf-bridge/dist/scripts/omp-receive-facts.js"
4575
4649
  # 0.12.5 — node_modules-safe plugin hook + lib (see check-pack). The installed
@@ -5355,7 +5429,7 @@ sys.exit(0 if any(isinstance(s,str) and s.rstrip('/')== '$npm2_pkg' for s in src
5355
5429
  #!/usr/bin/env bash
5356
5430
  printf '%s\n' "$*" >> "${FAKE_CLAUDE_LOG:?}"
5357
5431
  case "$1${2:+ $2}" in
5358
- "--version") echo "2.1.217 (Claude Code)" ;;
5432
+ "--version") echo "2.1.306 (Claude Code)" ;;
5359
5433
  "plugin validate") : ;;
5360
5434
  "plugin uninstall") : ;;
5361
5435
  "plugin marketplace") : ;;
@@ -5431,6 +5505,8 @@ SH
5431
5505
  mkdir -p "$nm_probe/lib"
5432
5506
  cp "$installed_hook" "$nm_probe/meta-bridge-hook.js"
5433
5507
  cp "$stable_asm/entwurf-meta-receive/lib/meta-session.js" "$nm_probe/lib/meta-session.js"
5508
+ # The real installer already supplies this hook import; the node_modules probe must too.
5509
+ cp "$stable_asm/entwurf-meta-receive/lib/entwurf-v2-lock.js" "$nm_probe/lib/entwurf-v2-lock.js"
5434
5510
  cp "$stable_asm/entwurf-meta-receive/lib/session-id.js" "$nm_probe/lib/session-id.js"
5435
5511
  cp "$stable_asm/entwurf-meta-receive/entwurf-capabilities.json" "$nm_probe/entwurf-capabilities.json"
5436
5512
  local probe_env='{"session_id":"pack-probe","transcript_path":"/tmp/x.jsonl","cwd":"/tmp","hook_event_name":"SessionStart","model":{"id":"probe"}}'
@@ -5669,7 +5745,7 @@ function finish(trimmed) {
5669
5745
  // gate that exists to catch "green clone, dead consumer". Equality also catches the
5670
5746
  // other direction the narrow-surface rule cares about: an extra or duplicated verb
5671
5747
  // reaching hosts without a decision. Written sorted; `names` is sorted above.
5672
- const EXPECT_TOOLS = ['entwurf_callback', 'entwurf_fresh_call', 'entwurf_inbox_read', 'entwurf_peers', 'entwurf_register_native', 'entwurf_resume_call', 'entwurf_self', 'entwurf_v2'];
5748
+ 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'];
5673
5749
  if (names.length !== EXPECT_TOOLS.length || EXPECT_TOOLS.some((n, i) => names[i] !== n)) {
5674
5750
  console.error('installed MCP tool set MISMATCH — want exactly [' + EXPECT_TOOLS.join(',') + '] got [' + names.join(',') + ']');
5675
5751
  process.exit(1);
@@ -6231,12 +6307,12 @@ function finishOk(trimmed) {
6231
6307
  }
6232
6308
  const names = tools.map((t) => t?.name).sort();
6233
6309
  // EXACT set, not a floor. This is the assertion behind the operator-facing claim that
6234
- // `entwurf check-bridge` proves the installed bytes list the eight garden tools — and
6310
+ // `entwurf check-bridge` proves the installed bytes list the nine garden tools — and
6235
6311
  // under an installed package this launcher IS the dist branch of start.sh, so a subset
6236
6312
  // check here let an artifact missing entwurf_fresh_call / entwurf_resume_call read as a
6237
6313
  // green bridge. Equality also refuses an undecided extra verb. Written sorted; `names`
6238
6314
  // is sorted above.
6239
- const expected = ['entwurf_callback', 'entwurf_fresh_call', 'entwurf_inbox_read', 'entwurf_peers', 'entwurf_register_native', 'entwurf_resume_call', 'entwurf_self', 'entwurf_v2'];
6315
+ 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'];
6240
6316
  if (names.length !== expected.length || expected.some((n, i) => names[i] !== n)) {
6241
6317
  console.error(`MCP tool set MISMATCH — want exactly [${expected.join(',')}] got [${names.join(',')}]`);
6242
6318
  process.exit(1);
@@ -7476,6 +7552,24 @@ case "$cmd" in
7476
7552
  check-control-send-receipt)
7477
7553
  check_control_send_receipt
7478
7554
  ;;
7555
+ check-control-retire)
7556
+ check_control_retire
7557
+ ;;
7558
+ check-pi-retire-request)
7559
+ check_pi_retire_request
7560
+ ;;
7561
+ check-cc-retire-pidfd)
7562
+ check_cc_retire_pidfd
7563
+ ;;
7564
+ check-cc-retire-request)
7565
+ check_cc_retire_request
7566
+ ;;
7567
+ check-retirement-event)
7568
+ check_retirement_event
7569
+ ;;
7570
+ check-entwurf-v2-retire)
7571
+ check_entwurf_v2_retire
7572
+ ;;
7479
7573
  check-win32-preview)
7480
7574
  check_win32_preview
7481
7575
  ;;
@@ -7506,6 +7600,9 @@ case "$cmd" in
7506
7600
  check-acp-foreground-only)
7507
7601
  check_acp_foreground_only
7508
7602
  ;;
7603
+ check-fresh-call-provider)
7604
+ check_fresh_call_provider
7605
+ ;;
7509
7606
  check-acp-set-model-readback)
7510
7607
  check_acp_set_model_readback
7511
7608
  ;;
@@ -8412,6 +8509,21 @@ case "$cmd" in
8412
8509
  shift || true
8413
8510
  run_ts scripts/meta-facts.ts "$@"
8414
8511
  ;;
8512
+ check-remote-pair-installed)
8513
+ # #107 rail 3, CHECKOUT-ONLY install gate: installs a caller-supplied candidate tarball into a
8514
+ # sandboxed consumer prefix and drives that install's bridge and entry through the T2 scene,
8515
+ # auditing that the subject processes resolve nothing outside the prefix. Never repacks.
8516
+ shift || true
8517
+ run_ts scripts/check-remote-pair-installed.ts "$@"
8518
+ ;;
8519
+ remote-pair-receive)
8520
+ # #107 rail 1: the receiving host's entry for one declared remote pair. The pair
8521
+ # check runs before the v2 dispatch, so a claim outside the pair mutates nothing;
8522
+ # a match reaches the same record/owner/deliverability checks every local send does.
8523
+ # Installed, run_ts runs its compiled twin; the sending host's ssh reaches it through the bin.
8524
+ shift || true
8525
+ run_ts scripts/remote-pair-receive.ts "$@"
8526
+ ;;
8415
8527
  peer-facts)
8416
8528
  # #116 M2-a READ-ONLY projection of the OBSERVED peer listing: one provider
8417
8529
  # (`listEntwurfFacts`) through one renderer (`renderEntwurfPeers().payload`), so
@@ -8481,6 +8593,9 @@ case "$cmd" in
8481
8593
  check-pi-queue-oracle)
8482
8594
  check_pi_queue_oracle
8483
8595
  ;;
8596
+ check-remote-pair-installed-audit)
8597
+ check_remote_pair_installed_audit
8598
+ ;;
8484
8599
  check-demo-reproducibility)
8485
8600
  check_demo_reproducibility
8486
8601
  ;;
@@ -102,7 +102,7 @@ PERMISSION_STATE_SCHEMA_VERSION = 2
102
102
  # EVERY call. The bridge was registered and two thirds of it was unusable — the same failure the
103
103
  # doctor already names for entwurf_v2, just never checked for the other two.
104
104
  #
105
- # The four tools deliberately NOT here, because a grant we do not need is a grant we should not take:
105
+ # The five tools deliberately NOT here, because a grant we do not need is a grant we should not take:
106
106
  # entwurf_inbox_read — native-push has no inbox to drain. An agy citizen has no meta-mailbox
107
107
  # directory at all (measured), so granting it would pre-approve a rail this
108
108
  # backend does not have.
@@ -113,6 +113,9 @@ PERMISSION_STATE_SCHEMA_VERSION = 2
113
113
  # refuse without a pane anchor. An agy conversation is not a tmux client,
114
114
  # so these are not on its normal path either; the count moved from two to
115
115
  # four when 0.14 shipped the mux verbs, and the granted set did not.
116
+ # entwurf_retire_call — one retirement attempt on a target a human operator explicitly named.
117
+ # Pre-approving it would replace that request with an installer grant, so
118
+ # it stays at agy's default Ask; the count moved from four to five (#147).
116
119
  ALLOW_RULES = tuple(f"mcp({SERVER_KEY}/{tool})" for tool in ("entwurf_v2", "entwurf_peers", "entwurf_self"))
117
120
  # Rules that MATCH one of our tools: its exact grant, the server-wide rule, and the action wildcard.
118
121
  # Membership here is a statement about agy's matcher, not about which list the rule sits in — the
@@ -36,3 +36,17 @@ TSC="$REPO_DIR/node_modules/.bin/tsc"
36
36
  # dependent function called from its own location always resolves).
37
37
  cp "$REPO_DIR/pi/entwurf-capabilities.json" \
38
38
  "$REPO_DIR/mcp/entwurf-bridge/dist/pi-extensions/entwurf-capabilities.json"
39
+
40
+ # The Claude Code retire helper must TRAVEL WITH the bundle too (#147 G3).
41
+ #
42
+ # `ccRetireHelperPath()` resolves ONE fixed basename beside its own module, so the emitted
43
+ # dist/pi-extensions/lib/cc-retire-request.js finds the helper only if it sits next to it. The
44
+ # canonical bytes are pi-extensions/lib/cc-retire-pidfd.py; this is a byte copy, never an edit.
45
+ # A missing or non-regular source fails the build — there is no fallback location. Gated by
46
+ # check-capability-bundle-reach (the compiled adapter asks from where it lives, bytes compared).
47
+ CC_RETIRE_HELPER="$REPO_DIR/pi-extensions/lib/cc-retire-pidfd.py"
48
+ if [ ! -f "$CC_RETIRE_HELPER" ] || [ -L "$CC_RETIRE_HELPER" ]; then
49
+ echo "build-bridge: $CC_RETIRE_HELPER is missing or not a regular file" >&2
50
+ exit 1
51
+ fi
52
+ cp "$CC_RETIRE_HELPER" "$REPO_DIR/mcp/entwurf-bridge/dist/pi-extensions/lib/cc-retire-pidfd.py"