@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-spawn-production — DETERMINISTic gate for the 5c-3c production
|
|
3
|
+
* SpawnBgResumeDeps factory. It proves every injectable seam WITHOUT a real pi spawn,
|
|
4
|
+
* socket, or timer (the heavy live path is a separate opt-in smoke,
|
|
5
|
+
* smoke-entwurf-v2-spawn-live, kept OUT of pnpm check — D5):
|
|
6
|
+
*
|
|
7
|
+
* 1. socketWatchVerdict (the R2 watch policy, pure): address-conflict → forged (reject,
|
|
8
|
+
* never wait); alive → alive (resolve); dead / indeterminate → wait (keep polling).
|
|
9
|
+
* 2. spawnChild: resolves identity (injected) → buildResumePiArgs(v2-control) → spawnFn.
|
|
10
|
+
* The captured argv carries --entwurf-control, NO --no-extensions, -p + prompt final,
|
|
11
|
+
* plan.launchArgs (--approve), the ext args, provider/model, and the header cwd.
|
|
12
|
+
* 3. awaitSocketAlive: connectable → resolve; forged (symlink) → reject WITHOUT connecting
|
|
13
|
+
* (probe never called); dead → wait one interval, re-poll → alive resolves; abort
|
|
14
|
+
* during the wait → reject.
|
|
15
|
+
* 4. awaitChildExit: a child 'exit' resolves the code (null on signal); abort rejects and
|
|
16
|
+
* removes the exit listener (no leak).
|
|
17
|
+
* 5. awaitTimeout: schedules via the injected timer; abort clears it and rejects.
|
|
18
|
+
* 6. killChild: SIGTERM on the shared proc.
|
|
19
|
+
* 7. releaseLock: delegates to the injected release fn.
|
|
20
|
+
* 8. mis-wire: a child with no `proc` (not one we spawned) fails loud in killChild /
|
|
21
|
+
* awaitChildExit.
|
|
22
|
+
*
|
|
23
|
+
* All IO is injected; a controllable scheduler makes the timer/poll paths deterministic.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import assert from "node:assert/strict";
|
|
27
|
+
import type { LockClaim } from "../pi-extensions/lib/entwurf-v2-lock.ts";
|
|
28
|
+
import {
|
|
29
|
+
isV2ResumeResidentAuthorized,
|
|
30
|
+
V2_RESUME_RESIDENT_SESSION_ENV,
|
|
31
|
+
} from "../pi-extensions/lib/entwurf-v2-resume-marker.ts";
|
|
32
|
+
import type { SpawnBgPlan, SpawnBgResumeDeps, SpawnedChild } from "../pi-extensions/lib/entwurf-v2-spawn.ts";
|
|
33
|
+
import {
|
|
34
|
+
type LaunchIdentity,
|
|
35
|
+
makeProductionSpawnBgResumeDeps,
|
|
36
|
+
type ProductionSpawnOpts,
|
|
37
|
+
type SpawnedProcHandle,
|
|
38
|
+
socketWatchVerdict,
|
|
39
|
+
} from "../pi-extensions/lib/entwurf-v2-spawn-production.ts";
|
|
40
|
+
import type { LstatLike } from "../pi-extensions/lib/socket-discovery.ts";
|
|
41
|
+
import type { SocketLiveness } from "../pi-extensions/lib/socket-probe.ts";
|
|
42
|
+
|
|
43
|
+
let passed = 0;
|
|
44
|
+
function ok(label: string, cond: boolean): void {
|
|
45
|
+
assert.ok(cond, label);
|
|
46
|
+
console.log(` ok ${label}`);
|
|
47
|
+
passed++;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const GID = "20260613T091000-98363c";
|
|
51
|
+
const SOCK = `/fake/entwurf-control/${GID}.sock`;
|
|
52
|
+
const flush = (): Promise<void> => new Promise((res) => setImmediate(res));
|
|
53
|
+
|
|
54
|
+
const IDENTITY: LaunchIdentity = {
|
|
55
|
+
cwd: "/home/test/repo",
|
|
56
|
+
explicitExtensionArgs: ["-e", "/path/to/entwurf/index.ts"],
|
|
57
|
+
provider: "entwurf",
|
|
58
|
+
model: "claude-opus-4-8",
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
// True if `p` has NOT settled after a macrotask tick (all pending microtasks drained).
|
|
62
|
+
// Attaches a settled-handler (so a later rejection is never unhandled), then checks.
|
|
63
|
+
async function notSettled(p: Promise<unknown>): Promise<boolean> {
|
|
64
|
+
let settled = false;
|
|
65
|
+
void p.then(
|
|
66
|
+
() => {
|
|
67
|
+
settled = true;
|
|
68
|
+
},
|
|
69
|
+
() => {
|
|
70
|
+
settled = true;
|
|
71
|
+
},
|
|
72
|
+
);
|
|
73
|
+
await flush();
|
|
74
|
+
return !settled;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function spawnBgPlan(): SpawnBgPlan {
|
|
78
|
+
return {
|
|
79
|
+
transport: "spawn-bg",
|
|
80
|
+
action: "resume",
|
|
81
|
+
targetGardenId: GID,
|
|
82
|
+
sessionId: GID,
|
|
83
|
+
cwd: "/home/test/repo",
|
|
84
|
+
prompt: "continue the task",
|
|
85
|
+
launchArgs: ["--approve"],
|
|
86
|
+
expectedSocketPath: SOCK,
|
|
87
|
+
observeTimeoutMs: 30_000,
|
|
88
|
+
releaseWhen: "socket-alive-or-child-exited",
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function lockClaim(): LockClaim {
|
|
93
|
+
return {
|
|
94
|
+
gardenId: GID,
|
|
95
|
+
pid: 4242,
|
|
96
|
+
hostname: "test-host",
|
|
97
|
+
createdAt: "2026-06-13T00:00:00.000Z",
|
|
98
|
+
nonce: "deadbeefcafef00d",
|
|
99
|
+
owner: "entwurf_v2",
|
|
100
|
+
lockPath: `/fake/locks/${GID}.lock`,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// A controllable scheduler: setTimeoutFn captures callbacks; the gate fires them by hand.
|
|
105
|
+
function makeScheduler() {
|
|
106
|
+
let nextId = 1;
|
|
107
|
+
const timers = new Map<number, () => void>();
|
|
108
|
+
return {
|
|
109
|
+
setTimeoutFn: ((cb: () => void, _ms: number) => {
|
|
110
|
+
const id = nextId++;
|
|
111
|
+
timers.set(id, cb);
|
|
112
|
+
return id as unknown as ReturnType<typeof setTimeout>;
|
|
113
|
+
}) as (cb: () => void, ms: number) => ReturnType<typeof setTimeout>,
|
|
114
|
+
clearTimeoutFn: ((t: ReturnType<typeof setTimeout>) => {
|
|
115
|
+
timers.delete(t as unknown as number);
|
|
116
|
+
}) as (t: ReturnType<typeof setTimeout>) => void,
|
|
117
|
+
fireNext: (): void => {
|
|
118
|
+
const first = [...timers.entries()][0];
|
|
119
|
+
if (first) {
|
|
120
|
+
timers.delete(first[0]);
|
|
121
|
+
first[1]();
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
pending: (): number => timers.size,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// A fake proc handle (EventEmitter-lite) recording kills and listener churn.
|
|
129
|
+
function fakeProc() {
|
|
130
|
+
const listeners = new Map<string, Set<(...a: unknown[]) => void>>();
|
|
131
|
+
const calls = { kills: [] as string[] };
|
|
132
|
+
const proc: SpawnedProcHandle = {
|
|
133
|
+
pid: 9191,
|
|
134
|
+
kill: (sig) => {
|
|
135
|
+
calls.kills.push(String(sig));
|
|
136
|
+
return true;
|
|
137
|
+
},
|
|
138
|
+
on: (ev, l) => {
|
|
139
|
+
let set = listeners.get(ev);
|
|
140
|
+
if (!set) {
|
|
141
|
+
set = new Set();
|
|
142
|
+
listeners.set(ev, set);
|
|
143
|
+
}
|
|
144
|
+
set.add(l);
|
|
145
|
+
return proc;
|
|
146
|
+
},
|
|
147
|
+
removeListener: (ev, l) => {
|
|
148
|
+
listeners.get(ev)?.delete(l);
|
|
149
|
+
return proc;
|
|
150
|
+
},
|
|
151
|
+
};
|
|
152
|
+
return {
|
|
153
|
+
proc,
|
|
154
|
+
calls,
|
|
155
|
+
emit: (ev: string, ...args: unknown[]): void => {
|
|
156
|
+
for (const l of [...(listeners.get(ev) ?? [])]) l(...args);
|
|
157
|
+
},
|
|
158
|
+
listenerCount: (ev: string): number => listeners.get(ev)?.size ?? 0,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function statLike(kind: "socket" | "symlink" | "other"): LstatLike {
|
|
163
|
+
return {
|
|
164
|
+
isSymbolicLink: () => kind === "symlink",
|
|
165
|
+
isSocket: () => kind === "socket",
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Spawn a child through the factory (so it carries the eager exitPromise), driving the
|
|
170
|
+
// 'spawn' event so spawnChild resolves. Returns the deps + the started child.
|
|
171
|
+
async function spawnedChild(
|
|
172
|
+
fp: ReturnType<typeof fakeProc>,
|
|
173
|
+
opts: ProductionSpawnOpts = {},
|
|
174
|
+
): Promise<{ deps: SpawnBgResumeDeps; child: SpawnedChild }> {
|
|
175
|
+
const deps = makeProductionSpawnBgResumeDeps({ resolveIdentity: () => IDENTITY, spawnChild: () => fp.proc, ...opts });
|
|
176
|
+
const p = deps.spawnChild(spawnBgPlan());
|
|
177
|
+
await flush();
|
|
178
|
+
fp.emit("spawn");
|
|
179
|
+
const child = await p;
|
|
180
|
+
return { deps, child };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async function main(): Promise<void> {
|
|
184
|
+
// ── 1. socketWatchVerdict (pure R2 policy) ───────────────────────────────────────
|
|
185
|
+
ok("1 verdict: address-conflict → forged", socketWatchVerdict({ addressConflict: true }) === "forged");
|
|
186
|
+
ok("1 verdict: alive → alive", socketWatchVerdict({ liveness: "alive", socketPath: SOCK }) === "alive");
|
|
187
|
+
ok("1 verdict: dead → wait", socketWatchVerdict({ liveness: "dead", socketPath: SOCK }) === "wait");
|
|
188
|
+
ok("1 verdict: indeterminate → wait", socketWatchVerdict({ liveness: "indeterminate", socketPath: SOCK }) === "wait");
|
|
189
|
+
|
|
190
|
+
// ── 2. spawnChild builds the v2-control argv, then WAITS for the 'spawn' event (B1) ─
|
|
191
|
+
{
|
|
192
|
+
// A holder (not a closure-assigned `let`) so flow analysis sees the capture.
|
|
193
|
+
const cap: { value: { cmd: string; args: readonly string[]; cwd: string; env: NodeJS.ProcessEnv } | null } = {
|
|
194
|
+
value: null,
|
|
195
|
+
};
|
|
196
|
+
const fp = fakeProc();
|
|
197
|
+
const deps = makeProductionSpawnBgResumeDeps({
|
|
198
|
+
resolveIdentity: () => IDENTITY,
|
|
199
|
+
spawnChild: (cmd, args, cwd, env) => {
|
|
200
|
+
cap.value = { cmd, args, cwd, env };
|
|
201
|
+
return fp.proc;
|
|
202
|
+
},
|
|
203
|
+
});
|
|
204
|
+
const childP = deps.spawnChild(spawnBgPlan());
|
|
205
|
+
await flush(); // spawnChild has installed exit+spawn+error listeners and is awaiting 'spawn'
|
|
206
|
+
ok("2 spawnChild does NOT resolve before the 'spawn' event (B1)", await notSettled(childP));
|
|
207
|
+
fp.emit("spawn"); // a real start is confirmed → spawnChild resolves
|
|
208
|
+
const child = await childP;
|
|
209
|
+
ok("2 child carries pid from proc", child.pid === 9191);
|
|
210
|
+
const captured = cap.value;
|
|
211
|
+
assert.ok(captured, "spawnChild invoked the injected spawnFn");
|
|
212
|
+
const args = captured.args;
|
|
213
|
+
ok("2 spawns the pi binary", captured.cmd === "pi");
|
|
214
|
+
ok("2 cwd = header authority", captured.cwd === "/home/test/repo");
|
|
215
|
+
ok("2 argv has --entwurf-control", args.includes("--entwurf-control"));
|
|
216
|
+
ok("2 argv has NO --no-extensions", !args.includes("--no-extensions"));
|
|
217
|
+
ok("2 argv keeps -p", args.includes("-p"));
|
|
218
|
+
ok("2 argv prompt is final positional", args[args.length - 1] === "continue the task");
|
|
219
|
+
ok("2 argv carries plan.launchArgs (--approve)", args.includes("--approve"));
|
|
220
|
+
ok("2 argv carries the ext args", args.includes("-e"));
|
|
221
|
+
ok("2 argv carries provider", args[args.indexOf("--provider") + 1] === "entwurf");
|
|
222
|
+
ok("2 argv carries model", args[args.indexOf("--model") + 1] === "claude-opus-4-8");
|
|
223
|
+
ok("2 argv carries session-id", args[args.indexOf("--session-id") + 1] === GID);
|
|
224
|
+
// the sessionId-bound authorization marker is planted on the child env so the resumed
|
|
225
|
+
// `--entwurf-control` resident is an AUTHORIZED Entwurf child (entwurf-control.ts guard),
|
|
226
|
+
// not a "corrupt resident session name" crash. Bound to plan.sessionId exactly.
|
|
227
|
+
ok(
|
|
228
|
+
"2 child env carries the v2 resume resident marker = sessionId",
|
|
229
|
+
captured.env[V2_RESUME_RESIDENT_SESSION_ENV] === GID,
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// ── 2b. B1: an 'error' before 'spawn' (ENOENT pi / exec failure) → spawnChild REJECTS ─
|
|
234
|
+
// so the watcher turns it into spawn-start-failed (release), not a stalled spawn-started.
|
|
235
|
+
{
|
|
236
|
+
const fp = fakeProc();
|
|
237
|
+
const deps = makeProductionSpawnBgResumeDeps({ resolveIdentity: () => IDENTITY, spawnChild: () => fp.proc });
|
|
238
|
+
const childP = deps.spawnChild(spawnBgPlan());
|
|
239
|
+
await flush();
|
|
240
|
+
fp.emit("error", new Error("ENOENT: pi not found"));
|
|
241
|
+
let threw = false;
|
|
242
|
+
try {
|
|
243
|
+
await childP;
|
|
244
|
+
} catch (e) {
|
|
245
|
+
threw = e instanceof Error && e.message.includes("ENOENT");
|
|
246
|
+
}
|
|
247
|
+
ok("2b spawn-time 'error' before 'spawn' → spawnChild rejects", threw);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// ── 3. awaitSocketAlive: connectable resolves; forged rejects without connecting ──
|
|
251
|
+
{
|
|
252
|
+
// connectable: socket-file + probe alive → resolve, probe called once.
|
|
253
|
+
let probes = 0;
|
|
254
|
+
const deps = makeProductionSpawnBgResumeDeps({
|
|
255
|
+
lstatFn: async () => statLike("socket"),
|
|
256
|
+
probeFn: async () => {
|
|
257
|
+
probes++;
|
|
258
|
+
return "alive";
|
|
259
|
+
},
|
|
260
|
+
});
|
|
261
|
+
await deps.awaitSocketAlive(SOCK, new AbortController().signal);
|
|
262
|
+
ok("3 connectable socket resolves", true);
|
|
263
|
+
ok("3 connectable probed once", probes === 1);
|
|
264
|
+
}
|
|
265
|
+
{
|
|
266
|
+
// forged: a symlink is an address-conflict — reject WITHOUT ever connecting.
|
|
267
|
+
let probes = 0;
|
|
268
|
+
const deps = makeProductionSpawnBgResumeDeps({
|
|
269
|
+
lstatFn: async () => statLike("symlink"),
|
|
270
|
+
probeFn: async () => {
|
|
271
|
+
probes++;
|
|
272
|
+
return "alive";
|
|
273
|
+
},
|
|
274
|
+
});
|
|
275
|
+
let threw = false;
|
|
276
|
+
try {
|
|
277
|
+
await deps.awaitSocketAlive(SOCK, new AbortController().signal);
|
|
278
|
+
} catch (e) {
|
|
279
|
+
threw = e instanceof Error && e.message.includes("forged");
|
|
280
|
+
}
|
|
281
|
+
ok("3 forged (symlink) rejects", threw);
|
|
282
|
+
ok("3 forged never connects (probe not called)", probes === 0);
|
|
283
|
+
}
|
|
284
|
+
{
|
|
285
|
+
// dead → wait one interval → re-poll alive resolves. The scheduler fires the sleep.
|
|
286
|
+
const sched = makeScheduler();
|
|
287
|
+
const seq: SocketLiveness[] = ["dead", "alive"];
|
|
288
|
+
let i = 0;
|
|
289
|
+
const deps = makeProductionSpawnBgResumeDeps({
|
|
290
|
+
lstatFn: async () => statLike("socket"),
|
|
291
|
+
probeFn: async () => seq[i++] ?? "alive",
|
|
292
|
+
setTimeoutFn: sched.setTimeoutFn,
|
|
293
|
+
clearTimeoutFn: sched.clearTimeoutFn,
|
|
294
|
+
});
|
|
295
|
+
const p = deps.awaitSocketAlive(SOCK, new AbortController().signal);
|
|
296
|
+
await flush(); // poll 1 (dead) → schedules the inter-poll sleep
|
|
297
|
+
ok("3 dead → waits (a sleep is scheduled)", sched.pending() === 1);
|
|
298
|
+
sched.fireNext(); // sleep elapses → poll 2 (alive)
|
|
299
|
+
await p;
|
|
300
|
+
ok("3 dead → wait → alive resolves", true);
|
|
301
|
+
}
|
|
302
|
+
{
|
|
303
|
+
// abort during the wait → reject, scheduled sleep cleared.
|
|
304
|
+
const sched = makeScheduler();
|
|
305
|
+
const ctrl = new AbortController();
|
|
306
|
+
const deps = makeProductionSpawnBgResumeDeps({
|
|
307
|
+
lstatFn: async () => statLike("socket"),
|
|
308
|
+
probeFn: async () => "dead",
|
|
309
|
+
setTimeoutFn: sched.setTimeoutFn,
|
|
310
|
+
clearTimeoutFn: sched.clearTimeoutFn,
|
|
311
|
+
});
|
|
312
|
+
const p = deps.awaitSocketAlive(SOCK, ctrl.signal);
|
|
313
|
+
await flush(); // poll 1 (dead) → sleeping
|
|
314
|
+
ctrl.abort();
|
|
315
|
+
let threw = false;
|
|
316
|
+
try {
|
|
317
|
+
await p;
|
|
318
|
+
} catch {
|
|
319
|
+
threw = true;
|
|
320
|
+
}
|
|
321
|
+
ok("3 abort during wait → rejects", threw);
|
|
322
|
+
ok("3 abort clears the scheduled sleep", sched.pending() === 0);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// ── 4. awaitChildExit reads the EAGER exitPromise (installed at spawn) ────────────
|
|
326
|
+
{
|
|
327
|
+
// 4a: exit AFTER awaitChildExit is waiting → resolves the code.
|
|
328
|
+
const fp = fakeProc();
|
|
329
|
+
const { deps, child } = await spawnedChild(fp);
|
|
330
|
+
const p = deps.awaitChildExit(child, new AbortController().signal);
|
|
331
|
+
fp.emit("exit", 137);
|
|
332
|
+
ok("4 exit resolves the code", (await p) === 137);
|
|
333
|
+
}
|
|
334
|
+
{
|
|
335
|
+
// 4b: abort → rejects.
|
|
336
|
+
const fp = fakeProc();
|
|
337
|
+
const { deps, child } = await spawnedChild(fp);
|
|
338
|
+
const ctrl = new AbortController();
|
|
339
|
+
const p = deps.awaitChildExit(child, ctrl.signal);
|
|
340
|
+
await flush();
|
|
341
|
+
ctrl.abort();
|
|
342
|
+
let threw = false;
|
|
343
|
+
try {
|
|
344
|
+
await p;
|
|
345
|
+
} catch {
|
|
346
|
+
threw = true;
|
|
347
|
+
}
|
|
348
|
+
ok("4 abort → rejects", threw);
|
|
349
|
+
}
|
|
350
|
+
{
|
|
351
|
+
// 4c: a signal kill (null code) → null.
|
|
352
|
+
const fp = fakeProc();
|
|
353
|
+
const { deps, child } = await spawnedChild(fp);
|
|
354
|
+
const p = deps.awaitChildExit(child, new AbortController().signal);
|
|
355
|
+
fp.emit("exit", null);
|
|
356
|
+
ok("4 signal exit → null code", (await p) === null);
|
|
357
|
+
}
|
|
358
|
+
{
|
|
359
|
+
// 4d (B2): the child exits BEFORE awaitChildExit is even called. The eager exitPromise
|
|
360
|
+
// captured it at spawn, so awaitChildExit resolves immediately — it does NOT hang into a
|
|
361
|
+
// timeout→kill→wrongful-retained.
|
|
362
|
+
const fp = fakeProc();
|
|
363
|
+
const { deps, child } = await spawnedChild(fp);
|
|
364
|
+
fp.emit("exit", 0); // exit in the gap, before awaitChildExit
|
|
365
|
+
const code = await deps.awaitChildExit(child, new AbortController().signal);
|
|
366
|
+
ok("4d B2: fast exit before awaitChildExit is NOT missed", code === 0);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// ── 5. awaitTimeout: schedules; abort clears + rejects ───────────────────────────
|
|
370
|
+
{
|
|
371
|
+
const sched = makeScheduler();
|
|
372
|
+
const ctrl = new AbortController();
|
|
373
|
+
const deps = makeProductionSpawnBgResumeDeps({
|
|
374
|
+
setTimeoutFn: sched.setTimeoutFn,
|
|
375
|
+
clearTimeoutFn: sched.clearTimeoutFn,
|
|
376
|
+
});
|
|
377
|
+
const p = deps.awaitTimeout(30_000, ctrl.signal);
|
|
378
|
+
await flush();
|
|
379
|
+
ok("5 awaitTimeout schedules a timer", sched.pending() === 1);
|
|
380
|
+
// fire it → resolves
|
|
381
|
+
sched.fireNext();
|
|
382
|
+
await p;
|
|
383
|
+
ok("5 awaitTimeout resolves when fired", true);
|
|
384
|
+
|
|
385
|
+
// abort path clears the timer.
|
|
386
|
+
const sched2 = makeScheduler();
|
|
387
|
+
const ctrl2 = new AbortController();
|
|
388
|
+
const deps2 = makeProductionSpawnBgResumeDeps({
|
|
389
|
+
setTimeoutFn: sched2.setTimeoutFn,
|
|
390
|
+
clearTimeoutFn: sched2.clearTimeoutFn,
|
|
391
|
+
});
|
|
392
|
+
const q = deps2.awaitTimeout(30_000, ctrl2.signal);
|
|
393
|
+
await flush();
|
|
394
|
+
ctrl2.abort();
|
|
395
|
+
let threw = false;
|
|
396
|
+
try {
|
|
397
|
+
await q;
|
|
398
|
+
} catch {
|
|
399
|
+
threw = true;
|
|
400
|
+
}
|
|
401
|
+
ok("5 abort → awaitTimeout rejects", threw);
|
|
402
|
+
ok("5 abort clears the timer", sched2.pending() === 0);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// ── 6 & 7. killChild SIGTERM; releaseLock delegates ──────────────────────────────
|
|
406
|
+
{
|
|
407
|
+
const fp = fakeProc();
|
|
408
|
+
const holder: { released: LockClaim | null } = { released: null };
|
|
409
|
+
const { deps, child } = await spawnedChild(fp, { releaseFn: (lock) => (holder.released = lock) });
|
|
410
|
+
deps.killChild(child);
|
|
411
|
+
ok("6 killChild sends SIGTERM", fp.calls.kills.length === 1 && fp.calls.kills[0] === "SIGTERM");
|
|
412
|
+
const lock = lockClaim();
|
|
413
|
+
deps.releaseLock(lock);
|
|
414
|
+
ok("7 releaseLock delegates to the injected release fn", holder.released === lock);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// ── 8. mis-wire: a child with no proc fails loud ─────────────────────────────────
|
|
418
|
+
{
|
|
419
|
+
const deps = makeProductionSpawnBgResumeDeps({});
|
|
420
|
+
let killThrew = false;
|
|
421
|
+
try {
|
|
422
|
+
deps.killChild({ pid: 1 });
|
|
423
|
+
} catch (e) {
|
|
424
|
+
killThrew = e instanceof Error && e.message.includes("no proc");
|
|
425
|
+
}
|
|
426
|
+
ok("8 killChild on a proc-less child fails loud", killThrew);
|
|
427
|
+
|
|
428
|
+
let exitThrew = false;
|
|
429
|
+
try {
|
|
430
|
+
await deps.awaitChildExit({ pid: 1 }, new AbortController().signal);
|
|
431
|
+
} catch (e) {
|
|
432
|
+
exitThrew = e instanceof Error && e.message.includes("no proc");
|
|
433
|
+
}
|
|
434
|
+
ok("8 awaitChildExit on a proc-less child fails loud", exitThrew);
|
|
435
|
+
}
|
|
436
|
+
ok(
|
|
437
|
+
"9 exact sessionId marker → authorized",
|
|
438
|
+
isV2ResumeResidentAuthorized(GID, { [V2_RESUME_RESIDENT_SESSION_ENV]: GID }),
|
|
439
|
+
);
|
|
440
|
+
ok("9 absent marker → NOT authorized", !isV2ResumeResidentAuthorized(GID, {}));
|
|
441
|
+
ok(
|
|
442
|
+
"9 wrong-session marker → NOT authorized (binding is to the exact id)",
|
|
443
|
+
!isV2ResumeResidentAuthorized(GID, { [V2_RESUME_RESIDENT_SESSION_ENV]: "20260101T000000-000000" }),
|
|
444
|
+
);
|
|
445
|
+
|
|
446
|
+
console.log(`\ncheck-entwurf-v2-spawn-production: ${passed} checks passed`);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
main().catch((err) => {
|
|
450
|
+
console.error(err);
|
|
451
|
+
process.exit(1);
|
|
452
|
+
});
|