@junghanacs/entwurf 0.23.1 → 0.25.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/BASELINE.md +1 -0
- package/CHANGELOG.md +628 -0
- package/DELIVERY.md +24 -1
- package/FAQ.md +47 -0
- package/README.md +5 -3
- package/VERIFY.md +20 -2
- package/demo/README.md +5 -2
- package/docs/acp-backend-rail.md +49 -2
- package/docs/setup-clean-host.md +8 -7
- package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +38 -5
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/context.js +19 -5
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/callback-env.js +68 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/control-send-receipt.js +101 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-contract.js +6 -2
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-production.js +17 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-runner.js +4 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-send.js +53 -6
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-surface.js +38 -3
- 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 +20 -6
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/herdr-placement.js +24 -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/protocol.js +0 -7
- package/mcp/entwurf-bridge/dist/scripts/doctor-pi-provider.js +40 -3
- package/mcp/entwurf-bridge/dist/scripts/herdr-rails.mjs +72 -0
- package/mcp/entwurf-bridge/dist/scripts/probe-bridge-command.js +1 -0
- package/mcp/entwurf-bridge/src/index.ts +46 -5
- package/mcp/entwurf-bridge/test.sh +2 -2
- package/package.json +11 -9
- package/pi-extensions/entwurf-control.ts +83 -154
- package/pi-extensions/lib/acp/backend.ts +16 -6
- package/pi-extensions/lib/acp/context.ts +30 -10
- package/pi-extensions/lib/acp/session-store.ts +2 -2
- package/pi-extensions/lib/callback-env.ts +78 -0
- package/pi-extensions/lib/compaction-send-guard.ts +6 -1
- package/pi-extensions/lib/control-send-receipt.ts +108 -0
- package/pi-extensions/lib/entwurf-v2-contract-schema.ts +1 -1
- package/pi-extensions/lib/entwurf-v2-contract.ts +6 -2
- package/pi-extensions/lib/entwurf-v2-production.ts +19 -1
- package/pi-extensions/lib/entwurf-v2-runner.ts +23 -1
- package/pi-extensions/lib/entwurf-v2-send.ts +82 -6
- package/pi-extensions/lib/entwurf-v2-surface.ts +42 -3
- package/pi-extensions/lib/fresh-call-composition.ts +56 -22
- package/pi-extensions/lib/herdr-fresh-call.ts +25 -7
- package/pi-extensions/lib/herdr-placement.ts +24 -4
- package/pi-extensions/lib/mux-fresh-call.ts +23 -8
- package/pi-extensions/meta-bridge-omp.ts +35 -48
- package/protocol.js +0 -8
- package/run.sh +306 -53
- 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 +33 -9
- package/scripts/check-entwurf-bridge-boot.ts +16 -1
- package/scripts/check-entwurf-v2-surface.ts +289 -10
- package/scripts/check-fresh-call-dispatch.ts +4 -5
- package/scripts/check-gate-qualification.ts +51 -9
- package/scripts/check-herdr-activation.ts +73 -0
- package/scripts/check-herdr-fresh-call.ts +30 -6
- package/scripts/check-herdr-placement.ts +51 -0
- package/scripts/check-herdr-plugin-build.ts +11 -4
- package/scripts/check-herdr-plugin.ts +745 -38
- package/scripts/check-herdr-sandbox.ts +14 -2
- package/scripts/check-install-container.sh +1 -1
- package/scripts/check-mux-launcher-fence.ts +37 -0
- package/scripts/check-pi-launch.ts +8 -2
- package/scripts/check-release-gate-outcomes.ts +165 -0
- package/scripts/doctor-pi-provider.ts +44 -4
- package/scripts/herdr-rails.mjs +75 -0
- package/scripts/lib/mutation-qualify.ts +1 -1
- package/scripts/meta-bridge-state.py +18 -1
- package/scripts/mutants/acp-backend-preflight.json +20 -0
- package/scripts/mutants/bridge-boot-resume.json +26 -0
- package/scripts/mutants/bridge-command-boot.json +38 -0
- package/scripts/mutants/control-send-receipt.json +235 -0
- package/scripts/mutants/herdr-activation.json +16 -0
- package/scripts/mutants/herdr-fresh-call.json +15 -4
- package/scripts/mutants/herdr-plugin-build.json +7 -11
- package/scripts/mutants/herdr-plugin.json +177 -9
- package/scripts/mutants/meta-retire.json +22 -0
- package/scripts/mutants/mux-fresh-call.json +21 -8
- package/scripts/mutants/omp-fresh.json +5 -9
- package/scripts/mutants/pack-install.json +4 -4
- package/scripts/mutants/pi-floor.json +17 -0
- package/scripts/mutants/release-gate.json +48 -0
- package/scripts/mutants/retired-sent-surface.json +32 -0
- package/scripts/mutants/v2-surface.json +7 -7
- package/scripts/probe-bridge-command.ts +1 -0
- package/scripts/smoke-acp-session-reuse-live.ts +23 -5
- package/scripts/smoke-agy-install-state.sh +2 -2
- package/scripts/smoke-codex-fresh-live.ts +94 -13
- package/scripts/smoke-copilot-mcp-state.sh +1 -1
- package/scripts/smoke-herdr-fresh-call-live.ts +71 -36
- package/scripts/smoke-herdr-raw-install-live.sh +90 -5
- package/scripts/smoke-meta-install-state.sh +107 -1
- package/scripts/smoke-mux-lifecycle-live.ts +9 -2
- 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 +108 -2
- package/scripts/smoke-setup-verdict.sh +18 -2
|
@@ -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);
|
|
@@ -288,10 +288,30 @@ function main(): void {
|
|
|
288
288
|
// env object that assigns the fixture XDG_DATA_HOME also relocates HOME", which is the
|
|
289
289
|
// relation that actually keeps the vendor's store and its launcher in one tree.
|
|
290
290
|
const dataRootAssignment = (file: string): RegExp => (file.endsWith(".sh") ? /XDG_DATA_HOME=/ : /XDG_DATA_HOME:/);
|
|
291
|
+
/** A shell `env ... \` invocation is ONE logical command, and that command — not a brace
|
|
292
|
+
* pair — is the env block there. `${...}` is everywhere in a shell script, so the brace
|
|
293
|
+
* walk below would answer with a two-character slice and read HOME from a neighbouring
|
|
294
|
+
* line that assigns nothing. Walk the backslash continuations instead. */
|
|
295
|
+
const shellCommandAround = (src: string, at: number): string => {
|
|
296
|
+
const lines = src.split("\n");
|
|
297
|
+
let idx = 0;
|
|
298
|
+
let line = 0;
|
|
299
|
+
for (; line < lines.length - 1; line += 1) {
|
|
300
|
+
const next = idx + lines[line].length + 1;
|
|
301
|
+
if (next > at) break;
|
|
302
|
+
idx = next;
|
|
303
|
+
}
|
|
304
|
+
let first = line;
|
|
305
|
+
while (first > 0 && lines[first - 1].endsWith("\\")) first -= 1;
|
|
306
|
+
let last = line;
|
|
307
|
+
while (last < lines.length - 1 && lines[last].endsWith("\\")) last += 1;
|
|
308
|
+
return lines.slice(first, last + 1).join("\n");
|
|
309
|
+
};
|
|
291
310
|
/** The object literal / env block that carries the fixture data root, not the whole file. */
|
|
292
311
|
const envBlockAround = (src: string, file: string): string => {
|
|
293
312
|
const at = src.search(dataRootAssignment(file));
|
|
294
313
|
if (at < 0) return "";
|
|
314
|
+
if (file.endsWith(".sh")) return shellCommandAround(src, at);
|
|
295
315
|
const from = src.lastIndexOf("{", at);
|
|
296
316
|
if (from < 0) return src.slice(Math.max(0, at - 800), at + 800);
|
|
297
317
|
let depth = 0;
|
|
@@ -309,6 +329,11 @@ function main(): void {
|
|
|
309
329
|
reason: "it relocates HOME into the same sandbox, so store and launcher stay in one tree",
|
|
310
330
|
holds: (src, file) => /(^|\n)\s*HOME[,:]/.test(envBlockAround(src, file)),
|
|
311
331
|
},
|
|
332
|
+
"smoke-herdr-raw-install-live.sh": {
|
|
333
|
+
reason:
|
|
334
|
+
"its single fixture data root rides one `env` invocation that relocates HOME into the same mktemp sandbox, so the child it starts there never meets a fixture data root beside the operator's real HOME",
|
|
335
|
+
holds: (src, file) => /(^|\s)HOME=/.test(envBlockAround(src, file)),
|
|
336
|
+
},
|
|
312
337
|
};
|
|
313
338
|
/** Where each smoke's FIRST Claude-capable child begins. Named per smoke, because the
|
|
314
339
|
* three rails start children in three different ways and a generic "spawn" would match
|
|
@@ -360,6 +385,18 @@ function main(): void {
|
|
|
360
385
|
"the exemption discriminates rather than excuses: the exempt smoke relocates HOME in the SAME env block that fences its data root, and moving that line out of the block would put it back in the constituency",
|
|
361
386
|
Object.entries(EXEMPT).every(([f, e]) => e.holds(read(path.join("scripts", f)), f)),
|
|
362
387
|
);
|
|
388
|
+
{
|
|
389
|
+
// The shell half of that same sentence, proved rather than asserted: the exemption is
|
|
390
|
+
// read off the `env` invocation, so deleting HOME from THAT line — and only that line —
|
|
391
|
+
// must put the smoke straight back into the constituency.
|
|
392
|
+
const file = "smoke-herdr-raw-install-live.sh";
|
|
393
|
+
const src = read(path.join("scripts", file));
|
|
394
|
+
const withoutHome = src.replace(/ HOME="\$HOMELESS"/, "");
|
|
395
|
+
ok(
|
|
396
|
+
"the shell exemption is read from the env INVOCATION, not from the file: deleting the HOME assignment out of the same `env` line puts smoke-herdr-raw-install-live.sh back in the constituency",
|
|
397
|
+
withoutHome !== src && !EXEMPT[file].holds(withoutHome, file),
|
|
398
|
+
);
|
|
399
|
+
}
|
|
363
400
|
{
|
|
364
401
|
const IMPORT =
|
|
365
402
|
'import { assessLauncherCleanup, snapshotClaudeLauncher, verifyClaudeLauncher } from "./lib/claude-launcher-fence.ts";';
|
|
@@ -15,7 +15,9 @@
|
|
|
15
15
|
* six receiver-state facts before it execs, because setting its flag is a PROMISE of a
|
|
16
16
|
* doorbell that may not exist. `entwurf pi` promises nothing pi does not already own: on a
|
|
17
17
|
* host where the entwurf extension is not registered, `pi --entwurf-control` refuses itself
|
|
18
|
-
* with `Error: Unknown option: --entwurf-control` and exit 1 (
|
|
18
|
+
* with `Error: Unknown option: --entwurf-control` and exit 1 (`[측정 2026-09-20]` pi 0.86.0,
|
|
19
|
+
* `[측정 2026-09-22]` pi 0.87.0 and `[측정 2026-09-23]` pi 0.87.1 — sandbox HOME +
|
|
20
|
+
* PI_CODING_AGENT_DIR, byte-identical stderr line and exit 1 at all three floors). A
|
|
19
21
|
* pre-check here would duplicate that refusal and go stale the day pi renames it.
|
|
20
22
|
*
|
|
21
23
|
* WHY ONE RECURSION FENCE AND NOT TWO. The sentinel closes the only real loop — a PATH
|
|
@@ -26,7 +28,11 @@
|
|
|
26
28
|
*
|
|
27
29
|
* WHY THE FLAG IS ADDED AND NEVER DEDUPLICATED. `pit`/`pius`-style operator wrappers already
|
|
28
30
|
* pass `--entwurf-control`; passing it twice was measured byte-identical to passing it once
|
|
29
|
-
* (pi 0.85.1, extension
|
|
31
|
+
* (pi 0.85.1, extension REGISTERED — and that receipt has NOT been retaken at 0.86.0, 0.87.0 or 0.87.1.
|
|
32
|
+
* It needs a host where the extension is registered, which means opening a control session on the
|
|
33
|
+
* operator's own pi; the unregistered refusal above is what gets re-measured at each floor
|
|
34
|
+
* instead. So this line is a 0.85.1 fact and is not promoted to a 0.87.1 one — a LIVE observation
|
|
35
|
+
* candidate for a future release, never part of a deterministic green.) The launcher therefore injects exactly ONE and leaves
|
|
30
36
|
* the operator's copies alone — an argv scan would be code earning nothing.
|
|
31
37
|
*/
|
|
32
38
|
|
|
@@ -868,6 +868,171 @@ function runSubcommand(sub: string, env: Record<string, string | undefined>): {
|
|
|
868
868
|
);
|
|
869
869
|
}
|
|
870
870
|
|
|
871
|
+
// ===========================================================================
|
|
872
|
+
// A MUST step's ABSENT operator input is a SKIP; a WRONG one is a FAIL
|
|
873
|
+
//
|
|
874
|
+
// The step above is a MUST that takes three values only the operator can give.
|
|
875
|
+
// `[측정 2026-09-23]` all three were `ok()` assertions, and `ok()` throws, so
|
|
876
|
+
// their absence exited as FAIL — while `release_gate()` supplies none of them.
|
|
877
|
+
// The step therefore declined as a FAILURE inside every aggregate on every host,
|
|
878
|
+
// and 0.25.0's candidate receipt (`MUST PASS=23 FAIL=1`, `cut: BLOCKED (MUST
|
|
879
|
+
// FAIL)`) carries a prose classification of that red, and reading it as a floor
|
|
880
|
+
// would require a per-cut exception. The release skill says "Do not waive a MUST
|
|
881
|
+
// failure without diagnosing and explicitly classifying the failing axis", and
|
|
882
|
+
// classifying is what that record does -- but a contract that manufactures the same
|
|
883
|
+
// red every cut invites the exception rather than earning a floor. (The cut stayed
|
|
884
|
+
// BLOCKED; nothing was approved.)
|
|
885
|
+
//
|
|
886
|
+
// This cell pins the repaired boundary in BOTH directions, because either half
|
|
887
|
+
// alone is a different defect: make absence a FAIL and the exception comes back;
|
|
888
|
+
// make a wrong answer a SKIP and a dead pid or a foreign process buys silence.
|
|
889
|
+
// It is deliberately separate from the claim above — that one owns WHERE the step
|
|
890
|
+
// is wired, this one owns WHAT its outcome means — so one mutant cannot satisfy
|
|
891
|
+
// both, and it reads the smoke with comments stripped so a promise in prose
|
|
892
|
+
// cannot stand in for the arm that actually executes.
|
|
893
|
+
// ===========================================================================
|
|
894
|
+
{
|
|
895
|
+
const SMOKE = "scripts/smoke-codex-fresh-live.ts";
|
|
896
|
+
const smokeRaw = readFileSync(join(REPO_DIR, SMOKE), "utf8");
|
|
897
|
+
// Comments are where this exact defect hid for a release: the old skip line
|
|
898
|
+
// NAMED all three env vars while only LIVE=1 could produce a skip.
|
|
899
|
+
const smoke = smokeRaw
|
|
900
|
+
.replace(/\/\*[\s\S]*?\*\//g, "")
|
|
901
|
+
.split("\n")
|
|
902
|
+
.map((line) => line.replace(/(^|\s)\/\/.*$/, ""))
|
|
903
|
+
.join("\n");
|
|
904
|
+
const verify = readFileSync(join(REPO_DIR, "VERIFY.md"), "utf8");
|
|
905
|
+
const skill = readFileSync(join(REPO_DIR, ".claude/skills/entwurf-release/SKILL.md"), "utf8");
|
|
906
|
+
const TRIPLE = ["ENTWURF_CODEX_APP_SERVER_PID", "ENTWURF_CODEX_FRESH_MODEL", "ENTWURF_CODEX_FRESH_PI_MODEL"] as const;
|
|
907
|
+
const broken: string[] = [];
|
|
908
|
+
|
|
909
|
+
// (a) Absence must reach `skipLive`, and the GUARD that routes it there is part of
|
|
910
|
+
// the claim — not just the presence of a skipLive call somewhere nearby. A
|
|
911
|
+
// dead arm behind a constant-false condition would satisfy "there is a skip"
|
|
912
|
+
// while every absent input fell through to the assertions below and threw,
|
|
913
|
+
// which is precisely the pre-repair behaviour. So the emptiness test, the
|
|
914
|
+
// guard and the arm are pinned as one contiguous shape, and nothing may
|
|
915
|
+
// assert in between.
|
|
916
|
+
const absenceArm = smoke.match(
|
|
917
|
+
/const absentInputs\s*=\s*OPERATOR_INPUTS\.filter\(([^\n]*?)===\s*""\);\s*if \(absentInputs\.length > 0\) \{\s*skipLive\(([\s\S]{0,1400}?)\n\t\}/,
|
|
918
|
+
);
|
|
919
|
+
if (absenceArm === null) {
|
|
920
|
+
broken.push(
|
|
921
|
+
`${SMOKE}: the absent-operator-input arm is not the contiguous shape ` +
|
|
922
|
+
'`absentInputs = OPERATOR_INPUTS.filter(... === "")` -> `if (absentInputs.length > 0)` -> `skipLive(` — ' +
|
|
923
|
+
"an unset prerequisite would fall through to an assertion and exit as FAIL again",
|
|
924
|
+
);
|
|
925
|
+
} else if (!absenceArm[2]?.includes("absentInputs")) {
|
|
926
|
+
broken.push(`${SMOKE}: the skipLive arm does not report WHICH operator inputs were absent`);
|
|
927
|
+
}
|
|
928
|
+
for (const name of TRIPLE) {
|
|
929
|
+
if (!new RegExp(`\\[\\s*"${name}"`).test(smoke)) {
|
|
930
|
+
broken.push(`${SMOKE}: ${name} is not in the declared operator-input set, so its absence is unclassified`);
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
// (c) The reproducible green must be WRITTEN DOWN where a cut is taken from,
|
|
935
|
+
// or "supply the triple" is folklore. VERIFY owns the acceptance condition;
|
|
936
|
+
// the release skill owns the command an operator actually types at P5.
|
|
937
|
+
const p5 = skill.slice(skill.indexOf("## P5."), skill.indexOf("## P6."));
|
|
938
|
+
for (const name of TRIPLE) {
|
|
939
|
+
if (!p5.includes(name)) {
|
|
940
|
+
broken.push(`.claude/skills/entwurf-release/SKILL.md P5: the release-gate invocation does not carry ${name}`);
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
if (!verify.includes("`release-gate --cut` inherits that triple")) {
|
|
944
|
+
broken.push(
|
|
945
|
+
"VERIFY.md no longer states that the aggregate cut inherits the operator-supplied Codex triple — " +
|
|
946
|
+
"without that sentence a cut host cannot know why the MUST step skipped",
|
|
947
|
+
);
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
// (d) The policy half is unchanged: a MUST SKIP still blocks a cut. Asked of the
|
|
951
|
+
// SHARED shell authority rather than re-implemented here, so this cell cannot
|
|
952
|
+
// drift from the truth table the protocol owns. Reclassifying an absent
|
|
953
|
+
// prerequisite as SKIP is only honest while that answer stays NO.
|
|
954
|
+
const cutReleasable = (failc: number, skipc: number, cut: number): boolean =>
|
|
955
|
+
inShell(`if entwurf_release_releasable ${failc} ${skipc} ${cut}; then echo YES; else echo NO; fi`) === "YES";
|
|
956
|
+
if (cutReleasable(0, 1, 1) !== false) {
|
|
957
|
+
broken.push("a MUST SKIP no longer blocks --cut, so classifying an absent prerequisite as SKIP would hide it");
|
|
958
|
+
}
|
|
959
|
+
if (cutReleasable(0, 1, 0) !== true) {
|
|
960
|
+
broken.push("a MUST SKIP now blocks the unattended diagnostic run, which was never the contract");
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
assert.ok(
|
|
964
|
+
broken.length === 0,
|
|
965
|
+
"[QK:CODEX-OPERATOR-PREREQ-ABSENT-SKIPS] a MUST step that takes operator-supplied inputs must DECLINE when they " +
|
|
966
|
+
"are absent — protocol SKIP naming each missing one — while `--cut` keeps refusing that SKIP and the cut " +
|
|
967
|
+
"invocation that supplies the triple is written in VERIFY and the release skill. Absence classified as FAIL " +
|
|
968
|
+
"is what made this step red inside every cut and would require a per-cut exception. " +
|
|
969
|
+
`Broken: ${broken.join("; ")}`,
|
|
970
|
+
);
|
|
971
|
+
|
|
972
|
+
// The MIRROR claim, and its own token: a value the operator DID supply stays a
|
|
973
|
+
// FAIL when it is wrong. Kept apart from the arm above because softening this
|
|
974
|
+
// direction is the opposite defect and must not be killable by the same mutant —
|
|
975
|
+
// a dead pid, a foreign process or an app-server in the gate's own session would
|
|
976
|
+
// otherwise buy silence instead of a red.
|
|
977
|
+
const softened: string[] = [];
|
|
978
|
+
if (!/ok\(\s*\n?\s*"ENTWURF_CODEX_APP_SERVER_PID explicitly names/.test(smoke)) {
|
|
979
|
+
softened.push(
|
|
980
|
+
`${SMOKE}: the pid grammar is no longer an ok() assertion — a malformed operator answer must FAIL, not decline`,
|
|
981
|
+
);
|
|
982
|
+
}
|
|
983
|
+
// A model env asserted non-empty would put absence back behind a throw, making
|
|
984
|
+
// the decline arm above dead code. One direction repaired at the cost of the
|
|
985
|
+
// other is not the boundary.
|
|
986
|
+
if (/ok\([^)]*ENTWURF_CODEX_FRESH_(?:PI_)?MODEL[^)]*length\s*>\s*0/.test(smoke)) {
|
|
987
|
+
softened.push(`${SMOKE}: a model env is still asserted non-empty, which re-classifies its absence as FAIL`);
|
|
988
|
+
}
|
|
989
|
+
assert.ok(
|
|
990
|
+
softened.length === 0,
|
|
991
|
+
"[QK:CODEX-OPERATOR-PREREQ-WRONG-FAILS] the same step must still FAIL on an operator input that is PRESENT and " +
|
|
992
|
+
"wrong. Declining there would let a dead pid or a non-Codex process pass as a missing prerequisite, and a " +
|
|
993
|
+
`cut would read silence as acceptance. Broken: ${softened.join("; ")}`,
|
|
994
|
+
);
|
|
995
|
+
|
|
996
|
+
// A THIRD claim, because this one is about a different subject: not what the smoke
|
|
997
|
+
// does with an input, but WHERE the release operator's input comes from.
|
|
998
|
+
//
|
|
999
|
+
// `[측정 2026-09-23]` the first cut of the P5 instructions above derived the pid with
|
|
1000
|
+
// `pgrep -f 'codex.*app-server' | head -1`. That is exactly the inference the step
|
|
1001
|
+
// refuses, written into the page an operator follows — and worse than a stale
|
|
1002
|
+
// sentence, because a host can hold more than one app-server and `head -1` picks an
|
|
1003
|
+
// arbitrary one, which the smoke would then dutifully verify as "the operator's
|
|
1004
|
+
// answer". A no-inference rule that the operating instructions violate is enforced
|
|
1005
|
+
// nowhere. So provenance is pinned in the skill itself: each value is STATED, the
|
|
1006
|
+
// three `:` guards refuse an unset or empty one before any LIVE cost, and no process
|
|
1007
|
+
// search or command substitution may manufacture them.
|
|
1008
|
+
const provenance: string[] = [];
|
|
1009
|
+
const SKILL_REL = ".claude/skills/entwurf-release/SKILL.md";
|
|
1010
|
+
for (const forbidden of ["pgrep", "pidof", "ps -", "$(pgrep"]) {
|
|
1011
|
+
if (p5.includes(forbidden)) {
|
|
1012
|
+
provenance.push(
|
|
1013
|
+
`${SKILL_REL} P5: builds an operator input with \`${forbidden}\` — the pid is stated, never searched for`,
|
|
1014
|
+
);
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
for (const name of ["CODEX_APP_SERVER_PID", "CODEX_FRESH_MODEL", "CODEX_FRESH_PI_MODEL"]) {
|
|
1018
|
+
// `: "${VAR:?...}"` — an explicit required-parameter guard. Anything weaker
|
|
1019
|
+
// (a default via `:-`, or no guard) lets a forgotten export reach a LIVE run.
|
|
1020
|
+
if (!new RegExp(`: "\\$\\{${name}:\\?[^"]+}"`).test(p5)) {
|
|
1021
|
+
provenance.push(`${SKILL_REL} P5: ${name} has no \`: "\${${name}:?…}"\` required-value guard`);
|
|
1022
|
+
}
|
|
1023
|
+
if (new RegExp(`${name}=\\$\\(`).test(p5)) {
|
|
1024
|
+
provenance.push(`${SKILL_REL} P5: ${name} is assigned from a command substitution rather than stated`);
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
assert.ok(
|
|
1028
|
+
provenance.length === 0,
|
|
1029
|
+
"[QK:CODEX-OPERATOR-PREREQ-STATED-NOT-SEARCHED] the release instructions must make the operator STATE each Codex " +
|
|
1030
|
+
"input and guard it with an explicit required-value check, never derive one by process search or command " +
|
|
1031
|
+
"substitution. A page that infers the app-server pid teaches the very inference the gate refuses, and picks an " +
|
|
1032
|
+
`arbitrary process when a host runs several. Broken: ${provenance.join("; ")}`,
|
|
1033
|
+
);
|
|
1034
|
+
}
|
|
1035
|
+
|
|
871
1036
|
// ===========================================================================
|
|
872
1037
|
// The operator's CONFIGURED bridge invocation is proven BEFORE the cost-bearing LIVE tier
|
|
873
1038
|
//
|
|
@@ -159,11 +159,24 @@ log(` EFFECTIVE (${effectiveScope}): ${effectiveDesc}`);
|
|
|
159
159
|
|
|
160
160
|
// install-state ownership (user scope). absent state on a configured effective is either a
|
|
161
161
|
// pre-Task-2 install or a user-override we deliberately did not own.
|
|
162
|
+
//
|
|
163
|
+
// `command` is read beside it because it — not the bare name — is the CANONICAL managed command
|
|
164
|
+
// (#116 M3-b2). `register-pi-provider.py` writes the same string into the settings entry and into
|
|
165
|
+
// this record (:332-342, :368-369), and in plugin mode that string is an absolute bridge under the
|
|
166
|
+
// certified stable runtime, because a Herdr-plugin host has nothing entwurf on PATH. Reading it
|
|
167
|
+
// here is what lets this doctor judge drift without re-deriving the stable runtime root: the root
|
|
168
|
+
// is the writer's business, and a second derivation in this file would be a second address axis.
|
|
169
|
+
//
|
|
170
|
+
// `ownership` is the PREIMAGE classification — what stood at the key BEFORE that install — so
|
|
171
|
+
// `absent` means "we created it", not "nobody owns it". Treating it as an ownership verdict is what
|
|
172
|
+
// made every plugin-mode host red while its bridge booted and served all eight verbs.
|
|
162
173
|
let ownership: string | undefined;
|
|
174
|
+
let stateCommand: string | undefined;
|
|
163
175
|
if (existsSync(statePath)) {
|
|
164
176
|
try {
|
|
165
|
-
const st = JSON.parse(readFileSync(statePath, "utf8")) as { ownership?: string };
|
|
177
|
+
const st = JSON.parse(readFileSync(statePath, "utf8")) as { ownership?: string; command?: string };
|
|
166
178
|
ownership = typeof st.ownership === "string" ? st.ownership : undefined;
|
|
179
|
+
stateCommand = typeof st.command === "string" ? st.command : undefined;
|
|
167
180
|
log(` state: install-state present (ownership=${ownership}).`);
|
|
168
181
|
} catch {
|
|
169
182
|
log(` state: FAIL — install-state ${statePath} is unreadable/corrupt.`);
|
|
@@ -219,9 +232,36 @@ if (effectiveEntry === undefined) {
|
|
|
219
232
|
}
|
|
220
233
|
}
|
|
221
234
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
235
|
+
// Ownership classification never rounds a broken runtime up to green. It only says who may
|
|
236
|
+
// repair the entry after the independent boot verdict above.
|
|
237
|
+
//
|
|
238
|
+
// A MANAGED record is the pair (known preimage ownership, recorded command). For that pair the
|
|
239
|
+
// recorded command is canonical and the comparison runs REGARDLESS of whether the effective
|
|
240
|
+
// command happens to be the bare name. Keeping this decision inside a `!isBare` branch is how
|
|
241
|
+
// real drift escaped: a record naming the absolute plugin bridge, with the settings repointed to
|
|
242
|
+
// the bare `entwurf-bridge`, is drift — and a bare bridge that boots would otherwise carry it to
|
|
243
|
+
// green. "The effective command is spellable as our old default" is not evidence that it is the
|
|
244
|
+
// command this install wrote.
|
|
245
|
+
//
|
|
246
|
+
// Everything else — a record that names no command, or one whose ownership is a word this
|
|
247
|
+
// doctor does not know — falls through to the ORIGINAL logic below, unchanged. Repairing this
|
|
248
|
+
// defect is not a licence to migrate the state schema, to grant an amnesty to records written
|
|
249
|
+
// before `command` existed, or to let an unknown ownership word buy a managed green because a
|
|
250
|
+
// string happened to match. The state is rewritten by `./run.sh setup`, not by its reader.
|
|
251
|
+
const knownManagedOwnership =
|
|
252
|
+
ownership === "absent" || ownership === "managed-current" || ownership === "managed-legacy";
|
|
253
|
+
if (knownManagedOwnership && stateCommand !== undefined) {
|
|
254
|
+
if (cmd !== stateCommand) {
|
|
255
|
+
log(
|
|
256
|
+
` FAIL: state records '${stateCommand}' as the managed command (ownership=${ownership}), but the effective command is '${cmd}' — the key drifted to something entwurf never wrote.`,
|
|
257
|
+
);
|
|
258
|
+
hardFail = 1;
|
|
259
|
+
} else {
|
|
260
|
+
log(
|
|
261
|
+
` ok: effective command is the managed command this install recorded ('${stateCommand}', preimage ownership=${ownership}).`,
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
} else if (!isBare) {
|
|
225
265
|
if (ownership && ownership !== "user-override") {
|
|
226
266
|
log(` FAIL: state owns entwurf-bridge (ownership=${ownership}) but the effective command drifted to '${cmd}'.`);
|
|
227
267
|
hardFail = 1;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* herdr-rails — the ONE place the herdr rail's backend set and its per-backend model SYNTAX
|
|
3
|
+
* example are written down (#116 A7).
|
|
4
|
+
*
|
|
5
|
+
* WHY THIS FILE EXISTS. Three surfaces were about to say the same two things in three
|
|
6
|
+
* dialects: `herdr-fresh-call.ts` owned the closed backend tuple, `build.mjs` narrated one
|
|
7
|
+
* model id per harness at install time, and the status pane was about to print a RAILS block.
|
|
8
|
+
* Three copies of one fact is how a rail quietly grows a fourth backend in prose that no
|
|
9
|
+
* dispatch honours. So the fact lives here and the three read it.
|
|
10
|
+
*
|
|
11
|
+
* WHY `.mjs` AND NOT `.ts`. It is read by two runtimes that cannot agree on anything else:
|
|
12
|
+
* the Herdr plugin runs `node lib/status.mjs` with no loader flags at all, and the pi
|
|
13
|
+
* extension is typechecked by `tsc`. A `.ts` leaf would be unloadable by the first; a second
|
|
14
|
+
* copy would defeat the point. `protocol.js` set this precedent for exactly the same reason
|
|
15
|
+
* (root tsconfig.json `allowJs`), and this file keeps its shape: dependency-free, no imports,
|
|
16
|
+
* no environment, no IO, so importing it can never be a side effect.
|
|
17
|
+
*
|
|
18
|
+
* WHAT THIS FILE IS NOT, and each absence is load-bearing:
|
|
19
|
+
* - NOT a model catalogue. One example per backend, chosen to show the SHAPE of the string
|
|
20
|
+
* (`provider/model` vs a bare vendor id). Adding a second example starts a list, a list
|
|
21
|
+
* invites completeness, and completeness is a catalogue Entwurf would then be expected to
|
|
22
|
+
* keep current for every vendor. `entwurf_fresh_call` takes a model string with no default
|
|
23
|
+
* and validates nothing; that is the contract, and an example must not imply otherwise.
|
|
24
|
+
* - NOT a supported-model set, a recommendation, or a validator. Nothing reads MODEL_SYNTAX_EXAMPLE
|
|
25
|
+
* to decide anything — it is display text on three surfaces.
|
|
26
|
+
* - NOT a provider, billing or quota statement. No rail here names a provider, a subscription
|
|
27
|
+
* or a cost. `#76` is about which rail a sibling may be launched on, and a syntax example
|
|
28
|
+
* that mentioned a provider would read as an answer to it. It does not answer it.
|
|
29
|
+
* - NOT a launcher, and not a claim that any binary exists. `HERDR_AGENT_KIND` (the herdr
|
|
30
|
+
* `--kind` token) stays with the rail that spends it; this leaf carries no argv.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The closed pilot set for the herdr launch rail. Herdr's own `--kind` enum is much larger and
|
|
35
|
+
* that is not evidence of support: every other backend is a named pre-mutation reject.
|
|
36
|
+
*
|
|
37
|
+
* The JSDoc cast is what keeps the TYPE half of this single source honest. Without it TypeScript
|
|
38
|
+
* infers `readonly string[]` from a `.mjs` file and `HerdrFreshCallBackend` silently widens to
|
|
39
|
+
* `string` — the tuple would still be one source at runtime while the compiler had stopped
|
|
40
|
+
* checking anything. A gate pins both halves.
|
|
41
|
+
*
|
|
42
|
+
* @type {readonly ["pi", "claude-code"]}
|
|
43
|
+
*/
|
|
44
|
+
export const HERDR_FRESH_CALL_BACKENDS = Object.freeze(["pi", "claude-code"]);
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* ONE model string per backend, and it is an example of SYNTAX, never of choice.
|
|
48
|
+
*
|
|
49
|
+
* `[관측: GLG, 날것 PC, 2026-09-17]` "일단 정확한 모델명을 모른다" — after a green install the first
|
|
50
|
+
* thing missing is a call that works, and the two runtimes spell a model differently enough that
|
|
51
|
+
* guessing fails: pi wants a provider-qualified path, Claude Code wants the vendor id alone.
|
|
52
|
+
* That difference is the whole content of this map.
|
|
53
|
+
*
|
|
54
|
+
* @type {Readonly<Record<"pi" | "claude-code", string>>}
|
|
55
|
+
*/
|
|
56
|
+
export const MODEL_SYNTAX_EXAMPLE = Object.freeze({
|
|
57
|
+
pi: "openai-codex/gpt-5.6-terra",
|
|
58
|
+
"claude-code": "claude-sonnet-5",
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* How a citizen on each backend is REACHED once it exists, in the operator's words. This is the
|
|
63
|
+
* rail fact the status pane's RAILS block renders and the install narration states — the same
|
|
64
|
+
* sentence, so a host cannot be told two different things about one backend.
|
|
65
|
+
*
|
|
66
|
+
* `[관측: GLG, 날것 PC, 2026-09-17]` from the operator's chair a plain `pi` looks identical to a
|
|
67
|
+
* citizen pi: the install says green, `pi` starts, and nothing is there. Citizenship is argv-gated
|
|
68
|
+
* on purpose, and that gate is invisible unless something says it out loud.
|
|
69
|
+
*
|
|
70
|
+
* @type {Readonly<Record<"pi" | "claude-code", string>>}
|
|
71
|
+
*/
|
|
72
|
+
export const RAIL_ENTRY_NOTE = Object.freeze({
|
|
73
|
+
pi: "start it as `pi --entwurf-control` to be a garden citizen — a plain `pi` loads the extension but has no garden id, no control socket and no entwurf tools",
|
|
74
|
+
"claude-code": "nothing to add — an ordinary `claude` picks up the entwurf tools through MCP",
|
|
75
|
+
});
|
|
@@ -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) {
|
|
@@ -81,13 +81,18 @@ LEGACY_PERMISSION_ALLOW = ["mcp__pi-tools-bridge__*"]
|
|
|
81
81
|
# native rail stops owning `autoCompactEnabled` / `env.DISABLE_AUTOCOMPACT` too.
|
|
82
82
|
# Backend-native context management is allowed; we add no guard and only measure.
|
|
83
83
|
# Both keys live in RETIRED_SETTINGS_SCALARS below.
|
|
84
|
+
#
|
|
85
|
+
# NEITHER IS `showTurnDuration`. It reports how long a turn took, which is
|
|
86
|
+
# OPERATOR INFORMATION — it opens no background autonomy surface and drives no
|
|
87
|
+
# single-driver behaviour, so the policy reason the rest of this list stands on
|
|
88
|
+
# never applied to it. GLG's ruling: the operator owns that scalar and entwurf
|
|
89
|
+
# must not force it to false. It joins RETIRED_SETTINGS_SCALARS below.
|
|
84
90
|
MANAGED_SETTINGS_SCALARS: list[tuple[str, list[str], Any]] = [
|
|
85
91
|
("cleanupPeriodDays", ["cleanupPeriodDays"], 365),
|
|
86
92
|
("promptSuggestionEnabled", ["promptSuggestionEnabled"], False),
|
|
87
93
|
("awaySummaryEnabled", ["awaySummaryEnabled"], False),
|
|
88
94
|
("autoMemoryEnabled", ["autoMemoryEnabled"], False),
|
|
89
95
|
("verbose", ["verbose"], False),
|
|
90
|
-
("showTurnDuration", ["showTurnDuration"], False),
|
|
91
96
|
("terminalProgressBarEnabled", ["terminalProgressBarEnabled"], False),
|
|
92
97
|
("useAutoModeDuringPlan", ["useAutoModeDuringPlan"], False),
|
|
93
98
|
("enableWorkflows", ["enableWorkflows"], False),
|
|
@@ -108,6 +113,18 @@ RETIRED_SETTINGS_SCALARS: list[tuple[str, list[str], Any]] = [
|
|
|
108
113
|
# reach it.
|
|
109
114
|
("env.DISABLE_AUTOCOMPACT", ["env", "DISABLE_AUTOCOMPACT"], "1"),
|
|
110
115
|
("autoCompactEnabled", ["autoCompactEnabled"], False),
|
|
116
|
+
# `showTurnDuration` returns to the operator for the same structural reason
|
|
117
|
+
# and by the same one-shot path. It is retired rather than deleted because an
|
|
118
|
+
# existing install-state ALREADY carries its entry — measured on the source
|
|
119
|
+
# host 2026-09-23: `~/.claude/entwurf.install-state.json` records
|
|
120
|
+
# `original {existed: true, value: false}` while live settings say `true`,
|
|
121
|
+
# i.e. the operator turned it on after our install. Deleting the constant
|
|
122
|
+
# would orphan that entry, and uninstall() walks every ledger entry through
|
|
123
|
+
# restore_entry(), so it would write `false` back over the operator's own
|
|
124
|
+
# choice. relinquish_retired_scalar() is what makes the inverse honest: the
|
|
125
|
+
# live `true` is not what we last managed, so it is left untouched and the
|
|
126
|
+
# entry is consumed, after which no code path can reach that key again.
|
|
127
|
+
("showTurnDuration", ["showTurnDuration"], False),
|
|
111
128
|
]
|
|
112
129
|
|
|
113
130
|
_managed_scalar_names = {name for name, _path, _desired in MANAGED_SETTINGS_SCALARS}
|
|
@@ -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
|
}
|
|
@@ -37,6 +37,44 @@
|
|
|
37
37
|
"signature": "[QK:PI-DOCTOR-PROBES-OVERRIDE]",
|
|
38
38
|
"signatureSource": "scripts/smoke-pi-provider-state.sh"
|
|
39
39
|
},
|
|
40
|
+
{
|
|
41
|
+
"claim": "PI-DOCTOR-PLUGIN-COMMAND-IS-NOT-DRIFT",
|
|
42
|
+
"title": "the recorded-command comparison degenerates to 'always drifted', so a Herdr-plugin host whose effective command IS exactly the absolute bridge this install wrote is red again — the #116 M3-b2 false-red restored in the new wording. It is the always-red half of a symmetric pair with PI-DOCTOR-REAL-DRIFT-STILL-RED's always-green half; a mutation that instead re-scoped the comparison under `!isBare` would also kill that claim's bare cell, so this one deliberately does not touch the managedState predicate",
|
|
43
|
+
"subject": "scripts/doctor-pi-provider.ts",
|
|
44
|
+
"find": ["\t\tif (cmd !== stateCommand) {"],
|
|
45
|
+
"replace": ["\t\tif (true) {"],
|
|
46
|
+
"gate": ["bash", "scripts/smoke-pi-provider-state.sh"],
|
|
47
|
+
"timeoutSeconds": 120,
|
|
48
|
+
"signature": "[QK:PI-DOCTOR-PLUGIN-COMMAND-IS-NOT-DRIFT]",
|
|
49
|
+
"signatureSource": "scripts/smoke-pi-provider-state.sh"
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
"claim": "PI-DOCTOR-REAL-DRIFT-STILL-RED",
|
|
53
|
+
"title": "the drift comparison is dropped entirely, so a managed key repointed at a command entwurf never wrote reads as 'the managed command this install recorded' — the ownership axis silently stops existing",
|
|
54
|
+
"subject": "scripts/doctor-pi-provider.ts",
|
|
55
|
+
"find": ["\t\tif (cmd !== stateCommand) {"],
|
|
56
|
+
"replace": ["\t\tif (false) {"],
|
|
57
|
+
"gate": ["bash", "scripts/smoke-pi-provider-state.sh"],
|
|
58
|
+
"timeoutSeconds": 120,
|
|
59
|
+
"signature": "[QK:PI-DOCTOR-REAL-DRIFT-STILL-RED]",
|
|
60
|
+
"signatureSource": "scripts/smoke-pi-provider-state.sh"
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
"claim": "PI-DOCTOR-LEGACY-STATE-UNCHANGED",
|
|
64
|
+
"title": "the plugin-mode repair is turned into a schema amnesty: a managed record written before `command` existed stops being red and its unrecorded, uncomparable entry is adopted as a note — a green nobody measured, handed out by the doctor instead of by ./run.sh setup",
|
|
65
|
+
"subject": "scripts/doctor-pi-provider.ts",
|
|
66
|
+
"find": [
|
|
67
|
+
"\t\t\tlog(` FAIL: state owns entwurf-bridge (ownership=${ownership}) but the effective command drifted to '${cmd}'.`);",
|
|
68
|
+
"\t\t\thardFail = 1;"
|
|
69
|
+
],
|
|
70
|
+
"replace": [
|
|
71
|
+
"\t\t\tlog(` note: legacy record (ownership=${ownership}) — no recorded command, adopting '${cmd}'.`);"
|
|
72
|
+
],
|
|
73
|
+
"gate": ["bash", "scripts/smoke-pi-provider-state.sh"],
|
|
74
|
+
"timeoutSeconds": 120,
|
|
75
|
+
"signature": "[QK:PI-DOCTOR-LEGACY-STATE-UNCHANGED]",
|
|
76
|
+
"signatureSource": "scripts/smoke-pi-provider-state.sh"
|
|
77
|
+
},
|
|
40
78
|
{
|
|
41
79
|
"claim": "AGY-DOCTOR-PROBES-ARGS",
|
|
42
80
|
"title": "the agy invocation parser drops configured args, so the doctor boots command defaults while agy executes a failing argv",
|