@junghanacs/entwurf 0.13.1 → 0.14.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (158) hide show
  1. package/AGENTS.md +59 -15
  2. package/BASELINE.md +3 -3
  3. package/CHANGELOG.md +74 -0
  4. package/CONTRIBUTING.md +13 -9
  5. package/DELIVERY.md +7 -6
  6. package/README.md +37 -28
  7. package/VERIFY.md +22 -14
  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 +9 -4
  12. package/docs/external-mcp-host.md +4 -5
  13. package/docs/setup-clean-host.md +8 -7
  14. package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +155 -28
  15. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/overlay.js +8 -6
  16. package/mcp/entwurf-bridge/dist/pi-extensions/lib/classify-tmux-cwd.js +47 -0
  17. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-control-rpc.js +7 -5
  18. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-core.js +13 -14
  19. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-resume-args.js +45 -40
  20. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-contract.js +117 -95
  21. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-decider.js +23 -57
  22. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-lock.js +16 -7
  23. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-production.js +5 -53
  24. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-release.js +21 -36
  25. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-runner.js +3 -15
  26. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-send-fallback.js +12 -11
  27. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-send.js +2 -7
  28. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-surface.js +30 -67
  29. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-visible-resume.js +256 -0
  30. package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-session.js +91 -5
  31. package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +300 -0
  32. package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-launch.js +202 -0
  33. package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-placement.js +289 -0
  34. package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-resume-call.js +141 -0
  35. package/mcp/entwurf-bridge/dist/pi-extensions/lib/resume-launch-identity.js +136 -0
  36. package/mcp/entwurf-bridge/dist/pi-extensions/lib/session-id.js +8 -5
  37. package/mcp/entwurf-bridge/dist/pi-extensions/lib/socket-discovery.js +3 -3
  38. package/mcp/entwurf-bridge/dist/scripts/meta-facts.js +51 -0
  39. package/mcp/entwurf-bridge/dist/scripts/new-session-id.js +9 -4
  40. package/mcp/entwurf-bridge/src/index.ts +173 -28
  41. package/mcp/entwurf-bridge/start.sh +2 -2
  42. package/mcp/entwurf-bridge/test.sh +23 -9
  43. package/mcp/entwurf-bridge/tsconfig.build.json +11 -2
  44. package/package.json +22 -11
  45. package/pi-extensions/entwurf-control.ts +227 -40
  46. package/pi-extensions/lib/acp/backend.ts +71 -12
  47. package/pi-extensions/lib/acp/overlay.ts +8 -6
  48. package/pi-extensions/lib/classify-tmux-cwd.ts +50 -0
  49. package/pi-extensions/lib/entwurf-control-rpc.ts +7 -5
  50. package/pi-extensions/lib/entwurf-core.ts +15 -15
  51. package/pi-extensions/lib/entwurf-resume-args.ts +41 -52
  52. package/pi-extensions/lib/entwurf-v2-contract-schema.ts +1 -1
  53. package/pi-extensions/lib/entwurf-v2-contract.ts +120 -99
  54. package/pi-extensions/lib/entwurf-v2-decider.ts +30 -91
  55. package/pi-extensions/lib/entwurf-v2-lock.ts +16 -7
  56. package/pi-extensions/lib/entwurf-v2-production.ts +4 -78
  57. package/pi-extensions/lib/entwurf-v2-release.ts +25 -49
  58. package/pi-extensions/lib/entwurf-v2-runner.ts +6 -21
  59. package/pi-extensions/lib/entwurf-v2-send-fallback.ts +12 -11
  60. package/pi-extensions/lib/entwurf-v2-send.ts +2 -7
  61. package/pi-extensions/lib/entwurf-v2-surface.ts +36 -76
  62. package/pi-extensions/lib/entwurf-v2-visible-resume.ts +370 -0
  63. package/pi-extensions/lib/meta-session.ts +93 -5
  64. package/pi-extensions/lib/mux-fresh-call.ts +381 -0
  65. package/pi-extensions/lib/mux-launch.ts +267 -0
  66. package/pi-extensions/lib/mux-placement.ts +387 -0
  67. package/pi-extensions/lib/mux-resume-call.ts +189 -0
  68. package/pi-extensions/lib/resume-launch-identity.ts +162 -0
  69. package/pi-extensions/lib/session-id.js +8 -5
  70. package/pi-extensions/lib/socket-discovery.ts +3 -3
  71. package/run.sh +471 -201
  72. package/scripts/agy-bridge-config.py +5 -1
  73. package/scripts/check-acp-backend-preflight.ts +1 -1
  74. package/scripts/check-acp-overlay.ts +13 -3
  75. package/scripts/check-acp-stream-hooks.ts +504 -0
  76. package/scripts/check-elapsed.sh +25 -0
  77. package/scripts/check-entwurf-bridge-boot.ts +51 -4
  78. package/scripts/check-entwurf-bridge-pi-free.ts +6 -5
  79. package/scripts/check-entwurf-control-rpc.ts +4 -3
  80. package/scripts/check-entwurf-resume-args.ts +72 -70
  81. package/scripts/check-entwurf-session-identity.ts +14 -10
  82. package/scripts/check-entwurf-v2-contract.ts +34 -59
  83. package/scripts/check-entwurf-v2-decider.ts +17 -177
  84. package/scripts/check-entwurf-v2-lock.ts +5 -2
  85. package/scripts/check-entwurf-v2-matrix.ts +3 -53
  86. package/scripts/check-entwurf-v2-production.ts +2 -91
  87. package/scripts/check-entwurf-v2-release.ts +10 -105
  88. package/scripts/check-entwurf-v2-runner.ts +4 -85
  89. package/scripts/check-entwurf-v2-send-fallback.ts +5 -6
  90. package/scripts/check-entwurf-v2-send.ts +0 -28
  91. package/scripts/check-entwurf-v2-surface.ts +157 -128
  92. package/scripts/check-entwurf-v2-visible-resume.ts +445 -0
  93. package/scripts/check-fresh-cut-gate.sh +1 -1
  94. package/scripts/check-gate-qualification.ts +99 -7
  95. package/scripts/check-install-container.sh +10 -2
  96. package/scripts/check-install-surface.ts +1 -1
  97. package/scripts/check-keyset-overlap.py +1 -1
  98. package/scripts/check-meta-facts.ts +249 -0
  99. package/scripts/check-meta-identity-consumers.ts +1 -1
  100. package/scripts/check-meta-session.ts +169 -0
  101. package/scripts/check-mux-launch-tmux.ts +316 -0
  102. package/scripts/check-mux-launch.ts +288 -0
  103. package/scripts/check-mux-launcher-fence.ts +264 -0
  104. package/scripts/check-mux-parent-artifact.ts +195 -0
  105. package/scripts/check-mux-placement-tmux.ts +322 -0
  106. package/scripts/check-mux-placement.ts +323 -0
  107. package/scripts/check-mux-resume-call.ts +283 -0
  108. package/scripts/check-probe-cli-shim.ts +25 -22
  109. package/scripts/check-probe-ordering.ts +84 -76
  110. package/scripts/check-release-gate-outcomes.ts +127 -7
  111. package/scripts/check-resume-launch-identity.ts +244 -0
  112. package/scripts/check-socket-discovery.ts +1 -1
  113. package/scripts/fixtures/mux-parent-transcript.scrubbed.jsonl +3 -0
  114. package/scripts/inventory-verification-surface.ts +349 -0
  115. package/scripts/lib/claude-launcher-fence.ts +322 -0
  116. package/scripts/lib/mutation-qualify.ts +109 -3
  117. package/scripts/meta-bridge-doctor.sh +6 -8
  118. package/scripts/meta-bridge-state.py +75 -1
  119. package/scripts/meta-facts.ts +60 -0
  120. package/scripts/mutants/acp-overlay.json +17 -0
  121. package/scripts/mutants/acp-stream-hooks.json +158 -0
  122. package/scripts/mutants/bridge-boot-resume.json +45 -0
  123. package/scripts/mutants/meta-facts.json +50 -0
  124. package/scripts/mutants/meta-identity.json +36 -0
  125. package/scripts/mutants/meta-retire.json +47 -0
  126. package/scripts/mutants/mux-boundary.json +196 -0
  127. package/scripts/mutants/mux-fresh-call.json +229 -0
  128. package/scripts/mutants/mux-launcher-fence.json +123 -0
  129. package/scripts/mutants/mux-parent-artifact.json +39 -0
  130. package/scripts/mutants/mux-resume-call.json +148 -0
  131. package/scripts/mutants/probe-ordering.json +0 -1037
  132. package/scripts/mutants/release-gate.json +35 -0
  133. package/scripts/mutants/resume-args.json +76 -0
  134. package/scripts/mutants/resume-launch-identity.json +96 -0
  135. package/scripts/mutants/v2-surface.json +58 -18
  136. package/scripts/mutants/v2-visible-resume.json +215 -0
  137. package/scripts/new-session-id.ts +9 -4
  138. package/scripts/smoke-acp-raw-turn-live.ts +1 -1
  139. package/scripts/smoke-agy-native-push-live.ts +6 -17
  140. package/scripts/smoke-entwurf-chain-live.ts +11 -3
  141. package/scripts/smoke-entwurf-v2-matrix-live.ts +1 -1
  142. package/scripts/smoke-meta-honesty.sh +1 -1
  143. package/scripts/smoke-meta-install-state.sh +169 -3
  144. package/scripts/smoke-mux-fresh-call-live.ts +365 -0
  145. package/scripts/smoke-mux-lifecycle-live.ts +1136 -0
  146. package/scripts/smoke-pi-attach.ts +1 -1
  147. package/scripts/smoke-user-scope-citizen.sh +1 -1
  148. package/scripts/tsconfig.json +1 -0
  149. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-preflight.js +0 -160
  150. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-spawn-production.js +0 -273
  151. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-spawn.js +0 -216
  152. package/pi-extensions/lib/entwurf-v2-spawn-production.ts +0 -373
  153. package/pi-extensions/lib/entwurf-v2-spawn.ts +0 -323
  154. package/scripts/check-acp-sdk-surface.ts +0 -275
  155. package/scripts/check-entwurf-v2-spawn-production.ts +0 -551
  156. package/scripts/check-entwurf-v2-spawn.ts +0 -399
  157. package/scripts/smoke-entwurf-v2-spawn-live.ts +0 -188
  158. package/scripts/smoke-entwurf-v2-spawn-resume-live.ts +0 -467
