@junghanacs/entwurf 0.13.0 → 0.14.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 (193) hide show
  1. package/AGENTS.md +52 -17
  2. package/BASELINE.md +60 -206
  3. package/CHANGELOG.md +64 -0
  4. package/CONTRIBUTING.md +14 -10
  5. package/DELIVERY.md +118 -261
  6. package/README.md +91 -431
  7. package/VERIFY.md +74 -95
  8. package/demo/README.md +1 -1
  9. package/demo/demo-baseline.sh +1 -3
  10. package/demo/demo.sh +2 -5
  11. package/docs/acp-backend-rail.md +236 -0
  12. package/docs/external-mcp-host.md +131 -0
  13. package/docs/fresh-cut-policy.md +99 -0
  14. package/docs/setup-clean-host.md +124 -328
  15. package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +160 -67
  16. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/acp-client.js +12 -0
  17. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/backend-adapter.js +11 -10
  18. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/config.js +1 -1
  19. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/engraving.js +42 -1
  20. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/models.js +4 -4
  21. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/overlay.js +11 -9
  22. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-control-rpc.js +7 -5
  23. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-core.js +13 -14
  24. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-resume-args.js +45 -40
  25. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-contract.js +117 -95
  26. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-decider.js +23 -57
  27. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-lock.js +16 -7
  28. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-production.js +5 -53
  29. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-release.js +21 -36
  30. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-runner.js +3 -15
  31. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-send-fallback.js +12 -11
  32. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-send.js +2 -7
  33. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-surface.js +30 -67
  34. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-visible-resume.js +256 -0
  35. package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-session.js +91 -5
  36. package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +258 -0
  37. package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-launch.js +202 -0
  38. package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-placement.js +289 -0
  39. package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-resume-call.js +170 -0
  40. package/mcp/entwurf-bridge/dist/pi-extensions/lib/resume-launch-identity.js +136 -0
  41. package/mcp/entwurf-bridge/dist/pi-extensions/lib/session-id.js +8 -5
  42. package/mcp/entwurf-bridge/dist/pi-extensions/lib/socket-discovery.js +3 -3
  43. package/mcp/entwurf-bridge/dist/scripts/meta-facts.js +51 -0
  44. package/mcp/entwurf-bridge/dist/scripts/new-session-id.js +9 -4
  45. package/mcp/entwurf-bridge/src/index.ts +176 -67
  46. package/mcp/entwurf-bridge/start.sh +2 -2
  47. package/mcp/entwurf-bridge/test.sh +23 -9
  48. package/mcp/entwurf-bridge/tsconfig.build.json +11 -2
  49. package/package.json +25 -11
  50. package/pi-extensions/entwurf-control.ts +220 -71
  51. package/pi-extensions/lib/acp/acp-client.ts +22 -0
  52. package/pi-extensions/lib/acp/augment.ts +42 -3
  53. package/pi-extensions/lib/acp/backend-adapter.ts +12 -11
  54. package/pi-extensions/lib/acp/backend.ts +366 -38
  55. package/pi-extensions/lib/acp/config.ts +1 -1
  56. package/pi-extensions/lib/acp/engraving.ts +43 -1
  57. package/pi-extensions/lib/acp/event-mapper.ts +16 -13
  58. package/pi-extensions/lib/acp/models.ts +4 -4
  59. package/pi-extensions/lib/acp/overlay.ts +11 -9
  60. package/pi-extensions/lib/entwurf-control-rpc.ts +7 -5
  61. package/pi-extensions/lib/entwurf-core.ts +15 -15
  62. package/pi-extensions/lib/entwurf-resume-args.ts +41 -52
  63. package/pi-extensions/lib/entwurf-v2-contract-schema.ts +1 -1
  64. package/pi-extensions/lib/entwurf-v2-contract.ts +120 -99
  65. package/pi-extensions/lib/entwurf-v2-decider.ts +30 -91
  66. package/pi-extensions/lib/entwurf-v2-lock.ts +16 -7
  67. package/pi-extensions/lib/entwurf-v2-production.ts +4 -78
  68. package/pi-extensions/lib/entwurf-v2-release.ts +25 -49
  69. package/pi-extensions/lib/entwurf-v2-runner.ts +6 -21
  70. package/pi-extensions/lib/entwurf-v2-send-fallback.ts +12 -11
  71. package/pi-extensions/lib/entwurf-v2-send.ts +2 -7
  72. package/pi-extensions/lib/entwurf-v2-surface.ts +36 -76
  73. package/pi-extensions/lib/entwurf-v2-visible-resume.ts +370 -0
  74. package/pi-extensions/lib/meta-session.ts +93 -5
  75. package/pi-extensions/lib/mux-fresh-call.ts +328 -0
  76. package/pi-extensions/lib/mux-launch.ts +267 -0
  77. package/pi-extensions/lib/mux-placement.ts +387 -0
  78. package/pi-extensions/lib/mux-resume-call.ts +221 -0
  79. package/pi-extensions/lib/resume-launch-identity.ts +162 -0
  80. package/pi-extensions/lib/session-id.js +8 -5
  81. package/pi-extensions/lib/socket-discovery.ts +3 -3
  82. package/prompts/engraving.md +13 -5
  83. package/run.sh +688 -282
  84. package/scripts/agy-bridge-config.py +5 -1
  85. package/scripts/check-acp-backend-preflight.ts +1 -1
  86. package/scripts/check-acp-carrier-augment.ts +246 -10
  87. package/scripts/check-acp-cortex.ts +5 -5
  88. package/scripts/check-acp-overlay.ts +13 -3
  89. package/scripts/check-acp-prompt-lifecycle.ts +565 -0
  90. package/scripts/check-acp-stop-reason.ts +342 -0
  91. package/scripts/check-acp-stream-hooks.ts +504 -0
  92. package/scripts/check-elapsed.sh +25 -0
  93. package/scripts/check-entwurf-bridge-boot.ts +51 -4
  94. package/scripts/check-entwurf-bridge-pi-free.ts +6 -5
  95. package/scripts/check-entwurf-control-rpc.ts +4 -3
  96. package/scripts/check-entwurf-resume-args.ts +72 -70
  97. package/scripts/check-entwurf-session-identity.ts +14 -10
  98. package/scripts/check-entwurf-v2-contract.ts +34 -59
  99. package/scripts/check-entwurf-v2-decider.ts +17 -177
  100. package/scripts/check-entwurf-v2-lock.ts +5 -2
  101. package/scripts/check-entwurf-v2-matrix.ts +3 -53
  102. package/scripts/check-entwurf-v2-production.ts +2 -91
  103. package/scripts/check-entwurf-v2-release.ts +10 -105
  104. package/scripts/check-entwurf-v2-runner.ts +4 -85
  105. package/scripts/check-entwurf-v2-send-fallback.ts +5 -6
  106. package/scripts/check-entwurf-v2-send.ts +0 -28
  107. package/scripts/check-entwurf-v2-surface.ts +198 -128
  108. package/scripts/check-entwurf-v2-visible-resume.ts +445 -0
  109. package/scripts/check-fresh-cut-gate.sh +1 -1
  110. package/scripts/check-gate-qualification.ts +101 -7
  111. package/scripts/check-install-container.sh +10 -2
  112. package/scripts/check-install-surface.ts +1 -1
  113. package/scripts/check-keyset-overlap.py +1 -1
  114. package/scripts/check-meta-facts.ts +249 -0
  115. package/scripts/check-meta-identity-consumers.ts +1 -1
  116. package/scripts/check-meta-session.ts +169 -0
  117. package/scripts/check-mux-launch-tmux.ts +316 -0
  118. package/scripts/check-mux-launch.ts +288 -0
  119. package/scripts/check-mux-launcher-fence.ts +264 -0
  120. package/scripts/check-mux-parent-artifact.ts +195 -0
  121. package/scripts/check-mux-placement-tmux.ts +322 -0
  122. package/scripts/check-mux-placement.ts +323 -0
  123. package/scripts/check-mux-resume-call.ts +282 -0
  124. package/scripts/check-probe-cli-shim.ts +25 -22
  125. package/scripts/check-probe-ordering.ts +121 -81
  126. package/scripts/check-release-gate-outcomes.ts +452 -0
  127. package/scripts/check-resume-launch-identity.ts +244 -0
  128. package/scripts/check-socket-discovery.ts +1 -1
  129. package/scripts/fixtures/mux-parent-transcript.scrubbed.jsonl +3 -0
  130. package/scripts/inventory-verification-surface.ts +349 -0
  131. package/scripts/lib/claude-launcher-fence.ts +322 -0
  132. package/scripts/lib/live-skip.ts +33 -0
  133. package/scripts/lib/mutation-qualify.ts +109 -3
  134. package/scripts/lib/probe-acp-turn.ts +33 -12
  135. package/scripts/lib/step-outcome.sh +88 -0
  136. package/scripts/meta-bridge-doctor.sh +6 -8
  137. package/scripts/meta-facts.ts +60 -0
  138. package/scripts/mutants/acp-augment.json +106 -0
  139. package/scripts/mutants/acp-cortex.json +2 -2
  140. package/scripts/mutants/acp-overlay.json +17 -0
  141. package/scripts/mutants/acp-prompt-lifecycle.json +100 -0
  142. package/scripts/mutants/acp-stop-reason.json +80 -0
  143. package/scripts/mutants/acp-stream-hooks.json +158 -0
  144. package/scripts/mutants/bridge-boot-resume.json +45 -0
  145. package/scripts/mutants/meta-facts.json +50 -0
  146. package/scripts/mutants/meta-identity.json +36 -0
  147. package/scripts/mutants/mux-boundary.json +196 -0
  148. package/scripts/mutants/mux-fresh-call.json +185 -0
  149. package/scripts/mutants/mux-launcher-fence.json +123 -0
  150. package/scripts/mutants/mux-parent-artifact.json +39 -0
  151. package/scripts/mutants/mux-resume-call.json +148 -0
  152. package/scripts/mutants/probe-ordering.json +6 -1021
  153. package/scripts/mutants/release-gate.json +140 -0
  154. package/scripts/mutants/resume-args.json +76 -0
  155. package/scripts/mutants/resume-launch-identity.json +96 -0
  156. package/scripts/mutants/v2-surface.json +74 -23
  157. package/scripts/mutants/v2-visible-resume.json +215 -0
  158. package/scripts/new-session-id.ts +9 -4
  159. package/scripts/smoke-acp-bundled-mcp-live.ts +4 -3
  160. package/scripts/smoke-acp-carrier-augment-live.ts +2 -2
  161. package/scripts/smoke-acp-cortex-live.ts +8 -14
  162. package/scripts/smoke-acp-long-turn-live.ts +185 -0
  163. package/scripts/smoke-acp-mcp-live.ts +2 -2
  164. package/scripts/smoke-acp-memory-containment-live.ts +2 -2
  165. package/scripts/smoke-acp-ordering-probe-live.ts +2 -2
  166. package/scripts/smoke-acp-overlay-live.ts +2 -2
  167. package/scripts/smoke-acp-provider-live.ts +2 -2
  168. package/scripts/smoke-acp-raw-turn-live.ts +3 -3
  169. package/scripts/smoke-acp-session-reuse-live.ts +2 -2
  170. package/scripts/smoke-acp-skill-live.ts +2 -2
  171. package/scripts/smoke-acp-socket-citizen-live.ts +4 -3
  172. package/scripts/smoke-acp-v2-send-live.ts +4 -3
  173. package/scripts/smoke-agy-native-push-live.ts +10 -20
  174. package/scripts/smoke-claude-native-resume-live.sh +13 -3
  175. package/scripts/smoke-entwurf-chain-live.ts +352 -0
  176. package/scripts/smoke-entwurf-v2-matrix-live.ts +3 -3
  177. package/scripts/smoke-meta-honesty.sh +1 -1
  178. package/scripts/smoke-meta-install-state.sh +4 -0
  179. package/scripts/smoke-mux-fresh-call-live.ts +365 -0
  180. package/scripts/smoke-mux-lifecycle-live.ts +1136 -0
  181. package/scripts/smoke-pi-attach.ts +1 -1
  182. package/scripts/smoke-user-scope-citizen.sh +1 -1
  183. package/scripts/tsconfig.json +1 -0
  184. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-preflight.js +0 -160
  185. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-spawn-production.js +0 -273
  186. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-spawn.js +0 -216
  187. package/pi-extensions/lib/entwurf-v2-spawn-production.ts +0 -373
  188. package/pi-extensions/lib/entwurf-v2-spawn.ts +0 -323
  189. package/scripts/check-acp-sdk-surface.ts +0 -236
  190. package/scripts/check-entwurf-v2-spawn-production.ts +0 -551
  191. package/scripts/check-entwurf-v2-spawn.ts +0 -399
  192. package/scripts/smoke-entwurf-v2-spawn-live.ts +0 -188
  193. package/scripts/smoke-entwurf-v2-spawn-resume-live.ts +0 -469
