@junghanacs/entwurf 0.13.1 → 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 +48 -15
- package/BASELINE.md +3 -3
- package/CHANGELOG.md +45 -0
- package/CONTRIBUTING.md +13 -9
- package/DELIVERY.md +7 -6
- package/README.md +27 -27
- package/VERIFY.md +22 -14
- 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 +9 -4
- package/docs/external-mcp-host.md +4 -5
- package/docs/setup-clean-host.md +8 -7
- package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +148 -28
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/overlay.js +8 -6
- 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 +164 -28
- 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 +22 -11
- package/pi-extensions/entwurf-control.ts +218 -40
- package/pi-extensions/lib/acp/backend.ts +71 -12
- package/pi-extensions/lib/acp/overlay.ts +8 -6
- 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/run.sh +471 -201
- package/scripts/agy-bridge-config.py +5 -1
- package/scripts/check-acp-backend-preflight.ts +1 -1
- package/scripts/check-acp-overlay.ts +13 -3
- 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 +157 -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 +98 -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 +84 -76
- package/scripts/check-release-gate-outcomes.ts +127 -7
- 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/mutation-qualify.ts +109 -3
- package/scripts/meta-bridge-doctor.sh +6 -8
- package/scripts/meta-facts.ts +60 -0
- package/scripts/mutants/acp-overlay.json +17 -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 +0 -1037
- package/scripts/mutants/release-gate.json +35 -0
- package/scripts/mutants/resume-args.json +76 -0
- package/scripts/mutants/resume-launch-identity.json +96 -0
- package/scripts/mutants/v2-surface.json +58 -18
- package/scripts/mutants/v2-visible-resume.json +215 -0
- package/scripts/new-session-id.ts +9 -4
- package/scripts/smoke-acp-raw-turn-live.ts +1 -1
- package/scripts/smoke-agy-native-push-live.ts +6 -17
- package/scripts/smoke-entwurf-v2-matrix-live.ts +1 -1
- package/scripts/smoke-meta-honesty.sh +1 -1
- 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 -275
- 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 -467
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
// exactly what this gate must not make. They are cheap — each smoke declines
|
|
33
33
|
// before it does any work.
|
|
34
34
|
//
|
|
35
|
-
// Pure + subprocess, no network/model — IN pnpm check.
|
|
35
|
+
// Pure + subprocess, no network/model — IN pnpm run check:full.
|
|
36
36
|
|
|
37
37
|
import { strict as assert } from "node:assert";
|
|
38
38
|
import { execFileSync } from "node:child_process";
|
|
@@ -243,8 +243,8 @@ function runSubcommand(sub: string, env: Record<string, string | undefined>): {
|
|
|
243
243
|
viaWrapper.code,
|
|
244
244
|
LIVE_SKIP_EXIT,
|
|
245
245
|
`[QK:WRAPPER-SKIP-IS-PROTOCOL-EXIT] a run.sh smoke WRAPPER that declines a prerequisite must return the protocol ` +
|
|
246
|
-
`SKIP code, not 0 — got exit ${viaWrapper.code}. The wrapper is a skip surface of its own (cortex
|
|
247
|
-
"
|
|
246
|
+
`SKIP code, not 0 — got exit ${viaWrapper.code}. The wrapper is a skip surface of its own (cortex and matrix ` +
|
|
247
|
+
"both decline before their .ts is ever reached), so fixing only the TypeScript " +
|
|
248
248
|
`half would leave the aggregate counting those as PASS. Output: ${JSON.stringify(viaWrapper.output.slice(-300))}`,
|
|
249
249
|
);
|
|
250
250
|
|
|
@@ -269,7 +269,9 @@ function runSubcommand(sub: string, env: Record<string, string | undefined>): {
|
|
|
269
269
|
// the gate RUNS; it says nothing about steps the gate never lists. Three LIVE
|
|
270
270
|
// smokes (cortex, spawn-live, claude-native-resume) sat outside the aggregate
|
|
271
271
|
// with no stated reason until 2026-07-31, so a green cut was silent about the
|
|
272
|
-
// second backend, the spawn substrate, and native resume.
|
|
272
|
+
// second backend, the spawn substrate, and native resume. (spawn-live was
|
|
273
|
+
// deleted with its transport in the visible-first cut; the incident is kept
|
|
274
|
+
// verbatim because it is what this rule was derived from.)
|
|
273
275
|
//
|
|
274
276
|
// So: every LIVE smoke is either WIRED into release_gate or EXCLUDED for a
|
|
275
277
|
// reason an operator can read in the docs. The exclusion half is checked
|
|
@@ -284,13 +286,14 @@ function runSubcommand(sub: string, env: Record<string, string | undefined>): {
|
|
|
284
286
|
// name → the sentence in the docs that carries its exclusion, and where.
|
|
285
287
|
const DOCUMENTED_EXCLUSIONS: Record<string, [file: string, sentence: string]> = {
|
|
286
288
|
"smoke-acp-long-turn-live": ["VERIFY.md", "on-demand, not part of `release-gate`"],
|
|
289
|
+
"smoke-mux-fresh-call-live": ["VERIFY.md", "Fresh-call LIVE is on-demand, not part of `release-gate`"],
|
|
287
290
|
"smoke-agy-native-push-live": ["VERIFY.md", "Aggregate release-gate does not own an agy conversation id"],
|
|
288
291
|
"smoke-acp-ordering-probe-live": ["docs/acp-backend-rail.md", "opt-in paired observation"],
|
|
289
292
|
// Cortex needs an external Snowflake connection the HOST owns, so an aggregate
|
|
290
293
|
// that required it would block every cut taken without that account. Excluded —
|
|
291
294
|
// NOT waived: its direct call stays required for a Cortex-rail cut, and running
|
|
292
295
|
// it without the connection still reports protocol SKIP rather than a pass.
|
|
293
|
-
"smoke-acp-cortex-live": ["VERIFY.md", "The
|
|
296
|
+
"smoke-acp-cortex-live": ["VERIFY.md", "The release aggregate does not re-certify Cortex"],
|
|
294
297
|
};
|
|
295
298
|
|
|
296
299
|
const allLive = globSync("scripts/smoke-*live*.{ts,sh}", { cwd: REPO_DIR })
|
|
@@ -309,7 +312,7 @@ function runSubcommand(sub: string, env: Record<string, string | undefined>): {
|
|
|
309
312
|
excused,
|
|
310
313
|
`[QK:NO-SILENT-AGGREGATE-OMISSION] ${name} is neither wired into release_gate nor excluded in the docs. ` +
|
|
311
314
|
"A LIVE smoke that exists but is never listed makes a green cut silent about the axis it covers — exactly " +
|
|
312
|
-
"how cortex (the second shipped backend)
|
|
315
|
+
"how cortex (the second shipped backend) and claude-native-resume went unrun for releases. " +
|
|
313
316
|
"Wire it, or state the exclusion where an operator reads it.",
|
|
314
317
|
);
|
|
315
318
|
const [file, sentence] = excused as [string, string];
|
|
@@ -321,6 +324,121 @@ function runSubcommand(sub: string, env: Record<string, string | undefined>): {
|
|
|
321
324
|
}
|
|
322
325
|
}
|
|
323
326
|
|
|
327
|
+
// ===========================================================================
|
|
328
|
+
// 8. qualification scheduling topology — the subtraction has its own oracle.
|
|
329
|
+
// check-gate-qualification left the default check chains (operator
|
|
330
|
+
// inner-loop cost, 2026-08 subtraction). That move is a gate/release
|
|
331
|
+
// contract: the step must stay REACHABLE on the axes that now own it — the
|
|
332
|
+
// CI check job on every push and release_gate as its own MUST step — and
|
|
333
|
+
// must not silently return to the default chain. Without this cell,
|
|
334
|
+
// deleting the release_gate qualification block or the CI line leaves every
|
|
335
|
+
// focused gate green while a cut quietly loses its discriminating-power
|
|
336
|
+
// step.
|
|
337
|
+
//
|
|
338
|
+
// TWO claims, because #70 added a second independent contract here. 8a is
|
|
339
|
+
// REACHABILITY (absent from the default chain; present exactly once in CI and
|
|
340
|
+
// exactly once as a wired release_gate MUST step; named in VERIFY). 8b is what
|
|
341
|
+
// the CI step qualifies (the FULL floor, before the qualification run). Each
|
|
342
|
+
// carries its own replant — one mutant must never stand in for both.
|
|
343
|
+
// ===========================================================================
|
|
344
|
+
{
|
|
345
|
+
// The default chain is tiered (#70): `check` (core) and `check:full` compose the
|
|
346
|
+
// named group scripts, so the reachability scan covers EVERY check* script —
|
|
347
|
+
// qualification sneaking into any group re-doubles the closure floor.
|
|
348
|
+
const pkgScripts = (
|
|
349
|
+
JSON.parse(readFileSync(join(REPO_DIR, "package.json"), "utf8")) as { scripts: Record<string, string> }
|
|
350
|
+
).scripts;
|
|
351
|
+
const pkgCheck = Object.entries(pkgScripts)
|
|
352
|
+
.filter(([name]) => name === "check" || name.startsWith("check:"))
|
|
353
|
+
.map(([, body]) => body)
|
|
354
|
+
.join(" && ");
|
|
355
|
+
const ciYml = readFileSync(join(REPO_DIR, ".github/workflows/ci.yml"), "utf8");
|
|
356
|
+
const ciHits = ciYml.split("- run: ./run.sh check-gate-qualification").length - 1;
|
|
357
|
+
const runShQ = readFileSync(join(REPO_DIR, "run.sh"), "utf8");
|
|
358
|
+
const qualGateBody = runShQ.slice(runShQ.indexOf("release_gate() {"), runShQ.indexOf("# 5. Summary"));
|
|
359
|
+
const invocations = qualGateBody.split('bash "$self" check-gate-qualification').length - 1;
|
|
360
|
+
const verifyDoc = readFileSync(join(REPO_DIR, "VERIFY.md"), "utf8");
|
|
361
|
+
|
|
362
|
+
// One claim token, one assert (the qualification runner requires the exact-once
|
|
363
|
+
// signature); each broken axis names itself in the joined message.
|
|
364
|
+
const holes: string[] = [];
|
|
365
|
+
if (pkgCheck.includes("check-gate-qualification"))
|
|
366
|
+
holes.push(
|
|
367
|
+
"package.json's default `check` chain contains check-gate-qualification again (doubles every closure floor)",
|
|
368
|
+
);
|
|
369
|
+
if (ciHits !== 1) holes.push(`the CI check job runs check-gate-qualification ${ciHits}x (need exactly once)`);
|
|
370
|
+
if (invocations !== 1)
|
|
371
|
+
holes.push(`release_gate invokes check-gate-qualification ${invocations}x (need exactly one MUST step)`);
|
|
372
|
+
if (
|
|
373
|
+
!qualGateBody.includes('results+=("PASS check-gate-qualification")') ||
|
|
374
|
+
!qualGateBody.includes('results+=("FAIL check-gate-qualification")')
|
|
375
|
+
)
|
|
376
|
+
holes.push("the release_gate qualification step does not wire PASS/FAIL into the MUST counters");
|
|
377
|
+
if (!verifyDoc.includes("in the CI `check` job on every push, and as a release-gate MUST step"))
|
|
378
|
+
holes.push("VERIFY.md no longer names the owners of the moved qualification step");
|
|
379
|
+
assert.ok(
|
|
380
|
+
holes.length === 0,
|
|
381
|
+
"[QK:QUALIFICATION-SCHEDULING-REACHABLE] check-gate-qualification left the default check chains " +
|
|
382
|
+
"deliberately, so it must stay REACHABLE on the axes that own it now — absent from the default chain, " +
|
|
383
|
+
"exactly once in the CI check job, exactly once as a release_gate MUST step with its " +
|
|
384
|
+
`outcome wired, and named in VERIFY. Broken: ${holes.join("; ")}`,
|
|
385
|
+
);
|
|
386
|
+
|
|
387
|
+
// 8b. WHAT the CI qualification step qualifies — its own claim, not a branch
|
|
388
|
+
// folded into the one above. #70 split the deterministic floor into tiers,
|
|
389
|
+
// so "CI runs qualification once" stopped being sufficient evidence on its
|
|
390
|
+
// own: qualification on top of the ≤60s core would certify kill-power over
|
|
391
|
+
// a floor no candidate ships on. This is ONE contract with one condition —
|
|
392
|
+
// the full floor is present in the CI check job AND qualification follows
|
|
393
|
+
// it — because a floor that is absent and a floor that runs afterwards
|
|
394
|
+
// break the same promise identically. The committed replant qualifies the
|
|
395
|
+
// downgrade/omission axis (the tier rename downgrading CI back to core);
|
|
396
|
+
// ordering stays directly asserted here and has no incident-earned mutant.
|
|
397
|
+
const ciFloorAt = ciYml.indexOf("- run: pnpm run check:full");
|
|
398
|
+
const ciQualAt = ciYml.indexOf("- run: ./run.sh check-gate-qualification");
|
|
399
|
+
assert.ok(
|
|
400
|
+
ciFloorAt !== -1 && ciQualAt > ciFloorAt,
|
|
401
|
+
"[QK:CI-FULL-FLOOR-QUALIFIED] the CI check job owns the FULL deterministic floor (#70): it must run " +
|
|
402
|
+
"`pnpm run check:full` — not the ≤60s core — and check-gate-qualification must come AFTER it, so the " +
|
|
403
|
+
"kill-power proof covers the floor a candidate actually ships on. The committed replant qualifies the " +
|
|
404
|
+
"downgrade/omission axis; ordering is directly asserted by this same oracle. " +
|
|
405
|
+
`Broken: check:full at index ${ciFloorAt}, qualification at index ${ciQualAt}.`,
|
|
406
|
+
);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// ===========================================================================
|
|
410
|
+
// The integrated mux lifecycle is a MUST, and the focused fresh-call LIVE is not
|
|
411
|
+
//
|
|
412
|
+
// Two LIVE smokes now cover the mux rail and they are NOT interchangeable:
|
|
413
|
+
// `smoke-mux-fresh-call-live` drives the composition from SOURCE and is on-demand,
|
|
414
|
+
// while `smoke-mux-lifecycle-live` enters through the real MCP surface and follows a
|
|
415
|
+
// citizen through resume and recall. A cut that ran only the focused one would be
|
|
416
|
+
// green about the axis nobody proved. So the MUST wiring is pinned here, exactly
|
|
417
|
+
// once, together with the doc sentence that keeps the two apart for an operator.
|
|
418
|
+
// ===========================================================================
|
|
419
|
+
{
|
|
420
|
+
const runSh = readFileSync(join(REPO_DIR, "run.sh"), "utf8");
|
|
421
|
+
const gateBody = runSh.slice(runSh.indexOf("release_gate() {"), runSh.indexOf("# 5. Summary"));
|
|
422
|
+
const verify = readFileSync(join(REPO_DIR, "VERIFY.md"), "utf8");
|
|
423
|
+
const mustSteps = gateBody.split("\n").filter((l) => l.includes('"$self" smoke-mux-lifecycle-live'));
|
|
424
|
+
const gaps: string[] = [];
|
|
425
|
+
if (mustSteps.length !== 1)
|
|
426
|
+
gaps.push(`release_gate runs smoke-mux-lifecycle-live ${mustSteps.length}x (need exactly one MUST step)`);
|
|
427
|
+
if (mustSteps.length === 1 && !mustSteps[0]?.includes("run_live_step"))
|
|
428
|
+
gaps.push("the lifecycle step does not go through run_live_step, so its SKIP would not reach the classifier");
|
|
429
|
+
if (!verify.includes("Fresh-call LIVE is on-demand, not part of `release-gate`"))
|
|
430
|
+
gaps.push("VERIFY.md no longer excuses the focused fresh-call LIVE from the aggregate");
|
|
431
|
+
if (!verify.includes("smoke-mux-lifecycle-live"))
|
|
432
|
+
gaps.push("VERIFY.md does not name the integrated lifecycle MUST at all");
|
|
433
|
+
assert.ok(
|
|
434
|
+
gaps.length === 0,
|
|
435
|
+
"[QK:MUX-LIFECYCLE-IS-RELEASE-MUST] the integrated mux lifecycle LIVE must be a release-gate MUST exactly once " +
|
|
436
|
+
"and go through run_live_step (so a missing prerequisite is a SKIP the cut refuses, never a silent pass), " +
|
|
437
|
+
"while the focused fresh-call LIVE stays on-demand and VERIFY keeps the two distinguishable — a cut that " +
|
|
438
|
+
`ran only the source-level smoke would be green about the surface no one entered. Broken: ${gaps.join("; ")}`,
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
|
|
324
442
|
console.log(
|
|
325
443
|
"[check-release-gate-outcomes] ok — STEP OUTCOME protocol: one skip exit code shared by the shell and TS halves " +
|
|
326
444
|
`(${LIVE_SKIP_EXIT}, clear of the per-tool 0..4 and shell 126+ bands), classifier maps 0→PASS / skip→SKIP / ` +
|
|
@@ -328,5 +446,7 @@ console.log(
|
|
|
328
446
|
"exit 0, no LIVE smoke still carries the pre-P1 exit-0 skip shape, and both real skip surfaces were INVOKED and " +
|
|
329
447
|
"observed to propagate the code: a smoke with LIVE unset (through run_ts, with its operator-readable marker) and " +
|
|
330
448
|
"a run.sh wrapper declining its own prerequisite (including the measured LIVE=1 no-cortex-connection cell); and every " +
|
|
331
|
-
"LIVE smoke is either wired into release_gate or excluded by a sentence the docs still carry"
|
|
449
|
+
"LIVE smoke is either wired into release_gate or excluded by a sentence the docs still carry; and the moved " +
|
|
450
|
+
"check-gate-qualification stays reachable on its owners (absent from the default chain, exactly once in CI, " +
|
|
451
|
+
"exactly once as a release-gate MUST step) and the CI step qualifies the FULL floor, which runs before it",
|
|
332
452
|
);
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* check-resume-launch-identity — deterministic gate for `resume-launch-identity.ts`, the
|
|
3
|
+
* record-authoritative leaf preserved when the visible-first cut deleted `spawn-bg` and every
|
|
4
|
+
* caller it had.
|
|
5
|
+
*
|
|
6
|
+
* WHY A GATE FOR A CONSUMER-ZERO MODULE. Identity authority is a fail-closed risk class: this
|
|
7
|
+
* leaf's whole job is deciding WHICH being a resume turn lands in. A preserved leaf whose only
|
|
8
|
+
* guarantee is its own header comment is exactly the rot preservation was meant to prevent — it
|
|
9
|
+
* would be re-adopted, months later, by a visible-resume composition that trusts prose. The cells
|
|
10
|
+
* below are transplanted from the deleted `check-entwurf-v2-spawn-production` §9 (the plan
|
|
11
|
+
* parameter became a bare garden id; nothing else about the identity contract changed), plus one
|
|
12
|
+
* new cell for the #52 read the leaf's header claims and the deleted gate never isolated.
|
|
13
|
+
*
|
|
14
|
+
* What is certified, all through a temp `ENTWURF_META_SESSIONS_DIR` fixture store:
|
|
15
|
+
*
|
|
16
|
+
* 1. HAPPY — gardenId → record.transcriptPath → header id === record.nativeSessionId →
|
|
17
|
+
* LaunchIdentity carrying the header's own cwd/provider/model (#50 C2, #9).
|
|
18
|
+
* 2. C3 INTEGRITY — a transcript whose header id is NOT this citizen's `nativeSessionId` is
|
|
19
|
+
* refused, never resumed. This is the one that keeps a turn out of another being's session.
|
|
20
|
+
* 3. #52 ADDRESSABLE READ — a garden id that does not hold its `nativeSessionId` ALONE is
|
|
21
|
+
* refused. Two records sharing one native session would each pass their own per-record
|
|
22
|
+
* integrity check and resume one transcript twice under two per-garden-id locks, so the
|
|
23
|
+
* plain targeted read is not enough here and the leaf must not be "simplified" back to it.
|
|
24
|
+
* 4. CAUSE FIDELITY — each impossible resume names ITS OWN cause: absent record, out-of-domain
|
|
25
|
+
* backend, no recorded transcript, recorded-but-missing file, no model, headerless file. The
|
|
26
|
+
* missing-file cell is a regression pin: before the existence check that case fell through
|
|
27
|
+
* `readSessionIdentity`'s ENOENT swallow and lied "no recorded model".
|
|
28
|
+
* 4b. THE ONE EXPECTED REFUSAL — a backend with no same-id resume is not a defect at all: the
|
|
29
|
+
* record is fine and the operator reached past a capability boundary. It alone carries a
|
|
30
|
+
* TYPED reason (`target-not-pi`) so the visible-resume composition can match a FIELD instead
|
|
31
|
+
* of parsing a sentence or re-reading the record. Everything else here stays a bare throw.
|
|
32
|
+
* 4c. ABSOLUTE TRANSCRIPT — a relative recorded path would resolve against the WINDOW's own cwd
|
|
33
|
+
* at launch, not against this process's, so it names a different file than the one checked
|
|
34
|
+
* here. Refused before anything opens.
|
|
35
|
+
* 5. SSOT — the module header names THIS gate, so the preserved leaf cannot advertise a
|
|
36
|
+
* certification that does not exist.
|
|
37
|
+
*
|
|
38
|
+
* No spawn, no socket, no timer: the launch this identity feeds is deliberately not in scope.
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
import assert from "node:assert/strict";
|
|
42
|
+
import * as fs from "node:fs";
|
|
43
|
+
import * as os from "node:os";
|
|
44
|
+
import * as path from "node:path";
|
|
45
|
+
import { serializeMetaIdentity, upsertMetaSession } from "../pi-extensions/lib/meta-session.ts";
|
|
46
|
+
import {
|
|
47
|
+
ResumeBackendUnsupportedError,
|
|
48
|
+
resolveResumeLaunchIdentity,
|
|
49
|
+
} from "../pi-extensions/lib/resume-launch-identity.ts";
|
|
50
|
+
|
|
51
|
+
let passed = 0;
|
|
52
|
+
function ok(label: string, cond: boolean): void {
|
|
53
|
+
assert.ok(cond, label);
|
|
54
|
+
console.log(` ok ${label}`);
|
|
55
|
+
passed++;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const HERE = path.dirname(new URL(import.meta.url).pathname);
|
|
59
|
+
const REPO = path.join(HERE, "..");
|
|
60
|
+
const LEAF_SRC = path.join(REPO, "pi-extensions/lib/resume-launch-identity.ts");
|
|
61
|
+
|
|
62
|
+
function main(): void {
|
|
63
|
+
const world = fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), "resume-launch-id-"));
|
|
64
|
+
const storeDir = path.join(world, "meta-sessions");
|
|
65
|
+
fs.mkdirSync(storeDir, { recursive: true });
|
|
66
|
+
const prevStore = process.env.ENTWURF_META_SESSIONS_DIR;
|
|
67
|
+
process.env.ENTWURF_META_SESSIONS_DIR = storeDir;
|
|
68
|
+
try {
|
|
69
|
+
const cwd = path.join(world, "repo");
|
|
70
|
+
fs.mkdirSync(cwd, { recursive: true });
|
|
71
|
+
const sessionLine = (id: string) => `${JSON.stringify({ type: "session", id, cwd })}\n`;
|
|
72
|
+
const modelLine = `${JSON.stringify({ type: "model_change", provider: "openai-codex", modelId: "gpt-5.4" })}\n`;
|
|
73
|
+
const writeTranscript = (name: string, content: string): string => {
|
|
74
|
+
const p = path.join(world, name);
|
|
75
|
+
fs.writeFileSync(p, content);
|
|
76
|
+
return p;
|
|
77
|
+
};
|
|
78
|
+
const mintRecord = (nativeSessionId: string, transcriptPath: string | null, backend = "pi"): string =>
|
|
79
|
+
upsertMetaSession({
|
|
80
|
+
input: { backend: backend as "pi", nativeSessionId, cwd, model: "gpt-5.4", transcriptPath },
|
|
81
|
+
dir: storeDir,
|
|
82
|
+
}).record.gardenId;
|
|
83
|
+
const rejects = (gid: string, needle: string, label: string): void => {
|
|
84
|
+
let msg = "";
|
|
85
|
+
try {
|
|
86
|
+
resolveResumeLaunchIdentity(gid);
|
|
87
|
+
} catch (e) {
|
|
88
|
+
msg = e instanceof Error ? e.message : String(e);
|
|
89
|
+
}
|
|
90
|
+
ok(label, msg.includes(needle));
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
// ── 1. HAPPY ──────────────────────────────────────────────────────────────
|
|
94
|
+
const nativeOk = "0199aaaa-1111-4222-8333-444455556666";
|
|
95
|
+
const fileOk = writeTranscript("own.jsonl", sessionLine(nativeOk) + modelLine);
|
|
96
|
+
const gidOk = mintRecord(nativeOk, fileOk);
|
|
97
|
+
const launch = resolveResumeLaunchIdentity(gidOk);
|
|
98
|
+
ok("1: record-backed resume resolves the recorded transcript", launch.sessionFile === fileOk);
|
|
99
|
+
ok("1: resume cwd = transcript header authority (#9 cold-resume)", launch.cwd === cwd);
|
|
100
|
+
ok(
|
|
101
|
+
"1: resume model/provider = first model_change",
|
|
102
|
+
launch.model === "gpt-5.4" && launch.provider === "openai-codex",
|
|
103
|
+
);
|
|
104
|
+
// (No arity/shape assertion here on purpose: "the garden id is the only input" is carried
|
|
105
|
+
// by the type system and by every cell below actually resolving through the record.
|
|
106
|
+
// A `fn.length === 1` check would be a claim whose mutant could only be synthetic.)
|
|
107
|
+
|
|
108
|
+
// ── 2. C3 INTEGRITY — header id must be THIS citizen's ────────────────────
|
|
109
|
+
const nativeMine = "0199bbbb-1111-4222-8333-444455556666";
|
|
110
|
+
const foreignFile = writeTranscript(
|
|
111
|
+
"foreign.jsonl",
|
|
112
|
+
sessionLine("0199cccc-9999-4999-8999-999999999999") + modelLine,
|
|
113
|
+
);
|
|
114
|
+
const gidForeign = mintRecord(nativeMine, foreignFile);
|
|
115
|
+
rejects(
|
|
116
|
+
gidForeign,
|
|
117
|
+
"does not match the record's nativeSessionId",
|
|
118
|
+
"2: header ≠ nativeSessionId → refused, never resumed (C3) [QK:RESUME-ID-HEADER-INTEGRITY]",
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
// ── 4. CAUSE FIDELITY ─────────────────────────────────────────────────────
|
|
122
|
+
rejects("20260101T000000-facade", "not a garden citizen", "4: recordless gid → not a citizen");
|
|
123
|
+
|
|
124
|
+
const gidClaude = mintRecord("claude-native-1", null, "claude-code");
|
|
125
|
+
rejects(
|
|
126
|
+
gidClaude,
|
|
127
|
+
"is a claude-code citizen",
|
|
128
|
+
"4: out-of-domain citizen → refused as a capability-domain miss, not as citizen rank",
|
|
129
|
+
);
|
|
130
|
+
// This one refusal is EXPECTED, not a defect: the record is perfectly good and the operator
|
|
131
|
+
// has reached past a capability boundary. So it is the only failure here that carries a
|
|
132
|
+
// typed `reason` a caller can match on a FIELD — the visible-resume composition turns it
|
|
133
|
+
// into `target-not-pi`, and if it degraded to a bare Error that caller would either parse
|
|
134
|
+
// this sentence or read the record a second time to ask which backend it was.
|
|
135
|
+
{
|
|
136
|
+
let caught: Error | null = null;
|
|
137
|
+
try {
|
|
138
|
+
resolveResumeLaunchIdentity(gidClaude);
|
|
139
|
+
} catch (err) {
|
|
140
|
+
caught = err as Error;
|
|
141
|
+
}
|
|
142
|
+
ok(
|
|
143
|
+
"4: the backend miss is a TYPED refusal carrying reason + backend, not a bare Error [QK:RESUME-ID-BACKEND-TYPED-REFUSAL]",
|
|
144
|
+
caught instanceof ResumeBackendUnsupportedError &&
|
|
145
|
+
caught.reason === "target-not-pi" &&
|
|
146
|
+
caught.backend === "claude-code",
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// A RELATIVE recorded path passes existsSync here whenever this process happens to sit in
|
|
151
|
+
// the right directory, but the launch resolves `--session` inside the window's own
|
|
152
|
+
// `-c <record cwd>`. Two directories, two files, one receipt that looks correct — so the
|
|
153
|
+
// leaf refuses before anything opens rather than resuming an unknown transcript.
|
|
154
|
+
{
|
|
155
|
+
const nativeRel = "0199ffff-1111-4222-8333-444455556666";
|
|
156
|
+
writeTranscript("relative.jsonl", sessionLine(nativeRel) + modelLine);
|
|
157
|
+
const gidRel = mintRecord(nativeRel, "relative.jsonl");
|
|
158
|
+
rejects(
|
|
159
|
+
gidRel,
|
|
160
|
+
"RELATIVE transcriptPath",
|
|
161
|
+
"4: a relative recorded transcriptPath → refused before launch, because --session would resolve against the WINDOW's cwd, not this one [QK:RESUME-ID-SESSION-ABSOLUTE]",
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const gidNoFile = mintRecord("0199dddd-1111-4222-8333-444455556666", null);
|
|
166
|
+
rejects(gidNoFile, "no recorded transcriptPath", "4: transcriptPath null → nothing to resume");
|
|
167
|
+
|
|
168
|
+
// Regression pin (F7): before the existsSync guard this fell through readSessionIdentity's
|
|
169
|
+
// ENOENT swallow and reported "no recorded model" — a true refusal naming a false cause,
|
|
170
|
+
// which sends an operator to look for a model setting instead of a deleted transcript.
|
|
171
|
+
const gidGhost = mintRecord("0199abcd-1111-4222-8333-444455556666", path.join(world, "never-written.jsonl"));
|
|
172
|
+
rejects(
|
|
173
|
+
gidGhost,
|
|
174
|
+
"does not exist on disk",
|
|
175
|
+
"4: recorded transcript missing on disk → refused as MISSING, not as no-model [QK:RESUME-ID-MISSING-TRANSCRIPT-CAUSE]",
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
const nativeNoModel = "0199eeee-1111-4222-8333-444455556666";
|
|
179
|
+
const fileNoModel = writeTranscript("no-model.jsonl", sessionLine(nativeNoModel));
|
|
180
|
+
const gidNoModel = mintRecord(nativeNoModel, fileNoModel);
|
|
181
|
+
rejects(gidNoModel, "no recorded model", "4: no model_change → refused (model preservation)");
|
|
182
|
+
|
|
183
|
+
// A headerless transcript has an UNDEFINED header id, which can never equal the record's
|
|
184
|
+
// nativeSessionId — so the integrity check catches it and says "(none)" rather than
|
|
185
|
+
// resuming a file whose ownership was never stated.
|
|
186
|
+
const nativeNoHeader = "0199ffff-1111-4222-8333-444455556666";
|
|
187
|
+
const fileNoHeader = writeTranscript("no-header.jsonl", modelLine);
|
|
188
|
+
const gidNoHeader = mintRecord(nativeNoHeader, fileNoHeader);
|
|
189
|
+
rejects(gidNoHeader, '"(none)"', "4: headerless transcript → refused as (none), never resumed");
|
|
190
|
+
|
|
191
|
+
// ── 3. #52 ADDRESSABLE READ ───────────────────────────────────────────────
|
|
192
|
+
// Planted LAST so the duplicate cannot disturb the cells above: a store-wide uniqueness
|
|
193
|
+
// scan sees every record, and `upsertMetaSession` would attach to the existing garden id
|
|
194
|
+
// rather than mint a rival, so the rival record is written directly.
|
|
195
|
+
const rivalGid = "20260301T120009-a1b2c3";
|
|
196
|
+
fs.writeFileSync(
|
|
197
|
+
path.join(storeDir, `${rivalGid}.meta.json`),
|
|
198
|
+
serializeMetaIdentity({
|
|
199
|
+
schemaVersion: 3,
|
|
200
|
+
gardenId: rivalGid,
|
|
201
|
+
backend: "pi",
|
|
202
|
+
nativeSessionId: nativeOk, // the SAME native session as gidOk
|
|
203
|
+
cwd,
|
|
204
|
+
model: "gpt-5.4",
|
|
205
|
+
transcriptPath: fileOk,
|
|
206
|
+
createdAt: "2026-03-01T12:00:00.000Z",
|
|
207
|
+
recordUpdatedAt: "2026-03-01T12:30:00.000Z",
|
|
208
|
+
}),
|
|
209
|
+
);
|
|
210
|
+
// The happy-path id resolved a moment ago; the ONLY thing that changed is a second record
|
|
211
|
+
// claiming its native session. If this leaf used the plain targeted read, that resolve
|
|
212
|
+
// would still succeed and two garden ids would resume one transcript under two locks.
|
|
213
|
+
rejects(
|
|
214
|
+
gidOk,
|
|
215
|
+
"must be unique",
|
|
216
|
+
"3: a garden id that no longer holds its nativeSessionId alone → refused by the ADDRESSABLE read [QK:RESUME-ID-ADDRESSABLE-READ]",
|
|
217
|
+
);
|
|
218
|
+
rejects(
|
|
219
|
+
rivalGid,
|
|
220
|
+
"must be unique",
|
|
221
|
+
"3: …refused from EITHER side of the pair (neither rival is silently preferred)",
|
|
222
|
+
);
|
|
223
|
+
} finally {
|
|
224
|
+
if (prevStore === undefined) delete process.env.ENTWURF_META_SESSIONS_DIR;
|
|
225
|
+
else process.env.ENTWURF_META_SESSIONS_DIR = prevStore;
|
|
226
|
+
fs.rmSync(world, { recursive: true, force: true });
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// ── 5. SSOT — the leaf's header must name the gate that actually certifies it ──
|
|
230
|
+
{
|
|
231
|
+
const src = fs.readFileSync(LEAF_SRC, "utf8");
|
|
232
|
+
ok(
|
|
233
|
+
"5: the leaf's header names check-resume-launch-identity (no certification it does not have) [QK:RESUME-ID-GATE-SSOT]",
|
|
234
|
+
src.includes("`check-resume-launch-identity`"),
|
|
235
|
+
);
|
|
236
|
+
// The preservation is deliberate and stated: a reader who finds an uncalled export must
|
|
237
|
+
// be able to tell "kept on purpose, for the visible lane" from "forgotten".
|
|
238
|
+
ok("5: the leaf states it is preserved with no consumer, on purpose", /no consumer in the shipped tree/.test(src));
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
console.log(`\ncheck-resume-launch-identity: ${passed} checks passed`);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
main();
|
|
@@ -291,7 +291,7 @@ async function main(): Promise<void> {
|
|
|
291
291
|
}
|
|
292
292
|
|
|
293
293
|
// ── inspectControlSocketPath (5c-3c R1 — the path-addressed SSOT) ────────────────
|
|
294
|
-
//
|
|
294
|
+
// A resume watcher observes an expected socket path and MUST inspect THAT exact
|
|
295
295
|
// path (no gid re-derivation). Proves the path-taking core classifies every branch
|
|
296
296
|
// identically to the gid wrapper, and that the wrapper just feeds it controlSocketPath(gid).
|
|
297
297
|
{
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
{"type":"fixture-provenance","note":"Structure extracted from a fixture-only real Pi parent retake (2026-08-06) and then scrubbed: a real record-backed pi citizen called the native entwurf_fresh_call once, and the fresh sibling's nonce callback arrived in that parent's own transcript. MEASURED and preserved: pi records NO separate toolCall row — the call surfaces as a toolResult row carrying toolCallId + toolName, and the callback arrives later as a custom_message of customType entwurf-message whose body embeds a <sender_info> JSON envelope. Event order, roles, content types, toolName, the nonce flowing from the launch receipt into the callback body, and the sender_info FIELD NAMES are real. Every id, uuid, toolCallId, timestamp, model, path, cwd and message body is a fixture value. This is a sample of the PARENT-SIDE SHAPE — it is NOT placement evidence and says nothing about whether a window opened."}
|
|
2
|
+
{"type":"message","id":"0199f000-0000-7000-8000-000000000000-1","parentId":"0199f000-0000-7000-8000-000000000000-0","timestamp":"2026-01-01T00:00:00.000Z","message":{"role":"toolResult","toolCallId":"fixture-toolcall-1","toolName":"entwurf_fresh_call","content":[{"type":"text","text":"[entwurf fresh call →]\n backend: pi (/fixture/bin/pi)\n model: fixture-provider/fixture-model (requested on the runtime CLI)\n window: @2 (index 2) in session $0\n pane: %2 pid 4242\n nonce: mux-fresh-call-000000000000000000000000\n\nThis is a LAUNCH receipt: tmux created that window and was asked to start the runtime with the model above. It does NOT mean the sibling is running, that its first turn ran, or that the task was delivered.\nThe sibling's garden id arrives separately — it calls entwurf_v2 back with the nonce above as its first action, and the sender envelope of THAT message is the address. Nothing is polling for it; if it never comes, the window is visible and can be read directly."}],"details":{"isError":false},"isError":false,"timestamp":1767225600000}}
|
|
3
|
+
{"type":"custom_message","customType":"entwurf-message","content":"mux-fresh-call-000000000000000000000000\n\n<sender_info>{\"sessionId\":\"20260101T000001-bbbbbb\",\"agentId\":\"fixture-provider/fixture-model\",\"cwd\":\"/fixture/project\",\"timestamp\":\"2026-01-01T00:00:00.000Z\",\"origin\":\"pi-session\",\"replyable\":true}</sender_info>","display":true,"id":"0199f000-0000-7000-8000-000000000000-2","parentId":"0199f000-0000-7000-8000-000000000000-1","timestamp":"2026-01-01T00:00:00.000Z"}
|