@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
|
@@ -0,0 +1,445 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* check-entwurf-v2-visible-resume — deterministic gate for the visible-resume composition
|
|
3
|
+
* (`pi-extensions/lib/entwurf-v2-visible-resume.ts`).
|
|
4
|
+
*
|
|
5
|
+
* Every IO seam is injected, so this gate drives the whole state machine — including the timeout
|
|
6
|
+
* branch — without a tmux, a lock file, a socket or a clock.
|
|
7
|
+
*
|
|
8
|
+
* Block order is deliberate: the REFUSALS run before the happy path. A defect that makes the
|
|
9
|
+
* composition launch something it should have refused must be reported by the assertion that owns
|
|
10
|
+
* that rule, not by whichever happy-path expectation happens to notice first.
|
|
11
|
+
*
|
|
12
|
+
* V2RESUME-LIVE-REFUSED an already-live citizen is refused, never reopened — that is
|
|
13
|
+
* also what stops two windows existing for one garden id
|
|
14
|
+
* V2RESUME-INDETERMINATE-NO-START nothing is started when the probe cannot prove dormancy
|
|
15
|
+
* V2RESUME-CONFLICT-NO-START a corrupt control-socket address starts nothing
|
|
16
|
+
* V2RESUME-LOCKED-NO-RESOLVE a locked target refuses before identity is even read
|
|
17
|
+
* V2RESUME-NO-TRANSCRIPT-FAILS-LOUD a citizen with no transcript keeps the leaf's own cause and
|
|
18
|
+
* opens no window
|
|
19
|
+
* V2RESUME-LOCK-BEFORE-LIVENESS the lock is taken BEFORE the liveness question is asked
|
|
20
|
+
* V2RESUME-IDENTITY-BEFORE-WINDOW identity is resolved before the launcher is ever called
|
|
21
|
+
* V2RESUME-ARGV-FROM-RECORD the launcher gets the record's cwd and the measured argv
|
|
22
|
+
* V2RESUME-OBSERVE-IS-BOUNDED-WAIT the observation WAITS (measured: the socket appeared ~2–4s
|
|
23
|
+
* after launch, so an immediate single probe would report a
|
|
24
|
+
* successful resume as unobserved)
|
|
25
|
+
* V2RESUME-RECEIPTS-SEPARATE launch and observation are two objects, not one verdict
|
|
26
|
+
* V2RESUME-RENDER-SEPARATES the rendered text keeps them apart by name
|
|
27
|
+
* V2RESUME-UNOBSERVED-IS-REAL a timeout is ok:true + resume-unobserved: the window is real
|
|
28
|
+
* V2RESUME-TIMEOUT-RELEASES …and the lock is released, the pane untouched, nothing retried
|
|
29
|
+
* V2RESUME-RELEASE-FAILURE-LOUD a release that does not return "released" throws
|
|
30
|
+
* V2RESUME-NOT-PI-REFUSED an out-of-domain backend is a NAMED refusal, not an error
|
|
31
|
+
* V2RESUME-NOT-PI-BY-FIELD …matched on the error's reason FIELD, backend quoted from it
|
|
32
|
+
* V2RESUME-SHARED-ID-GRAMMAR the address grammar is the session-id SSOT, not a local copy
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
import assert from "node:assert/strict";
|
|
36
|
+
import fs from "node:fs";
|
|
37
|
+
import path from "node:path";
|
|
38
|
+
import { fileURLToPath } from "node:url";
|
|
39
|
+
import type { AcquireLockResult, LockClaim, ReleaseResult } from "../pi-extensions/lib/entwurf-v2-lock.ts";
|
|
40
|
+
import {
|
|
41
|
+
makeVisibleResumeDeps,
|
|
42
|
+
observeSameGidSocket,
|
|
43
|
+
RESUME_OBSERVE_INTERVAL_MS,
|
|
44
|
+
RESUME_OBSERVE_TIMEOUT_MS,
|
|
45
|
+
renderVisibleResume,
|
|
46
|
+
type VisibleResumeDeps,
|
|
47
|
+
type VisibleResumeResult,
|
|
48
|
+
visibleResume,
|
|
49
|
+
} from "../pi-extensions/lib/entwurf-v2-visible-resume.ts";
|
|
50
|
+
import { type LaunchIdentity, ResumeBackendUnsupportedError } from "../pi-extensions/lib/resume-launch-identity.ts";
|
|
51
|
+
import type { TargetSocketInspection } from "../pi-extensions/lib/socket-discovery.ts";
|
|
52
|
+
import type { SocketLiveness } from "../pi-extensions/lib/socket-probe.ts";
|
|
53
|
+
|
|
54
|
+
let passed = 0;
|
|
55
|
+
function ok(label: string, cond: boolean): void {
|
|
56
|
+
assert.ok(cond, label);
|
|
57
|
+
console.log(` ok ${label}`);
|
|
58
|
+
passed++;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
62
|
+
const read = (rel: string): string => fs.readFileSync(path.join(ROOT, rel), "utf8");
|
|
63
|
+
|
|
64
|
+
const GID = "20260806T183749-145a26";
|
|
65
|
+
const SOCK = `/home/op/.pi/entwurf-control/${GID}.sock`;
|
|
66
|
+
|
|
67
|
+
const IDENTITY: LaunchIdentity = {
|
|
68
|
+
sessionFile: "/home/op/.pi/agent/sessions/-p/2026-08-06T09-37-48-973Z_019fd66f.jsonl",
|
|
69
|
+
cwd: "/home/op/project",
|
|
70
|
+
explicitExtensionArgs: ["-e", "/opt/entwurf"],
|
|
71
|
+
provider: "entwurf",
|
|
72
|
+
model: "claude-sonnet-5",
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const CLAIM: LockClaim = {
|
|
76
|
+
gardenId: GID,
|
|
77
|
+
pid: 4242,
|
|
78
|
+
hostname: "h",
|
|
79
|
+
createdAt: "2026-08-06T09:00:00.000Z",
|
|
80
|
+
nonce: "n",
|
|
81
|
+
owner: "entwurf_v2",
|
|
82
|
+
lockPath: `/tmp/locks/${GID}.lock`,
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
interface Harness {
|
|
86
|
+
deps: VisibleResumeDeps;
|
|
87
|
+
trace: string[];
|
|
88
|
+
readonly launches: number;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Records the ORDER of every seam, which is where most of this contract lives. */
|
|
92
|
+
function harness(opts: {
|
|
93
|
+
livenessSequence: SocketLiveness[];
|
|
94
|
+
inspectionKind?: TargetSocketInspection["kind"];
|
|
95
|
+
acquire?: AcquireLockResult;
|
|
96
|
+
release?: ReleaseResult;
|
|
97
|
+
resolveIdentity?: () => LaunchIdentity;
|
|
98
|
+
launchOk?: boolean;
|
|
99
|
+
}): Harness {
|
|
100
|
+
const trace: string[] = [];
|
|
101
|
+
let clock = 0;
|
|
102
|
+
let probeIdx = 0;
|
|
103
|
+
let launches = 0;
|
|
104
|
+
const kind = opts.inspectionKind ?? "socket-file";
|
|
105
|
+
const deps: VisibleResumeDeps = {
|
|
106
|
+
acquireLock: (gid) => {
|
|
107
|
+
trace.push(`acquireLock:${gid}`);
|
|
108
|
+
return opts.acquire ?? { ok: true, claim: CLAIM };
|
|
109
|
+
},
|
|
110
|
+
releaseLock: () => {
|
|
111
|
+
trace.push("releaseLock");
|
|
112
|
+
return opts.release ?? "released";
|
|
113
|
+
},
|
|
114
|
+
resolveIdentity: (gid) => {
|
|
115
|
+
trace.push(`resolveIdentity:${gid}`);
|
|
116
|
+
return (opts.resolveIdentity ?? (() => IDENTITY))();
|
|
117
|
+
},
|
|
118
|
+
inspectSocket: async () => {
|
|
119
|
+
trace.push("inspectSocket");
|
|
120
|
+
return kind === "absent"
|
|
121
|
+
? { kind: "absent", socketPath: SOCK }
|
|
122
|
+
: kind === "address-conflict"
|
|
123
|
+
? { kind: "address-conflict", socketPath: SOCK, reason: "symlink" }
|
|
124
|
+
: kind === "indeterminate"
|
|
125
|
+
? { kind: "indeterminate", socketPath: SOCK, error: "EACCES" }
|
|
126
|
+
: { kind: "socket-file", socketPath: SOCK };
|
|
127
|
+
},
|
|
128
|
+
probeSocket: async () => {
|
|
129
|
+
const value = opts.livenessSequence[Math.min(probeIdx, opts.livenessSequence.length - 1)] ?? "dead";
|
|
130
|
+
probeIdx++;
|
|
131
|
+
trace.push(`probeSocket:${value}`);
|
|
132
|
+
return value;
|
|
133
|
+
},
|
|
134
|
+
launch: (input) => {
|
|
135
|
+
launches++;
|
|
136
|
+
trace.push(`launch:${input.cwd}:${input.runtimeArgs.join(" ")}`);
|
|
137
|
+
return opts.launchOk === false
|
|
138
|
+
? { ok: false, reason: "cwd-missing", hint: "the recorded cwd no longer exists" }
|
|
139
|
+
: {
|
|
140
|
+
ok: true,
|
|
141
|
+
handle: {
|
|
142
|
+
serverPid: "1",
|
|
143
|
+
sessionId: "$0",
|
|
144
|
+
windowId: "@9",
|
|
145
|
+
windowIndex: "9",
|
|
146
|
+
paneId: "%9",
|
|
147
|
+
panePid: "999",
|
|
148
|
+
runtimePath: "/usr/bin/pi",
|
|
149
|
+
cwd: input.cwd,
|
|
150
|
+
},
|
|
151
|
+
};
|
|
152
|
+
},
|
|
153
|
+
sleep: async (ms) => {
|
|
154
|
+
trace.push(`sleep:${ms}`);
|
|
155
|
+
clock += ms;
|
|
156
|
+
},
|
|
157
|
+
now: () => clock,
|
|
158
|
+
};
|
|
159
|
+
return {
|
|
160
|
+
deps,
|
|
161
|
+
trace,
|
|
162
|
+
get launches() {
|
|
163
|
+
return launches;
|
|
164
|
+
},
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async function main(): Promise<void> {
|
|
169
|
+
// ══ REFUSALS FIRST — see the header note on block order ═════════════════════════
|
|
170
|
+
|
|
171
|
+
// ── a malformed target ───────────────────────────────────────────────────────────
|
|
172
|
+
{
|
|
173
|
+
const h = harness({ livenessSequence: ["dead"] });
|
|
174
|
+
const result = await visibleResume("../../etc/passwd", h.deps);
|
|
175
|
+
ok(
|
|
176
|
+
"a malformed garden id is refused BEFORE the lock — it names no citizen, so it never becomes a filesystem path",
|
|
177
|
+
!result.ok && result.reason === "target-invalid" && h.trace.length === 0,
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// ── a locked target ──────────────────────────────────────────────────────────────
|
|
182
|
+
{
|
|
183
|
+
const h = harness({
|
|
184
|
+
livenessSequence: ["dead"],
|
|
185
|
+
acquire: {
|
|
186
|
+
ok: false,
|
|
187
|
+
conflict: { reason: "target-locked", lockPath: "/tmp/l", holder: null, detail: "held by pid 7" },
|
|
188
|
+
},
|
|
189
|
+
});
|
|
190
|
+
const result = await visibleResume(GID, h.deps);
|
|
191
|
+
ok(
|
|
192
|
+
"[QK:V2RESUME-LOCKED-NO-RESOLVE] a locked target refuses without resolving identity, inspecting, or launching — the lock IS the gate, and reading a record behind someone else's lock is how two resumes race one transcript",
|
|
193
|
+
!result.ok &&
|
|
194
|
+
result.reason === "target-locked" &&
|
|
195
|
+
h.launches === 0 &&
|
|
196
|
+
!h.trace.some((t) => t.startsWith("resolveIdentity")),
|
|
197
|
+
);
|
|
198
|
+
ok("a failed acquire releases nothing — there is no claim to give back", !h.trace.includes("releaseLock"));
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// ── an already-live citizen ──────────────────────────────────────────────────────
|
|
202
|
+
{
|
|
203
|
+
const h = harness({ livenessSequence: ["alive"] });
|
|
204
|
+
const result = await visibleResume(GID, h.deps);
|
|
205
|
+
ok(
|
|
206
|
+
"[QK:V2RESUME-LIVE-REFUSED] a LIVE citizen is refused and no window is opened — it is addressed with entwurf_v2, and refusing here is also what stops two windows existing for one garden id",
|
|
207
|
+
!result.ok && result.reason === "target-live" && h.launches === 0,
|
|
208
|
+
);
|
|
209
|
+
ok("the live refusal still releases the lock", h.trace.includes("releaseLock"));
|
|
210
|
+
ok(
|
|
211
|
+
"the live refusal points the caller at the verb that DOES work",
|
|
212
|
+
!result.ok && /entwurf_v2 fire-and-forget/.test(result.detail),
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// ── an inconclusive probe ────────────────────────────────────────────────────────
|
|
217
|
+
{
|
|
218
|
+
const h = harness({ livenessSequence: ["indeterminate"] });
|
|
219
|
+
const result = await visibleResume(GID, h.deps);
|
|
220
|
+
ok(
|
|
221
|
+
"[QK:V2RESUME-INDETERMINATE-NO-START] an inconclusive probe starts NOTHING — 'not provably dormant' is not 'dormant', and resuming a citizen that is actually running would put two processes on one transcript",
|
|
222
|
+
!result.ok && result.reason === "target-indeterminate" && h.launches === 0,
|
|
223
|
+
);
|
|
224
|
+
ok("the indeterminate refusal releases the lock", h.trace.includes("releaseLock"));
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// ── a corrupt address ────────────────────────────────────────────────────────────
|
|
228
|
+
{
|
|
229
|
+
const h = harness({ livenessSequence: ["dead"], inspectionKind: "address-conflict" });
|
|
230
|
+
const result = await visibleResume(GID, h.deps);
|
|
231
|
+
ok(
|
|
232
|
+
"[QK:V2RESUME-CONFLICT-NO-START] a symlinked / non-socket control path is refused as an address conflict, unprobed and unlaunched — standing a citizen up behind a corrupt address would make the forgery addressable",
|
|
233
|
+
!result.ok && result.reason === "target-address-conflict" && h.launches === 0,
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// ── an unresumable record: the leaf's cause-rich throw survives ──────────────────
|
|
238
|
+
{
|
|
239
|
+
const h = harness({
|
|
240
|
+
livenessSequence: ["dead"],
|
|
241
|
+
resolveIdentity: () => {
|
|
242
|
+
throw new Error(
|
|
243
|
+
"resume-launch-identity: has no recorded transcriptPath — the citizen never wrote a session file (no turn yet)",
|
|
244
|
+
);
|
|
245
|
+
},
|
|
246
|
+
});
|
|
247
|
+
let thrown: Error | null = null;
|
|
248
|
+
await visibleResume(GID, h.deps).catch((err) => {
|
|
249
|
+
thrown = err as Error;
|
|
250
|
+
});
|
|
251
|
+
ok(
|
|
252
|
+
"[QK:V2RESUME-NO-TRANSCRIPT-FAILS-LOUD] a citizen with a socket but NO transcript (measured: a pi that never took a turn has both a record and a socket) fails LOUD with the leaf's own cause and opens no window — the cause is not re-typed into a reject enum, where the second copy is what goes stale",
|
|
253
|
+
thrown !== null && /no recorded transcriptPath/.test(String(thrown)) && h.launches === 0,
|
|
254
|
+
);
|
|
255
|
+
ok("the lock is released even when identity resolution throws", h.trace.includes("releaseLock"));
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// ── a citizen of a backend that has no same-id resume ────────────────────────────
|
|
259
|
+
{
|
|
260
|
+
const h = harness({
|
|
261
|
+
livenessSequence: ["dead"],
|
|
262
|
+
resolveIdentity: () => {
|
|
263
|
+
throw new ResumeBackendUnsupportedError(GID, "claude-code");
|
|
264
|
+
},
|
|
265
|
+
});
|
|
266
|
+
// The throw is CAUGHT here rather than allowed to escape: "this refusal was reported as a
|
|
267
|
+
// raw error" is precisely the defect under test, and an uncaught throw would take the gate
|
|
268
|
+
// down before this assertion could name it.
|
|
269
|
+
const result = await visibleResume(GID, h.deps).catch(
|
|
270
|
+
(err): VisibleResumeResult => ({
|
|
271
|
+
ok: false,
|
|
272
|
+
reason: "target-invalid",
|
|
273
|
+
detail: `threw instead of refusing: ${err instanceof Error ? err.message : String(err)}`,
|
|
274
|
+
}),
|
|
275
|
+
);
|
|
276
|
+
ok(
|
|
277
|
+
"[QK:V2RESUME-NOT-PI-REFUSED] a claude-code / agy target is a NAMED refusal, not an error report: nothing is corrupt, the record is right, and the operator simply reached past a capability boundary — only pi stands a control socket up",
|
|
278
|
+
!result.ok && result.reason === "target-not-pi" && h.launches === 0,
|
|
279
|
+
);
|
|
280
|
+
ok(
|
|
281
|
+
"[QK:V2RESUME-NOT-PI-BY-FIELD] the refusal is matched on the error's own reason FIELD and its backend is quoted from the error, so no message string is parsed and the record is never read a second time to ask which backend it was",
|
|
282
|
+
!result.ok &&
|
|
283
|
+
/claude-code/.test(result.detail) &&
|
|
284
|
+
h.trace.filter((t) => t.startsWith("resolveIdentity")).length === 1,
|
|
285
|
+
);
|
|
286
|
+
ok("the not-pi refusal releases the lock", h.trace.includes("releaseLock"));
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// ── a refused launch ─────────────────────────────────────────────────────────────
|
|
290
|
+
{
|
|
291
|
+
const h = harness({ livenessSequence: ["dead"], launchOk: false });
|
|
292
|
+
const result = await visibleResume(GID, h.deps);
|
|
293
|
+
ok(
|
|
294
|
+
"a refused launch is a named reject carrying the launcher's own reason and hint, and it is NOT retried",
|
|
295
|
+
!result.ok && result.reason === "launch-refused" && /cwd-missing/.test(result.detail) && h.launches === 1,
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// ══ THE HAPPY PATH ══════════════════════════════════════════════════════════════
|
|
300
|
+
{
|
|
301
|
+
// dead at preflight, still dead on the first observation, alive on the second — the
|
|
302
|
+
// measured shape: the window opens and the socket arrives a couple of seconds later.
|
|
303
|
+
const h = harness({ livenessSequence: ["dead", "dead", "alive"] });
|
|
304
|
+
const result = await visibleResume(GID, h.deps);
|
|
305
|
+
|
|
306
|
+
ok(
|
|
307
|
+
"[QK:V2RESUME-LOCK-BEFORE-LIVENESS] the lock is acquired BEFORE the liveness question — asked first, the answer could change under us while a competing resume passed its own dead check",
|
|
308
|
+
h.trace[0] === `acquireLock:${GID}` && h.trace.indexOf("inspectSocket") > 0,
|
|
309
|
+
);
|
|
310
|
+
ok(
|
|
311
|
+
"[QK:V2RESUME-IDENTITY-BEFORE-WINDOW] identity is resolved under the lock BEFORE the liveness question and long before the launcher — a record read after the preflight would mean the liveness answer applies to a citizen whose transcript has not been proven resumable yet, and any window would already be the wrong one",
|
|
312
|
+
h.trace.indexOf(`resolveIdentity:${GID}`) < h.trace.indexOf("inspectSocket") &&
|
|
313
|
+
h.trace.indexOf("inspectSocket") < h.trace.findIndex((t) => t.startsWith("launch:")),
|
|
314
|
+
);
|
|
315
|
+
ok(
|
|
316
|
+
"[QK:V2RESUME-ARGV-FROM-RECORD] the launcher receives the RECORD's cwd and the measured interactive argv — no prompt, no --mode, no -p, and the bridge args exactly where the runtime accepted them",
|
|
317
|
+
h.trace.includes(
|
|
318
|
+
`launch:${IDENTITY.cwd}:--entwurf-control -e /opt/entwurf --session ${IDENTITY.sessionFile} --provider entwurf --model claude-sonnet-5`,
|
|
319
|
+
),
|
|
320
|
+
);
|
|
321
|
+
ok(
|
|
322
|
+
"[QK:V2RESUME-OBSERVE-IS-BOUNDED-WAIT] the observation SLEEPS and re-inspects rather than probing once — measured, the resumed socket appeared ~2–4s after the window opened, so a single immediate probe would have reported a successful resume as unobserved",
|
|
323
|
+
result.ok &&
|
|
324
|
+
result.observation.kind === "socket-alive" &&
|
|
325
|
+
h.trace.filter((t) => t.startsWith("probeSocket")).length >= 3 &&
|
|
326
|
+
h.trace.some((t) => t.startsWith("sleep:")),
|
|
327
|
+
);
|
|
328
|
+
ok("the launcher was called exactly once — a startup wait is not a retry loop", h.launches === 1);
|
|
329
|
+
ok("the lock is released on the success path too", h.trace.filter((t) => t === "releaseLock").length === 1);
|
|
330
|
+
ok(
|
|
331
|
+
"[QK:V2RESUME-RECEIPTS-SEPARATE] launch and observation are two separate objects: the launch receipt carries the window handle and the transcript and says nothing about liveness, and only the observation says the citizen answered",
|
|
332
|
+
result.ok &&
|
|
333
|
+
result.launch.handle.windowId === "@9" &&
|
|
334
|
+
result.launch.sessionFile === IDENTITY.sessionFile &&
|
|
335
|
+
!("kind" in result.launch) &&
|
|
336
|
+
result.observation.socketPath === SOCK,
|
|
337
|
+
);
|
|
338
|
+
const rendered = renderVisibleResume(result.ok ? result : result);
|
|
339
|
+
ok(
|
|
340
|
+
"[QK:V2RESUME-RENDER-SEPARATES] the rendered text keeps the two receipts apart BY NAME, so a reader can tell WHICH fact is missing when only one is there, and it refuses to claim a turn ran",
|
|
341
|
+
rendered.text.includes("LAUNCH receipt") &&
|
|
342
|
+
rendered.text.includes("OBSERVATION receipt") &&
|
|
343
|
+
/No turn was run/.test(rendered.text) &&
|
|
344
|
+
!rendered.isError,
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// ══ THE TIMEOUT BRANCH ══════════════════════════════════════════════════════════
|
|
349
|
+
{
|
|
350
|
+
const h = harness({ livenessSequence: ["dead"] });
|
|
351
|
+
const result = await visibleResume(GID, h.deps);
|
|
352
|
+
ok(
|
|
353
|
+
"[QK:V2RESUME-UNOBSERVED-IS-REAL] a timeout is ok:true with observation=resume-unobserved — the window WAS opened and is left open, which is the same rule fresh-call already ships: a launch with no callback is a real outcome, not an error to retry",
|
|
354
|
+
result.ok && result.observation.kind === "resume-unobserved",
|
|
355
|
+
);
|
|
356
|
+
ok(
|
|
357
|
+
"[QK:V2RESUME-TIMEOUT-RELEASES] the timeout releases the lock, never relaunches, and never kills the pane — a retained lock on a visible artifact is the worse failure and would have to be cleared by hand on every slow host",
|
|
358
|
+
h.trace.includes("releaseLock") && h.launches === 1 && !h.trace.some((t) => /kill|relaunch/.test(t)),
|
|
359
|
+
);
|
|
360
|
+
ok(
|
|
361
|
+
"the observation stops AT the deadline rather than running forever",
|
|
362
|
+
result.ok && result.observation.waitedMs >= RESUME_OBSERVE_TIMEOUT_MS,
|
|
363
|
+
);
|
|
364
|
+
const rendered = renderVisibleResume(result);
|
|
365
|
+
ok(
|
|
366
|
+
"the unobserved text tells the operator exactly what was and was not done: window open, lock released, nothing retried, nothing killed",
|
|
367
|
+
/resume-unobserved/.test(rendered.text) && /nothing was retried/.test(rendered.text) && !rendered.isError,
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// ══ A FAILED RELEASE ════════════════════════════════════════════════════════════
|
|
372
|
+
{
|
|
373
|
+
const h = harness({ livenessSequence: ["dead", "alive"], release: "not-owned" });
|
|
374
|
+
let thrown: Error | null = null;
|
|
375
|
+
await visibleResume(GID, h.deps).catch((err) => {
|
|
376
|
+
thrown = err as Error;
|
|
377
|
+
});
|
|
378
|
+
ok(
|
|
379
|
+
'[QK:V2RESUME-RELEASE-FAILURE-LOUD] a release that does not return "released" THROWS — a lock this verb took and could not give back will refuse the next resume or dispatch, and folding it into a green receipt would send the operator looking at the wrong layer',
|
|
380
|
+
thrown !== null && /instead of/.test(String(thrown)) && /clear it by hand/.test(String(thrown)),
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// ══ THE OBSERVATION HELPER, DRIVEN DIRECTLY ═════════════════════════════════════
|
|
385
|
+
{
|
|
386
|
+
let clock = 0;
|
|
387
|
+
let calls = 0;
|
|
388
|
+
const receipt = await observeSameGidSocket(
|
|
389
|
+
GID,
|
|
390
|
+
SOCK,
|
|
391
|
+
{
|
|
392
|
+
inspectSocket: async () => ({ kind: "socket-file", socketPath: SOCK }),
|
|
393
|
+
probeSocket: async () => {
|
|
394
|
+
calls++;
|
|
395
|
+
return calls >= 5 ? "alive" : "dead";
|
|
396
|
+
},
|
|
397
|
+
sleep: async (ms) => {
|
|
398
|
+
clock += ms;
|
|
399
|
+
},
|
|
400
|
+
now: () => clock,
|
|
401
|
+
},
|
|
402
|
+
10_000,
|
|
403
|
+
500,
|
|
404
|
+
);
|
|
405
|
+
ok(
|
|
406
|
+
"the observation returns as soon as the socket answers, and reports how long it actually waited — the number is evidence, not decoration",
|
|
407
|
+
receipt.kind === "socket-alive" && receipt.waitedMs === 2000,
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// ══ CONSTANTS AND BOUNDARIES, ASSERTED ON SOURCE ════════════════════════════════
|
|
412
|
+
{
|
|
413
|
+
const SRC = read("pi-extensions/lib/entwurf-v2-visible-resume.ts");
|
|
414
|
+
// CODE, not prose. The module explains at length why there is no watcher and no retry, so
|
|
415
|
+
// scanning the raw file would forbid the explanation instead of the behaviour.
|
|
416
|
+
const CODE = SRC.replace(/\/\*[\s\S]*?\*\//g, "").replace(/^\s*\/\/.*$/gm, "");
|
|
417
|
+
ok(
|
|
418
|
+
"the timeout and interval are product constants with no env knob — a tunable would make 'unobserved' mean different things on different hosts and the receipt would stop being comparable",
|
|
419
|
+
RESUME_OBSERVE_TIMEOUT_MS === 30_000 && RESUME_OBSERVE_INTERVAL_MS === 500 && !/process\.env/.test(CODE),
|
|
420
|
+
);
|
|
421
|
+
ok(
|
|
422
|
+
"[QK:V2RESUME-SHARED-ID-GRAMMAR] the address grammar comes from the session-id SSOT, not a local regex — this value becomes a lock path and a control-socket path a few lines later, and a second copy could drift into accepting an id the record or the socket filename would reject",
|
|
423
|
+
/from "\.\/session-id\.js"/.test(CODE) &&
|
|
424
|
+
/isValidSessionId\(targetGardenId\)/.test(CODE) &&
|
|
425
|
+
!/\\d\{8\}T\\d\{6\}/.test(CODE),
|
|
426
|
+
);
|
|
427
|
+
ok(
|
|
428
|
+
"the composition ships no watcher and no retry machinery: no interval timer, no child-process handling, no kill",
|
|
429
|
+
!/setInterval|spawn|child_process|\.kill\(|SIGTERM/.test(CODE),
|
|
430
|
+
);
|
|
431
|
+
ok(
|
|
432
|
+
"production deps wire the real lock, record leaf, socket inspection and probe, and take the launcher as an argument",
|
|
433
|
+
typeof makeVisibleResumeDeps === "function" &&
|
|
434
|
+
/makeVisibleResumeDeps\(\s*launch/.test(SRC) &&
|
|
435
|
+
/resolveResumeLaunchIdentity/.test(SRC),
|
|
436
|
+
);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
console.log(`\ncheck-entwurf-v2-visible-resume: ${passed} checks passed`);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
main().catch((err) => {
|
|
443
|
+
console.error(`check-entwurf-v2-visible-resume: ${err instanceof Error ? err.message : String(err)}`);
|
|
444
|
+
process.exit(1);
|
|
445
|
+
});
|
|
@@ -1014,7 +1014,7 @@ esac
|
|
|
1014
1014
|
# The three probe rows are driven for real through fakes, never asserted from prose:
|
|
1015
1015
|
# the adapter takes its host scan from `pgrep`/`ss` on PATH and its metadata call from
|
|
1016
1016
|
# $AGY_BIN, so a PATH-local fake pins each row deterministically — including DEAD,
|
|
1017
|
-
# which must not depend on whether the operator running `pnpm check` happens to have
|
|
1017
|
+
# which must not depend on whether the operator running `pnpm run check:full` happens to have
|
|
1018
1018
|
# agy open.
|
|
1019
1019
|
echo "[check-fresh-cut-gate] H. native-push conversations (the marker-less surface)"
|
|
1020
1020
|
mkdir -p "$SANDBOX/npfake"
|
|
@@ -21,9 +21,9 @@
|
|
|
21
21
|
* before/after (P1-5 — porcelain text alone misses byte changes in already-
|
|
22
22
|
* modified files).
|
|
23
23
|
*
|
|
24
|
-
* No tiers: the
|
|
25
|
-
*
|
|
26
|
-
*
|
|
24
|
+
* No tiers: the committed mutant set is one tier — every run executes every mutant,
|
|
25
|
+
* one command, one truth, no advisory lane. Runtime is recorded when this runs on a
|
|
26
|
+
* frozen candidate; if the set ever outgrows its budget, re-open the fast/full split
|
|
27
27
|
* from the design record instead of silently skipping mutants.
|
|
28
28
|
*
|
|
29
29
|
* Evidence: claim IDs + killed mutant IDs, never assertion counts.
|
|
@@ -45,8 +45,10 @@ import {
|
|
|
45
45
|
originHead,
|
|
46
46
|
originWorkSurfaceSha,
|
|
47
47
|
qualifyMutants,
|
|
48
|
+
readVitestFailedTitles,
|
|
48
49
|
reportPassed,
|
|
49
50
|
runGateBounded,
|
|
51
|
+
signatureAttributedToFailure,
|
|
50
52
|
signatureOnFailureLine,
|
|
51
53
|
sweepStaleSnapshots,
|
|
52
54
|
validateManifest,
|
|
@@ -76,6 +78,83 @@ function throws(label: string, fn: () => void, naming: string): void {
|
|
|
76
78
|
assert.fail(`${label} — expected a loud refusal, got silence`);
|
|
77
79
|
}
|
|
78
80
|
|
|
81
|
+
/**
|
|
82
|
+
* Vitest attribution, proved on the MEASURED false-positive shape (issue #62 review).
|
|
83
|
+
*
|
|
84
|
+
* Vitest prints a code frame around the failing assertion, and that frame quotes the
|
|
85
|
+
* neighbouring source lines — including an adjacent PASSING test's `it("[QK:…]" …)`
|
|
86
|
+
* title. Measured verbatim on vitest 4.1.9:
|
|
87
|
+
*
|
|
88
|
+
* × [QK:B] fails on its first body line
|
|
89
|
+
* 4| it("[QK:A] one-liner that passes", () => expect(1).toBe(1));
|
|
90
|
+
* 5| it("[QK:B] fails on its first body line", () => {
|
|
91
|
+
*
|
|
92
|
+
* The legacy `ok`-line oracle answers TRUE for [QK:A] there — a mutant claiming A
|
|
93
|
+
* would be certified KILLED by a red that belongs to B. The structured oracle reads
|
|
94
|
+
* only the failed-test title set, so it must answer FALSE for A and TRUE for B.
|
|
95
|
+
*
|
|
96
|
+
* The cell is discriminating in BOTH directions: it asserts that the legacy scanner
|
|
97
|
+
* really does say TRUE on this input, so the negative is not vacuous.
|
|
98
|
+
*/
|
|
99
|
+
function checkVitestAttribution(): void {
|
|
100
|
+
// Declared ONCE as a literal: the manifest's exact-once signature rule counts
|
|
101
|
+
// occurrences of this token in signatureSource, so every other use goes through
|
|
102
|
+
// the binding.
|
|
103
|
+
const claimed = "[QK:VITEST-FAILED-TITLE-ATTRIBUTION]";
|
|
104
|
+
const actualFailure = "[QK:ADJACENT-ACTUAL-FAILURE]";
|
|
105
|
+
const codeFrame = [
|
|
106
|
+
"__ENTWURF_VITEST_JSON__",
|
|
107
|
+
` × ${actualFailure} fails on its first body line`,
|
|
108
|
+
` 4| it("${claimed} one-liner that passes", () => expect(1).toBe(1));`,
|
|
109
|
+
` 5| it("${actualFailure} fails on its first body line", () => {`,
|
|
110
|
+
].join("\n");
|
|
111
|
+
const report = JSON.stringify({
|
|
112
|
+
testResults: [
|
|
113
|
+
{
|
|
114
|
+
assertionResults: [
|
|
115
|
+
{ status: "passed", fullName: `frame probe ${claimed} one-liner that passes` },
|
|
116
|
+
{ status: "failed", fullName: `frame probe ${actualFailure} fails on its first body line` },
|
|
117
|
+
],
|
|
118
|
+
},
|
|
119
|
+
],
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "entwurf-attribution-selftest-"));
|
|
123
|
+
const reportPath = path.join(dir, "vitest-report.json");
|
|
124
|
+
try {
|
|
125
|
+
fs.writeFileSync(reportPath, report);
|
|
126
|
+
const titles = readVitestFailedTitles(codeFrame, reportPath);
|
|
127
|
+
|
|
128
|
+
ok(
|
|
129
|
+
"attribution self-test is not vacuous — the legacy failure-line oracle really does certify the adjacent PASSING claim on this measured code frame",
|
|
130
|
+
signatureOnFailureLine(codeFrame, claimed),
|
|
131
|
+
);
|
|
132
|
+
ok(
|
|
133
|
+
`${claimed} Vitest attribution reads only structured failed-test titles — an adjacent passing QK quoted by the failing code frame cannot certify the mutant`,
|
|
134
|
+
!signatureAttributedToFailure(codeFrame, claimed, titles) &&
|
|
135
|
+
signatureAttributedToFailure(codeFrame, actualFailure, titles),
|
|
136
|
+
);
|
|
137
|
+
ok(
|
|
138
|
+
"a structured lane whose report is missing is UNREADABLE, never a silent fallback to token scanning",
|
|
139
|
+
readVitestFailedTitles(codeFrame, path.join(dir, "absent.json")) === "unreadable" &&
|
|
140
|
+
!signatureAttributedToFailure(codeFrame, actualFailure, "unreadable"),
|
|
141
|
+
);
|
|
142
|
+
ok(
|
|
143
|
+
"a gate that printed no marker keeps the legacy failure-line oracle unchanged",
|
|
144
|
+
readVitestFailedTitles(" not ok 1 boom [QK:LEGACY]", reportPath) === "legacy" &&
|
|
145
|
+
signatureAttributedToFailure(" not ok 1 boom [QK:LEGACY]", "[QK:LEGACY]", "legacy"),
|
|
146
|
+
);
|
|
147
|
+
} finally {
|
|
148
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
checkVitestAttribution();
|
|
153
|
+
if (process.argv.includes("--attribution-self-test")) {
|
|
154
|
+
console.log(`[gate-qualification] attribution self-test: ${passed} checks passed`);
|
|
155
|
+
process.exit(0);
|
|
156
|
+
}
|
|
157
|
+
|
|
79
158
|
// ═══ Phase 1a — the pure classifier, exhausted as a truth table ═════════════
|
|
80
159
|
|
|
81
160
|
{
|
|
@@ -718,13 +797,28 @@ console.log(`\n[gate-qualification] self-test: ${passed} checks passed`);
|
|
|
718
797
|
// manifest lost in a merge must name itself here — "0/0 killed" is not a pass.
|
|
719
798
|
// Extend this inventory and the manifests TOGETHER, never silently.
|
|
720
799
|
const EXPECTED_LANE_MUTANTS: Record<string, number> = {
|
|
721
|
-
"acp-augment":
|
|
800
|
+
"acp-augment": 10,
|
|
722
801
|
"acp-cortex": 12,
|
|
802
|
+
"acp-overlay": 1,
|
|
803
|
+
"acp-prompt-lifecycle": 8,
|
|
804
|
+
"acp-stop-reason": 6,
|
|
805
|
+
"acp-stream-hooks": 10,
|
|
723
806
|
"agy-permission": 6,
|
|
724
|
-
"
|
|
725
|
-
"
|
|
807
|
+
"bridge-boot-resume": 3,
|
|
808
|
+
"meta-facts": 4,
|
|
809
|
+
"meta-identity": 4,
|
|
810
|
+
"mux-boundary": 14,
|
|
811
|
+
"mux-fresh-call": 15,
|
|
812
|
+
"mux-launcher-fence": 7,
|
|
813
|
+
"mux-parent-artifact": 3,
|
|
814
|
+
"mux-resume-call": 12,
|
|
815
|
+
"probe-ordering": 1,
|
|
816
|
+
"release-gate": 11,
|
|
817
|
+
"resume-args": 6,
|
|
818
|
+
"resume-launch-identity": 6,
|
|
726
819
|
"self-address": 3,
|
|
727
|
-
"v2-surface":
|
|
820
|
+
"v2-surface": 7,
|
|
821
|
+
"v2-visible-resume": 17,
|
|
728
822
|
};
|
|
729
823
|
const laneTally: Record<string, number> = {};
|
|
730
824
|
for (const man of manifests) laneTally[man.lane] = (laneTally[man.lane] ?? 0) + man.mutants.length;
|
|
@@ -316,8 +316,16 @@ function finish(t) {
|
|
|
316
316
|
if (done) return; done = true; clearTimeout(timer);
|
|
317
317
|
let msg; try { msg = JSON.parse(t); } catch { console.error('unparseable tools/list: ' + t.slice(0, 300)); process.exit(1); }
|
|
318
318
|
const names = (msg?.result?.tools ?? []).map((x) => x?.name).sort();
|
|
319
|
-
|
|
320
|
-
|
|
319
|
+
// EXACT set, not a floor. This cell consumes bytes it did not build (M3 hands it the
|
|
320
|
+
// preserved candidate via ENTWURF_CANDIDATE_TGZ), so "which surface did this exact
|
|
321
|
+
// artifact ship" is precisely the question it owns. The subset form named only the five
|
|
322
|
+
// pre-0.14 verbs and would have accepted a candidate with no entwurf_fresh_call /
|
|
323
|
+
// entwurf_resume_call; equality also refuses an extra or duplicated verb.
|
|
324
|
+
const EXPECT_TOOLS = ['entwurf_fresh_call', 'entwurf_inbox_read', 'entwurf_peers', 'entwurf_register_native', 'entwurf_resume_call', 'entwurf_self', 'entwurf_v2'];
|
|
325
|
+
if (names.length !== EXPECT_TOOLS.length || EXPECT_TOOLS.some((n, i) => names[i] !== n)) {
|
|
326
|
+
console.error('MCP tool set MISMATCH — want exactly [' + EXPECT_TOOLS.join(',') + '] got [' + names.join(',') + ']');
|
|
327
|
+
process.exit(1);
|
|
328
|
+
}
|
|
321
329
|
console.log(names.join(','));
|
|
322
330
|
child.kill('SIGTERM'); process.exit(0);
|
|
323
331
|
}
|
|
@@ -226,7 +226,7 @@ const operatorCmds = [...targets].filter(([cmd, ts]) => !isDevGate(cmd) && ts.le
|
|
|
226
226
|
// ─── S5 — verification must not rewire the operator's live host ──────────────────────
|
|
227
227
|
{
|
|
228
228
|
// A LIVE gate legitimately drives the real host (that is what LIVE means). The offline
|
|
229
|
-
// floor — everything in `pnpm check` — must not: it runs on every commit, and a smoke
|
|
229
|
+
// floor — everything in the check tiers (`pnpm check` / `pnpm run check:full`) — must not: it runs on every commit, and a smoke
|
|
230
230
|
// that writes the real ~/.claude, ~/.gemini or ~/.pi would uninstall the operator's
|
|
231
231
|
// own bridge as a side effect of "testing".
|
|
232
232
|
const LIVE_GATES = new Set(["smoke-resident-garden-guard.sh", "smoke-meta-async-drift.sh"]);
|
|
@@ -17,7 +17,7 @@ pi-owned key — exact match OR ancestor/descendant (a fragment that sets
|
|
|
17
17
|
Cross-repo and not hermetic (the fragment path is an argument), so it lives
|
|
18
18
|
OUTSIDE `pnpm check` / release-gate — it is for agent-config CI and manual
|
|
19
19
|
cross-checks. The synthetic-fixture regression of this script's own logic lives
|
|
20
|
-
in `smoke-meta-keyset-guard.sh`, which IS in `pnpm check`.
|
|
20
|
+
in `smoke-meta-keyset-guard.sh`, which IS in `pnpm run check:full`.
|
|
21
21
|
|
|
22
22
|
usage: check-keyset-overlap.py <consumer-fragment.json> [more.json ...]
|
|
23
23
|
exit 0 = disjoint (invariant holds); exit 1 = overlap (invariant violated);
|