@junghanacs/entwurf 0.23.0 → 0.24.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 +3 -1
- package/BASELINE.md +2 -0
- package/CHANGELOG.md +307 -0
- package/FAQ.md +47 -0
- package/README.md +5 -3
- package/VERIFY.md +43 -2
- package/demo/README.md +1 -1
- package/docs/acp-backend-rail.md +97 -18
- package/docs/setup-clean-host.md +8 -7
- package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +33 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/backend-adapter.js +2 -2
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/context.js +19 -5
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/tool-surface.js +2 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/callback-env.js +68 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-contract.js +2 -2
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-lock.js +1 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/fresh-call-composition.js +54 -22
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/herdr-fresh-call.js +7 -3
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/herdr-placement.js +13 -4
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +19 -10
- package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-omp.js +35 -42
- package/mcp/entwurf-bridge/dist/scripts/probe-bridge-command.js +1 -0
- package/mcp/entwurf-bridge/src/index.ts +41 -1
- package/mcp/entwurf-bridge/test.sh +2 -2
- package/package.json +15 -13
- package/pi-extensions/entwurf-control.ts +56 -0
- package/pi-extensions/lib/acp/acp-client.ts +3 -3
- package/pi-extensions/lib/acp/backend-adapter.ts +3 -3
- package/pi-extensions/lib/acp/backend.ts +20 -10
- package/pi-extensions/lib/acp/context.ts +30 -10
- package/pi-extensions/lib/acp/event-mapper.ts +5 -5
- package/pi-extensions/lib/acp/session-store.ts +2 -2
- package/pi-extensions/lib/acp/tool-surface.ts +2 -1
- package/pi-extensions/lib/callback-env.ts +78 -0
- package/pi-extensions/lib/compaction-send-guard.ts +4 -1
- package/pi-extensions/lib/entwurf-v2-contract.ts +2 -2
- package/pi-extensions/lib/entwurf-v2-lock.ts +1 -1
- package/pi-extensions/lib/entwurf-v2-runner.ts +1 -1
- package/pi-extensions/lib/fresh-call-composition.ts +56 -22
- package/pi-extensions/lib/herdr-fresh-call.ts +12 -4
- package/pi-extensions/lib/herdr-placement.ts +13 -4
- package/pi-extensions/lib/mux-fresh-call.ts +23 -8
- package/pi-extensions/meta-bridge-omp.ts +35 -48
- package/run.sh +327 -246
- package/scripts/check-acp-backend-preflight.ts +117 -9
- package/scripts/check-acp-carrier-augment.ts +6 -4
- package/scripts/check-acp-event-mapper.ts +17 -6
- package/scripts/check-acp-prompt-builder.ts +17 -9
- package/scripts/check-acp-prompt-lifecycle.ts +23 -6
- package/scripts/check-acp-session-reuse.ts +64 -51
- package/scripts/check-acp-session-store.ts +31 -8
- package/scripts/check-acp-stop-reason.ts +20 -2
- package/scripts/check-acp-stream-hooks.ts +22 -5
- package/scripts/check-acp-usage-accounting.ts +42 -18
- package/scripts/check-entwurf-bridge-boot.ts +16 -1
- package/scripts/check-fresh-call-dispatch.ts +4 -5
- package/scripts/check-gate-qualification.ts +45 -5
- package/scripts/check-herdr-activation.ts +73 -0
- package/scripts/check-herdr-fresh-call.ts +22 -6
- package/scripts/check-herdr-sandbox.ts +14 -2
- package/scripts/check-install-container.sh +1 -1
- package/scripts/check-pi-launch.ts +361 -0
- package/scripts/check-release-gate-outcomes.ts +7 -0
- package/scripts/inventory-verification-surface.ts +86 -10
- package/scripts/lib/mutation-qualify.ts +1 -1
- package/scripts/mutants/acp-backend-preflight.json +20 -0
- package/scripts/mutants/bridge-boot-resume.json +26 -0
- package/scripts/mutants/herdr-activation.json +16 -0
- package/scripts/mutants/herdr-fresh-call.json +11 -0
- package/scripts/mutants/herdr-plugin-build.json +1 -1
- package/scripts/mutants/mux-fresh-call.json +21 -8
- package/scripts/mutants/omp-fresh.json +5 -9
- package/scripts/mutants/pack-install.json +3 -3
- package/scripts/mutants/pi-launch.json +113 -0
- package/scripts/mutants/resume-args.json +6 -6
- package/scripts/mutants/v2-surface.json +2 -2
- package/scripts/mutants/v2-visible-resume.json +17 -17
- package/scripts/probe-bridge-command.ts +1 -0
- package/scripts/smoke-acp-raw-turn-live.ts +1 -1
- package/scripts/smoke-acp-session-reuse-live.ts +23 -5
- package/scripts/smoke-agy-install-state.sh +2 -2
- package/scripts/smoke-copilot-mcp-state.sh +1 -1
- package/scripts/smoke-entwurf-v2-matrix-live.ts +1 -1
- package/scripts/smoke-herdr-fresh-call-live.ts +56 -28
- package/scripts/smoke-herdr-raw-install-live.sh +877 -0
- package/scripts/smoke-omp-fresh-live.ts +4 -2
- package/scripts/smoke-omp-mcp-state.sh +1 -1
- package/scripts/smoke-pi-provider-state.sh +2 -2
- package/scripts/tsconfig.json +5 -0
- package/scripts/check-entwurf-facts.ts +0 -391
- package/scripts/check-entwurf-resume-args.ts +0 -122
- package/scripts/check-entwurf-v2-decider.ts +0 -657
- package/scripts/check-entwurf-v2-lock.ts +0 -371
- package/scripts/check-entwurf-v2-mailbox.ts +0 -261
- package/scripts/check-entwurf-v2-matrix.ts +0 -401
- package/scripts/check-entwurf-v2-native-push.ts +0 -221
- package/scripts/check-entwurf-v2-release.ts +0 -151
- package/scripts/check-entwurf-v2-runner.ts +0 -448
- package/scripts/check-entwurf-v2-send-fallback.ts +0 -350
- package/scripts/check-entwurf-v2-send.ts +0 -458
- package/scripts/check-entwurf-v2-visible-resume.ts +0 -445
- package/scripts/check-socket-discovery.ts +0 -401
|
@@ -291,7 +291,15 @@ async function main(): Promise<void> {
|
|
|
291
291
|
if (callerPane?.workspaceId === undefined) {
|
|
292
292
|
fail(`pane get of the caller pane failed or carried no workspace: ${callerGet.stderr || callerGet.stdout}`);
|
|
293
293
|
}
|
|
294
|
-
const
|
|
294
|
+
const tabCallback = {
|
|
295
|
+
target: "20260101T010101-aaaaaa",
|
|
296
|
+
nonce: "mux-fresh-call-deadbeefdeadbeefdeadbeef",
|
|
297
|
+
};
|
|
298
|
+
const tabRun = herdr(
|
|
299
|
+
bin,
|
|
300
|
+
sandbox,
|
|
301
|
+
buildHerdrTabCreateArgs({ workspaceId: callerPane.workspaceId, cwd: REPO_DIR, callback: tabCallback }),
|
|
302
|
+
);
|
|
295
303
|
const tab = tabRun.status === 0 ? parseHerdrTabCreateResponse(tabRun.stdout) : null;
|
|
296
304
|
if (tab === null) fail(`tab create failed or was unreadable: ${tabRun.stderr || tabRun.stdout}`);
|
|
297
305
|
const tabRootPane = tab.rootPane;
|
|
@@ -401,7 +409,11 @@ async function main(): Promise<void> {
|
|
|
401
409
|
);
|
|
402
410
|
}
|
|
403
411
|
|
|
404
|
-
const spareRun = herdr(
|
|
412
|
+
const spareRun = herdr(
|
|
413
|
+
bin,
|
|
414
|
+
sandbox,
|
|
415
|
+
buildHerdrTabCreateArgs({ workspaceId: callerPane.workspaceId, callback: tabCallback }),
|
|
416
|
+
);
|
|
405
417
|
const spareTab = spareRun.status === 0 ? parseHerdrTabCreateResponse(spareRun.stdout) : null;
|
|
406
418
|
if (spareTab === null)
|
|
407
419
|
fail(`the second tab create failed or was unreadable: ${spareRun.stderr || spareRun.stdout}`);
|
|
@@ -321,7 +321,7 @@ function finish(t) {
|
|
|
321
321
|
// artifact ship" is precisely the question it owns. The subset form named only the five
|
|
322
322
|
// pre-0.14 verbs and would have accepted a candidate with no entwurf_fresh_call /
|
|
323
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'];
|
|
324
|
+
const EXPECT_TOOLS = ['entwurf_callback', 'entwurf_fresh_call', 'entwurf_inbox_read', 'entwurf_peers', 'entwurf_register_native', 'entwurf_resume_call', 'entwurf_self', 'entwurf_v2'];
|
|
325
325
|
if (names.length !== EXPECT_TOOLS.length || EXPECT_TOOLS.some((n, i) => names[i] !== n)) {
|
|
326
326
|
console.error('MCP tool set MISMATCH — want exactly [' + EXPECT_TOOLS.join(',') + '] got [' + names.join(',') + ']');
|
|
327
327
|
process.exit(1);
|
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* check-pi-launch — deterministic gate for the MANAGED pi launch (`entwurf pi`, #118 홉 2).
|
|
3
|
+
* Hermetic: no real pi, no network, no model turn, no write outside its own temp root.
|
|
4
|
+
*
|
|
5
|
+
* SAME ORACLE AS check-copilot-launch, and deliberately the same one: the subject is a
|
|
6
|
+
* process REPLACEMENT, so the truth is a FAKE VENDOR — a real executable named `pi` on a
|
|
7
|
+
* sandbox PATH that reports the argv, environment and pid it was actually handed. Nothing
|
|
8
|
+
* below is asserted by reading the launcher's source, and the launcher is driven through
|
|
9
|
+
* its PUBLIC address (`run.sh pi`) because the dispatcher's own argv handling is part of
|
|
10
|
+
* the contract. Reusing that harness shape rather than importing it is the honest split:
|
|
11
|
+
* the two launchers share a posture, not a body, and a shared fixture would have to grow a
|
|
12
|
+
* vendor-name parameter that neither production path has.
|
|
13
|
+
*
|
|
14
|
+
* WHY THE PRECONDITION SET IS EMPTY, measured rather than assumed. `entwurf copilot` checks
|
|
15
|
+
* six receiver-state facts before it execs, because setting its flag is a PROMISE of a
|
|
16
|
+
* doorbell that may not exist. `entwurf pi` promises nothing pi does not already own: on a
|
|
17
|
+
* host where the entwurf extension is not registered, `pi --entwurf-control` refuses itself
|
|
18
|
+
* with `Error: Unknown option: --entwurf-control` and exit 1 (`[측정 2026-09-20]` pi 0.86.0,
|
|
19
|
+
* sandbox HOME + PI_CODING_AGENT_DIR, that exact stderr line and exit 1). A
|
|
20
|
+
* pre-check here would duplicate that refusal and go stale the day pi renames it.
|
|
21
|
+
*
|
|
22
|
+
* WHY ONE RECURSION FENCE AND NOT TWO. The sentinel closes the only real loop — a PATH
|
|
23
|
+
* executable named `pi` that shells back to `entwurf pi`. Copilot's second fence (resolve
|
|
24
|
+
* the binary, refuse our own entrypoints) closes a case that is NOT a loop here, and the
|
|
25
|
+
* cell below measures that rather than asserting it: a PATH `pi` symlinked to run.sh is
|
|
26
|
+
* exec'd as `run.sh --entwurf-control …`, an unknown verb this dispatcher already refuses.
|
|
27
|
+
*
|
|
28
|
+
* WHY THE FLAG IS ADDED AND NEVER DEDUPLICATED. `pit`/`pius`-style operator wrappers already
|
|
29
|
+
* pass `--entwurf-control`; passing it twice was measured byte-identical to passing it once
|
|
30
|
+
* (pi 0.85.1, extension registered — that receipt was NOT retaken at 0.86.0, because it needs a
|
|
31
|
+
* host with the extension registered and taking it would have meant opening a control session on
|
|
32
|
+
* the operator's own pi; the 0.86.0 re-measure above covers the unregistered refusal instead). The launcher therefore injects exactly ONE and leaves
|
|
33
|
+
* the operator's copies alone — an argv scan would be code earning nothing.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
import assert from "node:assert/strict";
|
|
37
|
+
import { execFileSync, spawnSync } from "node:child_process";
|
|
38
|
+
import {
|
|
39
|
+
chmodSync,
|
|
40
|
+
existsSync,
|
|
41
|
+
mkdirSync,
|
|
42
|
+
mkdtempSync,
|
|
43
|
+
readFileSync,
|
|
44
|
+
rmSync,
|
|
45
|
+
symlinkSync,
|
|
46
|
+
writeFileSync,
|
|
47
|
+
} from "node:fs";
|
|
48
|
+
import { tmpdir } from "node:os";
|
|
49
|
+
import * as path from "node:path";
|
|
50
|
+
import { fileURLToPath } from "node:url";
|
|
51
|
+
|
|
52
|
+
const REPO = path.join(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
53
|
+
const RUN_SH = path.join(REPO, "run.sh");
|
|
54
|
+
|
|
55
|
+
let passed = 0;
|
|
56
|
+
function ok(label: string, cond: boolean, detail = ""): void {
|
|
57
|
+
assert.ok(cond, detail ? `${label}\n${detail}` : label);
|
|
58
|
+
console.log(` ok ${label}`);
|
|
59
|
+
passed++;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ── the flag SSOT, read as text ──────────────────────────────────────────────
|
|
63
|
+
// The shell cannot import a TypeScript constant, so the launcher spells the flag as a
|
|
64
|
+
// literal and this gate is what keeps the two in step. The SSOT is the constant; every
|
|
65
|
+
// argv-composing site below must carry exactly its value.
|
|
66
|
+
const storeSrc = readFileSync(path.join(REPO, "pi-extensions", "lib", "acp", "session-store.ts"), "utf8");
|
|
67
|
+
const flagMatch = /export const ENTWURF_CONTROL_FLAG = "([^"]+)";/.exec(storeSrc);
|
|
68
|
+
assert.ok(flagMatch, "ENTWURF_CONTROL_FLAG is not declared in pi-extensions/lib/acp/session-store.ts");
|
|
69
|
+
const FLAG = flagMatch[1];
|
|
70
|
+
|
|
71
|
+
const root = mkdtempSync(path.join(tmpdir(), "entwurf-pi-launch."));
|
|
72
|
+
try {
|
|
73
|
+
const bin = path.join(root, "bin");
|
|
74
|
+
mkdirSync(bin, { recursive: true });
|
|
75
|
+
|
|
76
|
+
// The fake vendor, installed under the REAL name `pi`. There is no "which command
|
|
77
|
+
// stands in for the vendor" switch: a production env seam that can redirect an exec is
|
|
78
|
+
// an authority, not a test convenience.
|
|
79
|
+
const vendor = path.join(bin, "pi");
|
|
80
|
+
writeFileSync(
|
|
81
|
+
vendor,
|
|
82
|
+
`#!/usr/bin/env bash
|
|
83
|
+
echo "PID=$$"
|
|
84
|
+
echo "SENTINEL=[\${ENTWURF_PI_LAUNCH_ACTIVE-<unset>}]"
|
|
85
|
+
for a in "$@"; do printf 'ARG<%s>\\n' "$a"; done
|
|
86
|
+
exit "\${FAKE_PI_EXIT:-0}"
|
|
87
|
+
`,
|
|
88
|
+
);
|
|
89
|
+
chmodSync(vendor, 0o755);
|
|
90
|
+
|
|
91
|
+
// A PATH with no `pi` anywhere on it — built by dropping every real entry that holds
|
|
92
|
+
// one, rather than by emptying PATH (the launcher still needs bash and friends).
|
|
93
|
+
// The sandbox is TOTAL, the same way check-copilot-launch's is. The launcher writes
|
|
94
|
+
// nothing itself, but it EXECS a vendor, and a fixture that leaves the operator's HOME
|
|
95
|
+
// and XDG roots reachable is one careless vendor away from touching them. Naming every
|
|
96
|
+
// root explicitly is also what #119 asks of each test rather than of each gate.
|
|
97
|
+
const sandboxEnv = {
|
|
98
|
+
HOME: path.join(root, "home"),
|
|
99
|
+
XDG_DATA_HOME: path.join(root, "xdg", "data"),
|
|
100
|
+
XDG_CONFIG_HOME: path.join(root, "xdg", "config"),
|
|
101
|
+
XDG_STATE_HOME: path.join(root, "xdg", "state"),
|
|
102
|
+
XDG_CACHE_HOME: path.join(root, "xdg", "cache"),
|
|
103
|
+
PI_CODING_AGENT_DIR: path.join(root, "pi-agent"),
|
|
104
|
+
};
|
|
105
|
+
for (const d of Object.values(sandboxEnv)) mkdirSync(d, { recursive: true });
|
|
106
|
+
|
|
107
|
+
const pathWithoutVendor = (process.env.PATH ?? "")
|
|
108
|
+
.split(":")
|
|
109
|
+
.filter((d) => d !== "" && !existsSync(path.join(d, "pi")))
|
|
110
|
+
.join(":");
|
|
111
|
+
|
|
112
|
+
interface Run {
|
|
113
|
+
status: number | null;
|
|
114
|
+
out: string;
|
|
115
|
+
args: string[];
|
|
116
|
+
sentinel: string;
|
|
117
|
+
pid: string;
|
|
118
|
+
}
|
|
119
|
+
function launch(args: string[], extraEnv: Record<string, string> = {}, withVendor = true): Run {
|
|
120
|
+
const r = spawnSync("bash", [RUN_SH, "pi", ...args], {
|
|
121
|
+
cwd: root,
|
|
122
|
+
encoding: "utf8",
|
|
123
|
+
env: {
|
|
124
|
+
...process.env,
|
|
125
|
+
...sandboxEnv,
|
|
126
|
+
PATH: withVendor ? `${bin}:${pathWithoutVendor}` : pathWithoutVendor,
|
|
127
|
+
ENTWURF_PI_LAUNCH_ACTIVE: undefined as unknown as string,
|
|
128
|
+
FAKE_PI_EXIT: undefined as unknown as string,
|
|
129
|
+
...extraEnv,
|
|
130
|
+
},
|
|
131
|
+
});
|
|
132
|
+
const out = `${r.stdout ?? ""}${r.stderr ?? ""}`;
|
|
133
|
+
const argv: string[] = [];
|
|
134
|
+
for (const line of out.split("\n")) {
|
|
135
|
+
const m = /^ARG<([\s\S]*)>$/.exec(line);
|
|
136
|
+
if (m) argv.push(m[1]);
|
|
137
|
+
}
|
|
138
|
+
return {
|
|
139
|
+
status: r.status,
|
|
140
|
+
out,
|
|
141
|
+
args: argv,
|
|
142
|
+
sentinel: /^SENTINEL=\[(.*)\]$/m.exec(out)?.[1] ?? "<no-launch>",
|
|
143
|
+
pid: /^PID=(.*)$/m.exec(out)?.[1] ?? "",
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// ── 1. the flag literal agrees with its SSOT, everywhere argv is composed ──
|
|
148
|
+
{
|
|
149
|
+
// CODE only. The branch explains itself at length and quotes the flag four times in
|
|
150
|
+
// prose; a literal check that read those goes green on a launcher whose actual exec
|
|
151
|
+
// line carries a typo — measured, and it is why the comments are dropped first.
|
|
152
|
+
const branch = /^ {2}pi\)$[\s\S]*?^ {4};;$/m.exec(readFileSync(RUN_SH, "utf8"))?.[0] ?? "";
|
|
153
|
+
const launcher = branch
|
|
154
|
+
.split("\n")
|
|
155
|
+
.filter((line) => !/^\s*#/.test(line))
|
|
156
|
+
.join("\n");
|
|
157
|
+
// Deliberately narrow: this cell owns the LITERAL and nothing else. Asserting the
|
|
158
|
+
// whole exec line here would swallow every other claim below — a mutation of the
|
|
159
|
+
// argv order, the quoting or the exec itself would all die on this assertion first,
|
|
160
|
+
// and each mutant is supposed to name exactly one broken thing. The token boundary
|
|
161
|
+
// is what makes a near-miss like `--entwurf-controll` a failure rather than a
|
|
162
|
+
// substring hit.
|
|
163
|
+
ok(
|
|
164
|
+
"[QK:PILAUNCH-FLAG-SSOT] the run.sh launcher spells the flag exactly as ENTWURF_CONTROL_FLAG declares it — the shell cannot import the constant, so the literal is checked as a whole token",
|
|
165
|
+
new RegExp(`(^|\\s)${FLAG.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(\\s|$)`, "m").test(launcher),
|
|
166
|
+
`FLAG=${FLAG}\n--- launcher branch, comments dropped ---\n${launcher}`,
|
|
167
|
+
);
|
|
168
|
+
// EXHAUSTIVE, not a list. The first cut named five files and claimed "every
|
|
169
|
+
// argv-composing site"; cross-review found eight more that the list did not know
|
|
170
|
+
// about. They all happened to be spelled correctly, so there was no false success —
|
|
171
|
+
// but a drift OUTSIDE the list would have passed silently, which is the same gate
|
|
172
|
+
// rot in a slower form. So the sweep walks every tracked source file and requires
|
|
173
|
+
// that every `--entwurf-…` token it finds IS this flag. A typo, a rename, or a
|
|
174
|
+
// second spelling anywhere becomes a named red, and a new call site needs no edit
|
|
175
|
+
// here to be covered.
|
|
176
|
+
//
|
|
177
|
+
// Prose is excluded and that exclusion is the load-bearing part: comments and
|
|
178
|
+
// markdown quote the flag constantly, and a checker that read them would be a spell
|
|
179
|
+
// checker for documentation rather than a guard on what pi actually receives. Only
|
|
180
|
+
// lines that can compose argv are read.
|
|
181
|
+
const tracked = execFileSync(
|
|
182
|
+
"git",
|
|
183
|
+
["ls-files", "--", "scripts", "pi-extensions", "mcp", "demo", "plugins", "run.sh"],
|
|
184
|
+
{
|
|
185
|
+
cwd: REPO,
|
|
186
|
+
encoding: "utf8",
|
|
187
|
+
},
|
|
188
|
+
)
|
|
189
|
+
.split("\n")
|
|
190
|
+
.filter((f) => f.length > 0 && /\.(ts|mjs|js|sh)$/.test(f));
|
|
191
|
+
|
|
192
|
+
// Narrowed to the DRIFT class, and the first run is why. A pattern of `--entwurf-…`
|
|
193
|
+
// swept up `--entwurf-bootstrap` (scripts/check-omp-birth-hook.ts), which is a real and
|
|
194
|
+
// unrelated flag — this cell is not the registry of every entwurf flag. What it owns is
|
|
195
|
+
// that no VARIANT of the control flag exists: `--entwurf-controll`, `--entwurf-control-x`
|
|
196
|
+
// and friends. A wholesale rename to a different word is a different change, and the SSOT
|
|
197
|
+
// cell above catches it by reading the constant.
|
|
198
|
+
const FLAGLIKE = new RegExp(`${FLAG.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}[A-Za-z0-9-]*`, "g");
|
|
199
|
+
const wrong: string[] = [];
|
|
200
|
+
let sites = 0;
|
|
201
|
+
for (const rel of tracked) {
|
|
202
|
+
const body = readFileSync(path.join(REPO, rel), "utf8");
|
|
203
|
+
body.split("\n").forEach((line, i) => {
|
|
204
|
+
// A whole-line comment in any of the three syntaxes these files use. An inline
|
|
205
|
+
// trailing comment is NOT stripped: `#` and `//` both appear inside real argv
|
|
206
|
+
// strings here, and guessing where a comment starts would drop live code.
|
|
207
|
+
if (/^\s*(\/\/|#|\*|\/\*)/.test(line)) return;
|
|
208
|
+
for (const m of line.matchAll(FLAGLIKE)) {
|
|
209
|
+
sites++;
|
|
210
|
+
if (m[0] !== FLAG) wrong.push(`${rel}:${i + 1}: ${m[0]}`);
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
ok(
|
|
215
|
+
`[QK:PILAUNCH-FLAG-SITES] every control-flag PREFIX VARIANT in tracked code is exactly the constant (${sites} occurrences swept, prose excluded)`,
|
|
216
|
+
wrong.length === 0,
|
|
217
|
+
`FLAG=${FLAG}\ndisagreeing:\n${wrong.join("\n")}`,
|
|
218
|
+
);
|
|
219
|
+
// A sweep that found nothing would also report zero disagreements, so the count is
|
|
220
|
+
// asserted too: the launcher alone guarantees at least one.
|
|
221
|
+
ok(
|
|
222
|
+
"[QK:PILAUNCH-FLAG-SWEEP-REACHES] the sweep actually reached code that names the flag",
|
|
223
|
+
sites > 1,
|
|
224
|
+
`sites=${sites}`,
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// ── 2. the dispatcher verb must not reach the vendor ──────────────────────
|
|
229
|
+
{
|
|
230
|
+
const r = launch(["--model", "x"]);
|
|
231
|
+
ok(
|
|
232
|
+
"[QK:PILAUNCH-VERB-DROPPED] the dispatcher verb never reaches the vendor — without the shift, `pi` arrives as a prompt argument",
|
|
233
|
+
r.status === 0 && !r.args.includes("pi"),
|
|
234
|
+
`argv: ${JSON.stringify(r.args)}`,
|
|
235
|
+
);
|
|
236
|
+
ok(
|
|
237
|
+
"[QK:PILAUNCH-FLAG-FIRST] the injected flag is the FIRST token, so the control socket is stood up before anything the operator asked for",
|
|
238
|
+
r.args[0] === FLAG,
|
|
239
|
+
`argv: ${JSON.stringify(r.args)}`,
|
|
240
|
+
);
|
|
241
|
+
ok(
|
|
242
|
+
"[QK:PILAUNCH-NOTHING-ELSE-INJECTED] the launcher adds the flag and NOTHING else — operator taste is not a launcher decision (Rule 9)",
|
|
243
|
+
JSON.stringify(r.args) === JSON.stringify([FLAG, "--model", "x"]),
|
|
244
|
+
`argv: ${JSON.stringify(r.args)}`,
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// ── 3. operator argv crosses byte-identical ───────────────────────────────
|
|
249
|
+
{
|
|
250
|
+
const operator = ["--model", "a b", "", "-", "--", "한글 인자", " spaced "];
|
|
251
|
+
const r = launch(operator);
|
|
252
|
+
ok(
|
|
253
|
+
"[QK:PILAUNCH-ARGV-BYTES] every operator element crosses byte-identical — empty strings, embedded spaces, a bare dash, the terminator and non-ASCII",
|
|
254
|
+
JSON.stringify(r.args) === JSON.stringify([FLAG, ...operator]),
|
|
255
|
+
`expected: ${JSON.stringify([FLAG, ...operator])}\nactual: ${JSON.stringify(r.args)}`,
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// ── 4. the flag is ADDED, never deduplicated ──────────────────────────────
|
|
260
|
+
{
|
|
261
|
+
const r = launch([FLAG, "--model", "x"]);
|
|
262
|
+
ok(
|
|
263
|
+
"[QK:PILAUNCH-FLAG-NOT-DEDUPED] an operator who already passes the flag gets it twice, because pi was measured to eat the duplicate and a scan would earn nothing",
|
|
264
|
+
JSON.stringify(r.args) === JSON.stringify([FLAG, FLAG, "--model", "x"]),
|
|
265
|
+
`argv: ${JSON.stringify(r.args)}`,
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// ── 5. the vendor's exit status is this command's exit status ─────────────
|
|
270
|
+
// THIS CELL CARRIES NO MUTANT, and the reason is measured rather than conceded: run.sh
|
|
271
|
+
// is `set -euo pipefail` (run.sh:14), so every mutation that stops the vendor status
|
|
272
|
+
// from reaching the caller also stops `exec` from replacing the process. The non-exec
|
|
273
|
+
// form still exits 7, because set -e aborts before a planted `exit 0` can run. A mutant
|
|
274
|
+
// that kills two claims is not an attributable mutant, so this stands on its assertion
|
|
275
|
+
// alone until that coupling changes.
|
|
276
|
+
// Asserted BEFORE the pid cell on purpose: dropping `exec` for a plain call still
|
|
277
|
+
// propagates the status, while running the vendor and then exiting 0 breaks both. With
|
|
278
|
+
// the status checked first each of those two mutations lands on exactly one claim.
|
|
279
|
+
{
|
|
280
|
+
const r = launch([], { FAKE_PI_EXIT: "7" });
|
|
281
|
+
ok(
|
|
282
|
+
"[QK:PILAUNCH-EXIT-PASSTHROUGH] the vendor's exit status is the launch's exit status",
|
|
283
|
+
r.status === 7,
|
|
284
|
+
`status=${r.status}`,
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// ── 6. exec, not fork: the pid survives ───────────────────────────────────
|
|
289
|
+
{
|
|
290
|
+
const r = spawnSync("bash", ["-c", `echo "SHELL_PID=$$"; exec bash "${RUN_SH}" pi`], {
|
|
291
|
+
cwd: root,
|
|
292
|
+
encoding: "utf8",
|
|
293
|
+
env: {
|
|
294
|
+
...process.env,
|
|
295
|
+
...sandboxEnv,
|
|
296
|
+
PATH: `${bin}:${pathWithoutVendor}`,
|
|
297
|
+
ENTWURF_PI_LAUNCH_ACTIVE: undefined as unknown as string,
|
|
298
|
+
},
|
|
299
|
+
});
|
|
300
|
+
const shellPid = /^SHELL_PID=(\d+)$/m.exec(r.stdout ?? "")?.[1];
|
|
301
|
+
const vendorPid = /^PID=(\d+)$/m.exec(r.stdout ?? "")?.[1];
|
|
302
|
+
ok(
|
|
303
|
+
"[QK:PILAUNCH-EXEC-KEEPS-PID] the vendor REPLACES this process — a fork would leave run.sh as a parent that supervises nothing",
|
|
304
|
+
Boolean(shellPid) && shellPid === vendorPid,
|
|
305
|
+
`shell=${shellPid} vendor=${vendorPid}`,
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// ── 7. an absent vendor is a NAMED refusal, never a quiet success ─────────
|
|
310
|
+
{
|
|
311
|
+
const r = launch([], {}, false);
|
|
312
|
+
ok(
|
|
313
|
+
"[QK:PILAUNCH-VENDOR-ABSENT] with no `pi` on PATH the launch refuses by name and non-zero, and never reports success",
|
|
314
|
+
r.status !== 0 && r.out.includes("no 'pi' executable found on PATH") && r.pid === "",
|
|
315
|
+
`status=${r.status}\n${r.out}`,
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// ── 8. the recursion fence, and the sentinel that carries it ──────────────
|
|
320
|
+
{
|
|
321
|
+
const armed = launch([]);
|
|
322
|
+
ok(
|
|
323
|
+
"[QK:PILAUNCH-SENTINEL-EXPORTED] the sentinel is EXPORTED, so a PATH `pi` that shells back here sees it on re-entry",
|
|
324
|
+
armed.sentinel === "1",
|
|
325
|
+
`sentinel=${armed.sentinel}`,
|
|
326
|
+
);
|
|
327
|
+
const r = launch([], { ENTWURF_PI_LAUNCH_ACTIVE: "1" });
|
|
328
|
+
ok(
|
|
329
|
+
"[QK:PILAUNCH-RECURSION-FENCE] a re-entered launch refuses by name instead of spinning forever behind a hung terminal",
|
|
330
|
+
r.status !== 0 && r.out.includes("recursive managed launch detected") && r.pid === "",
|
|
331
|
+
`status=${r.status}\n${r.out}`,
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// ── 9. the case copilot's SECOND fence covers, measured here instead ───────
|
|
336
|
+
{
|
|
337
|
+
const loopBin = path.join(root, "loopbin");
|
|
338
|
+
mkdirSync(loopBin, { recursive: true });
|
|
339
|
+
symlinkSync(RUN_SH, path.join(loopBin, "pi"));
|
|
340
|
+
const r = spawnSync("bash", [RUN_SH, "pi"], {
|
|
341
|
+
cwd: root,
|
|
342
|
+
encoding: "utf8",
|
|
343
|
+
timeout: 30_000,
|
|
344
|
+
env: {
|
|
345
|
+
...process.env,
|
|
346
|
+
...sandboxEnv,
|
|
347
|
+
PATH: `${loopBin}:${pathWithoutVendor}`,
|
|
348
|
+
ENTWURF_PI_LAUNCH_ACTIVE: undefined as unknown as string,
|
|
349
|
+
},
|
|
350
|
+
});
|
|
351
|
+
ok(
|
|
352
|
+
"[QK:PILAUNCH-SYMLINK-NOT-A-LOOP] a PATH `pi` symlinked to run.sh is exec'd as an unknown verb and fails closed — this is why the second fence copilot carries is not needed here",
|
|
353
|
+
r.status !== 0 && r.signal === null,
|
|
354
|
+
`status=${r.status} signal=${r.signal}`,
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
console.log(`\ncheck-pi-launch: ${passed} checks passed`);
|
|
359
|
+
} finally {
|
|
360
|
+
rmSync(root, { recursive: true, force: true });
|
|
361
|
+
}
|
|
@@ -316,6 +316,13 @@ function runSubcommand(sub: string, env: Record<string, string | undefined>): {
|
|
|
316
316
|
// history that earns a place in the gate blocking every cut. On-demand, with the exclusion
|
|
317
317
|
// stated where an operator reads it.
|
|
318
318
|
"smoke-herdr-plugin-build-live": ["VERIFY.md", "The release aggregate does not require the plugin build smoke"],
|
|
319
|
+
// #118 홉 1, third on the same rail and for the same two reasons: herdr is OPTIONAL, so an
|
|
320
|
+
// aggregate that required this would block a cut on a host that does not have the rail at
|
|
321
|
+
// all; and first evidence precedes promotion — this smoke's acceptance runs are hours old,
|
|
322
|
+
// which is evidence the axis works rather than the run history that earns a place in the
|
|
323
|
+
// gate blocking every cut. It also needs a Docker daemon and the public network, neither of
|
|
324
|
+
// which the aggregate owns. On-demand, with the exclusion stated where an operator reads it.
|
|
325
|
+
"smoke-herdr-raw-install-live": ["VERIFY.md", "The plugin's FIRST USER PATH is its own on-demand axis"],
|
|
319
326
|
};
|
|
320
327
|
|
|
321
328
|
const allLive = globSync("scripts/smoke-*live*.{ts,sh}", { cwd: REPO_DIR })
|
|
@@ -69,7 +69,22 @@ function listWorkSurface(pathspec: string[]): string[] {
|
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
const legacyFiles = listWorkSurface(["scripts/"]);
|
|
72
|
-
|
|
72
|
+
// The framework axis is every vitest-managed lane, and since #119 V2 that is TWO locations:
|
|
73
|
+
// the migration lane under test/, and tests written beside the behaviour they certify. The
|
|
74
|
+
// pathspecs are `:(glob)` so `**` means "at any depth" and nothing but a test file enters —
|
|
75
|
+
// pi-extensions/ and plugins/ are product trees, and pulling them in whole would put 20k lines
|
|
76
|
+
// of product into a VERIFICATION denominator.
|
|
77
|
+
//
|
|
78
|
+
// This is what keeps the combined total honest through the V3 migration: a gate moving from
|
|
79
|
+
// scripts/ to a file beside its subject moves lines BETWEEN axes, and only a drop in the
|
|
80
|
+
// combined number is subtraction. Before this, such a gate left the denominator entirely and
|
|
81
|
+
// every migrated line read as a deletion.
|
|
82
|
+
const frameworkFiles = listWorkSurface([
|
|
83
|
+
"test/",
|
|
84
|
+
"vitest.config.ts",
|
|
85
|
+
":(glob)pi-extensions/**/*.test.ts",
|
|
86
|
+
":(glob)plugins/herdr/**/*.test.mjs",
|
|
87
|
+
]);
|
|
73
88
|
|
|
74
89
|
function countLines(rel: string): number {
|
|
75
90
|
const body = fs.readFileSync(path.join(REPO_DIR, rel), "utf8");
|
|
@@ -83,6 +98,20 @@ const H_TEXT = /readFileSync[^\n]*(?:pi-extensions|mcp\/|\.ts["'`]|SOURCE|SRC)|\
|
|
|
83
98
|
const H_PROC = /\b(?:spawn|spawnSync|execFile|execFileSync|execSync|fork)\s*\(|subprocess\.(?:run|Popen|check_)/;
|
|
84
99
|
const H_NET = /\b(?:http|net)\.createServer|\.listen\(/;
|
|
85
100
|
const H_FS = /from\s+["']node:fs["']|require\(["']node:fs["']\)/;
|
|
101
|
+
// H_IMPORTS asks for an ASCENDING path into a product tree, because every gate under scripts/
|
|
102
|
+
// has to climb to reach one. A test written beside its subject never climbs: it imports
|
|
103
|
+
// `./subject.ts`. The distinction is the file's own location, not its spelling — a lane living
|
|
104
|
+
// inside pi-extensions/ or plugins/ that imports a relative sibling is importing product by
|
|
105
|
+
// construction. Applied ONLY to those lanes: scripts/check-*.ts also import `./lib/*`, and that
|
|
106
|
+
// is the verification surface importing itself, which is a different fact the mutant inventory
|
|
107
|
+
// already counts as infra-subject.
|
|
108
|
+
const H_IMPORTS_SIBLING = /(?:from\s+["']|import\(\s*["'])\.{1,2}\//;
|
|
109
|
+
// Wider than the framework-axis glob (`plugins/herdr/**`) on purpose, and the asymmetry only
|
|
110
|
+
// runs one way: the glob decides what ENTERS the denominator, this decides how a file already
|
|
111
|
+
// in it is read. A test under some other plugins/ package would have to be admitted by the
|
|
112
|
+
// glob first, so the looser shape here cannot inflate a count — it only keeps the classifier
|
|
113
|
+
// from throwing on the day that glob widens.
|
|
114
|
+
const BESIDE_BEHAVIOUR = /^(pi-extensions|plugins)\/.*\.test\.(ts|mjs)$/;
|
|
86
115
|
// H_LIVE asks whether the file's OWN CODE reads the LIVE switch — not whether the
|
|
87
116
|
// three letters appear. The naive `\bLIVE=1\b` form classified
|
|
88
117
|
// scripts/check-release-gate-outcomes.ts as real-live because that gate QUOTES the
|
|
@@ -145,6 +174,45 @@ const CLASS_OVERRIDES: Record<string, { cls: SemanticClass; reason: string }> =
|
|
|
145
174
|
cls: "source-topology",
|
|
146
175
|
reason: "asserts the scrubbed parent-transcript fixture's recorded shape — an artifact contract, no product run",
|
|
147
176
|
},
|
|
177
|
+
// #119 V1 measured the classifier THROWING on these four at HEAD 5062147 — the
|
|
178
|
+
// assertion below did its job and named real drift, in three distinct shapes. None
|
|
179
|
+
// of them is reachable by widening H_IMPORTS: there is no literal `../plugins/`
|
|
180
|
+
// import anywhere under scripts/ or test/ (measured, 0 matches), so a `plugins`
|
|
181
|
+
// alternative in that predicate would match nothing and only claim coverage it
|
|
182
|
+
// does not have.
|
|
183
|
+
//
|
|
184
|
+
// Shape 1 — the herdr gates reach their subject through a RUNTIME-ASSEMBLED
|
|
185
|
+
// dynamic import, `await import(pathToFileURL(LEAF).href)`. H_IMPORTS matches a
|
|
186
|
+
// literal specifier by design (a computed one cannot be read as text without
|
|
187
|
+
// executing the gate), so these two are override territory, not predicate
|
|
188
|
+
// territory.
|
|
189
|
+
"scripts/check-herdr-plugin-profile.ts": {
|
|
190
|
+
cls: "behavioral-contract",
|
|
191
|
+
reason:
|
|
192
|
+
"dynamically imports plugins/herdr/lib/integration-profile.mjs and source-reads that same leaf — executes product code, no process",
|
|
193
|
+
},
|
|
194
|
+
"scripts/check-herdr-runtime-bootstrap.ts": {
|
|
195
|
+
cls: "behavioral-contract",
|
|
196
|
+
reason:
|
|
197
|
+
"dynamically imports the herdr runtime leaf and its plugin re-export and drives them against mkdtemp HOME/XDG fixtures — executes product code, spawns nothing",
|
|
198
|
+
},
|
|
199
|
+
// Shape 2 — the reads go through a local `read(rel)` helper, so H_TEXT's
|
|
200
|
+
// line-level form (readFileSync on the same line as the path literal) cannot see
|
|
201
|
+
// them. No product is executed: this is a manifest/installer/CI structure gate.
|
|
202
|
+
"scripts/check-herdr-supply.ts": {
|
|
203
|
+
cls: "source-topology",
|
|
204
|
+
reason:
|
|
205
|
+
"reads the supply manifest, the CI installer and the workflow as text through a read() helper; runs no product",
|
|
206
|
+
},
|
|
207
|
+
// Shape 3 — a vitest lane whose SUBJECT is the verification surface itself
|
|
208
|
+
// (scripts/lib), not pi-extensions/ or mcp/. The mutant inventory below already
|
|
209
|
+
// counts that as infra-subject; the class axis needs the same honesty rather than
|
|
210
|
+
// a widened "product import" predicate that would blur the two.
|
|
211
|
+
"test/tmux-coordinate-row.test.ts": {
|
|
212
|
+
cls: "pure-unit",
|
|
213
|
+
reason:
|
|
214
|
+
"executes the scripts/lib/tmux-coordinate-row leaf — an INFRA-subject lane; no fs, no text read, no process",
|
|
215
|
+
},
|
|
148
216
|
};
|
|
149
217
|
|
|
150
218
|
/**
|
|
@@ -187,9 +255,13 @@ function classify(rel: string, axis: "legacy" | "framework"): Row {
|
|
|
187
255
|
const body = isShell ? fs.readFileSync(path.join(REPO_DIR, rel), "utf8") : effectiveBody(rel);
|
|
188
256
|
const base = path.basename(rel);
|
|
189
257
|
|
|
258
|
+
// One notion of "imports product", used by BOTH axes below so the style table and the class
|
|
259
|
+
// table can never disagree about the same file.
|
|
260
|
+
const importsProduct = H_IMPORTS.test(body) || (BESIDE_BEHAVIOUR.test(rel) && H_IMPORTS_SIBLING.test(body));
|
|
261
|
+
|
|
190
262
|
const style: Style = (() => {
|
|
191
263
|
if (isShell) return "shell";
|
|
192
|
-
const axes = [
|
|
264
|
+
const axes = [importsProduct, H_TEXT.test(body), H_PROC.test(body)];
|
|
193
265
|
const n = axes.filter(Boolean).length;
|
|
194
266
|
if (n >= 2) return "mixed";
|
|
195
267
|
if (axes[0]) return "imports-product";
|
|
@@ -207,11 +279,11 @@ function classify(rel: string, axis: "legacy" | "framework"): Row {
|
|
|
207
279
|
? "package-install"
|
|
208
280
|
: isShell || H_PROC.test(body) || H_NET.test(body)
|
|
209
281
|
? "hermetic-integration"
|
|
210
|
-
: H_TEXT.test(body) && !
|
|
282
|
+
: H_TEXT.test(body) && !importsProduct
|
|
211
283
|
? "source-topology"
|
|
212
|
-
:
|
|
284
|
+
: importsProduct && (H_TEXT.test(body) || H_FS.test(body))
|
|
213
285
|
? "behavioral-contract"
|
|
214
|
-
:
|
|
286
|
+
: importsProduct
|
|
215
287
|
? "pure-unit"
|
|
216
288
|
: null;
|
|
217
289
|
if (cls === null) {
|
|
@@ -224,7 +296,7 @@ function classify(rel: string, axis: "legacy" | "framework"): Row {
|
|
|
224
296
|
|
|
225
297
|
// ── buckets ──────────────────────────────────────────────────────────────────
|
|
226
298
|
const legacyGates = legacyFiles.filter((f) => /^scripts\/(check-|smoke-)/.test(f));
|
|
227
|
-
const frameworkGates = frameworkFiles.filter((f) =>
|
|
299
|
+
const frameworkGates = frameworkFiles.filter((f) => /\.test\.(ts|mjs)$/.test(f));
|
|
228
300
|
const lib = legacyFiles.filter((f) => f.startsWith("scripts/lib/"));
|
|
229
301
|
const mutantManifests = legacyFiles.filter((f) => f.startsWith("scripts/mutants/") && f.endsWith(".json"));
|
|
230
302
|
const fixtures = legacyFiles.filter((f) => f.startsWith("scripts/fixtures/"));
|
|
@@ -254,14 +326,16 @@ rule predicates (re-derive any number from these):
|
|
|
254
326
|
H_PROC ${H_PROC}
|
|
255
327
|
H_NET ${H_NET}
|
|
256
328
|
H_FS ${H_FS}
|
|
329
|
+
H_IMPORTS_SIBLING ${H_IMPORTS_SIBLING} (only for ${BESIDE_BEHAVIOUR})
|
|
257
330
|
H_LIVE ${H_LIVE}
|
|
258
331
|
H_LIVE is applied to the CODE-ONLY projection (comments and inert literals blanked;
|
|
259
332
|
shell double-quoted expansions kept), so a gate that merely QUOTES "LIVE=1" is not
|
|
260
333
|
real-live. Two-tier gates are listed under the class breakdown.
|
|
261
334
|
semantic classes, first match wins: override → real-live (name -live | H_LIVE on code) →
|
|
262
335
|
package-install (name pack|install) → hermetic-integration (.sh | H_PROC | H_NET) →
|
|
263
|
-
source-topology (H_TEXT ∧ ¬
|
|
264
|
-
pure-unit (
|
|
336
|
+
source-topology (H_TEXT ∧ ¬importsProduct) → behavioral-contract (importsProduct ∧ (H_TEXT ∨ H_FS)) →
|
|
337
|
+
pure-unit (importsProduct) → ERROR (unclassified is asserted zero)
|
|
338
|
+
importsProduct = H_IMPORTS, plus H_IMPORTS_SIBLING for a BESIDE_BEHAVIOUR lane
|
|
265
339
|
test/*.test.ts is classified together with the ./helpers/* bodies it imports.
|
|
266
340
|
`);
|
|
267
341
|
|
|
@@ -275,9 +349,11 @@ console.log(` scripts/lib/: ${lib.length} files, ${total(lib
|
|
|
275
349
|
console.log(` scripts/mutants/: ${mutantManifests.length} manifests`);
|
|
276
350
|
console.log(` scripts/fixtures/: ${fixtures.length} files`);
|
|
277
351
|
console.log(` other: ${legacyOther.length} files, ${total(legacyOther)} lines`);
|
|
278
|
-
console.log(`framework axis (test/ + vitest.config.ts): ${frameworkFiles.length} files, ${frameworkTotal} lines`);
|
|
279
352
|
console.log(
|
|
280
|
-
`
|
|
353
|
+
`framework axis (vitest-managed, both locations): ${frameworkFiles.length} files, ${frameworkTotal} lines`,
|
|
354
|
+
);
|
|
355
|
+
console.log(
|
|
356
|
+
` lanes (test/** + beside behaviour): ${frameworkGates.length} files, ${sum(rows.filter((r) => r.axis === "framework"))} lines`,
|
|
281
357
|
);
|
|
282
358
|
console.log(` helpers/config: ${frameworkSupport.length} files, ${total(frameworkSupport)} lines`);
|
|
283
359
|
console.log(
|
|
@@ -617,7 +617,7 @@ function sha256File(file: string): string {
|
|
|
617
617
|
return createHash("sha256").update(fs.readFileSync(file)).digest("hex");
|
|
618
618
|
}
|
|
619
619
|
|
|
620
|
-
function countOccurrences(haystack: string, needle: string): number {
|
|
620
|
+
export function countOccurrences(haystack: string, needle: string): number {
|
|
621
621
|
let count = 0;
|
|
622
622
|
let at = haystack.indexOf(needle);
|
|
623
623
|
while (at !== -1) {
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": 1,
|
|
3
|
+
"lane": "acp-backend-preflight",
|
|
4
|
+
"mutants": [
|
|
5
|
+
{
|
|
6
|
+
"claim": "ACP-PREFLIGHT-REPLAYS-TRANSCRIPT-TOOLS",
|
|
7
|
+
"title": "reading a `context.tools` field instead of replaying the transcript makes the exclude-tools preflight unfireable on every pi 0.86 turn — the operator is told a tool is excluded while the backend still runs it",
|
|
8
|
+
"subject": "pi-extensions/lib/acp/backend.ts",
|
|
9
|
+
"find": ["\t\t\tconst activeToolNames = getCurrentTools(context.messages).map((t) => t.name);"],
|
|
10
|
+
"replace": [
|
|
11
|
+
"\t\t\tconst activeToolNames =",
|
|
12
|
+
"\t\t\t\t(context as unknown as { tools?: { name: string }[] }).tools?.map((t) => t.name) ?? [\"read\", \"bash\", \"edit\", \"write\"];"
|
|
13
|
+
],
|
|
14
|
+
"gate": ["bash", "run.sh", "check-acp-backend-preflight"],
|
|
15
|
+
"timeoutSeconds": 300,
|
|
16
|
+
"signature": "[QK:ACP-PREFLIGHT-REPLAYS-TRANSCRIPT-TOOLS]",
|
|
17
|
+
"signatureSource": "scripts/check-acp-backend-preflight.ts"
|
|
18
|
+
}
|
|
19
|
+
]
|
|
20
|
+
}
|
|
@@ -40,6 +40,32 @@
|
|
|
40
40
|
"timeoutSeconds": 120,
|
|
41
41
|
"signature": "[QK:BRIDGEBOOT-PUBLIC-SURFACE-EXACT-SET]",
|
|
42
42
|
"signatureSource": "scripts/check-entwurf-bridge-boot.ts"
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"claim": "BRIDGEBOOT-CALLBACK-REGISTERED",
|
|
46
|
+
"title": "the callback verb is imported but never registered — invisible to every host, while every import-level assertion stays green",
|
|
47
|
+
"subject": "mcp/entwurf-bridge/src/index.ts",
|
|
48
|
+
"find": ["server.tool(\n\t\"entwurf_callback\","],
|
|
49
|
+
"replace": [
|
|
50
|
+
"const _unregisteredCallbackTool = (...a: unknown[]) => void a;\n_unregisteredCallbackTool(\n\t\"entwurf_callback\","
|
|
51
|
+
],
|
|
52
|
+
"gate": ["bash", "run.sh", "check-entwurf-bridge-boot"],
|
|
53
|
+
"timeoutSeconds": 120,
|
|
54
|
+
"signature": "[QK:BRIDGEBOOT-CALLBACK-REGISTERED]",
|
|
55
|
+
"signatureSource": "scripts/check-entwurf-bridge-boot.ts"
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
"claim": "BRIDGEBOOT-CALLBACK-ZERO-ARG",
|
|
59
|
+
"title": "a target parameter is added to the callback schema, giving the model a way to supply an address the env pair was meant to own",
|
|
60
|
+
"subject": "mcp/entwurf-bridge/src/index.ts",
|
|
61
|
+
"find": ["\t\"MCP child). No arguments, no fallback to a model-supplied target.\",\n\t{},"],
|
|
62
|
+
"replace": [
|
|
63
|
+
"\t\"MCP child). No arguments, no fallback to a model-supplied target.\",\n\t{ target: z.string().optional() },"
|
|
64
|
+
],
|
|
65
|
+
"gate": ["bash", "run.sh", "check-entwurf-bridge-boot"],
|
|
66
|
+
"timeoutSeconds": 120,
|
|
67
|
+
"signature": "[QK:BRIDGEBOOT-CALLBACK-ZERO-ARG]",
|
|
68
|
+
"signatureSource": "scripts/check-entwurf-bridge-boot.ts"
|
|
43
69
|
}
|
|
44
70
|
]
|
|
45
71
|
}
|