@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
@@ -3,23 +3,22 @@
3
3
  * (0.11 Stage 0 step 5b). It turns a caller's (target, intent, …) request plus
4
4
  * already-resolvable facts into a single `DispatchDecision`: either a reject
5
5
  * receipt, or an execute receipt + an `ExecutionPlan` + (for in-domain backends) a
6
- * held `LockClaim`. It performs NO transport: no send, no enqueue, no spawn, no
7
- * release-watcher. Step 5c executes the chosen plan; step 5d registers the MCP
8
- * surface. The decider only DECIDES.
6
+ * held `LockClaim`. It performs NO transport: no send, no enqueue, no release-watcher.
7
+ * Step 5c executes the chosen plan; step 5d registers the MCP surface. The decider
8
+ * only DECIDES.
9
+ *
10
+ * Since the visible-first cut (contract header) this module also starts NO process:
11
+ * the resume verdict, its trust preflight, and the detached spawn-bg plan are gone.
12
+ * Every plan it can emit now targets a citizen that is already running.
9
13
  *
10
14
  * Why a separate pure module (step 4 discipline = gate-first → pure-before-IO →
11
15
  * wire): every IO surface the decision needs — the target lookup, the per-gid lock,
12
- * the lstat/connect socket inspection, the trust preflight, the mailbox-deliverability
13
- * seam — is INJECTED via `DispatchDeciderDeps`, so the gate drives every branch with
14
- * fakes and the live wrappers wire the real fns. The plan is shaped so 5c's
15
- * transport hand consumes it WITHOUT re-deriving any path/arg (socketPath,
16
- * mailboxDir, sessionsDir, launchArgs are all planted here once — 4c "재유도 금지"):
17
- * the hand is a plan-keyed dispatcher, never a second brain. The two deliberate
18
- * exceptions (GPT힣 1차 검수): the spawned child's runtime pid is NOT in the plan
19
- * (it is born during 5c execution → it is the watcher's release-context, not a plan
20
- * input), and the launch identity (provider/model) is read by the 5c launcher from
21
- * the saved session JSONL (its existing authority) — putting that read in the
22
- * decider would make it impure. So the plan carries no provider/model.
16
+ * the lstat/connect socket inspection, the mailbox-deliverability seam is INJECTED
17
+ * via `DispatchDeciderDeps`, so the gate drives every branch with fakes and the live
18
+ * wrappers wire the real fns. The plan is shaped so 5c's transport hand consumes it
19
+ * WITHOUT re-deriving any path/arg (socketPath, mailboxDir, sessionsDir are all
20
+ * planted here once — 4c "재유도 금지"): the hand is a plan-keyed dispatcher, never a
21
+ * second brain.
23
22
  *
24
23
  * The frozen 7-step order (NEXT.md "통합 decider 순서"):