@@ -7,11 +7,11 @@
7
7
  * 2. execute ⇒ plan present ∧ receipt.transport === plan.transport ∧
8
8
  * (in-domain ⇒ lock non-null, mailbox ⇒ lock null).
9
9
  * 3. pre-probe rejects (bad-target / target-locked / target-address-conflict)
10
- * carry observedLiveness=null; untrusted-fail-fast carries the measured
10
+ * carry observedLiveness=null; every post-probe reject carries the measured
11
11
  * `dead` (non-null), and the deny path releases the lock (nonce-owned).
12
- * 4. resume plan has NO mode, HAS wantsReply (#50 F2: the dormant rail's
13
- * <sender_info> carries the etiquette marker) + expectedSocketPath/
14
- * observeTimeoutMs/releaseWhen; meta-mailbox plan has NO mode.
12
+ * 4. meta-mailbox plan has NO mode. (The resume-plan cell no mode, wantsReply,
13
+ * expectedSocketPath/observeTimeoutMs/releaseWhen went with the transport in
14
+ * the visible-first cut; there is no plan variant left to hold those fields.)
15
15
  * 5. control-socket execute lock non-null; meta-mailbox execute lock null (?7).
16
16
  * 6. the unsupported (mailbox) path acquires NO lock (?7).
17
17
  * 7. a pre-probe address conflict rejects WITHOUT probing (inspectSocket unused).