@@ -17,20 +17,43 @@
17
17
  *
18
18
  * Source-verified invariants folded in (Opus 실측 + GPT 보정 + Fable R1-R5, 2026-06-11):
19
19
  * - F1: caller intent is DECLARED in the input, so the contract a caller
20
- * receives is deterministic — never computed from liveness at call time.
21
- * `owned-outcome` (caller owns completion) ≠ `fire-and-forget` (ack only).
20
+ * receives is deterministic — never computed from liveness at call time. One
21
+ * intent remains: `fire-and-forget` (the ack is the end of the contract). The
22
+ * second intent, `owned-outcome`, is GONE — see the visible-first note below.
23
+ *
24
+ * ── Visible-first subtraction (2026-08-06, GLG) ────────────────────────────
25
+ * `owned-outcome` and the `spawn-bg` transport are removed from this contract, not
26
+ * deprecated. The only resume this verb ever had launched a DETACHED, window-less
27
+ * `pi` child; under the visible-first operating rule a citizen the operator cannot
28
+ * see must not be started at all, and a reachable hidden path is worse than a
29
+ * temporary absence of the capability. So the subtraction is a hard cut: the intent
30
+ * leaves the enum, the transport leaves the table, and an old caller fails schema
31
+ * validation rather than meeting a polite reject string.
32
+ *
33
+ * What this leaves behind, deliberately: a DORMANT in-domain citizen is currently
34
+ * unreachable (`fire-and-forget` × dormant has always rejected). Visible same-id
35
+ * resume is a SEPARATE lifecycle capability — a distinct verb over the mux launch
36
+ * lane, not an intent on this delivery verb — and it is not implemented here. The
37
+ * leaves it will need (record-authoritative launch identity, the resume argv
38
+ * builder, the per-target lock, socket inspection) are preserved and still shipped.
39
+ *
40
+ * READ THE SCOPE EXACTLY. Background execution is not judged impossible and is not forbidden
41
+ * forever. It is retired NOW so that no hidden bypass exists while visible-first governance is
42
+ * being established. A future background lifecycle may be added back as an explicitly
43
+ * authorized, FIRST-CLASS capability with its own verb and its own receipts. What it must never
44
+ * be again is what it was here: an automatic fallback, or a hidden branch inside visible resume
45
+ * or `entwurf_v2`. The shape is what was removed, not the idea of a detached process.
22
46
  * - R1: liveness is defined PER CAPABILITY DOMAIN. The control-socket domain