25
24
  * 1. requireGardenId — runtime guard BEFORE any path is built (F2-P1; closes the
@@ -32,8 +31,7 @@
32
31
  * 4. acquireLock — IN-DOMAIN ONLY (?7), BEFORE lstat/connect, so the probe
33
32
  * happens under the lock (the TOCTOU 5a's lock closes).
34
33
  * 5. in-domain — inspectTargetControlSocket (lstat-then-connect, ?2) →
35
- * resolveDispatch → on a resume verdict, preflight the target cwd (1B: deny
36
- * nonce-owned release → untrusted-fail-fast) → plan.
34
+ * resolveDispatch → control-socket send plan, or a reject that releases the lock.
37
35
  * 6. unsupported — NO lock; deps.mailboxDeliverabilityFor (REQUIRED seam: wake-mode
38
36
  * capability AND a live active-receiver, fail-closed) → resolveDispatch → meta-mailbox
39
37
  * plan or reject. SE-2 2d-3: a terminated/drifted self-fetch citizen is refused, never
@@ -53,16 +51,6 @@ import { controlSocketPath, mapInspectionToLiveness } from "./socket-discovery.j
53
51
  // Re-export the shared conflict predicate so producers of a TargetResolution have a
54
52
  // single import site for it (it is the SAME fn the fact-provider listing uses).
55
53
  export { isOutOfSocketDomainGardenIdConflict };
56
- // ── observe timeout (?3) ───────────────────────────────────────────────────
57
- // The bounded wait 5c's release-watcher gives a spawned child to surface its
58
- // control socket / exit. Planted into the resume plan so the hand does not invent
59
- // a timeout. A standalone constant + env override (NOT a multiple of the probe
60
- // timeout — a different concern); 30s initial, tunable to 45s after live data.
61
- export const ENTWURF_V2_OBSERVE_TIMEOUT_MS = (() => {
62
- const raw = process.env.ENTWURF_V2_OBSERVE_TIMEOUT_MS;
63
- const n = raw !== undefined && raw !== "" ? Number(raw) : Number.NaN;
64
- return Number.isFinite(n) && n > 0 ? n : 30_000;
65
- })();
66
54
  export const ENTWURF_V2_MODE_DEFAULT = "follow_up";
67
55
  /**
68
56
  * F2-P1 defense in depth: never build a lock/socket path from an unvalidated gid.
@@ -100,10 +88,9 @@ export function resolveMailboxWakeModeCapability(identity, capabilityFor = metaC
100
88
  export async function decideDispatch(input, deps) {
101
89
  const mailboxDir = deps.mailboxDir ?? defaultMetaMailboxDir();
102
90
  const sessionsDir = deps.sessionsDir ?? defaultMetaSessionsDir();
103
- const observeTimeoutMs = deps.observeTimeoutMs ?? ENTWURF_V2_OBSERVE_TIMEOUT_MS;
104
91
  const mode = input.mode ?? ENTWURF_V2_MODE_DEFAULT;
105
92
  const wantsReply = input.wantsReply ?? false;
106
- const ctx = { mode, wantsReply, observeTimeoutMs };
93
+ const ctx = { mode, wantsReply };
107
94
  const reject = (receipt, diagnostic) => diagnostic ? { kind: "reject", receipt, diagnostic } : { kind: "reject", receipt };
108
95
  // 1. requireGardenId — BEFORE any path is built.
109
96
  const gardenId = requireGardenId(input.target);
@@ -179,9 +166,9 @@ export async function decideDispatch(input, deps) {
179
166
  return { kind: "execute", receipt, plan, lock: null };
180
167
  }
181
168
  // 4-5. control-socket domain (currently backend pi): lock → inspect → route.
182
- return decideInDomain(gardenId, input, deps, ctx, identity.cwd);
169
+ return decideInDomain(gardenId, input, deps, ctx);
183
170
  }
184
- async function decideInDomain(gardenId, input, deps, ctx, cwd) {
171
+ async function decideInDomain(gardenId, input, deps, ctx) {
185
172
  const { acquireLock, releaseLock, inspectSocket, probeSocket } = deps;
186
173
  // 4. acquire the per-gid lock BEFORE lstat/connect.
187
174
  const acq = acquireLock(gardenId);
@@ -211,35 +198,14 @@ async function decideInDomain(gardenId, input, deps, ctx, cwd) {
211
198
  const { liveness, socketPath } = mapped;
212
199
  const receipt = resolveDispatch(input.intent, liveness, false);
213
200
  if (!receipt.ok) {
214
- // resolver reject (owned-live-no-autosend / indeterminate-no-spawn / …) — the
215
- // lock was for an in-domain probe that yielded no execute, so release it.
201
+ // resolver reject (dormant-fire-forget-unsupported / indeterminate-no-spawn) —
202
+ // the lock was for an in-domain probe that yielded no execute, so release it.
216
203
  return rejectAfterRelease(receipt);
217
204
  }
218
- if (receipt.action === "resume") {
219
- // 1B: preflight runs ONLY here (the sole branch that launches a child into a
220
- // target cwd). deny nonce-owned release untrusted-fail-fast, with the
221
- // honest measured liveness (dormant = the `dead` we just probed).
222
- const outcome = await deps.preflightForCwd(cwd);
223
- if (outcome.kind === "deny") {
224
- return rejectAfterRelease(makeRejectReceipt("untrusted-fail-fast", liveness));
225
- }
226
- const plan = {
227
- transport: "spawn-bg",
228
- action: "resume",
229
- targetGardenId: gardenId,
230
- sessionId: gardenId, // D3: gid is the pi resume authority, not nativeSessionId.
231
- cwd,
232
- prompt: input.message,
233
- wantsReply: ctx.wantsReply,
234
- launchArgs: outcome.launchArgs,
235
- expectedSocketPath: socketPath,
236
- observeTimeoutMs: ctx.observeTimeoutMs,
237
- releaseWhen: "socket-alive-or-child-exited",
238
- };
239
- retainLock = true;
240
- return { kind: "execute", receipt, plan, lock };
241
- }
242
- // receipt.action === "send" → control-socket send (lock kept for 5c re-resolve).
205
+ // `send` is the only action this table can return since the visible-first cut, so
206
+ // there is exactly one execute shape here control-socket send (lock kept for the
207
+ // 5c re-resolve). The resume branch that used to sit above this — preflight the
208
+ // target cwd, then plan a detached spawn-bg child is gone with the transport.
243
209
  const plan = {
244
210
  transport: "control-socket",
245
211
  action: "send",
@@ -1,12 +1,21 @@
1
1
  /**
2
2
  * entwurf-v2-lock — the per-gid dispatch lock primitive (0.11 Stage 0 step 5a,
3
- * 버킷 B F2). LOAD-BEARING: the guard against a double-spawn of the same dormant
4
- * target by two V2 dispatchers that share the substrate through different entry
5
- * points. SCOPE (honest): this protects v2/v2 only. It was written while the legacy
6
- * `entwurf_resume` still ran unchanged (동결결정 10 scope A) without taking this
7
- * lock, which left a v2/legacy concurrent-resume gap. That verb was REMOVED in the
8
- * 0.12 cutover, so the gap is closed by subtraction not by this lock growing to
9
- * cover it. Any NEW resume entry point must take this lock or the gap reopens.
3
+ * 버킷 B F2). LOAD-BEARING, and its SHIPPED ROLE HAS NARROWED: today it serializes
4
+ * concurrent in-domain dispatch at one garden id, so two V2 dispatchers entering
5
+ * the substrate through different entry points cannot interleave against the same
6
+ * control socket.
7
+ *
8
+ * It was BORN as a double-spawn guard: the dormant cell's `spawn-bg` resume ran
9
+ * under this same lock, and "two dispatchers resume one citizen twice" was the
10
+ * failure it was designed against. That transport was withdrawn under the
11
+ * visible-first rule, so no shipped path can double-launch anything — the
12
+ * mechanism below is unchanged, the threat model it currently answers is smaller.
13
+ * Read every "double-spawn" note in this file as the ORIGINAL motivation, kept
14
+ * because it explains why the reclaim rules are as strict as they are.
15
+ *
16
+ * SCOPE (honest): this protects v2/v2 only. Any NEW resume entry point — the
17
+ * visible same-id resume, when it lands — MUST take this lock, or the original
18
+ * concurrent-resume gap reopens the day a relaunch path exists again.
10
19
  *
11
20
  * ENVIRONMENT ASSUMPTION (stale reclaim): `hostname` equality is used as the
12
21
  * proxy for "same machine", so a holder pid is reclaim-probed with kill(0) only
@@ -8,10 +8,10 @@
8
8
  *
9
9
  * Three invariants this factory exists to guarantee (each gate-proven):
10
10
  * - ONE lock domain (Q2/QB3). A single `release` closure bound to `lockDir` is the lock
11
- * authority for EVERY hand: the decider's `releaseLock`, the control-send hand's
12
- * `releaseLock`, AND the spawn watcher's `releaseFn`. The spawn factory's default
13
- * `releaseFn` ignores `lockDir` (it would release into the DEFAULT lock dir) passing
14
- * `release` explicitly is what closes that split-brain.
11
+ * authority for EVERY hand: the decider's `releaseLock` and the control-send hand's
12
+ * `releaseLock`. (A third hand used to share it — the spawn watcher's `releaseFn`,
13
+ * whose own default would have released into the DEFAULT lock dir; that split-brain
14
+ * went with the transport, the single-authority rule did not.)
15
15
  * - ONE mailbox sender (Q3). A single `makeProductionSendViaMailbox` instance is the
16
16
  * top-level meta-mailbox hand AND the control-send dead-fallback's mailbox enqueue, so a
17
17
  * direct send and a fallback send never drift in sender envelope / dirs.
@@ -28,7 +28,7 @@
28
28
  * canonical socket path; `indeterminate` (EACCES/unknown) fails loud rather than silently
29
29
  * claiming "no conflict". The decider's later `inspectSocket` probe is a SEPARATE step.
30
30
  */
31
- import { formatSenderInfoBlock, sendRpcCommand as realSendRpc, } from "./entwurf-control-rpc.js";
31
+ import { sendRpcCommand as realSendRpc, } from "./entwurf-control-rpc.js";
32
32
  import { mailboxConversationalDeliverable, receiverMarkerMatchesIdentity, } from "./entwurf-deliverability.js";
33
33
  import { isOutOfSocketDomainGardenIdConflict } from "./entwurf-facts.js";
34
34
  import { isLivenessSupported } from "./entwurf-v2-contract.js";
@@ -38,35 +38,10 @@ import { makeProductionSendViaMailbox } from "./entwurf-v2-mailbox.js";
38
38
  import { makeNativePushSend } from "./entwurf-v2-native-push.js";
39
39
  import { executeControlSocketSend, } from "./entwurf-v2-send.js";
40
40
  import { resolveDeadControlSendFallback } from "./entwurf-v2-send-fallback.js";
41
- import { executeSpawnBgResume } from "./entwurf-v2-spawn.js";
42
- import { makeProductionSpawnBgResumeDeps } from "./entwurf-v2-spawn-production.js";
43
41
  import { defaultMetaMailboxDir, defaultMetaSessionsDir, enqueueMetaMessage, metaCapabilityFor, metaRecordExistsByGardenId, readAddressableMetaIdentity, readMetaReceiverMarker, } from "./meta-session.js";
44
42
  import { resolveNativePushAdapter as realResolveNativePushAdapter, } from "./native-push/adapter.js";
45
43
  import { CONTROL_SOCKET_DIR, controlSocketPath, inspectControlSocketPath, inspectTargetControlSocket, isRecordLessSocketCandidate, } from "./socket-discovery.js";
46
44
  import { classifyConnectError, probeSocketLiveness } from "./socket-probe.js";
47
- /**
48
- * 0.12.1 B-2: the production `preflight` seam default. preflight value-imports
49
- * `@earendil-works/pi-coding-agent` (ProjectTrustStore), so importing it eagerly
50
- * would pull pi into the harness-neutral MCP bridge's boot closure. This wrapper
51
- * defers that to a lazy `await import()` reached ONLY on the owned-outcome resume
52
- * branch (the decider awaits it). peers/self/list/mailbox-deliver therefore boot
53
- * with no pi package present.
54
- *
55
- * WHAT THIS DEFERRAL DOES NOT DO — corrected 0.12.8. The old sentence framed the
56
- * pi-less case as an edge ("a pi-less environment that DOES hit a spawn-bg resume
57
- * surfaces an honest module-not-found"). In a published consumer tree that is not an
58
- * edge, it is the DEFAULT: `@earendil-works/pi-coding-agent` is an optional peer that
59
- * a neutral `npm install` does not resolve (entwurf-preflight.ts:51), so this import
60
- * throws for every installed user and the owned-outcome lane has never lived in any
61
- * published version. The deferral still buys what it claims — a pi-free boot — and
62
- * "module-not-found" is still honest, but it is a permanent floor on the install path,
63
- * not a rare condition. Reviving that lane (declared dep / pi CLI subprocess / PATH
64
- * resolution) is a separate decision; nothing here should read as if it works today.
65
- */
66
- async function lazyProductionPreflight(input) {
67
- const { preflight } = await import("./entwurf-preflight.js");
68
- return preflight(input);
69
- }
70
45
  /** Map a record-side socket inspection to the singleton (socketGids, symlinkedGids) the
71
46
  * `isOutOfSocketDomainGardenIdConflict` predicate consumes. `indeterminate` fails LOUD (QB2): an
72
47
  * unprovable conflict must NOT be folded to "no conflict" — that would silently allow an
@@ -107,12 +82,10 @@ export function makeProductionEntwurfV2Deps(opts) {
107
82
  releaseLock: s.releaseLock ?? realReleaseLock,
108
83
  inspectSocket: s.inspectSocket ?? inspectTargetControlSocket,
109
84
  probeSocket: s.probeSocket ?? probeSocketLiveness,
110
- preflight: s.preflight ?? lazyProductionPreflight,
111
85
  classifyConnect: s.classifyConnect ?? classifyConnectError,
112
86
  sendRpc: s.sendRpc ?? realSendRpc,
113
87
  enqueue: s.enqueue ?? enqueueMetaMessage,
114
88
  resolveNativePushAdapter: s.resolveNativePushAdapter ?? realResolveNativePushAdapter,
115
- spawnOverrides: s.spawnOverrides ?? {},
116
89
  };
117
90
  // ── ONE lock domain (Q2/QB3): a single lockDir-bound release for ALL hands ─
118
91
  const acquire = (gid) => io.acquireLock(gid, { dir: lockDir });
@@ -185,7 +158,6 @@ export function makeProductionEntwurfV2Deps(opts) {
185
158
  releaseLock: release,
186
159
  inspectSocket,
187
160
  probeSocket,
188
- preflightForCwd: (cwd) => io.preflight({ cwd, agentDir: opts.agentDir, prefixRoots: opts.prefixRoots }),
189
161
  mailboxDeliverabilityFor,
190
162
  // 봉인 4: resolve the native-push adapter for this backend + probe the conversation.
191
163
  // Only reached on a nativePushSupported backend (the decider gates it), so the resolver
@@ -193,7 +165,6 @@ export function makeProductionEntwurfV2Deps(opts) {
193
165
  nativePushProbe: (identity) => Promise.resolve(io.resolveNativePushAdapter(identity.backend).probe(identity.nativeSessionId)),
194
166
  mailboxDir,
195
167
  sessionsDir,
196
- observeTimeoutMs: opts.observeTimeoutMs,
197
168
  };
198
169
  // ── control-send hand deps (5c-2): the dead-fallback shares resolveTarget /
199
170
  // inspect / probe / dirs with the decider; the mailbox enqueue is the SAME
@@ -224,25 +195,6 @@ export function makeProductionEntwurfV2Deps(opts) {
224
195
  // ── executor: the three transport hands, each pre-bound ───────────────────
225
196
  const executor = {
226
197
  sendControl: (plan, lock) => executeControlSocketSend(plan, lock, controlSendDeps),
227
- resumeSpawnBg: (plan, lock) => {
228
- const spawnPlan = plan;
229
- // Caller-edge preservation (#50 C3): the dormant rail delivers the SAME
230
- // structured <sender_info> the live socket rail synthesizes at its
231
- // receiver — one formatter (entwurf-control-rpc SSOT), appended after the
232
- // task text exactly as a live delivery would render it. Without this, a
233
- // resumed citizen woke with an anonymous prompt while every other rail
234
- // carried the sender envelope.
235
- const sender = opts.senderProvider();
236
- const prompt = sender ? spawnPlan.prompt + formatSenderInfoBlock(sender, spawnPlan.wantsReply) : spawnPlan.prompt;
237
- return executeSpawnBgResume({ ...spawnPlan, prompt }, lock,
238
- // QB3: inject the shared lockDir-bound `release` — never the spawn factory's
239
- // default releaseFn (which would release into the DEFAULT lock dir).
240
- makeProductionSpawnBgResumeDeps({
241
- ...io.spawnOverrides,
242
- killGraceMs: opts.killGraceMs ?? io.spawnOverrides.killGraceMs,
243
- releaseFn: release,
244
- }));
245
- },
246
198
  sendMailbox: (plan, _lock) => sendViaMailbox(plan, _lock),
247
199
  // native-push (봉인 4): the SAME injected adapter resolver drives the executor send,
248
200
  // so the decider's probe and the delivery use one adapter. Lock-free (lock ignored).
@@ -6,24 +6,25 @@
6
6
  * AT MOST ONCE.
7
7
  *
8
8
  * Why a pure reducer before any transport IO (5b discipline = gate-first →
9
- * pure-before-IO → wire): the load-bearing risk of 5c is NOT the spawn/send IO, it
10
- * is the RELEASE TIMING (Fable 3, "release-after-observation"). A spawn-bg dispatch
11
- * that releases its lock too early on spawn-started/ack instead of on an observed
12
- * liveness transition reopens the exact double-spawn window 5a's per-gid lock
13
- * exists to close. So the timing logic is isolated here as a pure state machine the
14
- * gate drives over every event order, and 5c-2/5c-3/5c-4 only WIRE the real
15
- * send/spawn/mailbox IO onto it. The hand feeds events; this module decides release.
9
+ * pure-before-IO → wire): the load-bearing risk of 5c is NOT the send IO, it is the
10
+ * RELEASE TIMING. A dispatch that releases its lock too early — on an ack instead of
11
+ * on a final outcome reopens the window 5a's per-gid lock exists to close. So the
12
+ * timing logic is isolated here as a pure state machine the gate drives over every
13
+ * event order, and 5c-2/5c-4 only WIRE the real send/mailbox IO onto it. The hand
14
+ * feeds events; this module decides release.
16
15
  *
17
- * The three policies (one per ExecutionPlan transport):
18
- * - no-lock — meta-mailbox (?7: no lock was ever held).
19
- * Nothing to release on any event.
16
+ * The two policies (one per ExecutionPlan transport):
17
+ * - no-lock — meta-mailbox and native-push (?7 / 봉인 4: no
18
+ * lock was ever held). Nothing to release on any event.
20
19
  * - release-after-send-final — control-socket send. The lock is held only for
21
20
  * the at-most-once re-resolve; release once the send reaches a FINAL outcome.
22
- * - release-after-spawn-observation — spawn-bg resume. The lock is held until the
23
- * FIRST observable transition: socket-alive OR child-exited (any code). A
24
- * spawn-started event is explicitly NOT a release trigger (that is the whole
25
- * point). A spawn that never started (spawn-start-failed) has no child to watch,
26
- * so it releases to free the gidkeeping the lock would pin it forever.
21
+ *
22
+ * There was a third, `release-after-spawn-observation`, for the spawn-bg resume: hold
23
+ * the lock until an OBSERVED liveness transition (socket-alive / child-exited), never
24
+ * on spawn-started. It went with that transport in the visible-first cut, and with it
25
+ * the spawn event vocabulary. The rule it encoded a launch is finalized by an
26
+ * observation, not by having started — is the one a visible resume will have to
27
+ * restate for itself; it is not inherited by anything shipped here.
27
28
  *
28
29
  * The release authority is ALWAYS the LockClaim the decider handed over (5c wires it
29
30
  * through). This module never re-derives a lock by gid, never re-resolves a path —
@@ -33,7 +34,7 @@ export function initialReleaseState() {
33
34
  return { released: false };
34
35
  }
35
36
  /**
36
- * An in-domain execute (control-socket / spawn-bg) must hold a lock whose gardenId
37
+ * An in-domain execute (control-socket) must hold a lock whose gardenId
37
38
  * IS the plan's target. Both halves are 5b decider-contract invariants, not runtime
38
39
  * conditions: a null lock or a lock paired with the WRONG plan are the same grade of
39
40
  * mis-wiring (a later release would free nothing, or free a DIFFERENT gid's lock), so
@@ -51,7 +52,7 @@ function assertInDomainLock(plan, lock) {
51
52
  /**
52
53
  * Derive the release policy from the plan, cross-checking the lock invariants the 5b
53
54
  * decider guarantees: meta-mailbox ⇒ lock null (?7); in-domain (control-socket /
54
- * spawn-bg) ⇒ lock non-null AND lock.gardenId === plan.targetGardenId. A mismatch is
55
+ * control-socket) ⇒ lock non-null AND lock.gardenId === plan.targetGardenId. A mismatch is
55
56
  * a decider contract violation, not a runtime condition — it throws so a mis-wired
56
57
  * hand fails loud instead of silently dropping or mis-releasing a lock.
57
58
  */
@@ -74,17 +75,15 @@ export function decideReleasePolicy(plan, lock) {
74
75
  case "control-socket":
75
76
  assertInDomainLock(plan, lock);
76
77
  return { kind: "release-after-send-final" };
77
- case "spawn-bg":
78
- assertInDomainLock(plan, lock);
79
- return { kind: "release-after-spawn-observation" };
80
78
  }
81
79
  }
82
80
  /**
83
81
  * Fold one execution event into the release decision. `shouldRelease` is true
84
82
  * EXACTLY ONCE — on the first event that satisfies the policy's release condition;
85
83
  * every later event (after `state.released`) returns false. The single-release
86
- * guarantee is what lets the spawn watcher race socket-alive against child-exit
87
- * without a double release: whichever fires first releases, the other is a no-op.
84
+ * guarantee is what let the retired spawn watcher race two observations without a
85
+ * double release, and it is why the control-socket hand can feed a final outcome more
86
+ * than once without paying for it twice.
88
87
  */
89
88
  export function reduceRelease(policy, state, event) {
90
89
  if (state.released) {
@@ -99,19 +98,5 @@ export function reduceRelease(policy, state, event) {
99
98
  case "release-after-send-final":
100
99
  // Release once the send reaches a final outcome; hold before that.
101
100
  return event.kind === "send-final" ? release() : hold();
102
- case "release-after-spawn-observation":
103
- // Fable 3: spawn-started is NOT a release event. Release on the first
104
- // observed transition (socket-alive / child-exited, any code), or on a
105
- // failed start (no child to watch).
106
- switch (event.kind) {
107
- case "socket-alive":
108
- case "child-exited":
109
- case "spawn-start-failed":
110
- return release();
111
- case "spawn-started":
112
- case "send-final":
113
- case "mailbox-enqueued":
114
- return hold();
115
- }
116
101
  }
117
102
  }
@@ -6,9 +6,9 @@
6
6
  * to the right 5c transport hand, returning ONE outcome-rich `EntwurfV2RunResult`. It performs
7
7
  * ZERO IO of its own and makes ZERO routing decisions — `decideDispatch` chose the plan,
8
8
  * this only DISPATCHES it. Each hand is an injected dep (the gate fakes them; 5d-2 wires
9
- * the production `executeControlSocketSend` / `executeSpawnBgResume` / production
10
- * `sendViaMailbox`), so the routing + result mapping is gate-provable without a socket,
11
- * spawn, or timer — the same pure-before-IO, IO-via-dep discipline 5b/5c kept.
9
+ * the production `executeControlSocketSend` / `sendViaMailbox` / native-push sender), so
10
+ * the routing + result mapping is gate-provable without a socket — the same
11
+ * pure-before-IO, IO-via-dep discipline 5b/5c kept.
12
12
  *
13
13
  * Why a result type richer than the receipt: the carry-over contracts from 5c demand it.
14
14
  * - N3 (5c-2b): a dead-path re-resolve `rejected` carries the resolver's `rejectReason`
@@ -19,8 +19,6 @@
19
19
  * re-send would double-deliver. The runner surfaces this as `execution-failed` with
20
20
  * `finalizedOutcome` + `releaseFailed` so the surface renders "delivered, lock dirty,
21
21
  * do NOT retry", never "send failed".
22
- * - spawn-bg `lock-retained` is a RETURNED result (fail-closed, not a throw): it rides
23
- * the `executed` branch so 5d's surface can render the retained-lock diagnostic.
24
22
  *
25
23
  * `retrySafe` is conservatively `false` on EVERY `execution-failed`: a thrown send is
26
24
  * never confidently retry-safe (an `indeterminate` connect may have delivered to an
@@ -71,16 +69,6 @@ export async function executeDispatch(decision, deps) {
71
69
  return { kind: "execution-failed", receipt, transport, error: errorMessage(err), retrySafe: false };
72
70
  }
73
71
  }
74
- case "spawn-bg": {
75
- try {
76
- // `lock-retained` is a RETURNED result (fail-closed), not a throw — it rides `executed`.
77
- const result = await deps.resumeSpawnBg(plan, lock);
78
- return { kind: "executed", receipt, transport, outcome: { transport: "spawn-bg", result } };
79
- }
80
- catch (err) {
81
- return { kind: "execution-failed", receipt, transport, error: errorMessage(err), retrySafe: false };
82
- }
83
- }
84
72
  case "meta-mailbox": {
85
73
  try {
86
74
  // lock is null here (?7) — passed verbatim; the production adapter ignores it.
@@ -18,17 +18,18 @@
18
18
  * must read the same row. That pin is also what makes "no resume promotion" structural,
19
19
  * not just a 5c-2a guard: the `fire-and-forget` row has NO resume cell —
20
20
  * live → send / control-socket (the retry)
21
- * dormant(dead) → reject (dormant-fire-forget-unsupported) ← NOT resume/spawn-bg
21
+ * dormant(dead) → reject (dormant-fire-forget-unsupported) ← nothing is launched
22
22
  * indeterminate → reject (indeterminate-no-spawn)
23
- * Only `owned-outcome + dormant` is resume/spawn-bg, and an owned-outcome dispatch never
24
- * yields a control-socket send plan in the first place. So a dead re-resolve can only
25
- * retry (alive), reject (dead/indeterminate pi), or — on an UNSUPPORTED backend, via the
26
- * separate mailbox mini-table — enqueue to a deliverable citizen. It can never spawn.
23
+ * Since the visible-first cut there is no resume cell ANYWHERE — the intent that owned one
24
+ * is gone from the contract so a dead re-resolve can only retry (alive), reject
25
+ * (dead/indeterminate pi), or — on an UNSUPPORTED backend, via the separate mailbox
26
+ * mini-table — enqueue to a deliverable citizen. It can never start a process.
27
27
  *
28
- * The N2 asymmetry (frozen in entwurf-v2-contract): in-domain `dormant` is a CONFIRMED
29
- * not-running pi reject (enqueuing would be a silent pileup; resume is the honest
30
- * place, and resume is out of a SEND fallback's scope). `unsupported` is UNKNOWN liveness
31
- * with no socket axis the mailbox is its honest channel when deliverable.
28
+ * The N2 asymmetry (frozen in entwurf-v2-contract) is a RECEIVER question, not a backend
29
+ * privilege: a mailbox is offered only where an active receiver actually drains it. An
30
+ * `unsupported` citizen gets one exactly while `mailboxDeliverable` holds, and is rejected as
31
+ * `mailbox-undeliverable` the moment it does not. An in-domain `dormant` pi is a CONFIRMED
32
+ * not-running session, so nothing would ever drain that enqueue — a silent pileup → reject.
32
33
  */
33
34
  import { isLivenessSupported, resolveDispatch } from "./entwurf-v2-contract.js";
34
35
  import { defaultMetaMailboxDir, defaultMetaSessionsDir } from "./meta-session.js";
@@ -104,8 +105,8 @@ export async function resolveDeadControlSendFallback(plan, lock, deps) {
104
105
  return { kind: "reject", reason: receipt.reason };
105
106
  }
106
107
  // fire-and-forget + live is the ONLY in-domain allow cell → control-socket send. A
107
- // resume/spawn-bg or meta-mailbox transport here would be a frozen-table drift, so
108
- // fail loud rather than mis-route a SEND fallback into a child spawn.
108
+ // meta-mailbox transport here would be a frozen-table drift, so fail loud rather than
109
+ // mis-route a SEND fallback onto another rail.
109
110
  if (receipt.transport !== "control-socket") {
110
111
  throw new Error(`entwurf-v2-send-fallback: in-domain fire-and-forget re-resolve yielded unexpected transport (${receipt.transport}) — table drift.`);
111
112
  }
@@ -45,7 +45,7 @@ export async function executeControlSocketSend(plan, lock, deps) {
45
45
  const policy = decideReleasePolicy(plan, lock);
46
46
  const held = lock;
47
47
  // Lock-leak backstop: once the lock is held, ANY throw out of the drive — a
48
- // contract-violation guard (spawn-bg re-resolve), the mis-route assert, even a buggy
48
+ // contract-violation guard, the mis-route assert, even a buggy
49
49
  // dep that throws where it should return — must still release the lock before it
50
50
  // propagates. A leaked lock pins the gid forever (5a's worst failure). So convert any
51
51
  // such throw into a `failed` final outcome, run finalizeRelease (which releases on
@@ -127,14 +127,9 @@ async function driveDeadFallback(plan, lock, deps) {
127
127
  catch (err) {
128
128
  return { outcome: "failed", error: err };
129
129
  }
130
- case "spawn-bg":
131
- // A SEND fallback must never re-resolve into a spawn — that is a decider
132
- // contract violation (a send and a spawn are different actions), so fail loud
133
- // rather than silently mis-execute.
134
- throw new Error("entwurf-v2-send: re-resolve returned a spawn-bg plan for a send fallback (contract violation).");
135
130
  case "native-push":
136
131
  // The dead-control-socket fallback re-resolves only the pi socket domain
137
- // (control-socket / mailbox / spawn); it never routes the native-push rail. A
132
+ // (control-socket / mailbox); it never routes the native-push rail. A
138
133
  // native-push rePlan here is structurally impossible — fail loud.
139
134
  throw new Error("entwurf-v2-send: re-resolve returned a native-push plan for a send fallback (contract violation).");
140
135
  }