@@ -21,8 +21,7 @@
21
21
  * a self-fetch citizen with an INACTIVE receiver is refused (SE-2 2d-3), no plan,
22
22
  * no lock, no probe; the seam is consulted exactly once on the resolved identity.
23
23
  * 9. an invalid garden id throws BEFORE any path/lock is built (F2-P1).
24
- * 10. no spawn-bg plan carries provider/model (D4: 5c-owned launch identity).
25
- * 11. (B2) a throw AFTER the lock is acquired (inspect/probe/preflight) releases
24
+ * 11. (B2) a throw AFTER the lock is acquired (inspect/probe) releases
26
25
  * the held lock before the error propagates — no leak. A reject-path
27
26
  * releaseLock that itself throws is RETRIED (the unlink never happened, so the
28
27
  * lock is still ours) and the ORIGINAL error propagates — retry-pinned so a
@@ -31,18 +30,16 @@
31
30
  * diagnostic (pid/lockPath/detail), including the corrupt null-holder case;
32
31
  * no other reject carries one.
33
32
  *
34
- * No IO — the target lookup, lock, socket inspection/probe, and preflight are all
33
+ * No IO — the target lookup, lock, and socket inspection/probe are all
35
34
  * REQUIRED injected deps (the decider keeps NO live IO default); lock
36
35
  * acquire/release calls are tracked so "no-lock-retained" is proven, not assumed.
37
36
  */
38
37
 
39
38
  import assert from "node:assert/strict";
40
- import type { PreflightOutcome } from "../pi-extensions/lib/entwurf-preflight.ts";
41
39
  import {
42
40
  type DispatchDeciderDeps,
43
41
  type DispatchDecision,
44
42
  decideDispatch,
45
- ENTWURF_V2_OBSERVE_TIMEOUT_MS,
46
43
  type ExecutionPlan,
47
44
  resolveMailboxWakeModeCapability,
48
45
  type TargetResolution,
@@ -89,34 +86,6 @@ function lockClaim(gardenId: string): LockClaim {
89
86
  };
90
87
  }
91
88
 
92
- const APPROVE: PreflightOutcome = {
93
- kind: "approve",
94
- reason: "saved-true",
95
- launchArgs: ["--approve"],
96
- trustStoreDecision: true,
97
- trustStoreInherited: false,
98
- hasTrustInputs: true,
99
- canonicalCwd: CWD,
100
- };
101
- const TRUSTED_NO_ARG: PreflightOutcome = {
102
- kind: "trusted-no-arg",
103
- reason: "no-trust-inputs",
104
- launchArgs: [],
105
- trustStoreDecision: null,
106
- trustStoreInherited: false,
107
- hasTrustInputs: false,
108
- canonicalCwd: CWD,
109
- };
110
- const DENY: PreflightOutcome = {
111
- kind: "deny",
112
- reason: "fail-fast",
113
- launchArgs: [],
114
- trustStoreDecision: null,
115
- trustStoreInherited: false,
116
- hasTrustInputs: true,
117
- canonicalCwd: CWD,
118
- };
119
-
120
89
  function capability(wakeMode: "self-fetch" | "direct-inject"): MetaCapability {
121
90
  return { wakeMode, deliveryLevel: "D6", nativeIdLabel: "session" };
122
91
  }
