@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.
- package/AGENTS.md +240 -0
- package/BASELINE.md +227 -0
- package/CHANGELOG.md +1210 -0
- package/CONTRIBUTING.md +63 -0
- package/DELIVERY.md +209 -0
- package/LICENSE +21 -0
- package/README.md +504 -0
- package/VERIFY.md +260 -0
- package/demo/README.md +188 -0
- package/demo/demo-baseline.sh +156 -0
- package/demo/demo.sh +183 -0
- package/docs/assets/entwurf-demo.gif +0 -0
- package/docs/assets/entwurf-doomemacs.gif +0 -0
- package/docs/assets/entwurf-entwurf.gif +0 -0
- package/docs/assets/entwurf-hero.jpg +0 -0
- package/docs/setup-clean-host.md +305 -0
- package/mcp/entwurf-bridge/src/index.ts +513 -0
- package/mcp/entwurf-bridge/start.sh +25 -0
- package/mcp/entwurf-bridge/test.sh +54 -0
- package/mcp/tsconfig.json +29 -0
- package/package.json +130 -0
- package/pi/entwurf-capabilities.json +9 -0
- package/pi/entwurf-targets.json +20 -0
- package/pi/meta-bridge/.claude-plugin/marketplace.json +12 -0
- package/pi/meta-bridge/entwurf-meta-receive/.claude-plugin/plugin.json +5 -0
- package/pi/meta-bridge/entwurf-meta-receive/hooks/hooks.json +50 -0
- package/pi/meta-bridge/entwurf-meta-receive/scripts/doorbell.sh +54 -0
- package/pi/settings.reference.json +43 -0
- package/pi/skill-plugin-example/.claude-plugin/plugin.json +4 -0
- package/pi/skill-plugin-example/skills/hello/SKILL.md +18 -0
- package/pi-extensions/acp-provider.ts +63 -0
- package/pi-extensions/entwurf-control.ts +1692 -0
- package/pi-extensions/lib/acp/acp-client.ts +90 -0
- package/pi-extensions/lib/acp/augment.ts +238 -0
- package/pi-extensions/lib/acp/backend-adapter.ts +319 -0
- package/pi-extensions/lib/acp/backend.ts +873 -0
- package/pi-extensions/lib/acp/config.ts +552 -0
- package/pi-extensions/lib/acp/context.ts +177 -0
- package/pi-extensions/lib/acp/engraving.ts +123 -0
- package/pi-extensions/lib/acp/event-mapper.ts +339 -0
- package/pi-extensions/lib/acp/models.ts +102 -0
- package/pi-extensions/lib/acp/overlay.ts +220 -0
- package/pi-extensions/lib/acp/prompts/engraving.md +1 -0
- package/pi-extensions/lib/acp/session-store.ts +418 -0
- package/pi-extensions/lib/acp/tool-surface.ts +184 -0
- package/pi-extensions/lib/entwurf-control-rpc.ts +208 -0
- package/pi-extensions/lib/entwurf-core.ts +2033 -0
- package/pi-extensions/lib/entwurf-deliverability.ts +123 -0
- package/pi-extensions/lib/entwurf-fact-provider.ts +158 -0
- package/pi-extensions/lib/entwurf-facts.ts +251 -0
- package/pi-extensions/lib/entwurf-mailbox-guard.ts +100 -0
- package/pi-extensions/lib/entwurf-peers-render.ts +154 -0
- package/pi-extensions/lib/entwurf-preflight.ts +247 -0
- package/pi-extensions/lib/entwurf-resume-args.ts +88 -0
- package/pi-extensions/lib/entwurf-self-address.ts +117 -0
- package/pi-extensions/lib/entwurf-v2-contract.ts +427 -0
- package/pi-extensions/lib/entwurf-v2-decider.ts +441 -0
- package/pi-extensions/lib/entwurf-v2-lock.ts +412 -0
- package/pi-extensions/lib/entwurf-v2-mailbox.ts +87 -0
- package/pi-extensions/lib/entwurf-v2-production.ts +326 -0
- package/pi-extensions/lib/entwurf-v2-release.ts +149 -0
- package/pi-extensions/lib/entwurf-v2-resume-marker.ts +34 -0
- package/pi-extensions/lib/entwurf-v2-runner.ts +196 -0
- package/pi-extensions/lib/entwurf-v2-send-fallback.ts +166 -0
- package/pi-extensions/lib/entwurf-v2-send.ts +271 -0
- package/pi-extensions/lib/entwurf-v2-spawn-production.ts +337 -0
- package/pi-extensions/lib/entwurf-v2-spawn.ts +323 -0
- package/pi-extensions/lib/entwurf-v2-surface.ts +205 -0
- package/pi-extensions/lib/meta-mailbox-body.ts +80 -0
- package/pi-extensions/lib/meta-session.ts +1951 -0
- package/pi-extensions/lib/project-trust-handler.ts +154 -0
- package/pi-extensions/lib/session-id.js +57 -0
- package/pi-extensions/lib/socket-discovery.ts +346 -0
- package/pi-extensions/lib/socket-probe.ts +92 -0
- package/pi-extensions/meta-bridge-hook.ts +250 -0
- package/pi-extensions/model-lock.ts +235 -0
- package/prompts/engraving.md +27 -0
- package/protocol.js +31 -0
- package/run.sh +2832 -0
- package/scripts/check-acp-backend-preflight.ts +130 -0
- package/scripts/check-acp-carrier-augment.ts +297 -0
- package/scripts/check-acp-config.ts +322 -0
- package/scripts/check-acp-event-mapper.ts +253 -0
- package/scripts/check-acp-overlay.ts +154 -0
- package/scripts/check-acp-prompt-builder.ts +191 -0
- package/scripts/check-acp-provider-surface.ts +159 -0
- package/scripts/check-acp-sdk-surface.ts +211 -0
- package/scripts/check-acp-session-reuse.ts +756 -0
- package/scripts/check-acp-session-store.ts +387 -0
- package/scripts/check-acp-tool-surface.ts +159 -0
- package/scripts/check-entwurf-bridge-boot.ts +199 -0
- package/scripts/check-entwurf-capabilities.ts +123 -0
- package/scripts/check-entwurf-control-rpc.ts +187 -0
- package/scripts/check-entwurf-deliverability.ts +158 -0
- package/scripts/check-entwurf-fact-provider.ts +316 -0
- package/scripts/check-entwurf-facts.ts +358 -0
- package/scripts/check-entwurf-mailbox-guard.ts +264 -0
- package/scripts/check-entwurf-peers-surface.ts +235 -0
- package/scripts/check-entwurf-resume-args.ts +149 -0
- package/scripts/check-entwurf-self-address.ts +209 -0
- package/scripts/check-entwurf-session-identity.ts +703 -0
- package/scripts/check-entwurf-v2-contract.ts +536 -0
- package/scripts/check-entwurf-v2-decider.ts +795 -0
- package/scripts/check-entwurf-v2-lock.ts +368 -0
- package/scripts/check-entwurf-v2-mailbox.ts +228 -0
- package/scripts/check-entwurf-v2-matrix.ts +437 -0
- package/scripts/check-entwurf-v2-production.ts +406 -0
- package/scripts/check-entwurf-v2-release.ts +245 -0
- package/scripts/check-entwurf-v2-runner.ts +452 -0
- package/scripts/check-entwurf-v2-send-fallback.ts +371 -0
- package/scripts/check-entwurf-v2-send.ts +454 -0
- package/scripts/check-entwurf-v2-spawn-production.ts +452 -0
- package/scripts/check-entwurf-v2-spawn.ts +398 -0
- package/scripts/check-entwurf-v2-surface.ts +352 -0
- package/scripts/check-keyset-overlap.py +120 -0
- package/scripts/check-mailbox-receipt-state.ts +170 -0
- package/scripts/check-meta-capability-source.ts +112 -0
- package/scripts/check-meta-dual-consumers.ts +154 -0
- package/scripts/check-meta-dual-read.ts +158 -0
- package/scripts/check-meta-listing.ts +138 -0
- package/scripts/check-meta-mailbox-state-write.ts +135 -0
- package/scripts/check-meta-migration.ts +212 -0
- package/scripts/check-meta-receiver-marker.ts +185 -0
- package/scripts/check-meta-record-v2.ts +191 -0
- package/scripts/check-meta-session.ts +673 -0
- package/scripts/check-model-lock.ts +408 -0
- package/scripts/check-package-source-routing.ts +253 -0
- package/scripts/check-pi-preflight.ts +304 -0
- package/scripts/check-project-trust-handler.ts +265 -0
- package/scripts/check-shell-quote.ts +121 -0
- package/scripts/check-socket-discovery.ts +428 -0
- package/scripts/check-socket-probe.ts +106 -0
- package/scripts/fixtures/probe-mcp-server.ts +33 -0
- package/scripts/gnew-rpc-drive.ts +211 -0
- package/scripts/lib/acp-child-cleanup.ts +116 -0
- package/scripts/meta-bridge-doctor.sh +315 -0
- package/scripts/meta-bridge-hook-log.sh +26 -0
- package/scripts/meta-bridge-install.sh +135 -0
- package/scripts/meta-bridge-prune.ts +199 -0
- package/scripts/meta-bridge-state.py +549 -0
- package/scripts/meta-bridge-statusline.sh +192 -0
- package/scripts/meta-bridge-store-doctor.ts +64 -0
- package/scripts/meta-bridge-uninstall.sh +39 -0
- package/scripts/new-session-id.ts +25 -0
- package/scripts/postinstall-chmod.cjs +58 -0
- package/scripts/raw-async-delivery/README.md +258 -0
- package/scripts/raw-async-delivery/cc-enqueue-addressed.sh +35 -0
- package/scripts/raw-async-delivery/cc-mailbox-rewake.sh +38 -0
- package/scripts/raw-async-delivery/cc-watch-filechanged.sh +19 -0
- package/scripts/raw-async-delivery/cc-watch-sessionstart.sh +15 -0
- package/scripts/raw-async-delivery/codex-local-appserver.sh +39 -0
- package/scripts/raw-async-delivery/plugin-entwurf-receive/.claude-plugin/plugin.json +5 -0
- package/scripts/raw-async-delivery/plugin-entwurf-receive/hooks/hooks.json +28 -0
- package/scripts/raw-async-delivery/plugin-entwurf-receive/scripts/watch-filechanged.sh +28 -0
- package/scripts/raw-async-delivery/plugin-entwurf-receive/scripts/watch-sessionstart.sh +27 -0
- package/scripts/raw-async-delivery/raw-agy-send.sh +29 -0
- package/scripts/raw-async-delivery/raw-claude-enqueue.sh +30 -0
- package/scripts/raw-async-delivery/raw-codex-ws-turn-start.py +164 -0
- package/scripts/raw-async-delivery/repro-addressed-routing.sh +96 -0
- package/scripts/raw-async-delivery/repro-plugin-idle-wake.sh +104 -0
- package/scripts/resolve-acp-bridge.ts +25 -0
- package/scripts/smoke-acp-bundled-mcp-live.ts +292 -0
- package/scripts/smoke-acp-carrier-augment-live.ts +192 -0
- package/scripts/smoke-acp-mcp-live.ts +129 -0
- package/scripts/smoke-acp-memory-containment-live.ts +389 -0
- package/scripts/smoke-acp-overlay-live.ts +314 -0
- package/scripts/smoke-acp-provider-live.ts +162 -0
- package/scripts/smoke-acp-raw-turn-live.ts +261 -0
- package/scripts/smoke-acp-session-reuse-live.ts +172 -0
- package/scripts/smoke-acp-skill-live.ts +144 -0
- package/scripts/smoke-acp-socket-citizen-live.ts +168 -0
- package/scripts/smoke-claude-native-resume-live.sh +198 -0
- package/scripts/smoke-entwurf-v2-matrix-live.ts +398 -0
- package/scripts/smoke-entwurf-v2-spawn-live.ts +175 -0
- package/scripts/smoke-entwurf-v2-spawn-resume-live.ts +428 -0
- package/scripts/smoke-meta-async-drift.sh +171 -0
- package/scripts/smoke-meta-honesty.sh +147 -0
- package/scripts/smoke-meta-install-state.sh +403 -0
- package/scripts/smoke-meta-keyset-guard.sh +111 -0
- package/scripts/smoke-meta-prune.sh +174 -0
- package/scripts/smoke-resident-garden-guard.sh +433 -0
- package/scripts/smoke-session-id-name.ts +187 -0
- package/scripts/tsconfig.json +34 -0
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* check-entwurf-v2-runner — deterministic gate for the 5d-1 execute-router
|
|
3
|
+
* (`executeDispatch`). It proves the DECISION→hand routing + the outcome-rich result
|
|
4
|
+
* mapping over injected fake hands, with NO socket/spawn/timer:
|
|
5
|
+
*
|
|
6
|
+
* 1. reject decision → kind "rejected", receipt+diagnostic carried, NO hand called.
|
|
7
|
+
* 2. control-socket execute → sendControl(plan, lock) called with the SAME plan + lock;
|
|
8
|
+
* result outcome "sent" → executed{control-socket, outcome:"sent"}.
|
|
9
|
+
* 3. spawn-bg execute → resumeSpawnBg(plan, lock); a `socket-alive` result rides
|
|
10
|
+
* executed; a `lock-retained` result ALSO rides executed (fail-closed, not a failure).
|
|
11
|
+
* 4. meta-mailbox execute → sendMailbox(plan, null) with the NULL lock (?7); success
|
|
12
|
+
* → executed{meta-mailbox, success:true}.
|
|
13
|
+
* 5. N3 — control rejected with rejectReason → executed{outcome:"rejected", rejectReason}
|
|
14
|
+
* carried verbatim.
|
|
15
|
+
* 6. N1 — control hand throws SendDeliveredReleaseFailedError → execution-failed with
|
|
16
|
+
* finalizedOutcome + releaseFailed:true + retrySafe:false.
|
|
17
|
+
* 7. control hand throws a PLAIN transport error → execution-failed, retrySafe:false,
|
|
18
|
+
* NO finalizedOutcome / releaseFailed (a failed send, lock already released).
|
|
19
|
+
* 8. spawn / mailbox hand throws → execution-failed, retrySafe:false.
|
|
20
|
+
* 9. mailbox hand returns {success:false} → CONTRACT VIOLATION (a mailbox has no
|
|
21
|
+
* in-band reject) → fail loud → execution-failed, NOT a silent success.
|
|
22
|
+
* (exactly ONE hand runs per execute — the other two are never called — is asserted
|
|
23
|
+
* inline in cases 2/3/4.)
|
|
24
|
+
*
|
|
25
|
+
* 5d-2a adds the `runEntwurfV2` COMPOSITION gate (cases 10–14): `decide → execute` joined
|
|
26
|
+
* over a FAKE `decide` (not the real decider — that is check-entwurf-v2-decider's job):
|
|
27
|
+
* 10. decide called EXACTLY once, with the SAME input.
|
|
28
|
+
* 11. reject decision → no executor hand called, the rejected result returned.
|
|
29
|
+
* 12. execute decision → the matching executor hand ran (control/spawn/mailbox).
|
|
30
|
+
* 13. decide THROWS → propagates (no decision = no receipt to wrap).
|
|
31
|
+
* 14. executeDispatch result returned VERBATIM (passthrough, no re-wrapping).
|
|
32
|
+
*
|
|
33
|
+
* No real IO — fake hands record (plan, lock) so "the decided plan + lock reach the
|
|
34
|
+
* matching hand, and only that hand" is asserted structurally.
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
import assert from "node:assert/strict";
|
|
38
|
+
import type {
|
|
39
|
+
DispatchDecision,
|
|
40
|
+
DispatchInput,
|
|
41
|
+
ExecutionPlan,
|
|
42
|
+
RejectReceipt,
|
|
43
|
+
SuccessReceipt,
|
|
44
|
+
} from "../pi-extensions/lib/entwurf-v2-decider.ts";
|
|
45
|
+
import type { LockClaim } from "../pi-extensions/lib/entwurf-v2-lock.ts";
|
|
46
|
+
import {
|
|
47
|
+
type DispatchExecutorDeps,
|
|
48
|
+
type EntwurfV2RunDeps,
|
|
49
|
+
executeDispatch,
|
|
50
|
+
runEntwurfV2,
|
|
51
|
+
} from "../pi-extensions/lib/entwurf-v2-runner.ts";
|
|
52
|
+
import type {
|
|
53
|
+
ControlSocketPlan,
|
|
54
|
+
ControlSocketSendResult,
|
|
55
|
+
MetaMailboxPlan,
|
|
56
|
+
RpcSendResult,
|
|
57
|
+
} from "../pi-extensions/lib/entwurf-v2-send.ts";
|
|
58
|
+
import { SendDeliveredReleaseFailedError } from "../pi-extensions/lib/entwurf-v2-send.ts";
|
|
59
|
+
import type { SpawnBgPlan, SpawnBgResumeResult } from "../pi-extensions/lib/entwurf-v2-spawn.ts";
|
|
60
|
+
|
|
61
|
+
let passed = 0;
|
|
62
|
+
function ok(label: string, cond: boolean): void {
|
|
63
|
+
assert.ok(cond, label);
|
|
64
|
+
console.log(` ok ${label}`);
|
|
65
|
+
passed++;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const GID = "20260613T100000-aaaaaa";
|
|
69
|
+
|
|
70
|
+
function lockClaim(gardenId = GID): LockClaim {
|
|
71
|
+
return {
|
|
72
|
+
gardenId,
|
|
73
|
+
pid: 4242,
|
|
74
|
+
hostname: "test-host",
|
|
75
|
+
createdAt: "2026-06-13T01:00:00.000Z",
|
|
76
|
+
nonce: "deadbeefcafef00d",
|
|
77
|
+
owner: "entwurf_v2",
|
|
78
|
+
lockPath: `/fake/locks/${gardenId}.lock`,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const CONTROL_PLAN: ControlSocketPlan = {
|
|
83
|
+
transport: "control-socket",
|
|
84
|
+
action: "send",
|
|
85
|
+
targetGardenId: GID,
|
|
86
|
+
socketPath: "/fake/ctl/s.sock",
|
|
87
|
+
mode: "follow_up",
|
|
88
|
+
wantsReply: false,
|
|
89
|
+
message: "m",
|
|
90
|
+
};
|
|
91
|
+
const SPAWN_PLAN: SpawnBgPlan = {
|
|
92
|
+
transport: "spawn-bg",
|
|
93
|
+
action: "resume",
|
|
94
|
+
targetGardenId: GID,
|
|
95
|
+
sessionId: GID,
|
|
96
|
+
cwd: "/home/junghan/repos/gh/entwurf",
|
|
97
|
+
prompt: "p",
|
|
98
|
+
launchArgs: [],
|
|
99
|
+
expectedSocketPath: "/fake/ctl/s.sock",
|
|
100
|
+
observeTimeoutMs: 30_000,
|
|
101
|
+
releaseWhen: "socket-alive-or-child-exited",
|
|
102
|
+
};
|
|
103
|
+
const MAILBOX_PLAN: MetaMailboxPlan = {
|
|
104
|
+
transport: "meta-mailbox",
|
|
105
|
+
action: "send",
|
|
106
|
+
targetGardenId: GID,
|
|
107
|
+
mailboxDir: "/fake/mailbox",
|
|
108
|
+
sessionsDir: "/fake/sessions",
|
|
109
|
+
wantsReply: false,
|
|
110
|
+
message: "m",
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const SUCCESS_RECEIPT: SuccessReceipt = {
|
|
114
|
+
ok: true,
|
|
115
|
+
action: "send",
|
|
116
|
+
transport: "control-socket",
|
|
117
|
+
ownership: "ack-only",
|
|
118
|
+
observedLiveness: "alive",
|
|
119
|
+
};
|
|
120
|
+
const REJECT_RECEIPT: RejectReceipt = { ok: false, reason: "bad-target", observedLiveness: null };
|
|
121
|
+
|
|
122
|
+
function executeDecision(plan: ExecutionPlan, lock: LockClaim | null): Extract<DispatchDecision, { kind: "execute" }> {
|
|
123
|
+
return { kind: "execute", receipt: SUCCESS_RECEIPT, plan, lock };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
interface Trace {
|
|
127
|
+
calls: string[];
|
|
128
|
+
controlArgs?: { plan: ControlSocketPlan; lock: LockClaim | null };
|
|
129
|
+
spawnArgs?: { plan: SpawnBgPlan; lock: LockClaim | null };
|
|
130
|
+
mailboxArgs?: { plan: MetaMailboxPlan; lock: LockClaim | null };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
interface FakeSpec {
|
|
134
|
+
control?: { result: ControlSocketSendResult } | { throw: unknown };
|
|
135
|
+
spawn?: { result: SpawnBgResumeResult } | { throw: unknown };
|
|
136
|
+
mailbox?: { result: RpcSendResult } | { throw: unknown };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function makeDeps(spec: FakeSpec): { deps: DispatchExecutorDeps; trace: Trace } {
|
|
140
|
+
const trace: Trace = { calls: [] };
|
|
141
|
+
const deps: DispatchExecutorDeps = {
|
|
142
|
+
async sendControl(plan, lock) {
|
|
143
|
+
trace.calls.push("sendControl");
|
|
144
|
+
trace.controlArgs = { plan, lock };
|
|
145
|
+
if (!spec.control) throw new Error("test: sendControl called but no spec");
|
|
146
|
+
if ("throw" in spec.control) throw spec.control.throw;
|
|
147
|
+
return spec.control.result;
|
|
148
|
+
},
|
|
149
|
+
async resumeSpawnBg(plan, lock) {
|
|
150
|
+
trace.calls.push("resumeSpawnBg");
|
|
151
|
+
trace.spawnArgs = { plan, lock };
|
|
152
|
+
if (!spec.spawn) throw new Error("test: resumeSpawnBg called but no spec");
|
|
153
|
+
if ("throw" in spec.spawn) throw spec.spawn.throw;
|
|
154
|
+
return spec.spawn.result;
|
|
155
|
+
},
|
|
156
|
+
async sendMailbox(plan, lock) {
|
|
157
|
+
trace.calls.push("sendMailbox");
|
|
158
|
+
trace.mailboxArgs = { plan, lock };
|
|
159
|
+
if (!spec.mailbox) throw new Error("test: sendMailbox called but no spec");
|
|
160
|
+
if ("throw" in spec.mailbox) throw spec.mailbox.throw;
|
|
161
|
+
return spec.mailbox.result;
|
|
162
|
+
},
|
|
163
|
+
};
|
|
164
|
+
return { deps, trace };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async function main(): Promise<void> {
|
|
168
|
+
// ── 1: reject → no hand called, receipt+diagnostic carried ────────────────
|
|
169
|
+
{
|
|
170
|
+
const { deps, trace } = makeDeps({});
|
|
171
|
+
const conflict = {
|
|
172
|
+
reason: "target-locked" as const,
|
|
173
|
+
lockPath: "/fake/locks/x.lock",
|
|
174
|
+
holder: lockClaim(),
|
|
175
|
+
detail: "held",
|
|
176
|
+
};
|
|
177
|
+
const decision: DispatchDecision = {
|
|
178
|
+
kind: "reject",
|
|
179
|
+
receipt: REJECT_RECEIPT,
|
|
180
|
+
diagnostic: { kind: "target-locked", conflict },
|
|
181
|
+
};
|
|
182
|
+
const res = await executeDispatch(decision, deps);
|
|
183
|
+
ok("1: reject → kind 'rejected'", res.kind === "rejected");
|
|
184
|
+
ok("1: reject → receipt carried", res.kind === "rejected" && res.receipt === REJECT_RECEIPT);
|
|
185
|
+
ok("1: reject → diagnostic carried", res.kind === "rejected" && res.diagnostic?.kind === "target-locked");
|
|
186
|
+
ok("1: reject → NO hand called", trace.calls.length === 0);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// ── 2: control-socket execute → sendControl(plan, lock), sent ─────────────
|
|
190
|
+
{
|
|
191
|
+
const lock = lockClaim();
|
|
192
|
+
const { deps, trace } = makeDeps({ control: { result: { outcome: "sent" } } });
|
|
193
|
+
const res = await executeDispatch(executeDecision(CONTROL_PLAN, lock), deps);
|
|
194
|
+
ok("2: only sendControl ran", trace.calls.length === 1 && trace.calls[0] === "sendControl");
|
|
195
|
+
ok(
|
|
196
|
+
"2: same plan + lock reach the hand",
|
|
197
|
+
trace.controlArgs?.plan === CONTROL_PLAN && trace.controlArgs?.lock === lock,
|
|
198
|
+
);
|
|
199
|
+
ok(
|
|
200
|
+
"2: executed{control-socket, sent}",
|
|
201
|
+
res.kind === "executed" && res.outcome.transport === "control-socket" && res.outcome.outcome === "sent",
|
|
202
|
+
);
|
|
203
|
+
ok("2: success receipt carried", res.kind === "executed" && res.receipt === SUCCESS_RECEIPT);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// ── 3: spawn-bg execute → socket-alive AND lock-retained both ride executed ─
|
|
207
|
+
{
|
|
208
|
+
const lock = lockClaim();
|
|
209
|
+
const alive = makeDeps({ spawn: { result: { kind: "socket-alive", released: true, pid: 7 } } });
|
|
210
|
+
const r1 = await executeDispatch(executeDecision(SPAWN_PLAN, lock), alive.deps);
|
|
211
|
+
ok("3: spawn → only resumeSpawnBg ran", alive.trace.calls.length === 1 && alive.trace.calls[0] === "resumeSpawnBg");
|
|
212
|
+
ok(
|
|
213
|
+
"3: spawn plan + lock reach the hand",
|
|
214
|
+
alive.trace.spawnArgs?.plan === SPAWN_PLAN && alive.trace.spawnArgs?.lock === lock,
|
|
215
|
+
);
|
|
216
|
+
ok(
|
|
217
|
+
"3: socket-alive → executed{spawn-bg, socket-alive}",
|
|
218
|
+
r1.kind === "executed" && r1.outcome.transport === "spawn-bg" && r1.outcome.result.kind === "socket-alive",
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
const retainedResult: SpawnBgResumeResult = {
|
|
222
|
+
kind: "lock-retained",
|
|
223
|
+
released: false,
|
|
224
|
+
reason: "observe-failed",
|
|
225
|
+
diagnostic: {
|
|
226
|
+
targetGardenId: GID,
|
|
227
|
+
lockPath: "/fake/locks/x.lock",
|
|
228
|
+
expectedSocketPath: "/fake/ctl/s.sock",
|
|
229
|
+
observeTimeoutMs: 30_000,
|
|
230
|
+
killGraceMs: 5_000,
|
|
231
|
+
},
|
|
232
|
+
};
|
|
233
|
+
const retained = makeDeps({ spawn: { result: retainedResult } });
|
|
234
|
+
const r2 = await executeDispatch(executeDecision(SPAWN_PLAN, lock), retained.deps);
|
|
235
|
+
ok(
|
|
236
|
+
"3: lock-retained rides executed (fail-closed, not a failure)",
|
|
237
|
+
r2.kind === "executed" && r2.outcome.transport === "spawn-bg" && r2.outcome.result.kind === "lock-retained",
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// ── 4: meta-mailbox execute → sendMailbox(plan, null), success ────────────
|
|
242
|
+
{
|
|
243
|
+
const { deps, trace } = makeDeps({ mailbox: { result: { success: true } } });
|
|
244
|
+
const res = await executeDispatch(executeDecision(MAILBOX_PLAN, null), deps);
|
|
245
|
+
ok("4: only sendMailbox ran", trace.calls.length === 1 && trace.calls[0] === "sendMailbox");
|
|
246
|
+
ok(
|
|
247
|
+
"4: NULL lock passed verbatim (?7)",
|
|
248
|
+
trace.mailboxArgs?.lock === null && trace.mailboxArgs?.plan === MAILBOX_PLAN,
|
|
249
|
+
);
|
|
250
|
+
ok(
|
|
251
|
+
"4: executed{meta-mailbox, success}",
|
|
252
|
+
res.kind === "executed" && res.outcome.transport === "meta-mailbox" && res.outcome.success === true,
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// ── 5: N3 — control rejected with rejectReason carried ────────────────────
|
|
257
|
+
{
|
|
258
|
+
const { deps } = makeDeps({
|
|
259
|
+
control: { result: { outcome: "rejected", rejectReason: "dormant-fire-forget-unsupported" } },
|
|
260
|
+
});
|
|
261
|
+
const res = await executeDispatch(executeDecision(CONTROL_PLAN, lockClaim()), deps);
|
|
262
|
+
ok(
|
|
263
|
+
"5: N3 rejectReason carried onto executed result",
|
|
264
|
+
res.kind === "executed" &&
|
|
265
|
+
res.outcome.transport === "control-socket" &&
|
|
266
|
+
res.outcome.rejectReason === "dormant-fire-forget-unsupported",
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// ── 6: N1 — SendDeliveredReleaseFailedError → execution-failed, no-retry ───
|
|
271
|
+
{
|
|
272
|
+
const releaseErr = new Error("release boom");
|
|
273
|
+
const { deps } = makeDeps({ control: { throw: new SendDeliveredReleaseFailedError("sent", releaseErr) } });
|
|
274
|
+
const res = await executeDispatch(executeDecision(CONTROL_PLAN, lockClaim()), deps);
|
|
275
|
+
ok("6: N1 → execution-failed", res.kind === "execution-failed");
|
|
276
|
+
ok(
|
|
277
|
+
"6: N1 → finalizedOutcome 'sent' + releaseFailed + retrySafe:false",
|
|
278
|
+
res.kind === "execution-failed" &&
|
|
279
|
+
res.finalizedOutcome === "sent" &&
|
|
280
|
+
res.releaseFailed === true &&
|
|
281
|
+
res.retrySafe === false,
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// ── 7: control PLAIN throw → execution-failed, no finalizedOutcome ────────
|
|
286
|
+
{
|
|
287
|
+
const { deps } = makeDeps({ control: { throw: new Error("connect indeterminate / dep boom") } });
|
|
288
|
+
const res = await executeDispatch(executeDecision(CONTROL_PLAN, lockClaim()), deps);
|
|
289
|
+
ok(
|
|
290
|
+
"7: plain throw → execution-failed, retrySafe:false",
|
|
291
|
+
res.kind === "execution-failed" && res.retrySafe === false,
|
|
292
|
+
);
|
|
293
|
+
ok(
|
|
294
|
+
"7: plain throw → no finalizedOutcome / releaseFailed",
|
|
295
|
+
res.kind === "execution-failed" && res.finalizedOutcome === undefined && res.releaseFailed === undefined,
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// ── 8: spawn / mailbox hand throw → execution-failed ──────────────────────
|
|
300
|
+
{
|
|
301
|
+
const s = makeDeps({ spawn: { throw: new Error("spawn boom") } });
|
|
302
|
+
const rs = await executeDispatch(executeDecision(SPAWN_PLAN, lockClaim()), s.deps);
|
|
303
|
+
ok(
|
|
304
|
+
"8: spawn throw → execution-failed",
|
|
305
|
+
rs.kind === "execution-failed" && rs.transport === "spawn-bg" && rs.retrySafe === false,
|
|
306
|
+
);
|
|
307
|
+
|
|
308
|
+
const m = makeDeps({ mailbox: { throw: new Error("enqueue boom") } });
|
|
309
|
+
const rm = await executeDispatch(executeDecision(MAILBOX_PLAN, null), m.deps);
|
|
310
|
+
ok(
|
|
311
|
+
"8: mailbox throw → execution-failed",
|
|
312
|
+
rm.kind === "execution-failed" && rm.transport === "meta-mailbox" && rm.retrySafe === false,
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// ── 9: mailbox returns {success:false} → CONTRACT VIOLATION, fail loud ─────
|
|
317
|
+
// 5c-4: a mailbox enqueue is {success:true} OR a throw — there is no in-band reject.
|
|
318
|
+
// A success:false must NOT be silently rendered as a success; the runner fails loud.
|
|
319
|
+
{
|
|
320
|
+
const { deps } = makeDeps({ mailbox: { result: { success: false, error: "should never happen" } } });
|
|
321
|
+
const res = await executeDispatch(executeDecision(MAILBOX_PLAN, null), deps);
|
|
322
|
+
ok(
|
|
323
|
+
"9: mailbox success:false → execution-failed (not a silent success)",
|
|
324
|
+
res.kind === "execution-failed" && res.transport === "meta-mailbox" && res.retrySafe === false,
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// ── 10–14: runEntwurfV2 composition (5d-2a) over a FAKE decide ─────────────
|
|
329
|
+
const DISPATCH_INPUT: DispatchInput = {
|
|
330
|
+
target: GID,
|
|
331
|
+
intent: "fire-and-forget",
|
|
332
|
+
mode: "follow_up",
|
|
333
|
+
wantsReply: false,
|
|
334
|
+
message: "m",
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
// A fake decide records how it was called and returns a scripted decision (or throws).
|
|
338
|
+
function makeRunDeps(
|
|
339
|
+
decideSpec: { decision: DispatchDecision } | { throw: unknown },
|
|
340
|
+
execSpec: FakeSpec,
|
|
341
|
+
): { deps: EntwurfV2RunDeps; decideCalls: DispatchInput[]; trace: Trace } {
|
|
342
|
+
const { deps: executor, trace } = makeDeps(execSpec);
|
|
343
|
+
const decideCalls: DispatchInput[] = [];
|
|
344
|
+
const deps: EntwurfV2RunDeps = {
|
|
345
|
+
decide(input) {
|
|
346
|
+
decideCalls.push(input);
|
|
347
|
+
if ("throw" in decideSpec) throw decideSpec.throw;
|
|
348
|
+
return decideSpec.decision;
|
|
349
|
+
},
|
|
350
|
+
executor,
|
|
351
|
+
};
|
|
352
|
+
return { deps, decideCalls, trace };
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// ── 10: decide called exactly once, with the same input ───────────────────
|
|
356
|
+
{
|
|
357
|
+
const decision: DispatchDecision = { kind: "reject", receipt: REJECT_RECEIPT };
|
|
358
|
+
const { deps, decideCalls } = makeRunDeps({ decision }, {});
|
|
359
|
+
await runEntwurfV2(DISPATCH_INPUT, deps);
|
|
360
|
+
ok("10: decide called exactly once", decideCalls.length === 1);
|
|
361
|
+
ok("10: decide got the SAME input", decideCalls[0] === DISPATCH_INPUT);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// ── 11: reject decision → no hand called, rejected result returned ────────
|
|
365
|
+
{
|
|
366
|
+
const decision: DispatchDecision = { kind: "reject", receipt: REJECT_RECEIPT };
|
|
367
|
+
const { deps, trace } = makeRunDeps({ decision }, {});
|
|
368
|
+
const res = await runEntwurfV2(DISPATCH_INPUT, deps);
|
|
369
|
+
ok("11: reject → rejected result", res.kind === "rejected" && res.receipt === REJECT_RECEIPT);
|
|
370
|
+
ok("11: reject → NO executor hand called", trace.calls.length === 0);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// ── 12: execute decision → the matching hand ran (per transport) ──────────
|
|
374
|
+
{
|
|
375
|
+
const lock = lockClaim();
|
|
376
|
+
const ctl = makeRunDeps(
|
|
377
|
+
{ decision: executeDecision(CONTROL_PLAN, lock) },
|
|
378
|
+
{ control: { result: { outcome: "sent" } } },
|
|
379
|
+
);
|
|
380
|
+
const rc = await runEntwurfV2(DISPATCH_INPUT, ctl.deps);
|
|
381
|
+
ok(
|
|
382
|
+
"12: control execute → sendControl ran, executed{control-socket}",
|
|
383
|
+
ctl.trace.calls.length === 1 &&
|
|
384
|
+
ctl.trace.calls[0] === "sendControl" &&
|
|
385
|
+
rc.kind === "executed" &&
|
|
386
|
+
rc.outcome.transport === "control-socket",
|
|
387
|
+
);
|
|
388
|
+
|
|
389
|
+
const spw = makeRunDeps(
|
|
390
|
+
{ decision: executeDecision(SPAWN_PLAN, lock) },
|
|
391
|
+
{ spawn: { result: { kind: "socket-alive", released: true, pid: 7 } } },
|
|
392
|
+
);
|
|
393
|
+
const rsp = await runEntwurfV2(DISPATCH_INPUT, spw.deps);
|
|
394
|
+
ok(
|
|
395
|
+
"12: spawn execute → resumeSpawnBg ran, executed{spawn-bg}",
|
|
396
|
+
spw.trace.calls.length === 1 &&
|
|
397
|
+
spw.trace.calls[0] === "resumeSpawnBg" &&
|
|
398
|
+
rsp.kind === "executed" &&
|
|
399
|
+
rsp.outcome.transport === "spawn-bg",
|
|
400
|
+
);
|
|
401
|
+
|
|
402
|
+
const mbx = makeRunDeps(
|
|
403
|
+
{ decision: executeDecision(MAILBOX_PLAN, null) },
|
|
404
|
+
{ mailbox: { result: { success: true } } },
|
|
405
|
+
);
|
|
406
|
+
const rmb = await runEntwurfV2(DISPATCH_INPUT, mbx.deps);
|
|
407
|
+
ok(
|
|
408
|
+
"12: mailbox execute → sendMailbox ran, executed{meta-mailbox}",
|
|
409
|
+
mbx.trace.calls.length === 1 &&
|
|
410
|
+
mbx.trace.calls[0] === "sendMailbox" &&
|
|
411
|
+
rmb.kind === "executed" &&
|
|
412
|
+
rmb.outcome.transport === "meta-mailbox",
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// ── 13: decide THROWS → propagates (no decision = no receipt to wrap) ─────
|
|
417
|
+
{
|
|
418
|
+
const boom = new Error("decider boom");
|
|
419
|
+
const { deps, trace } = makeRunDeps({ throw: boom }, {});
|
|
420
|
+
let caught: unknown;
|
|
421
|
+
try {
|
|
422
|
+
await runEntwurfV2(DISPATCH_INPUT, deps);
|
|
423
|
+
} catch (err) {
|
|
424
|
+
caught = err;
|
|
425
|
+
}
|
|
426
|
+
ok("13: decide throw → propagates the SAME error", caught === boom);
|
|
427
|
+
ok("13: decide throw → NO executor hand called", trace.calls.length === 0);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// ── 14: executeDispatch result returned VERBATIM (passthrough) ────────────
|
|
431
|
+
// A control hand throwing the N1 error must surface AS execution-failed{...} through
|
|
432
|
+
// runEntwurfV2 unchanged — the runner adds no wrapping over executeDispatch's mapping.
|
|
433
|
+
{
|
|
434
|
+
const releaseErr = new Error("release boom");
|
|
435
|
+
const { deps } = makeRunDeps(
|
|
436
|
+
{ decision: executeDecision(CONTROL_PLAN, lockClaim()) },
|
|
437
|
+
{ control: { throw: new SendDeliveredReleaseFailedError("sent", releaseErr) } },
|
|
438
|
+
);
|
|
439
|
+
const res = await runEntwurfV2(DISPATCH_INPUT, deps);
|
|
440
|
+
ok(
|
|
441
|
+
"14: N1 surfaces verbatim → execution-failed{finalizedOutcome,releaseFailed,retrySafe:false}",
|
|
442
|
+
res.kind === "execution-failed" &&
|
|
443
|
+
res.finalizedOutcome === "sent" &&
|
|
444
|
+
res.releaseFailed === true &&
|
|
445
|
+
res.retrySafe === false,
|
|
446
|
+
);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
console.log(`\ncheck-entwurf-v2-runner: ${passed} checks passed`);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
await main();
|