23
47
  * currently contains backend `pi`; claude-code is self-fetch with no socket,
24
48
  * so its socket liveness is `unsupported`, NOT folded into dead/indeterminate.
25
49
  * This is transport capability, not identity rank: every target is first a
26
50
  * record citizen. `unsupported` is a 4th FACT value, not a 4th table column.
27
51
  * - R2: `target` is the garden-id of an EXISTING citizen. spawn-new is out of
28
- * v2 scope — it was the legacy `entwurf` verb's, and since that verb's removal
29
- * fresh creation is a deferred capability with NO surface, not a fallback that
30
- * still exists elsewhere. Absent/typo gid = `bad-target` (so F6 "오타 gid가
31
- * 신규 spawn 사고 막기" holds automatically).
32
- * - N1/F3: an `indeterminate` target never spawns. N2: `fire-and-forget` to a
33
- * `dormant` target is "reject for now" (mailbox-wake lacks a reply-correlation
52
+ * v2 scope — fresh creation now has the separate `entwurf_fresh_call` surface,
53
+ * never a fallback inside this resolver. Absent/typo gid = `bad-target` (so F6
54
+ * "오타 gid 신규 spawn 사고 막기" holds automatically).
55
+ * - N1/F3: an `indeterminate` target is never launched into. N2: `fire-and-forget`
56
+ * to a `dormant` target is "reject for now" (mailbox-wake lacks a reply-correlation
34
57
  * id in the substrate; an additive extension later, not a permanent no).
35
58
  * - Q2: every cell is a SINGLE verdict — no "default", no escape hatch (a
36
59
  * "default reject" would re-admit the call-time nondeterminism F1 closes).
@@ -38,8 +61,7 @@
38
61
  * is NOT a reject — the 0.10.0 meta-bridge mailbox delivers without liveness.
39
62
  * `unsupported` is the "no liveness predicate" fact, not a delivery verdict; so
40
63
  * ff+unsupported routes to the `meta-mailbox` transport, gated by a SEPARATE
41
- * `mailboxDeliverable` fact (NOT a column of the 6-cell table — Fable (i)).
42
- * owned-outcome+unsupported still rejects (self-fetch needs real liveness).
64
+ * `mailboxDeliverable` fact (NOT a column of the 3-cell table — Fable (i)).
43
65
  *
44
66
  * The decision table here is a constant; `check-entwurf-v2-contract` asserts it
45
67
  * exhaustively + proves the "table cell ↔ receipt" round-trip. THAT round-trip
@@ -48,17 +70,25 @@
48
70
  */
49
71
  // ── Caller-declared intent (F1) ────────────────────────────────────────────
50
72
  // The outcome contract is an INPUT, not an inference. `fire-and-forget` = the
51
- // RPC ack is the end of the contract (entwurf-control.ts:29-37). `owned-outcome`
52
- // = the caller owns the dispatched session's completion.
53
- export const ENTWURF_INTENTS = ["fire-and-forget", "owned-outcome"];
73
+ // RPC ack is the end of the contract (entwurf-control.ts:29-37).
74
+ //
75
+ // This enum has ONE member on purpose. It kept a second — `owned-outcome`, whose
76
+ // only allow cell launched a hidden detached child — until the visible-first cut
77
+ // (header). A one-member enum is not a smell here: the axis is real (a caller
78
+ // still declares its outcome contract), and the value that is gone is gone from
79
+ // the wire, so an old caller is refused by schema validation instead of being
80
+ // quietly re-routed.
81
+ export const ENTWURF_INTENTS = ["fire-and-forget"];
54
82
  // ── Liveness axes ──────────────────────────────────────────────────────────
55
83
  // FactLiveness (R1/R3b) = what `entwurf_peers` exposes: the 3 socket-probe
56
84
  // values PLUS `unsupported` (predicate undefined for this backend). Four values.
57
85
  export const FACT_LIVENESSES = ["alive", "dead", "indeterminate", "unsupported"];
58
86
  // DispatchLiveness = the in-domain routing axis the table is keyed on. The
59
- // socket result maps: alive→live (send), dead→dormant (resume from disk),
60
- // indeterminate→indeterminate (never spawn). `unsupported` is NOT here — it is
61
- // handled by the domain guard before the table is consulted.
87
+ // socket result maps: alive→live (send), dead→dormant (unreachable since the
88
+ // visible-first cut), indeterminate→indeterminate (never dispatched into).
89
+ // `unsupported` is NOT here — it is handled by the domain guard before the table
90
+ // is consulted. The axis keeps all three values because they are FACTS about a
91
+ // target; only the verdicts behind two of them changed.
62
92
  export const DISPATCH_LIVENESSES = ["live", "dormant", "indeterminate"];
63
93
  // ── Backend liveness domain (R1 + F4) ──────────────────────────────────────
64
94
  // Backends whose SOCKET liveness predicate is DEFINED. The control-socket
@@ -110,17 +140,17 @@ export function dispatchLivenessOf(socket) {
110
140
  // attempted. A post-dispatch "send-fail fallback" (transport failed after the
111
141
  // verdict) is a SEPARATE axis (bucket B) and must NOT be merged into this enum.
112
142
  export const ENTWURF_V2_REJECT_REASONS = [
113
- "indeterminate-no-spawn", // N1/F3: never spawn an indeterminate target
143
+ // The name is inherited vocabulary and stays a frozen wire string: with the spawn
144
+ // transport gone there is nothing left to spawn, but the RULE it names is intact —
145
+ // an in-domain target whose probe was inconclusive is never dispatched into. Renaming
146
+ // a public reject string is its own contract cut, not a side effect of this one.
147
+ "indeterminate-no-spawn", // N1/F3: never dispatch into an indeterminate target
114
148
  "dormant-fire-forget-unsupported", // N2: fire-and-forget to a dormant target — reject for now
115
- "owned-live-no-autosend", // Q2/F1: owned-outcome to a live target is not an auto-send
116
- "backend-liveness-unsupported", // R1: backend has no liveness predicate (e.g. claude-code) — owned-outcome only
117
149
  "mailbox-undeliverable", // F-mailbox: fire-and-forget to an unsupported citizen whose mailbox is not deliverable (fail-closed; future pi-backend non-drainable mailbox)
118
- "native-push-target-dead", // 봉인 1: fire-and-forget to a native-push (agy) target whose adapter probe found NO live conversation. Post-probe; observedLiveness = dead. NOT `backend-liveness-unsupported` — a native-push backend IS measured, so that name would be a lie.
119
- "native-push-probe-indeterminate", // 봉인 1: fire-and-forget to a native-push target whose adapter probe was inconclusive (agy alive but no port served the conv, or a probe error). Post-probe; observedLiveness = indeterminate. Never spawns, never coerced to dead.
120
- "native-push-no-resume-authority", // 봉인 1: owned-outcome to a native-push target (any liveness — single, state-independent). A native-push backend has no resume/spawn authority (there is no pi-child to own), so the caller cannot own its completion; use fire-and-forget. Post-probe; observedLiveness = the measured value.
121
- "bad-target", // R2: absent/typo garden-id (no existing citizen); spawn-new out of v2 scope
122
- "untrusted-fail-fast", // 동결결정 5: controlled launch into an untrusted cwd
123
- "record-less-socket", // #50 C4: a gid-shaped non-symlink control socket exists but NO meta-record claims it. The record is the sole address authority (목표 ②), so a bare socket is not an addressable citizen — it is a diagnostic state, refused for EVERY intent before any lock/probe (pre-probe, observedLiveness=null). NOT `bad-target`: something real answers to this gid, and mislabeling it absent would hide the very state the reject exists to surface. Replaces the retired A1-narrow acceptance (and its post-probe `socket-only-no-resume-authority`).
150
+ "native-push-target-dead", // 봉인 1: fire-and-forget to a native-push (agy) target whose adapter probe found NO live conversation. Post-probe; observedLiveness = dead.
151
+ "native-push-probe-indeterminate", // 봉인 1: fire-and-forget to a native-push target whose adapter probe was inconclusive (agy alive but no port served the conv, or a probe error). Post-probe; observedLiveness = indeterminate. Never coerced to dead.
152
+ "bad-target", // R2: absent/typo garden-id (no existing citizen); fresh creation out of v2 scope
153
+ "record-less-socket", // #50 C4: a gid-shaped non-symlink control socket exists but NO meta-record claims it. The record is the sole address authority (목표 ②), so a bare socket is not an addressable citizen — it is a diagnostic state, refused for EVERY intent before any lock/probe (pre-probe, observedLiveness=null). NOT `bad-target`: something real answers to this gid, and mislabeling it absent would hide the very state the reject exists to surface.
124
154
  "target-locked", // R5 pre-claim for bucket B F2 per-gid lockfile conflict
125
155
  "target-address-conflict", // F3: a quarantined citizen (garden-id-socket-conflict / symlinked socket) — the gid resolves to two different receivers (record vs socket), so dispatch refuses to pick. The ONLY in-band honest channel for a dispatch-level identity-split (the listing diagnostic channel is not visible to a v2 caller, who only gets a receipt). Pre-resolver, like bad-target/target-locked — NOT a RESOLVER_REJECT_REASONS member.
126
156
  ];
@@ -134,10 +164,11 @@ export const ENTWURF_V2_REJECT_REASONS = [
134
164
  // in-domain probe was inconclusive (≠ "not looked yet"); `unsupported` means the
135
165
  // backend has no predicate (≠ "pre-probe"). So a pre-probe reject's
136
166
  // observedLiveness is `null`, NOT one of the four values. Every OTHER reject —
137
- // the RESOLVER_REJECT_REASONS (5, post-probe) plus `untrusted-fail-fast` (1B: it
138
- // now runs AFTER the lock+probe, only on a resume verdict, so its observedLiveness
139
- // is the honest measured `dormant`) carries a non-null FactLiveness, as does
140
- // every success. This null/non-null split is REASON-DEPENDENT, so the receipt
167
+ // the RESOLVER_REJECT_REASONS (3, post-probe) and the native-push set (2, also
168
+ // post-probe) carries a non-null FactLiveness, as does every success. (The
169
+ // visible-first cut removed the one post-probe reject that was NOT a resolver
170
+ // member: `untrusted-fail-fast`, which only ever fired behind a resume verdict.)
171
+ // This null/non-null split is REASON-DEPENDENT, so the receipt
141
172
  // schema (which allows null on every reject branch) cannot enforce it alone — the
142
173
  // semantic fixture in `check-entwurf-v2-contract` does, via `isPreProbeReject` /
143
174
  // `rejectObservedLivenessWellFormed` below (the SSOT 5b mints against).
@@ -155,33 +186,24 @@ export function isPreProbeReject(reason) {
155
186
  * pure SSOT predicate so 5b mints against it and the gate proves it: a pre-probe
156
187
  * reject MUST carry `null`; every other reject MUST carry a non-null FactLiveness.
157
188
  * Catches the illegal `{ok:false, reason:"bad-target", observedLiveness:"indeterminate"}`
158
- * (pre-probe with a stamped value) and `{ok:false, reason:"owned-live-no-autosend",
189
+ * (pre-probe with a stamped value) and `{ok:false, reason:"dormant-fire-forget-unsupported",
159
190
  * observedLiveness:null}` (post-probe with no value) — both reason-dependent, so
160
191
  * unreachable by the schema's blanket `FactLiveness | null`.
161
- *
162
- * NOTE this predicate FREEZES the 1B ordering into the contract: classifying
163
- * `untrusted-fail-fast` as post-probe (non-null required) encodes "preflight runs
164
- * AFTER the probe". Moving preflight back ahead of the probe would make its
165
- * observedLiveness un-measured (null) and reopen this predicate + the enum split.
166
192
  */
167
193
  export function rejectObservedLivenessWellFormed(reason, observedLiveness) {
168
194
  return isPreProbeReject(reason) ? observedLiveness === null : observedLiveness !== null;
169
195
  }
170
- // Reasons the RESOLVER emits — the in-domain 6-cell table cells PLUS the
171
- // unsupported domain-guard mini-table (backend-liveness-unsupported for
172
- // owned-outcome, mailbox-undeliverable for a fail-closed fire-and-forget). NOT
173
- // just the 6-cell table (the F-mailbox mini-table emits two of these), hence
174
- // RESOLVER_ not TABLE_. The remaining taxonomy members are produced by stages
175
- // OTHER than the resolver: `bad-target` (target resolution) and `target-locked`
176
- // (lockfile) run BEFORE the resolver, while `untrusted-fail-fast` is decided
177
- // AFTER it preflight runs only behind a resume verdict (1B), so it is a LATER
178
- // stage, not an earlier one. All three are pre-claimed in the enum so bucket B
179
- // does not reopen it.
196
+ // Reasons the RESOLVER emits — the in-domain 3-cell table cells PLUS the
197
+ // unsupported domain-guard mini-table (mailbox-undeliverable for a fail-closed
198
+ // fire-and-forget). NOT just the table (the F-mailbox mini-table emits one of
199
+ // these), hence RESOLVER_ not TABLE_. The remaining taxonomy members are produced
200
+ // by stages BEFORE the resolver: `bad-target` (target resolution) and
201
+ // `target-locked` (lockfile). Both are pre-claimed in the enum so bucket B does
202
+ // not reopen it. There is no longer any AFTER-the-resolver reject: the one that
203
+ // existed, `untrusted-fail-fast`, fired only behind the removed resume verdict.
180
204
  export const RESOLVER_REJECT_REASONS = [
181
205
  "indeterminate-no-spawn",
182
206
  "dormant-fire-forget-unsupported",
183
- "owned-live-no-autosend",
184
- "backend-liveness-unsupported",
185
207
  "mailbox-undeliverable",
186
208
  ];
187
209
  // ── Transport + verdict ────────────────────────────────────────────────────
@@ -193,16 +215,20 @@ export const RESOLVER_REJECT_REASONS = [
193
215
  // send arm (ack-only), but it requires a live-probe (NATIVE_PUSH_DISPATCH_TABLE),
194
216
  // where meta-mailbox is liveness-free. It is NOT a mailbox enqueue and NOT a pi socket
195
217
  // send — it is its own rail.
196
- export const ENTWURF_V2_TRANSPORTS = [
197
- "control-socket",
198
- "spawn-bg",
199
- "tmux-live",
200
- "meta-mailbox",
201
- "native-push",
202
- ];
218
+ //
219
+ // This list contains DELIVERY outcomes only. A mux may launch a live runtime, but
220
+ // its session handle is neither a receipt transport nor address authority. Once
221
+ // that runtime becomes a record-backed citizen, delivery still uses one of the
222
+ // rails below. `spawn-bg` — the detached, window-less resume child — was removed
223
+ // with `owned-outcome` (header): every remaining transport delivers to a citizen
224
+ // that is already running, so this verb no longer starts a process at all.
225
+ export const ENTWURF_V2_TRANSPORTS = ["control-socket", "meta-mailbox", "native-push"];
203
226
  // Allow-branch facets (exported so the schema↔types gate asserts every enum).
204
- export const ENTWURF_V2_ACTIONS = ["send", "resume"];
205
- export const ENTWURF_V2_OWNERSHIPS = ["ack-only", "owned"];
227
+ // Both are single-valued since the cut: the only `resume`/`owned` verdict was the
228
+ // spawn-bg cell. They stay as enums because they are the receipt's own vocabulary
229
+ // and a future visible-resume verb would speak them again.
230
+ export const ENTWURF_V2_ACTIONS = ["send"];
231
+ export const ENTWURF_V2_OWNERSHIPS = ["ack-only"];
206
232
  // Delivery mode of the message to the target (how it is injected) — steer =
207
233
  // interrupt the current turn, follow_up = queue after it. A SEPARATE axis from
208
234
  // both the intent/ownership axis (F1) and the liveness-routing axis. The removed
@@ -210,28 +236,26 @@ export const ENTWURF_V2_OWNERSHIPS = ["ack-only", "owned"];
210
236
  // inherited vocabulary, not a second live delivery verb.
211
237
  export const ENTWURF_V2_MODES = ["steer", "follow_up"];
212
238
  // ── The FROZEN decision table ──────────────────────────────────────────────
213
- // intent × dispatch-liveness → exactly one verdict (Q2). v2-initial ALLOWS
214
- // exactly two cells (fire-and-forget+live = send; owned-outcome+dormant =
215
- // resume); the other four reject. The reject cells are honest "지금은 없음"
216
- // locks (N2). They were written while the legacy 3-verb surface still covered
217
- // those flows; that surface is GONE, so a reject cell is now a real absence with
218
- // no fallback verb behind it — reopening one takes a new contract, never a
219
- // quiet re-admission.
239
+ // intent × dispatch-liveness → exactly one verdict (Q2). THREE cells since the
240
+ // visible-first cut, of which exactly ONE allows: fire-and-forget+live = send.
241
+ // The other two reject. The reject cells are honest "지금은 없음" locks (N2).
242
+ // They were written while the legacy 3-verb surface still covered those flows;
243
+ // that surface is GONE, so a reject cell is a real absence with no fallback verb
244
+ // behind it — reopening one takes a new contract, never a quiet re-admission.
245
+ //
246
+ // The `dormant` cell is the one that now carries the whole cost of the cut: an
247
+ // in-domain citizen that is not running cannot be reached by ANY intent. That is
248
+ // the intended fail-closed state, not an oversight — the resume that used to
249
+ // answer here was hidden, and a visible replacement is a separate capability.
220
250
  export const DISPATCH_TABLE = {
221
251
  "fire-and-forget": {
222
252
  live: { action: "send", transport: "control-socket", ownership: "ack-only" },
223
253
  dormant: { action: "reject", reason: "dormant-fire-forget-unsupported" },
224
254
  indeterminate: { action: "reject", reason: "indeterminate-no-spawn" },
225
255
  },
226
- "owned-outcome": {
227
- // wants_reply is etiquette, not ownership — owned+live never auto-sends (Q2/F1).
228
- live: { action: "reject", reason: "owned-live-no-autosend" },
229
- dormant: { action: "resume", transport: "spawn-bg", ownership: "owned" },
230
- indeterminate: { action: "reject", reason: "indeterminate-no-spawn" },
231
- },
232
256
  };
233
257
  // ── The unsupported-backend mailbox mini-table (F-mailbox) ─────────────────
234
- // SEPARATE from the in-domain 6-cell DISPATCH_TABLE (Fable (i)): a backend with no
258
+ // SEPARATE from the in-domain 3-cell DISPATCH_TABLE (Fable (i)): a backend with no
235
259
  // pi-socket liveness predicate never enters the liveness-keyed table. Instead the
236
260
  // domain guard routes it here, keyed on intent alone. Reaches here: claude-code
237
261
  // (self-fetch mailbox) and codex (no adapter yet). Does NOT reach here: antigravity —
@@ -244,17 +268,20 @@ export const DISPATCH_TABLE = {
244
268
  // fact is false (fail-closed). The ack is enqueue+doorbell, NOT read, and
245
269
  // observedLiveness stays `unsupported` — the receipt's `meta-mailbox` transport
246
270
  // is what says "this went to the mailbox".
247
- // - owned-outcome has no real liveness to own on a self-fetch backend → reject.
248
271
  //
249
272
  // N2 asymmetry (명문화 — without this the two tables read as contradictory):
250
273
  // fire-and-forget+dormant-PI = reject vs fire-and-forget+unsupported-CITIZEN = mailbox.
251
- // In-domain `dormant` is a CONFIRMED not-running pi, so enqueuing would be a
252
- // silent pileup (resume is the honest place). `unsupported` is UNKNOWN liveness
253
- // on a backend we cannot probe, so a best-effort mailbox doorbell is the most we
254
- // can honestly offer there is nothing to resume into.
274
+ // The axis is NOT which backend a citizen runs, and this cell grants no backend a
275
+ // privilege. It is one question asked of both: does an ACTIVE RECEIVER actually drain
276
+ // the mailbox this message would land in? A self-fetch citizen reaches this allow cell
277
+ // only through the separate `mailboxDeliverable` fact, which IS that receiver check —
278
+ // when nobody drains, this very cell rejects as `mailbox-undeliverable`. An in-domain
279
+ // `dormant` pi has no drainer at all: its mailbox reader was the running session, and
280
+ // the session is confirmed not running, so an enqueue would be a silent pileup no one
281
+ // ever reads. Same rule, two answers. (It reads as a backend split only if you skip
282
+ // the deliverability fact — which is exactly what makes claude-code look privileged.)
255
283
  export const UNSUPPORTED_DISPATCH_TABLE = {
256
284
  "fire-and-forget": { action: "send", transport: "meta-mailbox", ownership: "ack-only" },
257
- "owned-outcome": { action: "reject", reason: "backend-liveness-unsupported" },
258
285
  };
259
286
  /**
260
287
  * The ONLY sanctioned way to mint a reject receipt (?6 enforcement). A pure
@@ -263,9 +290,9 @@ export const UNSUPPORTED_DISPATCH_TABLE = {
263
290
  * observedLiveness:"indeterminate"}`, which the blanket `FactLiveness | null`
264
291
  * schema accepts. This constructor THROWS on a well-formedness violation, so
265
292
  * every reject path (resolveDispatch's own mints below + the 5b stages that
266
- * produce bad-target / target-locked / target-address-conflict / untrusted-
267
- * fail-fast) routes through one chokepoint and the bypass surface is zero. 5b
268
- * MUST build rejects with this, never by object literal.
293
+ * produce bad-target / target-locked / target-address-conflict / record-less-socket)
294
+ * routes through one chokepoint and the bypass surface is zero. 5b MUST build
295
+ * rejects with this, never by object literal.
269
296
  */
270
297
  export function makeRejectReceipt(reason, observedLiveness) {
271
298
  if (!rejectObservedLivenessWellFormed(reason, observedLiveness)) {
@@ -277,11 +304,11 @@ export function makeRejectReceipt(reason, observedLiveness) {
277
304
  * PURE dispatch decision over already-resolved facts. Before reaching here the
278
305
  * caller has resolved the target (→ `bad-target` if no existing citizen) and, for
279
306
  * an in-domain backend, acquired the per-gid lock (→ `target-locked`) and probed
280
- * liveness UNDER that lock. This function only decides the liveness-routed
281
- * verdict. preflight (→ `untrusted-fail-fast`) is NOT a precondition here: per 1B
282
- * it runs only AFTER this resolver returns a resume verdict (the sole branch that
283
- * launches a child into a target cwd), so a send/mailbox verdict never touches it.
284
- * Do NOT reintroduce a global pre-resolver preflight that re-breaks F-mailbox.
307
+ * liveness UNDER that lock. This function only decides the liveness-routed verdict.
308
+ * There is NO trust preflight anywhere on this path: it existed solely to guard the
309
+ * resume verdict that launched a child into a target cwd, and that verdict is gone.
310
+ * Do NOT reintroduce a global pre-resolver preflight that re-breaks F-mailbox,
311
+ * and this verb no longer launches anything into any cwd.
285
312
  *
286
313
  * Two facts in: `liveness` (the 4-value FactLiveness) and `mailboxDeliverable`
287
314
  * (F-mailbox — a SEPARATE axis from liveness, NOT a column of either table, NOT
@@ -293,8 +320,8 @@ export function makeRejectReceipt(reason, observedLiveness) {
293
320
  * backend the liveness-routed table is authoritative and the flag is ignored.
294
321
  *
295
322
  * R1 domain guard runs first: an `unsupported` liveness is routed through the
296
- * UNSUPPORTED_DISPATCH_TABLE (mailbox mini-table), never the 6-cell table.
297
- * No spawn, no send, no I/O — step 5 executes the chosen transport.
323
+ * UNSUPPORTED_DISPATCH_TABLE (mailbox mini-table), never the 3-cell table.
324
+ * No send, no I/O — step 5 executes the chosen transport.
298
325
  */
299
326
  export function resolveDispatch(intent, liveness, mailboxDeliverable) {
300
327
  if (liveness === "unsupported") {
@@ -339,20 +366,16 @@ export function resolveDispatch(intent, liveness, mailboxDeliverable) {
339
366
  // fire-and-forget × alive → native-push send (the ONE allow cell)
340
367
  // fire-and-forget × dead → reject native-push-target-dead
341
368
  // fire-and-forget × indeterminate → reject native-push-probe-indeterminate
342
- // owned-outcome × * → reject native-push-no-resume-authority (state-
343
- // independent: no pi-child to own; `backend-
344
- // liveness-unsupported` is NOT reused false name).
369
+ //
370
+ // The `owned-outcome` row is gone with the intent (header). It had rejected on every
371
+ // liveness with `native-push-no-resume-authority` a real fact about this rail that
372
+ // simply has no caller left to tell, since no caller can declare that intent.
345
373
  export const NATIVE_PUSH_DISPATCH_TABLE = {
346
374
  "fire-and-forget": {
347
375
  alive: { action: "send", transport: "native-push", ownership: "ack-only" },
348
376
  dead: { action: "reject", reason: "native-push-target-dead" },
349
377
  indeterminate: { action: "reject", reason: "native-push-probe-indeterminate" },
350
378
  },
351
- "owned-outcome": {
352
- alive: { action: "reject", reason: "native-push-no-resume-authority" },
353
- dead: { action: "reject", reason: "native-push-no-resume-authority" },
354
- indeterminate: { action: "reject", reason: "native-push-no-resume-authority" },
355
- },
356
379
  };
357
380
  // The reasons the native-push resolver emits — a THIRD post-probe reject set, parallel
358
381
  // to RESOLVER_REJECT_REASONS (pi/mailbox). All post-probe: resolveNativePushDispatch
@@ -361,7 +384,6 @@ export const NATIVE_PUSH_DISPATCH_TABLE = {
361
384
  export const NATIVE_PUSH_REJECT_REASONS = [
362
385
  "native-push-target-dead",
363
386
  "native-push-probe-indeterminate",
364
- "native-push-no-resume-authority",
365
387
  ];
366
388
  /**
367
389
  * PURE native-push dispatch decision (봉인 4). Given the caller intent and the adapter
@@ -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