@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
|
@@ -1,469 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* smoke-entwurf-v2-spawn-resume-live — the 0.11.0 (A) acceptance gate: a REAL `pi` child is
|
|
3
|
-
* spawned by the production spawn-bg resume path, stands its control socket up as a resident,
|
|
4
|
-
* runs the resume turn, and the lock is released exactly once. This is the evidence the whole
|
|
5
|
-
* 0.11.0 "spawn-bg resident lifecycle" headline rests on — every other gate proves a SLICE
|
|
6
|
-
* (the deterministic decider table, the matrix-live transport sentinel, the spawn-live OS
|
|
7
|
-
* substrate watcher), but none has ever watched a real child pi resume and DO a model turn.
|
|
8
|
-
* 5c-3c deferred that proof to "5d"; 5d's matrix-live built only the control/mailbox/guard
|
|
9
|
-
* cells. This smoke closes that acceptance debt — and it is the gate v1 deprecation (0.12)
|
|
10
|
-
* is predicated on: until a real spawn-bg resume is seen LIVE, there is no honest ground to
|
|
11
|
-
* turn the v1 entwurf surface off.
|
|
12
|
-
*
|
|
13
|
-
* The FULL loop (no synthetic stand-in, record authority — #50 C2/C3):
|
|
14
|
-
* 1. seed a REAL dormant pi session: a one-shot `pi --mode json -p --no-extensions` (NO
|
|
15
|
-
* --session-id / --name — pi owns its id and name, LOCKED PROTOCOL 2) writes a saved
|
|
16
|
-
* session JSONL into the REAL `~/.pi/agent/sessions`. No control socket survives the
|
|
17
|
-
* one-shot → the citizen-to-be is DORMANT.
|
|
18
|
-
* 2. mint the `backend=pi` meta-record in a temp store from the SEED's OWN identity:
|
|
19
|
-
* nativeSessionId = the seed's JSONL header id, transcriptPath = the seed file. The
|
|
20
|
-
* record's gardenId is the citizen's address; resolveResumeLaunchIdentity resolves the
|
|
21
|
-
* transcript from the record and verifies header id === nativeSessionId (C3).
|
|
22
|
-
* 3. drive the production `runEntwurfV2({intent:"owned-outcome"})` → the decider routes a
|
|
23
|
-
* dormant in-domain pi citizen to spawn-bg resume (DISPATCH_TABLE owned-outcome×dormant) →
|
|
24
|
-
* the production spawn factory launches a REAL detached `pi --entwurf-control` resume child.
|
|
25
|
-
* 4. assert: result = executed/spawn-bg/socket-alive/released; the lock was released EXACTLY
|
|
26
|
-
* once (release-seam counter) and no lock file remains; the resident pid is alive and its
|
|
27
|
-
* socket is connectable.
|
|
28
|
-
* 5. assert the child actually WORKED: poll the same session JSONL for the resume USER nonce
|
|
29
|
-
* (the turn was injected) and then the assistant OK nonce (the model actually replied) —
|
|
30
|
-
* socket-alive alone would prove "process up", not "resumed and did a turn".
|
|
31
|
-
*
|
|
32
|
-
* model-in-loop is IN here (unlike matrix-live, which is a transport/lock sentinel): the
|
|
33
|
-
* assistant nonce is the whole point — a resume that stands a socket up but never produces a
|
|
34
|
-
* model turn is not a resumed citizen. So this is LIVE-only and OUT of `pnpm check`; honest
|
|
35
|
-
* SKIP when LIVE!=1 (a release-gate that hard-fails without auth/model is unrunnable
|
|
36
|
-
* unattended — skip is CI safety, NOT an acceptance PASS).
|
|
37
|
-
* ENTWURF_LIVE_TARGET = "<provider>/<model>" (default "openai-codex/gpt-5.4")
|
|
38
|
-
* ENTWURF_SPAWN_RESUME_ASSISTANT_TIMEOUT_MS (default 180000)
|
|
39
|
-
* LIVE=1 ./run.sh smoke-entwurf-v2-spawn-resume-live
|
|
40
|
-
*
|
|
41
|
-
* Automation seams: the resident child is reaped (SIGTERM→socket-gone poll→SIGKILL) and its
|
|
42
|
-
* real control socket removed in `finally`; the ONE seed session file is deleted on a clean
|
|
43
|
-
* pass (never the whole real sessions dir) and PRESERVED with its path printed on failure,
|
|
44
|
-
* alongside the temp world / gid / lock path / pi seed stderr tail.
|
|
45
|
-
*/
|
|
46
|
-
|
|
47
|
-
import { spawnSync } from "node:child_process";
|
|
48
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
49
|
-
import * as fsp from "node:fs/promises";
|
|
50
|
-
import * as os from "node:os";
|
|
51
|
-
import * as path from "node:path";
|
|
52
|
-
import { fileURLToPath } from "node:url";
|
|
53
|
-
import type { SenderEnvelope } from "../pi-extensions/lib/entwurf-control-rpc.ts";
|
|
54
|
-
import { readSessionIdentity } from "../pi-extensions/lib/entwurf-core.ts";
|
|
55
|
-
import { type LockClaim, lockPathFor, releaseLock as realReleaseLock } from "../pi-extensions/lib/entwurf-v2-lock.ts";
|
|
56
|
-
import { makeProductionEntwurfV2Deps } from "../pi-extensions/lib/entwurf-v2-production.ts";
|
|
57
|
-
import type { EntwurfV2RunResult } from "../pi-extensions/lib/entwurf-v2-runner.ts";
|
|
58
|
-
import { runEntwurfV2 } from "../pi-extensions/lib/entwurf-v2-runner.ts";
|
|
59
|
-
import { resolveResumeLaunchIdentity } from "../pi-extensions/lib/entwurf-v2-spawn-production.ts";
|
|
60
|
-
import { upsertMetaSession } from "../pi-extensions/lib/meta-session.ts";
|
|
61
|
-
import { controlSocketPath } from "../pi-extensions/lib/socket-discovery.ts";
|
|
62
|
-
|
|
63
|
-
// pi owns the canonical control-socket dir; the resume child opens its socket HERE and the
|
|
64
|
-
// decider's expectedSocketPath is built from the SAME dir — they must coincide (a fresh gid
|
|
65
|
-
// keeps us off any live session's path; cleanup only ever touches our own gid's socket).
|
|
66
|
-
const REAL_CONTROL_DIR = path.join(os.homedir(), ".pi", "entwurf-control");
|
|
67
|
-
const SOCKET_SUFFIX = ".sock";
|
|
68
|
-
// Release-gate topology: repo-under-test, not deployment smoke. The spawned
|
|
69
|
-
// resident must load this checkout's extension, independent of global pi packages.
|
|
70
|
-
const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
71
|
-
const REPO_EXTENSION_ARGS = ["--no-extensions", "-e", REPO_ROOT] as const;
|
|
72
|
-
|
|
73
|
-
const SEED_TIMEOUT_MS = 120_000; // F5: a real seed model turn can be slow.
|
|
74
|
-
const OBSERVE_TIMEOUT_MS = 30_000; // spawn-bg watcher's socket-alive observe window.
|
|
75
|
-
const SEED_IDENTITY_POLL_MS = 10_000; // GPT pin: defend against seed file flush/rename timing.
|
|
76
|
-
const USER_TURN_TIMEOUT_MS = 60_000;
|
|
77
|
-
const ASSISTANT_TURN_TIMEOUT_MS = (() => {
|
|
78
|
-
const n = Number(process.env.ENTWURF_SPAWN_RESUME_ASSISTANT_TIMEOUT_MS);
|
|
79
|
-
return Number.isFinite(n) && n > 0 ? n : 180_000;
|
|
80
|
-
})();
|
|
81
|
-
const POLL_MS = 250;
|
|
82
|
-
|
|
83
|
-
let passed = 0;
|
|
84
|
-
const artifacts: Record<string, string> = {};
|
|
85
|
-
|
|
86
|
-
function ok(label: string, cond: boolean): void {
|
|
87
|
-
if (!cond) throw new Error(`SMOKE FAIL: ${label}`);
|
|
88
|
-
console.log(` ok ${label}`);
|
|
89
|
-
passed++;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
function resolveTarget(): { provider: string; model: string } {
|
|
93
|
-
const combined = process.env.ENTWURF_LIVE_TARGET?.trim();
|
|
94
|
-
if (combined) {
|
|
95
|
-
const slash = combined.indexOf("/");
|
|
96
|
-
if (slash <= 0 || slash === combined.length - 1) {
|
|
97
|
-
throw new Error(`ENTWURF_LIVE_TARGET must be "<provider>/<model>", got: ${JSON.stringify(combined)}`);
|
|
98
|
-
}
|
|
99
|
-
return { provider: combined.slice(0, slash), model: combined.slice(slash + 1) };
|
|
100
|
-
}
|
|
101
|
-
return {
|
|
102
|
-
provider: process.env.ENTWURF_LIVE_PROVIDER?.trim() || "openai-codex",
|
|
103
|
-
model: process.env.ENTWURF_LIVE_MODEL?.trim() || "gpt-5.4",
|
|
104
|
-
};
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
function sleep(ms: number): Promise<void> {
|
|
108
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
function pidAlive(pid: number): boolean {
|
|
112
|
-
try {
|
|
113
|
-
process.kill(pid, 0);
|
|
114
|
-
return true;
|
|
115
|
-
} catch {
|
|
116
|
-
return false;
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
// Tear the detached resident down for real: SIGTERM, wait for both the pid to die AND the
|
|
121
|
-
// socket to disappear, then a SIGKILL backstop. The child is detached (no ChildProcess handle),
|
|
122
|
-
// so liveness is observed via `kill(pid, 0)` + the socket file, not an `exit` event.
|
|
123
|
-
async function terminateResident(pid: number, sockPath: string, graceMs = 3_000): Promise<void> {
|
|
124
|
-
try {
|
|
125
|
-
process.kill(pid, "SIGTERM");
|
|
126
|
-
} catch {
|
|
127
|
-
return; // already gone
|
|
128
|
-
}
|
|
129
|
-
const deadline = Date.now() + graceMs;
|
|
130
|
-
while (Date.now() < deadline) {
|
|
131
|
-
if (!pidAlive(pid) && !existsSync(sockPath)) return;
|
|
132
|
-
await sleep(POLL_MS);
|
|
133
|
-
}
|
|
134
|
-
try {
|
|
135
|
-
process.kill(pid, "SIGKILL");
|
|
136
|
-
} catch {
|
|
137
|
-
// already gone
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
// Where pi's one-shot seed lands its session JSONL: the REAL agent sessions base,
|
|
142
|
-
// under the cwd-encoded dir for our unique temp cwd. The encoding is pi's layout
|
|
143
|
-
// ("--" + cwd with "/"→"-" + "--"); the temp cwd is unique to this run, so the
|
|
144
|
-
// dir contains ONLY our seed and removing it on success can touch nothing else.
|
|
145
|
-
function seedSessionDirFor(cwd: string): string {
|
|
146
|
-
const encoded = `--${cwd.replace(/^\//, "").replace(/\//g, "-")}--`;
|
|
147
|
-
return path.join(os.homedir(), ".pi", "agent", "sessions", encoded);
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
// The seed's own identity: header line of the one .jsonl pi wrote for our cwd.
|
|
151
|
-
// A local helper, not an entwurf-core import — the header-scan lookup died with
|
|
152
|
-
// C3; this reads pi's file for SEEDING purposes only (the production resume path
|
|
153
|
-
// never scans, it follows record.transcriptPath).
|
|
154
|
-
function readSeedHeader(file: string): { id?: string; cwd?: string } | null {
|
|
155
|
-
try {
|
|
156
|
-
const first = readFileSync(file, "utf8").split("\n", 1)[0]?.trim();
|
|
157
|
-
if (!first) return null;
|
|
158
|
-
const entry = JSON.parse(first) as { type?: string; id?: unknown; cwd?: unknown };
|
|
159
|
-
if (entry.type !== "session") return null;
|
|
160
|
-
return {
|
|
161
|
-
id: typeof entry.id === "string" && entry.id ? entry.id : undefined,
|
|
162
|
-
cwd: typeof entry.cwd === "string" && entry.cwd ? entry.cwd : undefined,
|
|
163
|
-
};
|
|
164
|
-
} catch {
|
|
165
|
-
return null;
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
function smokeSender(gardenId: string, cwd: string): SenderEnvelope {
|
|
170
|
-
return {
|
|
171
|
-
sessionId: gardenId,
|
|
172
|
-
agentId: "smoke/spawn-resume-live",
|
|
173
|
-
cwd,
|
|
174
|
-
timestamp: new Date(0).toISOString(),
|
|
175
|
-
origin: "pi-session",
|
|
176
|
-
replyable: false,
|
|
177
|
-
};
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
// Read the session JSONL and search ONLY the lines appended after the seed (append-only rigor:
|
|
181
|
-
// the resume nonces are unique so a whole-file scan would also be correct, but slicing past the
|
|
182
|
-
// seed line count proves the turn was added by the RESUME, not pre-existing). JSON-parse each
|
|
183
|
-
// line and match on the stringified entry — never assume a type/role shape (GPT pin 7).
|
|
184
|
-
async function pollForNonce(
|
|
185
|
-
sessionFile: string,
|
|
186
|
-
seedLineCount: number,
|
|
187
|
-
needle: string,
|
|
188
|
-
timeoutMs: number,
|
|
189
|
-
): Promise<boolean> {
|
|
190
|
-
const deadline = Date.now() + timeoutMs;
|
|
191
|
-
while (Date.now() < deadline) {
|
|
192
|
-
const lines = readFileSync(sessionFile, "utf8")
|
|
193
|
-
.split("\n")
|
|
194
|
-
.filter((l) => l.trim());
|
|
195
|
-
for (const line of lines.slice(seedLineCount)) {
|
|
196
|
-
let entry: unknown;
|
|
197
|
-
try {
|
|
198
|
-
entry = JSON.parse(line);
|
|
199
|
-
} catch {
|
|
200
|
-
continue;
|
|
201
|
-
}
|
|
202
|
-
if (JSON.stringify(entry).includes(needle)) return true;
|
|
203
|
-
}
|
|
204
|
-
await sleep(POLL_MS);
|
|
205
|
-
}
|
|
206
|
-
return false;
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
async function main(): Promise<void> {
|
|
210
|
-
if (process.env.LIVE !== "1") {
|
|
211
|
-
console.log(
|
|
212
|
-
"[smoke-entwurf-v2-spawn-resume-live] skipped — set LIVE=1 to run (spawns a real pi child + opens a real socket).",
|
|
213
|
-
);
|
|
214
|
-
return;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
const { provider, model } = resolveTarget();
|
|
218
|
-
// ACP override is OUT of scope for 0.11.0 (A): `--no-extensions --provider entwurf` is a
|
|
219
|
-
// broken combination, and this smoke proves spawn-bg resident lifecycle, not backend equality.
|
|
220
|
-
if (provider === "entwurf") {
|
|
221
|
-
throw new Error(
|
|
222
|
-
"spawn-resume-live: provider=entwurf is out of scope for 0.11.0 (A) — use a native target (e.g. openai-codex/gpt-5.4).",
|
|
223
|
-
);
|
|
224
|
-
}
|
|
225
|
-
console.log(`[smoke-entwurf-v2-spawn-resume-live] target = ${provider}/${model}`);
|
|
226
|
-
|
|
227
|
-
// ── temp world (META store / mailbox / locks are temp; pi's SESSION JSONL is REAL) ──
|
|
228
|
-
const tmp = await fsp.mkdtemp(path.join(os.tmpdir(), "v2resume-"));
|
|
229
|
-
const sessionsDir = path.join(tmp, "meta-sessions");
|
|
230
|
-
const mailboxDir = path.join(tmp, "mailbox");
|
|
231
|
-
const lockDir = path.join(tmp, "locks");
|
|
232
|
-
for (const d of [sessionsDir, mailboxDir, lockDir]) await fsp.mkdir(d, { recursive: true });
|
|
233
|
-
process.env.ENTWURF_META_SESSIONS_DIR = sessionsDir;
|
|
234
|
-
process.env.ENTWURF_META_MAILBOX_DIR = mailboxDir;
|
|
235
|
-
|
|
236
|
-
let gid = "";
|
|
237
|
-
let seedFile: string | null = null;
|
|
238
|
-
let childPid: number | null = null;
|
|
239
|
-
let seedStderrTail = "";
|
|
240
|
-
let succeeded = false;
|
|
241
|
-
|
|
242
|
-
// ── release-seam wrapper (F2): the production factory builds ONE shared `release` closure
|
|
243
|
-
// from `seams.releaseLock` and injects it into the decider AND the spawn watcher's releaseFn,
|
|
244
|
-
// so this single wrapper counts the spawn-bg release too. Production code is unchanged. ──
|
|
245
|
-
let releaseCount = 0;
|
|
246
|
-
const releasedGids: string[] = [];
|
|
247
|
-
|
|
248
|
-
try {
|
|
249
|
-
// ── 1. seed a REAL dormant pi session (one-shot; pi owns id + name — no
|
|
250
|
-
// --session-id / --name, LOCKED PROTOCOL 2) ──
|
|
251
|
-
const nonce = `${process.pid.toString(36)}${Date.now().toString(36)}`;
|
|
252
|
-
artifacts["nonce"] = nonce;
|
|
253
|
-
const seedArgs = [
|
|
254
|
-
"--mode",
|
|
255
|
-
"json",
|
|
256
|
-
"-p",
|
|
257
|
-
"--no-extensions",
|
|
258
|
-
"--approve",
|
|
259
|
-
"--provider",
|
|
260
|
-
provider,
|
|
261
|
-
"--model",
|
|
262
|
-
model,
|
|
263
|
-
`Reply exactly SEED_READY_${nonce} and nothing else.`,
|
|
264
|
-
];
|
|
265
|
-
const seed = spawnSync("pi", seedArgs, { cwd: tmp, encoding: "utf8", timeout: SEED_TIMEOUT_MS, env: process.env });
|
|
266
|
-
seedStderrTail = `${seed.stdout ?? ""}\n${seed.stderr ?? ""}`.split("\n").slice(-12).join("\n");
|
|
267
|
-
ok("seed pi one-shot exited 0", seed.status === 0);
|
|
268
|
-
|
|
269
|
-
// ── 2. discover the seed's OWN identity (file + header id) and mint the record ──
|
|
270
|
-
// The temp cwd is unique to this run, so pi's cwd-encoded session dir holds
|
|
271
|
-
// exactly our seed. Poll briefly: the JSONL flush/rename can lag process exit.
|
|
272
|
-
const seedDir = seedSessionDirFor(tmp);
|
|
273
|
-
artifacts["seedDir"] = seedDir;
|
|
274
|
-
let seedHeaderId: string | undefined;
|
|
275
|
-
let identity: ReturnType<typeof readSessionIdentity> = null;
|
|
276
|
-
{
|
|
277
|
-
const deadline = Date.now() + SEED_IDENTITY_POLL_MS;
|
|
278
|
-
while (Date.now() < deadline) {
|
|
279
|
-
let jsonls: string[] = [];
|
|
280
|
-
try {
|
|
281
|
-
jsonls = (await fsp.readdir(seedDir)).filter((f) => f.endsWith(".jsonl"));
|
|
282
|
-
} catch {
|
|
283
|
-
/* dir not created yet */
|
|
284
|
-
}
|
|
285
|
-
if (jsonls.length === 1) {
|
|
286
|
-
const file = path.join(seedDir, jsonls[0]);
|
|
287
|
-
const header = readSeedHeader(file);
|
|
288
|
-
const id = readSessionIdentity(file);
|
|
289
|
-
if (header?.id && id?.modelId && id.provider && id.cwd) {
|
|
290
|
-
seedFile = file;
|
|
291
|
-
seedHeaderId = header.id;
|
|
292
|
-
identity = id;
|
|
293
|
-
break;
|
|
294
|
-
}
|
|
295
|
-
} else if (jsonls.length > 1) {
|
|
296
|
-
throw new Error(`seed dir ${seedDir} holds ${jsonls.length} sessions — expected exactly the one seed.`);
|
|
297
|
-
}
|
|
298
|
-
await sleep(POLL_MS);
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
artifacts["seedFile"] = seedFile ?? "(none)";
|
|
302
|
-
ok("seed session file discovered under the seed cwd dir", seedFile !== null);
|
|
303
|
-
ok("seed header carries pi's own session id", typeof seedHeaderId === "string" && seedHeaderId.length > 0);
|
|
304
|
-
ok("seed recorded a resumable identity", identity !== null);
|
|
305
|
-
ok("seed header cwd === temp cwd (cold-resume authority, #9)", identity?.cwd === tmp);
|
|
306
|
-
ok("seed recorded provider matches the target", identity?.provider === provider);
|
|
307
|
-
ok("seed recorded modelId matches the target", identity?.modelId === model);
|
|
308
|
-
ok("seed header id === readSessionIdentity id (one reader, one truth)", identity?.sessionId === seedHeaderId);
|
|
309
|
-
|
|
310
|
-
// The record is minted FROM the seed's identity: nativeSessionId = pi's own
|
|
311
|
-
// header id, transcriptPath = the seed file (what a real turn_end records).
|
|
312
|
-
// Its gardenId is the citizen's address — never pi's session id (#50 C2).
|
|
313
|
-
const minted = upsertMetaSession({
|
|
314
|
-
input: {
|
|
315
|
-
backend: "pi",
|
|
316
|
-
nativeSessionId: seedHeaderId as string,
|
|
317
|
-
cwd: tmp,
|
|
318
|
-
model,
|
|
319
|
-
transcriptPath: seedFile,
|
|
320
|
-
},
|
|
321
|
-
dir: sessionsDir,
|
|
322
|
-
});
|
|
323
|
-
gid = minted.record.gardenId;
|
|
324
|
-
artifacts["gid"] = gid;
|
|
325
|
-
const sockPath = path.join(REAL_CONTROL_DIR, `${gid}${SOCKET_SUFFIX}`);
|
|
326
|
-
artifacts["socket"] = sockPath;
|
|
327
|
-
artifacts["lock"] = lockPathFor(gid, lockDir);
|
|
328
|
-
ok("record gardenId is not pi's session id (record mints the address)", gid !== seedHeaderId);
|
|
329
|
-
ok("fresh gid has no pre-existing control socket", !existsSync(controlSocketPath(gid, REAL_CONTROL_DIR)));
|
|
330
|
-
|
|
331
|
-
// resume-time append boundary: count seed lines so the nonce poll scans ONLY the resume.
|
|
332
|
-
const seedLineCount = readFileSync(seedFile as string, "utf8")
|
|
333
|
-
.split("\n")
|
|
334
|
-
.filter((l) => l.trim()).length;
|
|
335
|
-
|
|
336
|
-
// dormant precondition: the one-shot left NO live control socket behind.
|
|
337
|
-
ok("seed left no live control socket (citizen is dormant)", !existsSync(sockPath));
|
|
338
|
-
|
|
339
|
-
// ── 4. drive the production owned-outcome resume → real spawn-bg child ──
|
|
340
|
-
const prodDeps = makeProductionEntwurfV2Deps({
|
|
341
|
-
senderProvider: () => smokeSender(gid, tmp),
|
|
342
|
-
sessionsDir,
|
|
343
|
-
mailboxDir,
|
|
344
|
-
lockDir,
|
|
345
|
-
controlSocketDir: REAL_CONTROL_DIR,
|
|
346
|
-
prefixRoots: [tmp], // F4: preflight allow for a resume into the temp cwd.
|
|
347
|
-
observeTimeoutMs: OBSERVE_TIMEOUT_MS,
|
|
348
|
-
seams: {
|
|
349
|
-
releaseLock: (claim: LockClaim, deps: { dir?: string }) => {
|
|
350
|
-
releaseCount++;
|
|
351
|
-
releasedGids.push(claim.gardenId);
|
|
352
|
-
return realReleaseLock(claim, deps);
|
|
353
|
-
},
|
|
354
|
-
spawnOverrides: {
|
|
355
|
-
resolveIdentity: (plan) => {
|
|
356
|
-
const launch = resolveResumeLaunchIdentity(plan);
|
|
357
|
-
return {
|
|
358
|
-
...launch,
|
|
359
|
-
explicitExtensionArgs: [...REPO_EXTENSION_ARGS, ...launch.explicitExtensionArgs],
|
|
360
|
-
};
|
|
361
|
-
},
|
|
362
|
-
},
|
|
363
|
-
},
|
|
364
|
-
});
|
|
365
|
-
|
|
366
|
-
const userNeedle = `V2_SPAWN_RESUME_USER_${nonce}`;
|
|
367
|
-
const okNeedle = `V2_SPAWN_RESUME_OK_${nonce}`;
|
|
368
|
-
// `pollForNonce` scans the stringified entry regardless of role, so the USER prompt must NOT
|
|
369
|
-
// contain the contiguous okNeedle — otherwise the assistant poll passes the instant the user
|
|
370
|
-
// turn is appended (a false "model replied", GPT blocker 1). Ask the model to CONCATENATE the
|
|
371
|
-
// two parts with no separator so only a real assistant turn yields the contiguous okNeedle.
|
|
372
|
-
const resumeMessage =
|
|
373
|
-
`${userNeedle}\n` +
|
|
374
|
-
`Reply with exactly these two parts concatenated, no separator and nothing else: ` +
|
|
375
|
-
`the literal "V2_SPAWN_RESUME_OK_" then "${nonce}".`;
|
|
376
|
-
ok("resume prompt does not contain the contiguous assistant OK nonce", !resumeMessage.includes(okNeedle));
|
|
377
|
-
const result: EntwurfV2RunResult = await runEntwurfV2(
|
|
378
|
-
{ target: gid, intent: "owned-outcome", message: resumeMessage },
|
|
379
|
-
prodDeps,
|
|
380
|
-
);
|
|
381
|
-
|
|
382
|
-
// ── result PASS = executed / spawn-bg / socket-alive / released (F5 + GPT pin 4) ──
|
|
383
|
-
ok(
|
|
384
|
-
"owned-outcome on a dormant pi citizen executed a spawn-bg resume",
|
|
385
|
-
result.kind === "executed" && result.transport === "spawn-bg",
|
|
386
|
-
);
|
|
387
|
-
if (result.kind !== "executed" || result.outcome.transport !== "spawn-bg") {
|
|
388
|
-
throw new Error("spawn-resume-live: result was not an executed spawn-bg outcome (see asserts above).");
|
|
389
|
-
}
|
|
390
|
-
const sb = result.outcome.result;
|
|
391
|
-
// GPT blocker 2: capture any available pid BEFORE the socket-alive narrowing throws — a
|
|
392
|
-
// `lock-retained` outcome carries `diagnostic.pid` of a child the watcher killed but could
|
|
393
|
-
// not confirm dead, so a fail-path throw must still hand the finally a pid to reap.
|
|
394
|
-
const maybePid =
|
|
395
|
-
"pid" in sb && typeof sb.pid === "number"
|
|
396
|
-
? sb.pid
|
|
397
|
-
: sb.kind === "lock-retained" && typeof sb.diagnostic.pid === "number"
|
|
398
|
-
? sb.diagnostic.pid
|
|
399
|
-
: null;
|
|
400
|
-
if (maybePid !== null && maybePid > 0) {
|
|
401
|
-
childPid = maybePid;
|
|
402
|
-
artifacts["childPid"] = String(maybePid);
|
|
403
|
-
}
|
|
404
|
-
ok(
|
|
405
|
-
"spawn-bg observed socket-alive (resident lifecycle, not child-exited/lock-retained)",
|
|
406
|
-
sb.kind === "socket-alive",
|
|
407
|
-
);
|
|
408
|
-
if (sb.kind !== "socket-alive") {
|
|
409
|
-
throw new Error(`spawn-resume-live: spawn-bg result was ${sb.kind}, not socket-alive.`);
|
|
410
|
-
}
|
|
411
|
-
ok("spawn-bg released the lock on socket-alive", sb.released === true);
|
|
412
|
-
const pid = sb.pid;
|
|
413
|
-
ok("resident pid is a real live pid", typeof pid === "number" && pid > 0);
|
|
414
|
-
|
|
415
|
-
// release accounting (F2): exactly one release, for THIS gid, and no lock file remains.
|
|
416
|
-
ok("lock released exactly once", releaseCount === 1);
|
|
417
|
-
ok("the single release was for the target gid", releasedGids.length === 1 && releasedGids[0] === gid);
|
|
418
|
-
ok("no lock file remains for the target", !existsSync(lockPathFor(gid, lockDir)));
|
|
419
|
-
|
|
420
|
-
// resident "still standing" re-check (GPT pin 4): pid alive AND socket connectable-present.
|
|
421
|
-
ok("resident pid is still alive after the watcher returned", childPid !== null && pidAlive(childPid));
|
|
422
|
-
ok("resident control socket is present at the canonical path", existsSync(sockPath));
|
|
423
|
-
|
|
424
|
-
// ── 5. the child actually resumed AND did a model turn (the acceptance core) ──
|
|
425
|
-
const userSeen = await pollForNonce(seedFile as string, seedLineCount, userNeedle, USER_TURN_TIMEOUT_MS);
|
|
426
|
-
ok("resume USER nonce appended to the session (the turn was injected)", userSeen);
|
|
427
|
-
const okSeen = await pollForNonce(seedFile as string, seedLineCount, okNeedle, ASSISTANT_TURN_TIMEOUT_MS);
|
|
428
|
-
ok("resume ASSISTANT OK nonce appended (the model actually replied — real work)", okSeen);
|
|
429
|
-
|
|
430
|
-
succeeded = true;
|
|
431
|
-
console.log(
|
|
432
|
-
`\nsmoke-entwurf-v2-spawn-resume-live: ${passed} checks passed (real pi resume + model turn + lock release)`,
|
|
433
|
-
);
|
|
434
|
-
} catch (err) {
|
|
435
|
-
console.error("\n[smoke-entwurf-v2-spawn-resume-live] FAILED — diagnostic artifacts:");
|
|
436
|
-
for (const [k, v] of Object.entries(artifacts)) console.error(` ${k} = ${v}`);
|
|
437
|
-
console.error(` releaseCount = ${releaseCount} releasedGids = ${JSON.stringify(releasedGids)}`);
|
|
438
|
-
if (seedStderrTail.trim())
|
|
439
|
-
console.error(` seed pi stdout/stderr (tail):\n${seedStderrTail.replace(/^/gm, " ")}`);
|
|
440
|
-
throw err;
|
|
441
|
-
} finally {
|
|
442
|
-
// reap the detached resident + its real socket (always — even on a clean pass).
|
|
443
|
-
if (childPid !== null && gid) {
|
|
444
|
-
await terminateResident(childPid, path.join(REAL_CONTROL_DIR, `${gid}${SOCKET_SUFFIX}`)).catch(() => {});
|
|
445
|
-
}
|
|
446
|
-
if (gid) {
|
|
447
|
-
await fsp.rm(path.join(REAL_CONTROL_DIR, `${gid}${SOCKET_SUFFIX}`), { force: true }).catch(() => {});
|
|
448
|
-
}
|
|
449
|
-
if (succeeded) {
|
|
450
|
-
// delete ONLY the one seed session file we created (never the real sessions dir).
|
|
451
|
-
if (seedFile) {
|
|
452
|
-
await fsp.rm(seedFile, { force: true }).catch(() => {});
|
|
453
|
-
// pi nests the seed under a cwd-hash subdir keyed by our unique temp cwd, so removing
|
|
454
|
-
// it IF-empty leaves no litter and can never touch another session (rmdir refuses a
|
|
455
|
-
// non-empty dir — belt for a surprise sibling file).
|
|
456
|
-
await fsp.rmdir(path.dirname(seedFile)).catch(() => {});
|
|
457
|
-
}
|
|
458
|
-
await fsp.rm(tmp, { recursive: true, force: true }).catch(() => {});
|
|
459
|
-
} else {
|
|
460
|
-
console.error(`[smoke-entwurf-v2-spawn-resume-live] temp world preserved for inspection: ${tmp}`);
|
|
461
|
-
if (seedFile) console.error(`[smoke-entwurf-v2-spawn-resume-live] seed session PRESERVED: ${seedFile}`);
|
|
462
|
-
}
|
|
463
|
-
}
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
main().catch((err) => {
|
|
467
|
-
console.error(err);
|
|
468
|
-
process.exit(1);
|
|
469
|
-
});
|