@junghanacs/entwurf 0.12.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 (183) hide show
  1. package/AGENTS.md +240 -0
  2. package/BASELINE.md +227 -0
  3. package/CHANGELOG.md +1210 -0
  4. package/CONTRIBUTING.md +63 -0
  5. package/DELIVERY.md +209 -0
  6. package/LICENSE +21 -0
  7. package/README.md +504 -0
  8. package/VERIFY.md +260 -0
  9. package/demo/README.md +188 -0
  10. package/demo/demo-baseline.sh +156 -0
  11. package/demo/demo.sh +183 -0
  12. package/docs/assets/entwurf-demo.gif +0 -0
  13. package/docs/assets/entwurf-doomemacs.gif +0 -0
  14. package/docs/assets/entwurf-entwurf.gif +0 -0
  15. package/docs/assets/entwurf-hero.jpg +0 -0
  16. package/docs/setup-clean-host.md +305 -0
  17. package/mcp/entwurf-bridge/src/index.ts +513 -0
  18. package/mcp/entwurf-bridge/start.sh +25 -0
  19. package/mcp/entwurf-bridge/test.sh +54 -0
  20. package/mcp/tsconfig.json +29 -0
  21. package/package.json +130 -0
  22. package/pi/entwurf-capabilities.json +9 -0
  23. package/pi/entwurf-targets.json +20 -0
  24. package/pi/meta-bridge/.claude-plugin/marketplace.json +12 -0
  25. package/pi/meta-bridge/entwurf-meta-receive/.claude-plugin/plugin.json +5 -0
  26. package/pi/meta-bridge/entwurf-meta-receive/hooks/hooks.json +50 -0
  27. package/pi/meta-bridge/entwurf-meta-receive/scripts/doorbell.sh +54 -0
  28. package/pi/settings.reference.json +43 -0
  29. package/pi/skill-plugin-example/.claude-plugin/plugin.json +4 -0
  30. package/pi/skill-plugin-example/skills/hello/SKILL.md +18 -0
  31. package/pi-extensions/acp-provider.ts +63 -0
  32. package/pi-extensions/entwurf-control.ts +1692 -0
  33. package/pi-extensions/lib/acp/acp-client.ts +90 -0
  34. package/pi-extensions/lib/acp/augment.ts +238 -0
  35. package/pi-extensions/lib/acp/backend-adapter.ts +319 -0
  36. package/pi-extensions/lib/acp/backend.ts +873 -0
  37. package/pi-extensions/lib/acp/config.ts +552 -0
  38. package/pi-extensions/lib/acp/context.ts +177 -0
  39. package/pi-extensions/lib/acp/engraving.ts +123 -0
  40. package/pi-extensions/lib/acp/event-mapper.ts +339 -0
  41. package/pi-extensions/lib/acp/models.ts +102 -0
  42. package/pi-extensions/lib/acp/overlay.ts +220 -0
  43. package/pi-extensions/lib/acp/prompts/engraving.md +1 -0
  44. package/pi-extensions/lib/acp/session-store.ts +418 -0
  45. package/pi-extensions/lib/acp/tool-surface.ts +184 -0
  46. package/pi-extensions/lib/entwurf-control-rpc.ts +208 -0
  47. package/pi-extensions/lib/entwurf-core.ts +2033 -0
  48. package/pi-extensions/lib/entwurf-deliverability.ts +123 -0
  49. package/pi-extensions/lib/entwurf-fact-provider.ts +158 -0
  50. package/pi-extensions/lib/entwurf-facts.ts +251 -0
  51. package/pi-extensions/lib/entwurf-mailbox-guard.ts +100 -0
  52. package/pi-extensions/lib/entwurf-peers-render.ts +154 -0
  53. package/pi-extensions/lib/entwurf-preflight.ts +247 -0
  54. package/pi-extensions/lib/entwurf-resume-args.ts +88 -0
  55. package/pi-extensions/lib/entwurf-self-address.ts +117 -0
  56. package/pi-extensions/lib/entwurf-v2-contract.ts +427 -0
  57. package/pi-extensions/lib/entwurf-v2-decider.ts +441 -0
  58. package/pi-extensions/lib/entwurf-v2-lock.ts +412 -0
  59. package/pi-extensions/lib/entwurf-v2-mailbox.ts +87 -0
  60. package/pi-extensions/lib/entwurf-v2-production.ts +326 -0
  61. package/pi-extensions/lib/entwurf-v2-release.ts +149 -0
  62. package/pi-extensions/lib/entwurf-v2-resume-marker.ts +34 -0
  63. package/pi-extensions/lib/entwurf-v2-runner.ts +196 -0
  64. package/pi-extensions/lib/entwurf-v2-send-fallback.ts +166 -0
  65. package/pi-extensions/lib/entwurf-v2-send.ts +271 -0
  66. package/pi-extensions/lib/entwurf-v2-spawn-production.ts +337 -0
  67. package/pi-extensions/lib/entwurf-v2-spawn.ts +323 -0
  68. package/pi-extensions/lib/entwurf-v2-surface.ts +205 -0
  69. package/pi-extensions/lib/meta-mailbox-body.ts +80 -0
  70. package/pi-extensions/lib/meta-session.ts +1951 -0
  71. package/pi-extensions/lib/project-trust-handler.ts +154 -0
  72. package/pi-extensions/lib/session-id.js +57 -0
  73. package/pi-extensions/lib/socket-discovery.ts +346 -0
  74. package/pi-extensions/lib/socket-probe.ts +92 -0
  75. package/pi-extensions/meta-bridge-hook.ts +250 -0
  76. package/pi-extensions/model-lock.ts +235 -0
  77. package/prompts/engraving.md +27 -0
  78. package/protocol.js +31 -0
  79. package/run.sh +2832 -0
  80. package/scripts/check-acp-backend-preflight.ts +130 -0
  81. package/scripts/check-acp-carrier-augment.ts +297 -0
  82. package/scripts/check-acp-config.ts +322 -0
  83. package/scripts/check-acp-event-mapper.ts +253 -0
  84. package/scripts/check-acp-overlay.ts +154 -0
  85. package/scripts/check-acp-prompt-builder.ts +191 -0
  86. package/scripts/check-acp-provider-surface.ts +159 -0
  87. package/scripts/check-acp-sdk-surface.ts +211 -0
  88. package/scripts/check-acp-session-reuse.ts +756 -0
  89. package/scripts/check-acp-session-store.ts +387 -0
  90. package/scripts/check-acp-tool-surface.ts +159 -0
  91. package/scripts/check-entwurf-bridge-boot.ts +199 -0
  92. package/scripts/check-entwurf-capabilities.ts +123 -0
  93. package/scripts/check-entwurf-control-rpc.ts +187 -0
  94. package/scripts/check-entwurf-deliverability.ts +158 -0
  95. package/scripts/check-entwurf-fact-provider.ts +316 -0
  96. package/scripts/check-entwurf-facts.ts +358 -0
  97. package/scripts/check-entwurf-mailbox-guard.ts +264 -0
  98. package/scripts/check-entwurf-peers-surface.ts +235 -0
  99. package/scripts/check-entwurf-resume-args.ts +149 -0
  100. package/scripts/check-entwurf-self-address.ts +209 -0
  101. package/scripts/check-entwurf-session-identity.ts +703 -0
  102. package/scripts/check-entwurf-v2-contract.ts +536 -0
  103. package/scripts/check-entwurf-v2-decider.ts +795 -0
  104. package/scripts/check-entwurf-v2-lock.ts +368 -0
  105. package/scripts/check-entwurf-v2-mailbox.ts +228 -0
  106. package/scripts/check-entwurf-v2-matrix.ts +437 -0
  107. package/scripts/check-entwurf-v2-production.ts +406 -0
  108. package/scripts/check-entwurf-v2-release.ts +245 -0
  109. package/scripts/check-entwurf-v2-runner.ts +452 -0
  110. package/scripts/check-entwurf-v2-send-fallback.ts +371 -0
  111. package/scripts/check-entwurf-v2-send.ts +454 -0
  112. package/scripts/check-entwurf-v2-spawn-production.ts +452 -0
  113. package/scripts/check-entwurf-v2-spawn.ts +398 -0
  114. package/scripts/check-entwurf-v2-surface.ts +352 -0
  115. package/scripts/check-keyset-overlap.py +120 -0
  116. package/scripts/check-mailbox-receipt-state.ts +170 -0
  117. package/scripts/check-meta-capability-source.ts +112 -0
  118. package/scripts/check-meta-dual-consumers.ts +154 -0
  119. package/scripts/check-meta-dual-read.ts +158 -0
  120. package/scripts/check-meta-listing.ts +138 -0
  121. package/scripts/check-meta-mailbox-state-write.ts +135 -0
  122. package/scripts/check-meta-migration.ts +212 -0
  123. package/scripts/check-meta-receiver-marker.ts +185 -0
  124. package/scripts/check-meta-record-v2.ts +191 -0
  125. package/scripts/check-meta-session.ts +673 -0
  126. package/scripts/check-model-lock.ts +408 -0
  127. package/scripts/check-package-source-routing.ts +253 -0
  128. package/scripts/check-pi-preflight.ts +304 -0
  129. package/scripts/check-project-trust-handler.ts +265 -0
  130. package/scripts/check-shell-quote.ts +121 -0
  131. package/scripts/check-socket-discovery.ts +428 -0
  132. package/scripts/check-socket-probe.ts +106 -0
  133. package/scripts/fixtures/probe-mcp-server.ts +33 -0
  134. package/scripts/gnew-rpc-drive.ts +211 -0
  135. package/scripts/lib/acp-child-cleanup.ts +116 -0
  136. package/scripts/meta-bridge-doctor.sh +315 -0
  137. package/scripts/meta-bridge-hook-log.sh +26 -0
  138. package/scripts/meta-bridge-install.sh +135 -0
  139. package/scripts/meta-bridge-prune.ts +199 -0
  140. package/scripts/meta-bridge-state.py +549 -0
  141. package/scripts/meta-bridge-statusline.sh +192 -0
  142. package/scripts/meta-bridge-store-doctor.ts +64 -0
  143. package/scripts/meta-bridge-uninstall.sh +39 -0
  144. package/scripts/new-session-id.ts +25 -0
  145. package/scripts/postinstall-chmod.cjs +58 -0
  146. package/scripts/raw-async-delivery/README.md +258 -0
  147. package/scripts/raw-async-delivery/cc-enqueue-addressed.sh +35 -0
  148. package/scripts/raw-async-delivery/cc-mailbox-rewake.sh +38 -0
  149. package/scripts/raw-async-delivery/cc-watch-filechanged.sh +19 -0
  150. package/scripts/raw-async-delivery/cc-watch-sessionstart.sh +15 -0
  151. package/scripts/raw-async-delivery/codex-local-appserver.sh +39 -0
  152. package/scripts/raw-async-delivery/plugin-entwurf-receive/.claude-plugin/plugin.json +5 -0
  153. package/scripts/raw-async-delivery/plugin-entwurf-receive/hooks/hooks.json +28 -0
  154. package/scripts/raw-async-delivery/plugin-entwurf-receive/scripts/watch-filechanged.sh +28 -0
  155. package/scripts/raw-async-delivery/plugin-entwurf-receive/scripts/watch-sessionstart.sh +27 -0
  156. package/scripts/raw-async-delivery/raw-agy-send.sh +29 -0
  157. package/scripts/raw-async-delivery/raw-claude-enqueue.sh +30 -0
  158. package/scripts/raw-async-delivery/raw-codex-ws-turn-start.py +164 -0
  159. package/scripts/raw-async-delivery/repro-addressed-routing.sh +96 -0
  160. package/scripts/raw-async-delivery/repro-plugin-idle-wake.sh +104 -0
  161. package/scripts/resolve-acp-bridge.ts +25 -0
  162. package/scripts/smoke-acp-bundled-mcp-live.ts +292 -0
  163. package/scripts/smoke-acp-carrier-augment-live.ts +192 -0
  164. package/scripts/smoke-acp-mcp-live.ts +129 -0
  165. package/scripts/smoke-acp-memory-containment-live.ts +389 -0
  166. package/scripts/smoke-acp-overlay-live.ts +314 -0
  167. package/scripts/smoke-acp-provider-live.ts +162 -0
  168. package/scripts/smoke-acp-raw-turn-live.ts +261 -0
  169. package/scripts/smoke-acp-session-reuse-live.ts +172 -0
  170. package/scripts/smoke-acp-skill-live.ts +144 -0
  171. package/scripts/smoke-acp-socket-citizen-live.ts +168 -0
  172. package/scripts/smoke-claude-native-resume-live.sh +198 -0
  173. package/scripts/smoke-entwurf-v2-matrix-live.ts +398 -0
  174. package/scripts/smoke-entwurf-v2-spawn-live.ts +175 -0
  175. package/scripts/smoke-entwurf-v2-spawn-resume-live.ts +428 -0
  176. package/scripts/smoke-meta-async-drift.sh +171 -0
  177. package/scripts/smoke-meta-honesty.sh +147 -0
  178. package/scripts/smoke-meta-install-state.sh +403 -0
  179. package/scripts/smoke-meta-keyset-guard.sh +111 -0
  180. package/scripts/smoke-meta-prune.sh +174 -0
  181. package/scripts/smoke-resident-garden-guard.sh +433 -0
  182. package/scripts/smoke-session-id-name.ts +187 -0
  183. package/scripts/tsconfig.json +34 -0