@@ -126,7 +95,6 @@ interface ScenarioOpts {
126
95
  lock?: "ok" | "conflict";
127
96
  inspection?: TargetSocketInspection;
128
97
  probe?: SocketLiveness;
129
- preflight?: PreflightOutcome;
130
98
  /** SE-2 2d-3: the verdict the injected mailboxDeliverabilityFor seam returns on the
131
99
  * unsupported path. Default false (fail-closed) — the decider trusts the seam, never
132
100
  * wake-mode alone, so this is how a test asserts both the deliverable and inactive cells. */
@@ -179,7 +147,6 @@ function mkDeps(opts: ScenarioOpts): Tracked {
179
147
  return opts.inspection ?? { kind: "absent", socketPath: controlSocketPath(gardenId, "/fake/ctl") };
180
148
  },
181
149
  probeSocket: async (): Promise<SocketLiveness> => opts.probe ?? "dead",
182
- preflightForCwd: () => opts.preflight ?? APPROVE,
183
150
  mailboxDeliverabilityFor: (identity: MetaIdentity) => {
184
151
  mailboxCalls.push(identity);
185
152
  return { deliverable: opts.mailboxDeliverable ?? false, reason: "fake-deliverability" };
@@ -205,7 +172,7 @@ async function main(): Promise<void> {
205
172
  // ── 1+3: bad-target — no citizen → reject, observedLiveness null, NO lock ────
206
173
  {
207
174
  const t = mkDeps({ resolution: { identity: null, preProbeAddressConflict: false } });
208
- const d = await decideDispatch({ target: GID, intent: "owned-outcome", message: "hi" }, t.deps);
175
+ const d = await decideDispatch({ target: GID, intent: "fire-and-forget", message: "hi" }, t.deps);
209
176
  ok("bad-target: reject", d.kind === "reject");
210
177
  ok("bad-target: no plan field", !("plan" in d));
211
178
  ok("bad-target: observedLiveness null (pre-probe)", d.kind === "reject" && d.receipt.observedLiveness === null);
@@ -230,7 +197,7 @@ async function main(): Promise<void> {
230
197
  // ── target-locked: acquire conflict → reject, null, nothing to release ───────
231
198
  {
232
199
  const t = mkDeps({ resolution: { identity: identity("pi"), preProbeAddressConflict: false }, lock: "conflict" });
233
- const d = await decideDispatch({ target: GID, intent: "owned-outcome", message: "hi" }, t.deps);
200
+ const d = await decideDispatch({ target: GID, intent: "fire-and-forget", message: "hi" }, t.deps);
234
201
  ok("target-locked: reject", d.kind === "reject" && d.receipt.reason === "target-locked");
235
202
  ok("target-locked: observedLiveness null (pre-probe)", d.kind === "reject" && d.receipt.observedLiveness === null);
236
203
  ok("target-locked: lock acquire attempted", t.acquireCalls.length === 1);
@@ -253,7 +220,7 @@ async function main(): Promise<void> {
253
220
  // empty/corrupt and there is no pid to show.
254
221
  {
255
222
  const d = await decideDispatch(
256
- { target: GID, intent: "owned-outcome", message: "hi" },
223
+ { target: GID, intent: "fire-and-forget", message: "hi" },
257
224
  {
258
225
  resolveTarget: () => ({ identity: identity("pi"), preProbeAddressConflict: false }),
259
226
  acquireLock: () => ({
@@ -270,7 +237,6 @@ async function main(): Promise<void> {
270
237
  throw new Error("target-locked(corrupt): must not probe after a lock conflict");
271
238
  },
272
239
  probeSocket: async () => "dead",
273
- preflightForCwd: () => APPROVE,
274
240
  mailboxDeliverabilityFor: () => {
275
241
  throw new Error("target-locked(corrupt): in-domain pi never consults the mailbox seam");
276
242
  },
@@ -298,7 +264,7 @@ async function main(): Promise<void> {
298
264
  lock: "ok",
299
265
  inspection: { kind: "address-conflict", socketPath: "/fake/ctl/s.sock", reason: "symlink" },
300
266
  });
301
- const d = await decideDispatch({ target: GID, intent: "owned-outcome", message: "hi" }, t.deps);
267
+ const d = await decideDispatch({ target: GID, intent: "fire-and-forget", message: "hi" }, t.deps);
302
268
  ok(
303
269
  "postlock-conflict: reject target-address-conflict",
304
270
  d.kind === "reject" && d.receipt.reason === "target-address-conflict",
@@ -311,27 +277,6 @@ async function main(): Promise<void> {
311
277
  ok("postlock-conflict: lock RELEASED (no-lock-retained)", t.releaseCalls.length === 1);
312
278
  }
313
279
 
314
- // ── owned-outcome + LIVE → owned-live-no-autosend, release ───────────────────
315
- {
316
- const t = mkDeps({
317
- resolution: { identity: identity("pi"), preProbeAddressConflict: false },
318
- lock: "ok",
319
- inspection: { kind: "socket-file", socketPath: "/fake/ctl/s.sock" },
320
- probe: "alive",
321
- });
322
- const d = await decideDispatch({ target: GID, intent: "owned-outcome", message: "hi" }, t.deps);
323
- ok(
324
- "owned-live: reject owned-live-no-autosend",
325
- d.kind === "reject" && d.receipt.reason === "owned-live-no-autosend",
326
- );
327
- ok(
328
- "owned-live: observedLiveness alive (post-probe, non-null)",
329
- d.kind === "reject" && d.receipt.observedLiveness === "alive",
330
- );
331
- ok("owned-live: lock released", t.releaseCalls.length === 1);
332
- ok("owned-live: no plan", !("plan" in d));
333
- }
334
-
335
280
  // ── indeterminate → indeterminate-no-spawn, release ──────────────────────────
336
281
  {
337
282
  const t = mkDeps({
@@ -339,7 +284,7 @@ async function main(): Promise<void> {
339
284
  lock: "ok",
340
285
  inspection: { kind: "indeterminate", socketPath: "/fake/ctl/s.sock", error: "EACCES" },
341
286
  });
342
- const d = await decideDispatch({ target: GID, intent: "owned-outcome", message: "hi" }, t.deps);
287
+ const d = await decideDispatch({ target: GID, intent: "fire-and-forget", message: "hi" }, t.deps);
343
288
  ok(
344
289
  "indeterminate: reject indeterminate-no-spawn",
345
290
  d.kind === "reject" && d.receipt.reason === "indeterminate-no-spawn",
@@ -400,75 +345,6 @@ async function main(): Promise<void> {
400
345
  }
401
346
  }
402
347
 
403
- // ── 2+4+10: spawn-bg RESUME execute (owned + dormant + preflight allow) ──────
404
- for (const pf of [APPROVE, TRUSTED_NO_ARG]) {
405
- const t = mkDeps({
406
- resolution: { identity: identity("pi"), preProbeAddressConflict: false },
407
- lock: "ok",
408
- inspection: { kind: "absent", socketPath: "/fake/ctl/expected.sock" },
409
- preflight: pf,
410
- });
411
- const d = await decideDispatch(
412
- { target: GID, intent: "owned-outcome", mode: "steer", wantsReply: true, message: "do X" },
413
- t.deps,
414
- );
415
- ok(`resume-execute(${pf.kind}): kind execute`, isExecute(d));
416
- if (isExecute(d) && d.plan.transport === "spawn-bg") {
417
- ok(`resume-execute(${pf.kind}): receipt.transport === plan.transport`, d.receipt.transport === "spawn-bg");
418
- ok(`resume-execute(${pf.kind}): lock RETAINED`, d.lock !== null);
419
- ok(`resume-execute(${pf.kind}): NOT released`, t.releaseCalls.length === 0);
420
- ok(`resume-execute(${pf.kind}): sessionId === gardenId (D3)`, d.plan.sessionId === GID);
421
- ok(`resume-execute(${pf.kind}): prompt = message`, d.plan.prompt === "do X");
422
- ok(`resume-execute(${pf.kind}): wantsReply carried (#50 F2)`, d.plan.wantsReply === true);
423
- ok(`resume-execute(${pf.kind}): launchArgs from preflight`, d.plan.launchArgs === pf.launchArgs);
424
- ok(
425
- `resume-execute(${pf.kind}): expectedSocketPath planted`,
426
- d.plan.expectedSocketPath === "/fake/ctl/expected.sock",
427
- );
428
- ok(
429
- `resume-execute(${pf.kind}): observeTimeoutMs default`,
430
- d.plan.observeTimeoutMs === ENTWURF_V2_OBSERVE_TIMEOUT_MS,
431
- );
432
- ok(`resume-execute(${pf.kind}): releaseWhen A2 predicate`, d.plan.releaseWhen === "socket-alive-or-child-exited");
433
- assert.deepStrictEqual(
434
- planKeys(d.plan),
435
- [
436
- "action",
437
- "cwd",
438
- "expectedSocketPath",
439
- "launchArgs",
440
- "observeTimeoutMs",
441
- "prompt",
442
- "releaseWhen",
443
- "sessionId",
444
- "targetGardenId",
445
- "transport",
446
- "wantsReply",
447
- ],
448
- `spawn-bg plan keyset drift: ${planKeys(d.plan).join(",")}`,
449
- );
450
- ok(`resume-execute(${pf.kind}): NO mode/provider/model in plan`, true);
451
- }
452
- }
453
-
454
- // ── 1+3: untrusted-fail-fast — resume verdict + preflight DENY → release ─────
455
- {
456
- const t = mkDeps({
457
- resolution: { identity: identity("pi"), preProbeAddressConflict: false },
458
- lock: "ok",
459
- inspection: { kind: "absent", socketPath: "/fake/ctl/s.sock" },
460
- preflight: DENY,
461
- });
462
- const d = await decideDispatch({ target: GID, intent: "owned-outcome", message: "do X" }, t.deps);
463
- ok("untrusted: reject untrusted-fail-fast", d.kind === "reject" && d.receipt.reason === "untrusted-fail-fast");
464
- ok(
465
- "untrusted: observedLiveness dead (measured, non-null)",
466
- d.kind === "reject" && d.receipt.observedLiveness === "dead",
467
- );
468
- ok("untrusted: lock RELEASED (nonce-owned, no-lock-retained)", t.releaseCalls.length === 1);
469
- ok("untrusted: no plan", !("plan" in d));
470
- }
471
-
472
348
  // ── 2+5+6: meta-mailbox SEND execute (unsupported claude + ff + deliverable) ─
473
349
  // Deliverability comes from the required seam (active receiver), NOT wake-mode alone.
474
350
  {
@@ -538,20 +414,6 @@ async function main(): Promise<void> {
538
414
  ok("se2-inactive: deliverability seam consulted exactly once", t.mailboxCalls.length === 1);
539
415
  }
540
416
 
541
- // ── unsupported + owned-outcome → backend-liveness-unsupported, no lock ──────
542
- {
543
- const t = mkDeps({
544
- resolution: { identity: identity("claude-code"), preProbeAddressConflict: false },
545
- mailboxDeliverable: true,
546
- });
547
- const d = await decideDispatch({ target: GID, intent: "owned-outcome", message: "x" }, t.deps);
548
- ok(
549
- "unsupported-owned: reject backend-liveness-unsupported",
550
- d.kind === "reject" && d.receipt.reason === "backend-liveness-unsupported",
551
- );
552
- ok("unsupported-owned: acquireLock NOT called", t.acquireCalls.length === 0);
553
- }
554
-
555
417
  // ── #50 C4: record-less socket (identity null + recordLessSocket) ──────────
556
418
  // The record is the sole address authority, so a bare control socket is NOT an
557
419
  // addressable citizen — EVERY intent rejects pre-probe as `record-less-socket`
@@ -560,7 +422,7 @@ async function main(): Promise<void> {
560
422
  // the gid — "absent" would hide the state). The retired A1 narrow used to accept
561
423
  // ff sends into this socket; that acceptance must never come back.
562
424
  const recordLess: TargetResolution = { identity: null, preProbeAddressConflict: false, recordLessSocket: true };
563
- for (const intent of ["fire-and-forget", "owned-outcome"] as const) {
425
+ for (const intent of ["fire-and-forget"] as const) {
564
426
  const t = mkDeps({ resolution: recordLess });
565
427
  const d = await decideDispatch({ target: GID, intent, message: "ping" }, t.deps);
566
428
  ok(
@@ -595,7 +457,7 @@ async function main(): Promise<void> {
595
457
  let threw = false;
596
458
  try {
597
459
  await decideDispatch(
598
- { target: GID, intent: "owned-outcome", message: "x" },
460
+ { target: GID, intent: "fire-and-forget", message: "x" },
599
461
  {
600
462
  resolveTarget: () => ({ identity: identity("pi"), preProbeAddressConflict: false }),
601
463
  acquireLock: () => ({ ok: true, claim: lockClaim(GID) }),
@@ -604,7 +466,6 @@ async function main(): Promise<void> {
604
466
  },
605
467
  inspectSocket: async () => ({ kind: "absent", socketPath: "/fake/ctl/s.sock" }),
606
468
  probeSocket: async () => "dead",
607
- preflightForCwd: () => APPROVE,
608
469
  mailboxDeliverabilityFor: () => ({ deliverable: false, reason: "in-domain pi: seam unused" }),
609
470
  nativePushProbe: () => {
610
471
  throw new Error("lock-leak: in-domain pi never consults the native-push seam");
@@ -631,12 +492,6 @@ async function main(): Promise<void> {
631
492
  throw new Error("probe boom");
632
493
  },
633
494
  });
634
- await runThrowing("preflight throw", {
635
- // absent → dead → owned-outcome resume verdict → preflight is reached
636
- preflightForCwd: () => {
637
- throw new Error("preflight boom");
638
- },
639
- });
640
495
  }
641
496
 
642
497
  // ── B2 retry-pin (Fable 2차 권고): a reject-path releaseLock that THROWS is ──
@@ -650,7 +505,7 @@ async function main(): Promise<void> {
650
505
  let caught: unknown = null;
651
506
  try {
652
507
  await decideDispatch(
653
- { target: GID, intent: "owned-outcome", message: "x" },
508
+ { target: GID, intent: "fire-and-forget", message: "x" },
654
509
  {
655
510
  resolveTarget: () => ({ identity: identity("pi"), preProbeAddressConflict: false }),
656
511
  acquireLock: () => ({ ok: true, claim: lockClaim(GID) }),
@@ -658,10 +513,9 @@ async function main(): Promise<void> {
658
513
  released.push(c);
659
514
  throw new Error(`release boom ${released.length}`);
660
515
  },
661
- // owned-outcome + LIVEowned-live-no-autosend reject → rejectAfterRelease
516
+ // fire-and-forget + DORMANTdormant-fire-forget-unsupported → rejectAfterRelease
662
517
  inspectSocket: async () => ({ kind: "socket-file", socketPath: "/fake/ctl/s.sock" }),
663
- probeSocket: async () => "alive",
664
- preflightForCwd: () => APPROVE,
518
+ probeSocket: async () => "dead",
665
519
  mailboxDeliverabilityFor: () => ({ deliverable: false, reason: "in-domain pi: seam unused" }),
666
520
  nativePushProbe: () => {
667
521
  throw new Error("reject-release-throw: in-domain pi never consults the native-push seam");
@@ -701,7 +555,6 @@ async function main(): Promise<void> {
701
555
  probeSocket: async () => {
702
556
  throw new Error("invalid-gid: probeSocket must not be reached");
703
557
  },
704
- preflightForCwd: () => APPROVE,
705
558
  mailboxDeliverabilityFor: () => {
706
559
  throw new Error("invalid-gid: mailboxDeliverabilityFor must not be reached");
707
560
  },
@@ -710,7 +563,7 @@ async function main(): Promise<void> {
710
563
  },
711
564
  };
712
565
  try {
713
- await decideDispatch({ target: "../etc/passwd", intent: "owned-outcome", message: "x" }, deps);
566
+ await decideDispatch({ target: "../etc/passwd", intent: "fire-and-forget", message: "x" }, deps);
714
567
  } catch {
715
568
  threw = true;
716
569
  }
@@ -775,19 +628,6 @@ async function main(): Promise<void> {
775
628
  d.kind === "reject" && d.receipt.reason === "native-push-probe-indeterminate",
776
629
  );
777
630
  }
778
- {
779
- // owned × alive → reject native-push-no-resume-authority (state-independent), lock-free.
780
- const t = mkDeps({
781
- resolution: npResolution,
782
- nativePush: { status: "alive", route: { lsAddress: "127.0.0.1:5599" } },
783
- });
784
- const d = await decideDispatch({ target: GID, intent: "owned-outcome", message: "yo" }, t.deps);
785
- ok(
786
- "native-push owned+alive: reject native-push-no-resume-authority",
787
- d.kind === "reject" && d.receipt.reason === "native-push-no-resume-authority",
788
- );
789
- ok("native-push owned+alive: acquireLock NOT called (lock-free)", t.acquireCalls.length === 0);
790
- }
791
631
 
792
632
  // The wake-mode capability HELPER stays gate-pinned (renamed; the decider no longer
793
633
  // calls it directly — deliverability flows through the required seam). It answers the
@@ -1,8 +1,11 @@
1
1
  /**
2
2
  * check-entwurf-v2-lock — deterministic gate for 0.11 Stage 0 step 5a (버킷 B F2):
3
- * the per-gid dispatch lock primitive. The lockfile is the ONLY guard against a
3
+ * the per-gid dispatch lock primitive. Its SHIPPED role is serializing concurrent
4
+ * in-domain dispatch at one garden id; it was BORN as the only guard against a
4
5
  * double-spawn of the same dormant target (pi self-guards CREATE but not RESUME,
5
- * 검증원장 F2), so this gate pins every invariant the ledger froze.
6
+ * 검증원장 F2) and that transport left under the visible-first rule. The mechanism
7
+ * and every invariant the ledger froze are unchanged, so this gate is too — see
8
+ * entwurf-v2-lock.ts's header for why the strict reclaim rules are kept.
6
9
  *
7
10
  * Real temp dir (not faked): the `openSync(lockPath,"wx")` OS-level atomicity is
8
11
  * the thing under test, so the directory is real; only the CONTENT-shaping deps
@@ -32,7 +32,6 @@
32
32
  */
33
33
 
34
34
  import assert from "node:assert/strict";
35
- import type { PreflightOutcome } from "../pi-extensions/lib/entwurf-preflight.ts";
36
35
  import {
37
36
  type EntwurfIntent,
38
37
  type EntwurfV2Transport,
@@ -86,23 +85,12 @@ function lockClaim(gardenId: string): LockClaim {
86
85
  };
87
86
  }
88
87
 
89
- const APPROVE: PreflightOutcome = {
90
- kind: "approve",
91
- reason: "saved-true",
92
- launchArgs: ["--approve"],
93
- trustStoreDecision: true,
94
- trustStoreInherited: false,
95
- hasTrustInputs: true,
96
- canonicalCwd: CWD,
97
- };
98
-
99
88
  // ── injected fakes with call tracking (decider-gate shape, minimal copy) ─────
100
89
  interface ScenarioOpts {
101
90
  resolution: TargetResolution;
102
91
  lock?: "ok" | "conflict";
103
92
  inspection?: TargetSocketInspection;
104
93
  probe?: SocketLiveness;
105
- preflight?: PreflightOutcome;
106
94
  mailboxDeliverable?: boolean;
107
95
  }
108
96
  interface Tracked {
@@ -147,7 +135,6 @@ function mkDeps(opts: ScenarioOpts): Tracked {
147
135
  probeCalls.push(socketPath);
148
136
  return opts.probe ?? "dead";
149
137
  },
150
- preflightForCwd: () => opts.preflight ?? APPROVE,
151
138
  mailboxDeliverabilityFor: (id: MetaIdentity) => {
152
139
  mailboxCalls.push(id);
153
140
  return { deliverable: opts.mailboxDeliverable ?? false, reason: "fake-deliverability" };
@@ -208,7 +195,7 @@ const ROWS: Row[] = [
208
195
  {
209
196
  name: "bad-target",
210
197
  targetKind: "no citizen",
211
- intent: "owned-outcome",
198
+ intent: "fire-and-forget",
212
199
  scenario: { resolution: { identity: null, preProbeAddressConflict: false } },
213
200
  expect: { decision: "reject", reason: "bad-target", lock: "none" },
214
201
  },
@@ -246,16 +233,6 @@ const ROWS: Row[] = [
246
233
  },
247
234
  expect: { decision: "reject", reason: "mailbox-undeliverable", lock: "none" },
248
235
  },
249
- {
250
- name: "unsupported owned-outcome → reject",
251
- targetKind: "claude-code, owned intent",
252
- intent: "owned-outcome",
253
- scenario: {
254
- resolution: { identity: identity("claude-code"), preProbeAddressConflict: false },
255
- mailboxDeliverable: true,
256
- },
257
- expect: { decision: "reject", reason: "backend-liveness-unsupported", lock: "none" },
258
- },
259
236
  {
260
237
  name: "in-domain live ff → control-socket",
261
238
  targetKind: "pi, socket alive",
@@ -268,31 +245,6 @@ const ROWS: Row[] = [
268
245
  },
269
246
  expect: { decision: "execute", transport: "control-socket", lock: "held" },
270
247
  },
271
- {
272
- name: "in-domain dormant owned → spawn-bg",
273
- targetKind: "pi, socket absent (dormant)",
274
- intent: "owned-outcome",
275
- scenario: {
276
- resolution: { identity: identity("pi"), preProbeAddressConflict: false },
277
- lock: "ok",
278
- inspection: present({ kind: "absent", socketPath: "/fake/ctl/expected.sock" }),
279
- probe: "dead",
280
- preflight: APPROVE,
281
- },
282
- expect: { decision: "execute", transport: "spawn-bg", lock: "held" },
283
- },
284
- {
285
- name: "in-domain live owned → reject (no autosend)",
286
- targetKind: "pi, socket alive, owned intent",
287
- intent: "owned-outcome",
288
- scenario: {
289
- resolution: { identity: identity("pi"), preProbeAddressConflict: false },
290
- lock: "ok",
291
- inspection: present({ kind: "socket-file", socketPath: "/fake/ctl/s.sock" }),
292
- probe: "alive",
293
- },
294
- expect: { decision: "reject", reason: "owned-live-no-autosend", lock: "released" },
295
- },
296
248
  {
297
249
  name: "in-domain ff dormant → reject",
298
250
  targetKind: "pi, confirmed dormant, ff intent",
@@ -308,7 +260,7 @@ const ROWS: Row[] = [
308
260
  {
309
261
  name: "in-domain indeterminate → reject",
310
262
  targetKind: "pi, socket indeterminate (EACCES)",
311
- intent: "owned-outcome",
263
+ intent: "fire-and-forget",
312
264
  scenario: {
313
265
  resolution: { identity: identity("pi"), preProbeAddressConflict: false },
314
266
  lock: "ok",
@@ -418,7 +370,7 @@ async function main(): Promise<void> {
418
370
  if (row.expect.decision === "execute") transports.add(row.expect.transport);
419
371
  else rejectReasons.add(row.expect.reason);
420
372
  }
421
- for (const tr of ["control-socket", "meta-mailbox", "spawn-bg"]) {
373
+ for (const tr of ["control-socket", "meta-mailbox"]) {
422
374
  ok(`coverage: transport "${tr}" exercised`, transports.has(tr));
423
375
  }
424
376
  for (const lc of ["none", "held", "mailbox-null", "released", "acquire-fail"] as LockClass[]) {
@@ -429,8 +381,6 @@ async function main(): Promise<void> {
429
381
  "target-address-conflict",
430
382
  "target-locked",
431
383
  "mailbox-undeliverable",
432
- "backend-liveness-unsupported",
433
- "owned-live-no-autosend",
434
384
  "dormant-fire-forget-unsupported",
435
385
  "indeterminate-no-spawn",
436
386
  ]) {
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * check-entwurf-v2-production — deterministic gate for the 5d-2b PRODUCTION deps assembly
3
3
  * (`makeProductionEntwurfV2Deps`). It proves the WIRING — which closure/instance/dir reaches
4
- * which hand — over fake leaf-IO spies, with NO real socket/lock/spawn/meta-record. Real
4
+ * which hand — over fake leaf-IO spies, with NO real socket/lock/meta-record. Real
5
5
  * transport behaviour is each hand's own gate + the 5d-5 matrix; this gate's job is the
6
6
  * three production invariants the factory exists to guarantee:
7
7
  *
@@ -9,9 +9,6 @@
9
9
  * (a pi-alive citizen → control-socket execute; acquireLock spy saw {dir: lockDir}).
10
10
  * B. control `sendOverSocket` builds the RpcSendCommand (type/message/mode/wants_reply/
11
11
  * sender) and maps response.success→outcome; the hand releases under `lockDir`.
12
- * C. QB3 split-brain — the spawn watcher releases via the SHARED lockDir-bound `release`,
13
- * NOT the spawn factory's default releaseFn (which would hit the DEFAULT lock dir). A
14
- * throwing spawnChild → spawn-start-failed (released) → releaseLock spy saw {dir:lockDir}.
15
12
  * D. the meta-mailbox hand enqueues onto the wired sessionsDir/mailboxDir.
16
13
  * E. Q3 + Q5 — a dead control send re-resolves (claude-code citizen) to the mailbox and
17
14
  * enqueues through the SAME sendViaMailbox instance (same enqueue spy) on the SAME dirs
@@ -21,7 +18,6 @@
21
18
  */
22
19
 
23
20
  import assert from "node:assert/strict";
24
- import { formatSenderInfoBlock } from "../pi-extensions/lib/entwurf-control-rpc.ts";
25
21
  import type { DispatchInput } from "../pi-extensions/lib/entwurf-v2-decider.ts";
26
22
  import type { AcquireLockResult, LockClaim } from "../pi-extensions/lib/entwurf-v2-lock.ts";
27
23
  import {
@@ -30,7 +26,6 @@ import {
30
26
  } from "../pi-extensions/lib/entwurf-v2-production.ts";
31
27
  import { runEntwurfV2 } from "../pi-extensions/lib/entwurf-v2-runner.ts";
32
28
  import type { ControlSocketPlan, MetaMailboxPlan } from "../pi-extensions/lib/entwurf-v2-send.ts";
33
- import type { SpawnBgPlan } from "../pi-extensions/lib/entwurf-v2-spawn.ts";
34
29
  import type { MetaIdentity, MetaReceiverMarker } from "../pi-extensions/lib/meta-session.ts";
35
30
  import type { NativePushAdapter, NativePushProbeResult } from "../pi-extensions/lib/native-push/adapter.ts";
36
31
  import type { TargetSocketInspection } from "../pi-extensions/lib/socket-discovery.ts";
@@ -100,19 +95,6 @@ const CONTROL_PLAN: ControlSocketPlan = {
100
95
  wantsReply: true,
101
96
  message: "hello",
102
97
  };
103
- const SPAWN_PLAN: SpawnBgPlan = {
104
- transport: "spawn-bg",
105
- action: "resume",
106
- targetGardenId: GID,
107
- sessionId: GID,
108
- cwd: "/home/junghan/repos/gh/entwurf",
109
- prompt: "p",
110
- wantsReply: true,
111
- launchArgs: [],
112
- expectedSocketPath: `${CONTROL_DIR}/${GID}.sock`,
113
- observeTimeoutMs: 30_000,
114
- releaseWhen: "socket-alive-or-child-exited",
115
- };
116
98
  const MAILBOX_PLAN: MetaMailboxPlan = {
117
99
  transport: "meta-mailbox",
118
100
  action: "send",
@@ -144,11 +126,6 @@ function makeSpiedFactory(over: {
144
126
  probe?: "alive" | "dead" | "indeterminate";
145
127
  rpc?: "success" | "dead-throw";
146
128
  classifyDead?: boolean;
147
- spawnChildThrows?: boolean;
148
- /** #50 C3 — capture the plan reaching the spawn factory's resolveIdentity, then
149
- * stop the resume (throw → spawn-start-failed) so the prompt is observable
150
- * without a watcher. */
151
- spawnResolveCapture?: (plan: SpawnBgPlan) => void;
152
129
  /** #50 C3 — a caller with no authoritative sender (senderProvider → undefined). */
153
130
  noSender?: boolean;
154
131
  /** the native-push adapter probe result (only reached on a native-push backend). */
@@ -207,7 +184,6 @@ function makeSpiedFactory(over: {
207
184
  socketPath: `${CONTROL_DIR}/${GID}.sock`,
208
185
  }) as TargetSocketInspection,
209
186
  probeSocket: async () => over.probe ?? "alive",
210
- preflight: () => ({ kind: "approve" }) as never,
211
187
  classifyConnect: () => (over.classifyDead ? "dead" : "indeterminate"),
212
188
  sendRpc: async (socketPath, command) => {
213
189
  spies.rpc.push({ socketPath, command: command as unknown as Record<string, unknown> });
@@ -239,19 +215,6 @@ function makeSpiedFactory(over: {
239
215
  },
240
216
  };
241
217
  },
242
- spawnOverrides: {
243
- spawnChild: over.spawnChildThrows
244
- ? () => {
245
- throw new Error("spawn boom");
246
- }
247
- : undefined,
248
- resolveIdentity: over.spawnResolveCapture
249
- ? (plan) => {
250
- over.spawnResolveCapture?.(plan);
251
- throw new Error("capture stop");
252
- }
253
- : undefined,
254
- },
255
218
  },
256
219
  };
257
220
  return { deps: makeProductionEntwurfV2Deps(opts), spies };
@@ -349,59 +312,7 @@ async function main(): Promise<void> {
349
312
  );
350
313
  }
351
314
 
352
- // ── C: QB3 — spawn release uses the SHARED lockDir release, not the default ─
353
- {
354
- const { deps, spies } = makeSpiedFactory({ spawnChildThrows: true });
355
- const res = await deps.executor.resumeSpawnBg(SPAWN_PLAN, lockClaim());
356
- ok(
357
- "C: throwing spawnChild → spawn-start-failed (released)",
358
- res.kind === "spawn-start-failed" && res.released === true,
359
- );
360
- ok(
361
- "C: QB3 — spawn watcher released under the wired lockDir (not the default)",
362
- spies.release.length === 1 && spies.release[0].dir === LOCK_DIR,
363
- );
364
- }
365
-
366
315
  // ── C2: #50 C3 — the dormant rail carries the caller edge (<sender_info>) ──
367
- {
368
- // The spawn-bg prompt reaching the factory is the plan prompt PLUS the same
369
- // <sender_info> block the live socket rail's receiver synthesizes — one
370
- // formatter, one shape, so a resumed citizen never wakes to an anonymous task.
371
- const cap: { prompt: string | null } = { prompt: null };
372
- const { deps } = makeSpiedFactory({
373
- spawnResolveCapture: (plan) => {
374
- cap.prompt = plan.prompt;
375
- },
376
- });
377
- const res = await deps.executor.resumeSpawnBg(SPAWN_PLAN, lockClaim());
378
- ok("C2: capture-stop surfaced as spawn-start-failed (released)", res.kind === "spawn-start-failed");
379
- // #50 F2: the plan's wantsReply threads into the SAME formatter call the live
380
- // rail's receiver makes, so wants_reply:true survives the dormant rail.
381
- const expected =
382
- SPAWN_PLAN.prompt +
383
- formatSenderInfoBlock(
384
- { sessionId: "self", agentId: "pi/x", cwd: "/cwd", timestamp: "2026-06-13T00:00:00.000Z" },
385
- SPAWN_PLAN.wantsReply,
386
- );
387
- ok("C2: spawn-bg prompt = task + the SHARED <sender_info> block", cap.prompt === expected);
388
- ok("C2: wants_reply rides the dormant <sender_info> (#50 F2)", cap.prompt?.includes('"wants_reply":true') === true);
389
- ok("C2: the plan object handed to the executor is NOT mutated", SPAWN_PLAN.prompt === "p");
390
- }
391
- {
392
- // No authoritative sender → the prompt goes out untouched (never a half-empty
393
- // or fabricated envelope).
394
- const cap: { prompt: string | null } = { prompt: null };
395
- const { deps } = makeSpiedFactory({
396
- noSender: true,
397
- spawnResolveCapture: (plan) => {
398
- cap.prompt = plan.prompt;
399
- },
400
- });
401
- await deps.executor.resumeSpawnBg(SPAWN_PLAN, lockClaim());
402
- ok("C2b: no sender → raw prompt (no fabricated envelope)", cap.prompt === "p");
403
- }
404
-
405
316
  // ── D: meta-mailbox hand enqueues onto the wired dirs ─────────────────────
406
317
  {
407
318
  const { deps, spies } = makeSpiedFactory({});
@@ -483,7 +394,7 @@ async function main(): Promise<void> {
483
394
  // `record-less-socket` (a diagnostic state): no lock, no under-lock probe, no
484
395
  // plan — the record is the sole address authority. The same presence hint with a
485
396
  // symlink / absent socket stays plain bad-target (never trust a symlink).
486
- for (const intent of ["fire-and-forget", "owned-outcome"] as const) {
397
+ for (const intent of ["fire-and-forget"] as const) {
487
398
  const { deps, spies } = makeSpiedFactory({ recordExists: false, inspectKind: "socket-file", probe: "alive" });
488
399
  const decision = await deps.decide({ target: GID, intent, message: "m" });
489
400
  ok(