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