@@ -0,0 +1,371 @@
1
+ /**
2
+ * check-entwurf-v2-send-fallback — deterministic gate for the 5c-2b same-lock
3
+ * re-resolve resolver (`resolveDeadControlSendFallback`). Proves the dead-control-send
4
+ * fallback routing over injected fakes, with NO filesystem, AND that the resolver never
5
+ * releases / never spawns / never mis-routes off the held gid:
6
+ *
7
+ * 1. mis-wire: plan/lock gid mismatch → throws BEFORE any IO (no resolveTarget call).
8
+ * 2. bad-target (identity null) → reject, NO inspect/probe.
9
+ * 3. preProbeAddressConflict → reject, NO inspect/probe.
10
+ * 4. unsupported backend + seam deliverable → meta-mailbox plan, NO inspect/probe;
11
+ * message/wantsReply preserved, same target gid.
12
+ * 5. unsupported backend + seam undeliverable → reject, NO inspect/probe. A self-fetch
13
+ * citizen with an INACTIVE receiver is refused here too (SE-2 2d-3 active-receiver gate).
14
+ * 6. in-domain pi + socket-file + probe alive → control-socket plan with the INSPECTED
15
+ * socketPath; message/mode/wantsReply preserved, same target gid.
16
+ * 7. in-domain pi + absent (ENOENT → dead) → reject (dormant-fire-forget-unsupported);
17
+ * NEVER a spawn-bg plan, NEVER a mailbox.
18
+ * 8. in-domain pi + probe indeterminate → reject (indeterminate-no-spawn); no mailbox.
19
+ * 9. in-domain pi + inspect indeterminate → reject; no probe connect, no mailbox.
20
+ * 10. address-conflict (symlink) → reject (target-address-conflict).
21
+ * 11. probe throw / inspect throw → PROPAGATE (resolver does not catch; the 5c-2a hand
22
+ * owns failed+release). The resolver NEVER calls a release seam (it has none).
23
+ * 12. every execute plan targets plan.targetGardenId === lock.gardenId, and is one of
24
+ * control-socket / meta-mailbox — never spawn-bg.
25
+ *
26
+ * No real IO — fakes count inspect/probe calls so "short-circuits before probing" is
27
+ * asserted structurally.
28
+ */
29
+
30
+ import assert from "node:assert/strict";
31
+ import type { TargetResolution } from "../pi-extensions/lib/entwurf-v2-decider.ts";
32
+ import type { LockClaim } from "../pi-extensions/lib/entwurf-v2-lock.ts";
33
+ import type { ControlSocketPlan } from "../pi-extensions/lib/entwurf-v2-send.ts";
34
+ import {
35
+ type DeadFallbackDeps,
36
+ resolveDeadControlSendFallback,
37
+ } from "../pi-extensions/lib/entwurf-v2-send-fallback.ts";
38
+ import type { MetaBackendV2, MetaIdentity } from "../pi-extensions/lib/meta-session.ts";
39
+ import type { TargetSocketInspection } from "../pi-extensions/lib/socket-discovery.ts";
40
+ import type { SocketLiveness } from "../pi-extensions/lib/socket-probe.ts";
41
+
42
+ let passed = 0;
43
+ function ok(label: string, cond: boolean): void {
44
+ assert.ok(cond, label);
45
+ console.log(` ok ${label}`);
46
+ passed++;
47
+ }
48
+
49
+ const GID = "20260612T100000-aaaaaa";
50
+ const WRONG_GID = "20260612T999999-bbbbbb";
51
+ const CWD = "/home/junghan/repos/gh/entwurf";
52
+
53
+ function lockClaim(gardenId = GID): LockClaim {
54
+ return {
55
+ gardenId,
56
+ pid: 4242,
57
+ hostname: "test-host",
58
+ createdAt: "2026-06-12T01:00:00.000Z",
59
+ nonce: "deadbeefcafef00d",
60
+ owner: "entwurf_v2",
61
+ lockPath: `/fake/locks/${gardenId}.lock`,
62
+ };
63
+ }
64
+
65
+ function identity(backend: MetaBackendV2): MetaIdentity {
66
+ return {
67
+ schemaVersion: 2,
68
+ gardenId: GID,
69
+ backend,
70
+ nativeSessionId: `native-${GID}`,
71
+ cwd: CWD,
72
+ model: null,
73
+ transcriptPath: null,
74
+ parentGardenId: null,
75
+ isEntwurf: false,
76
+ createdAt: "2026-06-12T01:00:00.000Z",
77
+ recordUpdatedAt: "2026-06-12T01:00:00.000Z",
78
+ };
79
+ }
80
+
81
+ const CONTROL_PLAN = {
82
+ transport: "control-socket",
83
+ action: "send",
84
+ targetGardenId: GID,
85
+ socketPath: "/fake/ctl/orig.sock",
86
+ mode: "follow_up",
87
+ wantsReply: true,
88
+ message: "hello",
89
+ } as const satisfies ControlSocketPlan;
90
+
91
+ interface Trace {
92
+ inspectCalls: number;
93
+ probeCalls: number;
94
+ }
95
+
96
+ interface FakeSpec {
97
+ resolution: TargetResolution;
98
+ /** SE-2 2d-3: the verdict the injected mailboxDeliverabilityFor seam returns on the
99
+ * unsupported path. Default false (fail-closed) — the resolver trusts the seam's
100
+ * active-receiver judgement, never wake-mode alone. */
101
+ mailboxDeliverable?: boolean;
102
+ inspection?: TargetSocketInspection | { throw: unknown };
103
+ probe?: SocketLiveness | { throw: unknown };
104
+ }
105
+
106
+ function makeDeps(spec: FakeSpec): { deps: DeadFallbackDeps; trace: Trace } {
107
+ const trace: Trace = { inspectCalls: 0, probeCalls: 0 };
108
+ const deps: DeadFallbackDeps = {
109
+ async resolveTarget() {
110
+ return spec.resolution;
111
+ },
112
+ async inspectSocket(_gid) {
113
+ trace.inspectCalls++;
114
+ if (!spec.inspection) throw new Error("test: inspectSocket called but no inspection spec");
115
+ if ("throw" in spec.inspection) throw spec.inspection.throw;
116
+ return spec.inspection;
117
+ },
118
+ async probeSocket(_path) {
119
+ trace.probeCalls++;
120
+ if (spec.probe === undefined) throw new Error("test: probeSocket called but no probe spec");
121
+ if (typeof spec.probe === "object" && "throw" in spec.probe) throw spec.probe.throw;
122
+ return spec.probe;
123
+ },
124
+ mailboxDeliverabilityFor: () => ({ deliverable: spec.mailboxDeliverable ?? false, reason: "fake-deliverability" }),
125
+ };
126
+ return { deps, trace };
127
+ }
128
+
129
+ function present(identityArg: MetaIdentity): TargetResolution {
130
+ return { identity: identityArg, preProbeAddressConflict: false };
131
+ }
132
+
133
+ async function rejects(fn: () => Promise<unknown>): Promise<unknown> {
134
+ try {
135
+ await fn();
136
+ return Symbol("did-not-throw");
137
+ } catch (err) {
138
+ return err;
139
+ }
140
+ }
141
+
142
+ async function main(): Promise<void> {
143
+ // ── 1: mis-wire → throws before any IO ───────────────────────────────────
144
+ {
145
+ const { deps, trace } = makeDeps({ resolution: present(identity("pi")) });
146
+ const err = await rejects(() => resolveDeadControlSendFallback(CONTROL_PLAN, lockClaim(WRONG_GID), deps));
147
+ ok("mis-wire (plan/lock gid mismatch) → throws", err instanceof Error);
148
+ ok("mis-wire → no inspect/probe (fail-loud before IO)", trace.inspectCalls === 0 && trace.probeCalls === 0);
149
+ }
150
+
151
+ // ── 2: bad-target → reject, no inspect/probe ──────────────────────────────
152
+ {
153
+ const { deps, trace } = makeDeps({ resolution: { identity: null, preProbeAddressConflict: false } });
154
+ const r = await resolveDeadControlSendFallback(CONTROL_PLAN, lockClaim(), deps);
155
+ ok("bad-target → reject", r.kind === "reject" && r.reason === "bad-target");
156
+ ok("bad-target → no inspect/probe", trace.inspectCalls === 0 && trace.probeCalls === 0);
157
+ }
158
+
159
+ // ── 3: preProbeAddressConflict → reject, no inspect/probe ──────────────────
160
+ {
161
+ const { deps, trace } = makeDeps({
162
+ resolution: { identity: identity("pi"), preProbeAddressConflict: true },
163
+ });
164
+ const r = await resolveDeadControlSendFallback(CONTROL_PLAN, lockClaim(), deps);
165
+ ok("preProbeAddressConflict → reject", r.kind === "reject" && r.reason === "target-address-conflict");
166
+ ok("preProbeAddressConflict → no inspect/probe", trace.inspectCalls === 0 && trace.probeCalls === 0);
167
+ }
168
+
169
+ // ── 4: unsupported + deliverable → meta-mailbox plan, no inspect/probe ─────
170
+ // Deliverability is the seam's verdict (active receiver), not wake-mode alone.
171
+ {
172
+ const { deps, trace } = makeDeps({ resolution: present(identity("claude-code")), mailboxDeliverable: true });
173
+ const r = await resolveDeadControlSendFallback(CONTROL_PLAN, lockClaim(), deps);
174
+ ok("unsupported + deliverable → execute", r.kind === "execute");
175
+ ok(
176
+ "unsupported + deliverable → meta-mailbox plan, same target, msg preserved",
177
+ r.kind === "execute" &&
178
+ r.plan.transport === "meta-mailbox" &&
179
+ r.plan.targetGardenId === GID &&
180
+ r.plan.message === "hello" &&
181
+ r.plan.wantsReply === true,
182
+ );
183
+ ok("unsupported → no inspect/probe (mailbox mini-table)", trace.inspectCalls === 0 && trace.probeCalls === 0);
184
+ }
185
+
186
+ // ── 5: unsupported + seam undeliverable → reject, no inspect/probe ─────────
187
+ {
188
+ const { deps, trace } = makeDeps({ resolution: present(identity("codex")), mailboxDeliverable: false });
189
+ const r = await resolveDeadControlSendFallback(CONTROL_PLAN, lockClaim(), deps);
190
+ ok("unsupported + undeliverable → reject (mailbox-undeliverable)", r.kind === "reject");
191
+ ok("unsupported + undeliverable → no inspect/probe", trace.inspectCalls === 0 && trace.probeCalls === 0);
192
+ }
193
+
194
+ // ── 5b: SE-2 2d-3 — a self-fetch CITIZEN with an INACTIVE receiver (seam false) →
195
+ // reject, NO inspect/probe. The dead-fallback honours the same active-receiver gate as
196
+ // the decider, so a re-resolve cannot smuggle a reply into a terminated session's mailbox.
197
+ {
198
+ const { deps, trace } = makeDeps({ resolution: present(identity("claude-code")), mailboxDeliverable: false });
199
+ const r = await resolveDeadControlSendFallback(CONTROL_PLAN, lockClaim(), deps);
200
+ ok("self-fetch citizen + inactive receiver → reject (SE-2 2d-3)", r.kind === "reject");
201
+ ok("self-fetch citizen + inactive → no inspect/probe", trace.inspectCalls === 0 && trace.probeCalls === 0);
202
+ }
203
+
204
+ // ── 6: in-domain pi + socket-file + probe alive → control-socket retry ─────
205
+ {
206
+ const { deps, trace } = makeDeps({
207
+ resolution: present(identity("pi")),
208
+ inspection: { kind: "socket-file", socketPath: "/fake/ctl/fresh.sock" },
209
+ probe: "alive",
210
+ });
211
+ const r = await resolveDeadControlSendFallback(CONTROL_PLAN, lockClaim(), deps);
212
+ ok("pi + alive → execute control-socket", r.kind === "execute" && r.plan.transport === "control-socket");
213
+ ok(
214
+ "pi + alive → uses the INSPECTED socketPath, preserves mode/msg/wantsReply, same target",
215
+ r.kind === "execute" &&
216
+ r.plan.transport === "control-socket" &&
217
+ r.plan.socketPath === "/fake/ctl/fresh.sock" &&
218
+ r.plan.mode === "follow_up" &&
219
+ r.plan.message === "hello" &&
220
+ r.plan.wantsReply === true &&
221
+ r.plan.targetGardenId === GID,
222
+ );
223
+ ok("pi + alive → inspected once, probed once", trace.inspectCalls === 1 && trace.probeCalls === 1);
224
+ }
225
+
226
+ // ── 7: in-domain pi + absent (dead) → reject, NEVER spawn-bg/mailbox ───────
227
+ {
228
+ const { deps, trace } = makeDeps({
229
+ resolution: present(identity("pi")),
230
+ inspection: { kind: "absent", socketPath: "/fake/ctl/gone.sock" },
231
+ });
232
+ const r = await resolveDeadControlSendFallback(CONTROL_PLAN, lockClaim(), deps);
233
+ ok("pi + dead → reject (dormant-fire-forget-unsupported)", r.kind === "reject");
234
+ ok(
235
+ "pi + dead → reject reason is the fire-and-forget dormant cell",
236
+ r.kind === "reject" && r.reason === "dormant-fire-forget-unsupported",
237
+ );
238
+ ok("pi + dead → never probes (absent short-circuits)", trace.probeCalls === 0);
239
+ }
240
+
241
+ // ── 8: in-domain pi + probe indeterminate → reject, no mailbox ────────────
242
+ {
243
+ const { deps } = makeDeps({
244
+ resolution: present(identity("pi")),
245
+ inspection: { kind: "socket-file", socketPath: "/fake/ctl/stall.sock" },
246
+ probe: "indeterminate",
247
+ });
248
+ const r = await resolveDeadControlSendFallback(CONTROL_PLAN, lockClaim(), deps);
249
+ ok("pi + probe indeterminate → reject (indeterminate-no-spawn)", r.kind === "reject");
250
+ }
251
+
252
+ // ── 9: in-domain pi + inspect indeterminate → reject, no probe ────────────
253
+ {
254
+ const { deps, trace } = makeDeps({
255
+ resolution: present(identity("pi")),
256
+ inspection: { kind: "indeterminate", socketPath: "/fake/ctl/x.sock", error: "EACCES" },
257
+ });
258
+ const r = await resolveDeadControlSendFallback(CONTROL_PLAN, lockClaim(), deps);
259
+ ok("pi + inspect indeterminate → reject", r.kind === "reject");
260
+ ok("pi + inspect indeterminate → never connects (no probe)", trace.probeCalls === 0);
261
+ }
262
+
263
+ // ── 9b: A1 narrow — a socket-only pi endpoint (identity null + socketOnlyPi) re-resolves
264
+ // as IN-DOMAIN pi, NOT bad-target. This is the TOCTOU path for a record-less live pi whose
265
+ // socket died between the decision and the send: fire-and-forget → alive retries the
266
+ // control-send, dead is the honest dormant reject, never a mailbox/spawn. `inspectCalls === 1`
267
+ // proves it went through the in-domain probe path and did NOT short-circuit to bad-target
268
+ // (inspectCalls === 0) — the regression guard for the fallback fix. ─────────────────────────
269
+ const socketOnly: TargetResolution = { identity: null, preProbeAddressConflict: false, socketOnlyPi: true };
270
+ {
271
+ const { deps, trace } = makeDeps({
272
+ resolution: socketOnly,
273
+ inspection: { kind: "socket-file", socketPath: "/fake/ctl/fresh.sock" },
274
+ probe: "alive",
275
+ });
276
+ const r = await resolveDeadControlSendFallback(CONTROL_PLAN, lockClaim(), deps);
277
+ ok(
278
+ "socketOnly + alive → control-socket retry (NOT bad-target)",
279
+ r.kind === "execute" && r.plan.transport === "control-socket",
280
+ );
281
+ ok(
282
+ "socketOnly + alive → re-inspected once, probed once (in-domain path)",
283
+ trace.inspectCalls === 1 && trace.probeCalls === 1,
284
+ );
285
+ }
286
+ {
287
+ const { deps, trace } = makeDeps({
288
+ resolution: socketOnly,
289
+ inspection: { kind: "absent", socketPath: "/fake/ctl/gone.sock" },
290
+ });
291
+ const r = await resolveDeadControlSendFallback(CONTROL_PLAN, lockClaim(), deps);
292
+ ok(
293
+ "socketOnly + dead → reject dormant-fire-forget-unsupported (NOT bad-target, NOT mailbox)",
294
+ r.kind === "reject" && r.reason === "dormant-fire-forget-unsupported",
295
+ );
296
+ ok(
297
+ "socketOnly + dead → entered in-domain inspect (inspectCalls === 1, not short-circuit)",
298
+ trace.inspectCalls === 1,
299
+ );
300
+ }
301
+ {
302
+ const { deps } = makeDeps({
303
+ resolution: socketOnly,
304
+ inspection: { kind: "socket-file", socketPath: "/fake/ctl/stall.sock" },
305
+ probe: "indeterminate",
306
+ });
307
+ const r = await resolveDeadControlSendFallback(CONTROL_PLAN, lockClaim(), deps);
308
+ ok(
309
+ "socketOnly + probe indeterminate → reject indeterminate-no-spawn",
310
+ r.kind === "reject" && r.reason === "indeterminate-no-spawn",
311
+ );
312
+ }
313
+
314
+ // ── 10: address-conflict (symlink) → reject ───────────────────────────────
315
+ {
316
+ const { deps } = makeDeps({
317
+ resolution: present(identity("pi")),
318
+ inspection: { kind: "address-conflict", socketPath: "/fake/ctl/link.sock", reason: "symlink" },
319
+ });
320
+ const r = await resolveDeadControlSendFallback(CONTROL_PLAN, lockClaim(), deps);
321
+ ok(
322
+ "address-conflict → reject (target-address-conflict)",
323
+ r.kind === "reject" && r.reason === "target-address-conflict",
324
+ );
325
+ }
326
+
327
+ // ── 11: probe throw / inspect throw → PROPAGATE (resolver never catches) ───
328
+ {
329
+ const probeBoom = new Error("probe boom");
330
+ const { deps: d1 } = makeDeps({
331
+ resolution: present(identity("pi")),
332
+ inspection: { kind: "socket-file", socketPath: "/fake/ctl/p.sock" },
333
+ probe: { throw: probeBoom },
334
+ });
335
+ const e1 = await rejects(() => resolveDeadControlSendFallback(CONTROL_PLAN, lockClaim(), d1));
336
+ ok("probe throw → propagates (hand owns failed+release)", e1 === probeBoom);
337
+
338
+ const inspectBoom = new Error("inspect boom");
339
+ const { deps: d2 } = makeDeps({
340
+ resolution: present(identity("pi")),
341
+ inspection: { throw: inspectBoom },
342
+ });
343
+ const e2 = await rejects(() => resolveDeadControlSendFallback(CONTROL_PLAN, lockClaim(), d2));
344
+ ok("inspect throw → propagates", e2 === inspectBoom);
345
+ }
346
+
347
+ // ── 12: every execute plan targets the held gid, never spawn-bg ───────────
348
+ {
349
+ const cases: FakeSpec[] = [
350
+ { resolution: present(identity("claude-code")), mailboxDeliverable: true },
351
+ {
352
+ resolution: present(identity("pi")),
353
+ inspection: { kind: "socket-file", socketPath: "/fake/ctl/a.sock" },
354
+ probe: "alive",
355
+ },
356
+ ];
357
+ let allSameTargetNonSpawn = true;
358
+ for (const s of cases) {
359
+ const { deps } = makeDeps(s);
360
+ const r = await resolveDeadControlSendFallback(CONTROL_PLAN, lockClaim(), deps);
361
+ if (r.kind !== "execute") allSameTargetNonSpawn = false;
362
+ else if (r.plan.targetGardenId !== GID) allSameTargetNonSpawn = false;
363
+ else if (r.plan.transport === "spawn-bg") allSameTargetNonSpawn = false;
364
+ }
365
+ ok("every execute plan: same target gid, never spawn-bg", allSameTargetNonSpawn);
366
+ }
367
+
368
+ console.log(`\n[check-entwurf-v2-send-fallback] ${passed} assertions ok`);
369
+ }
370
+
371
+ await main();