@junghanacs/entwurf 0.13.1 → 0.14.1
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 +59 -15
- package/BASELINE.md +3 -3
- package/CHANGELOG.md +74 -0
- package/CONTRIBUTING.md +13 -9
- package/DELIVERY.md +7 -6
- package/README.md +37 -28
- package/VERIFY.md +22 -14
- package/demo/README.md +1 -1
- package/demo/demo-baseline.sh +1 -3
- package/demo/demo.sh +2 -5
- package/docs/acp-backend-rail.md +9 -4
- package/docs/external-mcp-host.md +4 -5
- package/docs/setup-clean-host.md +8 -7
- package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +155 -28
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/overlay.js +8 -6
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/classify-tmux-cwd.js +47 -0
- 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 +300 -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 +141 -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 +173 -28
- package/mcp/entwurf-bridge/start.sh +2 -2
- package/mcp/entwurf-bridge/test.sh +23 -9
- package/mcp/entwurf-bridge/tsconfig.build.json +11 -2
- package/package.json +22 -11
- package/pi-extensions/entwurf-control.ts +227 -40
- package/pi-extensions/lib/acp/backend.ts +71 -12
- package/pi-extensions/lib/acp/overlay.ts +8 -6
- package/pi-extensions/lib/classify-tmux-cwd.ts +50 -0
- 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 +381 -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 +189 -0
- package/pi-extensions/lib/resume-launch-identity.ts +162 -0
- package/pi-extensions/lib/session-id.js +8 -5
- package/pi-extensions/lib/socket-discovery.ts +3 -3
- package/run.sh +471 -201
- package/scripts/agy-bridge-config.py +5 -1
- package/scripts/check-acp-backend-preflight.ts +1 -1
- package/scripts/check-acp-overlay.ts +13 -3
- package/scripts/check-acp-stream-hooks.ts +504 -0
- package/scripts/check-elapsed.sh +25 -0
- package/scripts/check-entwurf-bridge-boot.ts +51 -4
- package/scripts/check-entwurf-bridge-pi-free.ts +6 -5
- package/scripts/check-entwurf-control-rpc.ts +4 -3
- package/scripts/check-entwurf-resume-args.ts +72 -70
- package/scripts/check-entwurf-session-identity.ts +14 -10
- package/scripts/check-entwurf-v2-contract.ts +34 -59
- package/scripts/check-entwurf-v2-decider.ts +17 -177
- package/scripts/check-entwurf-v2-lock.ts +5 -2
- package/scripts/check-entwurf-v2-matrix.ts +3 -53
- package/scripts/check-entwurf-v2-production.ts +2 -91
- package/scripts/check-entwurf-v2-release.ts +10 -105
- package/scripts/check-entwurf-v2-runner.ts +4 -85
- package/scripts/check-entwurf-v2-send-fallback.ts +5 -6
- package/scripts/check-entwurf-v2-send.ts +0 -28
- package/scripts/check-entwurf-v2-surface.ts +157 -128
- package/scripts/check-entwurf-v2-visible-resume.ts +445 -0
- package/scripts/check-fresh-cut-gate.sh +1 -1
- package/scripts/check-gate-qualification.ts +99 -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 +283 -0
- package/scripts/check-probe-cli-shim.ts +25 -22
- package/scripts/check-probe-ordering.ts +84 -76
- package/scripts/check-release-gate-outcomes.ts +127 -7
- package/scripts/check-resume-launch-identity.ts +244 -0
- package/scripts/check-socket-discovery.ts +1 -1
- package/scripts/fixtures/mux-parent-transcript.scrubbed.jsonl +3 -0
- package/scripts/inventory-verification-surface.ts +349 -0
- package/scripts/lib/claude-launcher-fence.ts +322 -0
- package/scripts/lib/mutation-qualify.ts +109 -3
- package/scripts/meta-bridge-doctor.sh +6 -8
- package/scripts/meta-bridge-state.py +75 -1
- package/scripts/meta-facts.ts +60 -0
- package/scripts/mutants/acp-overlay.json +17 -0
- package/scripts/mutants/acp-stream-hooks.json +158 -0
- package/scripts/mutants/bridge-boot-resume.json +45 -0
- package/scripts/mutants/meta-facts.json +50 -0
- package/scripts/mutants/meta-identity.json +36 -0
- package/scripts/mutants/meta-retire.json +47 -0
- package/scripts/mutants/mux-boundary.json +196 -0
- package/scripts/mutants/mux-fresh-call.json +229 -0
- package/scripts/mutants/mux-launcher-fence.json +123 -0
- package/scripts/mutants/mux-parent-artifact.json +39 -0
- package/scripts/mutants/mux-resume-call.json +148 -0
- package/scripts/mutants/probe-ordering.json +0 -1037
- package/scripts/mutants/release-gate.json +35 -0
- package/scripts/mutants/resume-args.json +76 -0
- package/scripts/mutants/resume-launch-identity.json +96 -0
- package/scripts/mutants/v2-surface.json +58 -18
- package/scripts/mutants/v2-visible-resume.json +215 -0
- package/scripts/new-session-id.ts +9 -4
- package/scripts/smoke-acp-raw-turn-live.ts +1 -1
- package/scripts/smoke-agy-native-push-live.ts +6 -17
- package/scripts/smoke-entwurf-chain-live.ts +11 -3
- package/scripts/smoke-entwurf-v2-matrix-live.ts +1 -1
- package/scripts/smoke-meta-honesty.sh +1 -1
- package/scripts/smoke-meta-install-state.sh +169 -3
- package/scripts/smoke-mux-fresh-call-live.ts +365 -0
- package/scripts/smoke-mux-lifecycle-live.ts +1136 -0
- package/scripts/smoke-pi-attach.ts +1 -1
- package/scripts/smoke-user-scope-citizen.sh +1 -1
- package/scripts/tsconfig.json +1 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-preflight.js +0 -160
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-spawn-production.js +0 -273
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-spawn.js +0 -216
- package/pi-extensions/lib/entwurf-v2-spawn-production.ts +0 -373
- package/pi-extensions/lib/entwurf-v2-spawn.ts +0 -323
- package/scripts/check-acp-sdk-surface.ts +0 -275
- package/scripts/check-entwurf-v2-spawn-production.ts +0 -551
- package/scripts/check-entwurf-v2-spawn.ts +0 -399
- package/scripts/smoke-entwurf-v2-spawn-live.ts +0 -188
- package/scripts/smoke-entwurf-v2-spawn-resume-live.ts +0 -467
|
@@ -78,6 +78,70 @@ export function signatureOnFailureLine(output: string, token: string): boolean {
|
|
|
78
78
|
return output.split("\n").some((line) => line.includes(token) && !/^\s*ok\b/.test(line));
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
+
/** run_vitest prints this line when the runner asked it for a structured report.
|
|
82
|
+
* Its PRESENCE — not the report's readability — is what declares the lane structured. */
|
|
83
|
+
export const VITEST_STRUCTURED_MARKER = "__ENTWURF_VITEST_JSON__";
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* What a gate run said about WHICH tests failed.
|
|
87
|
+
*
|
|
88
|
+
* - `"legacy"`: no structured marker — a hand-built node:assert/shell gate whose
|
|
89
|
+
* `ok`-line oracle is unchanged.
|
|
90
|
+
* - `"unreadable"`: the marker was printed but the report is missing or malformed.
|
|
91
|
+
* This NEVER falls back to token scanning: a structured lane that lost its report
|
|
92
|
+
* has no attribution at all, so the mutant is WRONG-REASON, not KILLED.
|
|
93
|
+
* - a title array: the exact `fullName` of every FAILED test.
|
|
94
|
+
*/
|
|
95
|
+
export type FailedTestTitles = "legacy" | "unreadable" | string[];
|
|
96
|
+
|
|
97
|
+
/** Read the failed-test titles a vitest gate wrote, if it declared itself structured. */
|
|
98
|
+
export function readVitestFailedTitles(output: string, reportPath: string | null): FailedTestTitles {
|
|
99
|
+
if (!output.split("\n").some((line) => line.trim() === VITEST_STRUCTURED_MARKER)) return "legacy";
|
|
100
|
+
if (!reportPath) return "unreadable";
|
|
101
|
+
let raw: string;
|
|
102
|
+
try {
|
|
103
|
+
raw = fs.readFileSync(reportPath, "utf8");
|
|
104
|
+
} catch {
|
|
105
|
+
return "unreadable";
|
|
106
|
+
}
|
|
107
|
+
let report: {
|
|
108
|
+
testResults?: Array<{ assertionResults?: Array<{ status?: string; fullName?: string; title?: string }> }>;
|
|
109
|
+
};
|
|
110
|
+
try {
|
|
111
|
+
report = JSON.parse(raw);
|
|
112
|
+
} catch {
|
|
113
|
+
return "unreadable";
|
|
114
|
+
}
|
|
115
|
+
if (!Array.isArray(report.testResults)) return "unreadable";
|
|
116
|
+
const titles: string[] = [];
|
|
117
|
+
for (const suite of report.testResults) {
|
|
118
|
+
for (const assertion of suite.assertionResults ?? []) {
|
|
119
|
+
if (assertion.status !== "failed") continue;
|
|
120
|
+
titles.push(assertion.fullName ?? assertion.title ?? "");
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return titles;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Attribute a kill to its claim. A Vitest failure's CODE FRAME quotes the source lines
|
|
128
|
+
* around the assertion — including an adjacent PASSING test's `it("[QK:…]" …)` title —
|
|
129
|
+
* so scanning output lines would certify a claim whose test never failed (measured:
|
|
130
|
+
* issue #62 review). Structured lanes therefore read only the failed-test title set.
|
|
131
|
+
*/
|
|
132
|
+
export function signatureAttributedToFailure(output: string, token: string, failed: FailedTestTitles): boolean {
|
|
133
|
+
if (failed === "legacy") return signatureOnFailureLine(output, token);
|
|
134
|
+
if (failed === "unreadable") return false;
|
|
135
|
+
return failed.some((title) => title.includes(token));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** One legible word for the report line, so a WRONG-REASON says WHY it could not attribute. */
|
|
139
|
+
export function describeAttribution(failed: FailedTestTitles): string {
|
|
140
|
+
if (failed === "legacy") return "failure-line";
|
|
141
|
+
if (failed === "unreadable") return "vitest-structured-but-unreadable";
|
|
142
|
+
return `vitest-failed-titles(${failed.length})`;
|
|
143
|
+
}
|
|
144
|
+
|
|
81
145
|
// ── manifest schema (fail-loud, exact keys) ─────────────────────────────────
|
|
82
146
|
|
|
83
147
|
export interface MutantSpec {
|
|
@@ -430,6 +494,8 @@ export interface GateRunResult {
|
|
|
430
494
|
timedOut: boolean;
|
|
431
495
|
output: string;
|
|
432
496
|
seconds: number;
|
|
497
|
+
/** Structured failed-test attribution; `"legacy"` for every non-vitest gate. */
|
|
498
|
+
failedTitles: FailedTestTitles;
|
|
433
499
|
}
|
|
434
500
|
|
|
435
501
|
const OUTPUT_CAP = 4 * 1024 * 1024;
|
|
@@ -437,12 +503,23 @@ const OUTPUT_CAP = 4 * 1024 * 1024;
|
|
|
437
503
|
/** Env prefixes the outer fence strips so a gate child starts from a neutral host. */
|
|
438
504
|
const STRIP_ENV_PREFIXES = ["ENTWURF_", "AGY_", "PI_SESSION_ID", "PI_AGENT_ID"];
|
|
439
505
|
|
|
506
|
+
/** Where a vitest-backed gate writes its machine report for this one invocation. */
|
|
507
|
+
function vitestReportPath(invocationDir: string): string {
|
|
508
|
+
return path.join(invocationDir, "vitest-report.json");
|
|
509
|
+
}
|
|
510
|
+
|
|
440
511
|
function fencedEnv(invocationDir: string): NodeJS.ProcessEnv {
|
|
441
512
|
const env: NodeJS.ProcessEnv = {};
|
|
442
513
|
for (const [k, v] of Object.entries(process.env)) {
|
|
443
514
|
if (STRIP_ENV_PREFIXES.some((p) => k === p || k.startsWith(p))) continue;
|
|
444
515
|
env[k] = v;
|
|
445
516
|
}
|
|
517
|
+
// A vitest-backed gate writes its machine report HERE, inside the per-invocation dir
|
|
518
|
+
// (outside the snapshot repo, so the work-surface purity hash never sees it). The
|
|
519
|
+
// report is a FILE and not stdout on purpose: `output` merges stdout+stderr, so one
|
|
520
|
+
// vite warning ahead of the JSON would break the parse and silently downgrade a real
|
|
521
|
+
// kill to WRONG-REASON. Legacy gates ignore the variable.
|
|
522
|
+
env.ENTWURF_MUTATION_VITEST_REPORT = vitestReportPath(invocationDir);
|
|
446
523
|
for (const d of ["home", "xdg-data", "xdg-config", "xdg-cache", "xdg-state"]) {
|
|
447
524
|
fs.mkdirSync(path.join(invocationDir, d), { recursive: true });
|
|
448
525
|
}
|
|
@@ -499,7 +576,9 @@ export function runGateBounded(opts: {
|
|
|
499
576
|
});
|
|
500
577
|
child.on("close", (code) => {
|
|
501
578
|
clearTimeout(timer);
|
|
502
|
-
|
|
579
|
+
// Read the report BEFORE the caller removes the invocation dir.
|
|
580
|
+
const failedTitles = readVitestFailedTitles(output, vitestReportPath(opts.invocationDir));
|
|
581
|
+
resolve({ exitCode: code, timedOut, output, failedTitles, seconds: (Date.now() - started) / 1000 });
|
|
503
582
|
});
|
|
504
583
|
});
|
|
505
584
|
}
|
|
@@ -548,6 +627,29 @@ function countOccurrences(haystack: string, needle: string): number {
|
|
|
548
627
|
return count;
|
|
549
628
|
}
|
|
550
629
|
|
|
630
|
+
/** Tail of a red control run's captured output that gets echoed into the log. */
|
|
631
|
+
const CONTROL_TAIL_LINES = 40;
|
|
632
|
+
|
|
633
|
+
/**
|
|
634
|
+
* Echo a RED control run's captured output. Without this the runner reports the
|
|
635
|
+
* verdict (`RED (exit=1 …)`) and DROPS the reason it already holds, which makes a
|
|
636
|
+
* CONTROL-RED that only reproduces on one host — a CI runner, say — undiagnosable
|
|
637
|
+
* from its log: the same single line no matter how often it is re-run. A bounded
|
|
638
|
+
* tail is enough, because a gate names its failure on the way out. Silence is a
|
|
639
|
+
* finding too, so an empty capture is reported as empty rather than skipped.
|
|
640
|
+
*/
|
|
641
|
+
function logControlOutput(output: string, log: (line: string) => void): void {
|
|
642
|
+
const body = output.replace(/\s+$/, "");
|
|
643
|
+
if (body === "") {
|
|
644
|
+
log(" │ (the control run produced no output — the failure is upstream of the gate's own reporting)");
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
647
|
+
const lines = body.split("\n");
|
|
648
|
+
const dropped = Math.max(0, lines.length - CONTROL_TAIL_LINES);
|
|
649
|
+
if (dropped > 0) log(` │ … ${dropped} earlier line(s) omitted`);
|
|
650
|
+
for (const line of lines.slice(-CONTROL_TAIL_LINES)) log(` │ ${line}`);
|
|
651
|
+
}
|
|
652
|
+
|
|
551
653
|
/**
|
|
552
654
|
* Run every mutant grouped by its gate command, with the control-mutant-restore-
|
|
553
655
|
* control state machine. A red CONTROL-PRE aborts the whole group (every mutant
|
|
@@ -593,6 +695,7 @@ export async function qualifyMutants(
|
|
|
593
695
|
` control-pre ${gate.join(" ")}: ${controlPreOk ? "green" : `RED (exit=${pre.exitCode} timedOut=${pre.timedOut})`} in ${pre.seconds.toFixed(1)}s`,
|
|
594
696
|
);
|
|
595
697
|
if (!controlPreOk) {
|
|
698
|
+
logControlOutput(pre.output, log);
|
|
596
699
|
group.control = "pre-red";
|
|
597
700
|
for (const m of groupMutants) {
|
|
598
701
|
group.mutants.push({
|
|
@@ -678,7 +781,7 @@ export async function qualifyMutants(
|
|
|
678
781
|
matchCount: 1,
|
|
679
782
|
timedOut: run.timedOut,
|
|
680
783
|
exitCode: run.exitCode,
|
|
681
|
-
signatureOnFailureLine:
|
|
784
|
+
signatureOnFailureLine: signatureAttributedToFailure(run.output, m.signature, run.failedTitles),
|
|
682
785
|
restoredOk,
|
|
683
786
|
});
|
|
684
787
|
if (!restoredOk) groupImpure = true;
|
|
@@ -687,7 +790,9 @@ export async function qualifyMutants(
|
|
|
687
790
|
verdict,
|
|
688
791
|
seconds: run.seconds,
|
|
689
792
|
subjectSha256: originalSha,
|
|
690
|
-
detail:
|
|
793
|
+
detail:
|
|
794
|
+
`exit=${run.exitCode} timedOut=${run.timedOut} signature=${m.signature}` +
|
|
795
|
+
` attribution=${describeAttribution(run.failedTitles)}`,
|
|
691
796
|
});
|
|
692
797
|
log(
|
|
693
798
|
` claim ${m.claim}: ${verdict} in ${run.seconds.toFixed(1)}s (subject ${m.subject} sha256=${originalSha.slice(0, 12)}…)`,
|
|
@@ -705,6 +810,7 @@ export async function qualifyMutants(
|
|
|
705
810
|
log(
|
|
706
811
|
` control-post ${gate.join(" ")}: ${controlPostOk ? "green" : `RED (exit=${post.exitCode} timedOut=${post.timedOut}) — restore contamination or gate state leak`} in ${post.seconds.toFixed(1)}s`,
|
|
707
812
|
);
|
|
813
|
+
if (!controlPostOk) logControlOutput(post.output, log);
|
|
708
814
|
}
|
|
709
815
|
|
|
710
816
|
const postTree = computeTreeManifest(snapshot.repoDir);
|
|
@@ -1207,14 +1207,12 @@ case "$REPO" in
|
|
|
1207
1207
|
fi
|
|
1208
1208
|
;;
|
|
1209
1209
|
esac
|
|
1210
|
-
#
|
|
1211
|
-
#
|
|
1212
|
-
#
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
ok "ENTWURF_PREFIX_ROOTS set → operator prefix-approve roots: $ENTWURF_PREFIX_ROOTS"
|
|
1217
|
-
fi
|
|
1210
|
+
# NOT REPORTED: ENTWURF_PREFIX_ROOTS. It was the operator-policy SSOT that both v2 surfaces
|
|
1211
|
+
# read to feed the dispatch-path trust preflight, and that preflight guarded the resume verdict
|
|
1212
|
+
# — so it left with `owned-outcome` in the visible-first cut. Nothing on the dispatch path reads
|
|
1213
|
+
# the variable now, and a doctor line saying "set → operator prefix-approve roots" would report a
|
|
1214
|
+
# knob that does nothing. Reporting an inert setting as live policy is the failure this doctor
|
|
1215
|
+
# exists to prevent, so the row is gone rather than softened.
|
|
1218
1216
|
|
|
1219
1217
|
echo
|
|
1220
1218
|
if [ "$fail" -eq 0 ]; then echo "meta-bridge doctor: PASS"; else echo "meta-bridge doctor: FAIL (see above)"; exit 1; fi
|
|
@@ -74,7 +74,6 @@ MANAGED_SETTINGS_SCALARS: list[tuple[str, list[str], Any]] = [
|
|
|
74
74
|
("promptSuggestionEnabled", ["promptSuggestionEnabled"], False),
|
|
75
75
|
("awaySummaryEnabled", ["awaySummaryEnabled"], False),
|
|
76
76
|
("autoMemoryEnabled", ["autoMemoryEnabled"], False),
|
|
77
|
-
("skipDangerousModePermissionPrompt", ["skipDangerousModePermissionPrompt"], True),
|
|
78
77
|
("verbose", ["verbose"], False),
|
|
79
78
|
("autoCompactEnabled", ["autoCompactEnabled"], False),
|
|
80
79
|
("showTurnDuration", ["showTurnDuration"], False),
|
|
@@ -84,6 +83,19 @@ MANAGED_SETTINGS_SCALARS: list[tuple[str, list[str], Any]] = [
|
|
|
84
83
|
("workflowKeywordTriggerEnabled", ["workflowKeywordTriggerEnabled"], False),
|
|
85
84
|
]
|
|
86
85
|
|
|
86
|
+
# Keys entwurf used to own but now returns to the operator. An old install-state
|
|
87
|
+
# entry is the only authority to undo our prior write; a bare matching value is
|
|
88
|
+
# not provenance and is never changed. apply() consumes each proven old entry
|
|
89
|
+
# exactly once, then uninstall can no longer restore over the operator's choice.
|
|
90
|
+
RETIRED_SETTINGS_SCALARS: list[tuple[str, list[str], Any]] = [
|
|
91
|
+
("skipDangerousModePermissionPrompt", ["skipDangerousModePermissionPrompt"], True),
|
|
92
|
+
]
|
|
93
|
+
|
|
94
|
+
_managed_scalar_names = {name for name, _path, _desired in MANAGED_SETTINGS_SCALARS}
|
|
95
|
+
_retired_scalar_names = {name for name, _path, _last in RETIRED_SETTINGS_SCALARS}
|
|
96
|
+
if overlap := _managed_scalar_names & _retired_scalar_names:
|
|
97
|
+
raise RuntimeError(f"settings scalar cannot be both managed and retired: {sorted(overlap)}")
|
|
98
|
+
|
|
87
99
|
|
|
88
100
|
class StateError(RuntimeError):
|
|
89
101
|
pass
|
|
@@ -391,6 +403,9 @@ def apply(repo: Path, asm: Path) -> None:
|
|
|
391
403
|
if not isinstance(root, dict):
|
|
392
404
|
die(f"{claude_root_config_path()} root must be a JSON object")
|
|
393
405
|
|
|
406
|
+
for name, path_, last_managed_value in RETIRED_SETTINGS_SCALARS:
|
|
407
|
+
relinquish_retired_scalar(state, settings, name, path_, last_managed_value)
|
|
408
|
+
|
|
394
409
|
set_nested(settings, ["enabledPlugins", PLUGIN_REF], True)
|
|
395
410
|
set_nested(settings, ["extraKnownMarketplaces", MARKETPLACE], desired_marketplace(asm))
|
|
396
411
|
for path_, desired in [(["permissions", "allow"], PERMISSION_ALLOW), (["permissions", "deny"], PERMISSION_DENY)]:
|
|
@@ -446,6 +461,53 @@ def restore_entry(obj: dict[str, Any], entry: dict[str, Any]) -> None:
|
|
|
446
461
|
die(f"unknown state entry kind: {kind}")
|
|
447
462
|
|
|
448
463
|
|
|
464
|
+
def settings_state_keys(state: dict[str, Any]) -> dict[str, Any]:
|
|
465
|
+
"""The settings key ledger, or a loud failure.
|
|
466
|
+
|
|
467
|
+
`load_state` validates only the envelope (schemaVersion/owner), so a consumer
|
|
468
|
+
that indexes straight into files.settings.keys turns a corrupt state file into
|
|
469
|
+
a bare KeyError traceback instead of an operator diagnostic. Ownership
|
|
470
|
+
decisions read this ledger, so an unreadable one must stop before any write.
|
|
471
|
+
"""
|
|
472
|
+
files = state.get("files")
|
|
473
|
+
entry = files.get("settings") if isinstance(files, dict) else None
|
|
474
|
+
keys = entry.get("keys") if isinstance(entry, dict) else None
|
|
475
|
+
if not isinstance(keys, dict):
|
|
476
|
+
die(f"install state {state_path()} has no files.settings.keys ledger; re-run install-meta-bridge")
|
|
477
|
+
return keys
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
def relinquish_retired_scalar(
|
|
481
|
+
state: dict[str, Any], settings: dict[str, Any], name: str, path: list[str], last_managed_value: Any
|
|
482
|
+
) -> None:
|
|
483
|
+
"""Return one formerly managed scalar to operator ownership.
|
|
484
|
+
|
|
485
|
+
Only a preserved install-state entry proves entwurf wrote this path. If the
|
|
486
|
+
current value still has the exact JSON scalar type+value we last managed,
|
|
487
|
+
restore the snapshot. A changed or absent current value is already the
|
|
488
|
+
operator's and stays untouched. Malformed provenance fails before any file
|
|
489
|
+
write; silently discarding it would make a still-dangerous value look clean.
|
|
490
|
+
"""
|
|
491
|
+
keys = settings_state_keys(state)
|
|
492
|
+
entry = keys.get(name)
|
|
493
|
+
if entry is None:
|
|
494
|
+
return
|
|
495
|
+
if not isinstance(entry, dict) or entry.get("kind") != "scalar" or entry.get("path") != path:
|
|
496
|
+
die(f"retired scalar state entry {name} is malformed; refusing to discard ownership evidence")
|
|
497
|
+
original = entry.get("original")
|
|
498
|
+
if (
|
|
499
|
+
not isinstance(original, dict)
|
|
500
|
+
or set(original) != {"existed", "value"}
|
|
501
|
+
or type(original.get("existed")) is not bool
|
|
502
|
+
):
|
|
503
|
+
die(f"retired scalar state entry {name} has malformed original; refusing to guess")
|
|
504
|
+
|
|
505
|
+
existed, value = get_nested(settings, path)
|
|
506
|
+
if existed and type(value) is type(last_managed_value) and value == last_managed_value:
|
|
507
|
+
restore_entry(settings, entry)
|
|
508
|
+
keys.pop(name)
|
|
509
|
+
|
|
510
|
+
|
|
449
511
|
def preflight_uninstall() -> None:
|
|
450
512
|
load_state(required=True)
|
|
451
513
|
print(f"[meta-bridge-state] uninstall preflight ok ({state_path()})")
|
|
@@ -536,6 +598,18 @@ def check(repo: Path, asm: Path) -> None:
|
|
|
536
598
|
if recorded_asm
|
|
537
599
|
else desired_marketplace(asm)
|
|
538
600
|
)
|
|
601
|
+
state_settings_keys = settings_state_keys(state)
|
|
602
|
+
for name, path_, last_managed_value in RETIRED_SETTINGS_SCALARS:
|
|
603
|
+
if name in state_settings_keys:
|
|
604
|
+
failures.append(f"install-state still owns retired scalar {name}; re-run install-meta-bridge to relinquish it")
|
|
605
|
+
continue
|
|
606
|
+
existed, value = get_nested(settings, path_)
|
|
607
|
+
if existed and type(value) is type(last_managed_value) and value == last_managed_value:
|
|
608
|
+
print(
|
|
609
|
+
f"NOTE: settings {name}={json.dumps(last_managed_value)} is operator-owned; "
|
|
610
|
+
f"entwurf no longer suppresses or restores this warning choice"
|
|
611
|
+
)
|
|
612
|
+
|
|
539
613
|
checks = [
|
|
540
614
|
(["enabledPlugins", PLUGIN_REF], True, "enabled plugin"),
|
|
541
615
|
(["extraKnownMarketplaces", MARKETPLACE], marketplace_expected, "known marketplace"),
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* meta-facts — owner-normalized, read-only projection of the meta-record store (#65).
|
|
4
|
+
*
|
|
5
|
+
* The one join a consumer cannot make anywhere else — `(gardenId, nativeSessionId,
|
|
6
|
+
* transcriptPath)` — lives only in the meta-record store, and reading that store
|
|
7
|
+
* correctly means carrying the whole certification contract (strict v3 keyset,
|
|
8
|
+
* parse-before-uniqueness, no-winner duplicates, O_NOFOLLOW regular-file reads).
|
|
9
|
+
* External consumers that copied it decayed the day the contract moved. This surface
|
|
10
|
+
* makes THE owner emit the join: it is `listAllMetaIdentities` over the real store —
|
|
11
|
+
* the same listing `entwurf_peers` trusts — serialized verbatim. No parallel parser,
|
|
12
|
+
* no liveness, no socket paths, no transcript contents, no state, no watching.
|
|
13
|
+
*
|
|
14
|
+
* stdout (deterministic, 2-space indent, trailing newline):
|
|
15
|
+
* {
|
|
16
|
+
* "schemaVersion": 1, // of THIS projection, not of the records
|
|
17
|
+
* "storeDir": "/abs/path",
|
|
18
|
+
* "citizens": [ <full verbatim strict 9-field v3 records, sorted by gardenId> ],
|
|
19
|
+
* "defects": [ { "filename", "message" }, … sorted by filename ]
|
|
20
|
+
* }
|
|
21
|
+
*
|
|
22
|
+
* Defects ride IN-BAND: a readable store with uncertifiable entries is a fact to
|
|
23
|
+
* report, not a failure of this command — the healthy citizens are still true, and
|
|
24
|
+
* the defect list is exactly what `listAllMetaIdentities` refused and why (a rival
|
|
25
|
+
* pair is refused on BOTH sides; a schema-invalid record can never quarantine a
|
|
26
|
+
* healthy neighbour, because parse runs before uniqueness).
|
|
27
|
+
*
|
|
28
|
+
* EXIT CONTRACT — the verdict is the exit code (store-doctor precedent):
|
|
29
|
+
* 0 — store readable; the JSON above is on stdout (defects, if any, in-band).
|
|
30
|
+
* A store that does not exist is a readable EMPTY store (ENOENT only).
|
|
31
|
+
* 2 — usage error (bad argv).
|
|
32
|
+
* 3 — the store could not be READ (EACCES/ENOTDIR/…). No JSON is emitted:
|
|
33
|
+
* an unreadable host must never look like an empty one.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
import { defaultMetaSessionsDir, listAllMetaIdentitiesDir } from "../pi-extensions/lib/meta-session.ts";
|
|
37
|
+
|
|
38
|
+
const arg = process.argv[2];
|
|
39
|
+
// A dash argv is a flag this command does not have, not a store directory — treating
|
|
40
|
+
// it as a path would answer `--help` with "empty store, exit 0", a silent wrong fact.
|
|
41
|
+
if (process.argv.length > 3 || (arg !== undefined && arg.startsWith("-"))) {
|
|
42
|
+
console.error("usage: entwurf meta-facts [meta-sessions-dir]");
|
|
43
|
+
process.exit(2);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
let facts: ReturnType<typeof listAllMetaIdentitiesDir>;
|
|
47
|
+
try {
|
|
48
|
+
facts = listAllMetaIdentitiesDir(arg ?? defaultMetaSessionsDir());
|
|
49
|
+
} catch (err) {
|
|
50
|
+
console.error(`FAIL: ${err instanceof Error ? err.message : String(err)}`);
|
|
51
|
+
process.exit(3); // see the EXIT CONTRACT above: 3 = unreadable store, never an empty one
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
process.stdout.write(
|
|
55
|
+
`${JSON.stringify(
|
|
56
|
+
{ schemaVersion: 1, storeDir: facts.dir, citizens: facts.identities, defects: facts.errors },
|
|
57
|
+
null,
|
|
58
|
+
2,
|
|
59
|
+
)}\n`,
|
|
60
|
+
);
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": 1,
|
|
3
|
+
"lane": "acp-overlay",
|
|
4
|
+
"mutants": [
|
|
5
|
+
{
|
|
6
|
+
"claim": "ACP-OVERLAY-UNATTENDED-BYPASS",
|
|
7
|
+
"title": "the isolated Claude overlay goes back to authoring permissions.defaultMode \"default\" — the overlay is the only settings authority the ACP child reads, so an unattended entwurf turn suspends on an interactive permission prompt nobody is there to answer, and the session stalls rather than fails",
|
|
8
|
+
"subject": "pi-extensions/lib/acp/overlay.ts",
|
|
9
|
+
"find": ["\t\t\tpermissions: { defaultMode: \"bypassPermissions\" },"],
|
|
10
|
+
"replace": ["\t\t\tpermissions: { defaultMode: \"default\" },"],
|
|
11
|
+
"gate": ["bash", "run.sh", "check-acp-overlay"],
|
|
12
|
+
"timeoutSeconds": 180,
|
|
13
|
+
"signature": "[QK:ACP-OVERLAY-UNATTENDED-BYPASS]",
|
|
14
|
+
"signatureSource": "scripts/check-acp-overlay.ts"
|
|
15
|
+
}
|
|
16
|
+
]
|
|
17
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": 1,
|
|
3
|
+
"lane": "acp-stream-hooks",
|
|
4
|
+
"mutants": [
|
|
5
|
+
{
|
|
6
|
+
"claim": "ACPHOOK-PAYLOAD-EXACT-WIRE",
|
|
7
|
+
"title": "the new-turn send site bypasses the payload hook entirely — the pi-vertex defect: extensions hooking before_provider_request silently observe nothing",
|
|
8
|
+
"subject": "pi-extensions/lib/acp/backend.ts",
|
|
9
|
+
"find": [
|
|
10
|
+
"\t\t\tconst wireParams = await applyProviderPayloadHook(options, { sessionId: acpSessionId, prompt }, model);"
|
|
11
|
+
],
|
|
12
|
+
"replace": ["\t\t\tconst wireParams = { sessionId: acpSessionId, prompt };"],
|
|
13
|
+
"gate": ["bash", "run.sh", "check-acp-stream-hooks"],
|
|
14
|
+
"timeoutSeconds": 300,
|
|
15
|
+
"signature": "[QK:ACPHOOK-PAYLOAD-EXACT-WIRE]",
|
|
16
|
+
"signatureSource": "scripts/check-acp-stream-hooks.ts"
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"claim": "ACPHOOK-REPLACEMENT-ON-WIRE",
|
|
20
|
+
"title": "the helper validates the replacement and then discards it — the original params always cross the wire, so a payload-rewriting extension silently does nothing",
|
|
21
|
+
"subject": "pi-extensions/lib/acp/backend.ts",
|
|
22
|
+
"find": ["\treturn replacement as T;"],
|
|
23
|
+
"replace": ["\treturn params;"],
|
|
24
|
+
"gate": ["bash", "run.sh", "check-acp-stream-hooks"],
|
|
25
|
+
"timeoutSeconds": 300,
|
|
26
|
+
"signature": "[QK:ACPHOOK-REPLACEMENT-ON-WIRE]",
|
|
27
|
+
"signatureSource": "scripts/check-acp-stream-hooks.ts"
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"claim": "ACPHOOK-REUSE-PARITY",
|
|
31
|
+
"title": "the reuse send site bypasses the hook, so resident-session requests become invisible to extensions while cold turns stay covered",
|
|
32
|
+
"subject": "pi-extensions/lib/acp/backend.ts",
|
|
33
|
+
"find": [
|
|
34
|
+
"\t\t\tconst wireParams = await applyProviderPayloadHook(options, { sessionId: session.acpSessionId, prompt }, model);"
|
|
35
|
+
],
|
|
36
|
+
"replace": ["\t\t\tconst wireParams = { sessionId: session.acpSessionId, prompt };"],
|
|
37
|
+
"gate": ["bash", "run.sh", "check-acp-stream-hooks"],
|
|
38
|
+
"timeoutSeconds": 300,
|
|
39
|
+
"signature": "[QK:ACPHOOK-REUSE-PARITY]",
|
|
40
|
+
"signatureSource": "scripts/check-acp-stream-hooks.ts"
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
"claim": "ACPHOOK-REJECTION-FAILS-LOUD",
|
|
44
|
+
"title": "the new-turn call site silently catches a hook rejection and falls back to the original params — a broken extension keeps 'working'",
|
|
45
|
+
"subject": "pi-extensions/lib/acp/backend.ts",
|
|
46
|
+
"find": [
|
|
47
|
+
"\t\t\tconst wireParams = await applyProviderPayloadHook(options, { sessionId: acpSessionId, prompt }, model);"
|
|
48
|
+
],
|
|
49
|
+
"replace": [
|
|
50
|
+
"\t\t\tconst wireParams = await applyProviderPayloadHook(options, { sessionId: acpSessionId, prompt }, model).catch(",
|
|
51
|
+
"\t\t\t\t() => ({ sessionId: acpSessionId, prompt }),",
|
|
52
|
+
"\t\t\t);"
|
|
53
|
+
],
|
|
54
|
+
"gate": ["bash", "run.sh", "check-acp-stream-hooks"],
|
|
55
|
+
"timeoutSeconds": 300,
|
|
56
|
+
"signature": "[QK:ACPHOOK-REJECTION-FAILS-LOUD]",
|
|
57
|
+
"signatureSource": "scripts/check-acp-stream-hooks.ts"
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
"claim": "ACPHOOK-INVALID-REPLACEMENT-REFUSED",
|
|
61
|
+
"title": "the non-null/non-array object shape check is dropped, so a scalar replacement is refused for the wrong reason (or not at all) instead of by name",
|
|
62
|
+
"subject": "pi-extensions/lib/acp/backend.ts",
|
|
63
|
+
"find": [
|
|
64
|
+
"\tif (typeof replacement !== \"object\" || replacement === null || Array.isArray(replacement)) {",
|
|
65
|
+
"\t\tthrow new Error(",
|
|
66
|
+
"\t\t\t\"entwurf: before_provider_request returned a non-object replacement — the ACP prompt payload must stay a non-null, non-array object\",",
|
|
67
|
+
"\t\t);",
|
|
68
|
+
"\t}"
|
|
69
|
+
],
|
|
70
|
+
"replace": ["\t// a JS extension can return anything; the sessionId check below is the real guard"],
|
|
71
|
+
"gate": ["bash", "run.sh", "check-acp-stream-hooks"],
|
|
72
|
+
"timeoutSeconds": 300,
|
|
73
|
+
"signature": "[QK:ACPHOOK-INVALID-REPLACEMENT-REFUSED]",
|
|
74
|
+
"signatureSource": "scripts/check-acp-stream-hooks.ts"
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
"claim": "ACPHOOK-EMPTY-PROMPT-REFUSED",
|
|
78
|
+
"title": "the non-empty prompt invariant is dropped from replacement validation, so an extension can rewrite the turn into an empty ACP send",
|
|
79
|
+
"subject": "pi-extensions/lib/acp/backend.ts",
|
|
80
|
+
"find": [
|
|
81
|
+
"\tconst replacementPrompt = (replacement as { prompt?: unknown }).prompt;",
|
|
82
|
+
"\tif (!Array.isArray(replacementPrompt) || replacementPrompt.length === 0) {",
|
|
83
|
+
"\t\tthrow new Error(",
|
|
84
|
+
"\t\t\t\"entwurf: before_provider_request returned a replacement without a non-empty prompt array — an empty ACP prompt cannot be sent\",",
|
|
85
|
+
"\t\t);",
|
|
86
|
+
"\t}"
|
|
87
|
+
],
|
|
88
|
+
"replace": ["\t// the send sites already refuse an empty BUILT prompt; trust the replacement"],
|
|
89
|
+
"gate": ["bash", "run.sh", "check-acp-stream-hooks"],
|
|
90
|
+
"timeoutSeconds": 300,
|
|
91
|
+
"signature": "[QK:ACPHOOK-EMPTY-PROMPT-REFUSED]",
|
|
92
|
+
"signatureSource": "scripts/check-acp-stream-hooks.ts"
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
"claim": "ACPHOOK-SESSIONID-TAMPER-REFUSED",
|
|
96
|
+
"title": "the sessionId invariant is dropped under a literal reading of 'use any returned replacement payload' — the prompt is delivered to a session entwurf never bootstrapped",
|
|
97
|
+
"subject": "pi-extensions/lib/acp/backend.ts",
|
|
98
|
+
"find": [
|
|
99
|
+
"\tif ((replacement as { sessionId?: unknown }).sessionId !== params.sessionId) {",
|
|
100
|
+
"\t\tthrow new Error(",
|
|
101
|
+
"\t\t\t\"entwurf: before_provider_request changed the ACP sessionId — entwurf cannot truthfully deliver to a session it did not bootstrap; prompt rewriting is allowed, session identity is not\",",
|
|
102
|
+
"\t\t);",
|
|
103
|
+
"\t}"
|
|
104
|
+
],
|
|
105
|
+
"replace": ["\t// upstream says use any returned replacement payload verbatim"],
|
|
106
|
+
"gate": ["bash", "run.sh", "check-acp-stream-hooks"],
|
|
107
|
+
"timeoutSeconds": 300,
|
|
108
|
+
"signature": "[QK:ACPHOOK-SESSIONID-TAMPER-REFUSED]",
|
|
109
|
+
"signatureSource": "scripts/check-acp-stream-hooks.ts"
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
"claim": "ACPHOOK-ABORT-DURING-HOOK-WINS",
|
|
113
|
+
"title": "the post-hook abort recheck is dropped, so an abort raised during a slow handler still sends the prompt to the wire",
|
|
114
|
+
"subject": "pi-extensions/lib/acp/backend.ts",
|
|
115
|
+
"find": [
|
|
116
|
+
"\t\t\tconst wireParams = await applyProviderPayloadHook(options, { sessionId: acpSessionId, prompt }, model);",
|
|
117
|
+
"\t\t\tif (signal?.aborted) throw new Error(\"aborted during payload hook\");"
|
|
118
|
+
],
|
|
119
|
+
"replace": [
|
|
120
|
+
"\t\t\tconst wireParams = await applyProviderPayloadHook(options, { sessionId: acpSessionId, prompt }, model);"
|
|
121
|
+
],
|
|
122
|
+
"gate": ["bash", "run.sh", "check-acp-stream-hooks"],
|
|
123
|
+
"timeoutSeconds": 300,
|
|
124
|
+
"signature": "[QK:ACPHOOK-ABORT-DURING-HOOK-WINS]",
|
|
125
|
+
"signatureSource": "scripts/check-acp-stream-hooks.ts"
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
"claim": "ACPHOOK-REUSE-ABORT-WINS",
|
|
129
|
+
"title": "the REUSE call site's post-hook abort recheck is dropped, so an abort raised during a slow handler still sends a second prompt to the resident session",
|
|
130
|
+
"subject": "pi-extensions/lib/acp/backend.ts",
|
|
131
|
+
"find": [
|
|
132
|
+
"\t\t\tconst wireParams = await applyProviderPayloadHook(options, { sessionId: session.acpSessionId, prompt }, model);",
|
|
133
|
+
"\t\t\tif (signal?.aborted) throw new Error(\"aborted during payload hook\");"
|
|
134
|
+
],
|
|
135
|
+
"replace": [
|
|
136
|
+
"\t\t\tconst wireParams = await applyProviderPayloadHook(options, { sessionId: session.acpSessionId, prompt }, model);"
|
|
137
|
+
],
|
|
138
|
+
"gate": ["bash", "run.sh", "check-acp-stream-hooks"],
|
|
139
|
+
"timeoutSeconds": 300,
|
|
140
|
+
"signature": "[QK:ACPHOOK-REUSE-ABORT-WINS]",
|
|
141
|
+
"signatureSource": "scripts/check-acp-stream-hooks.ts"
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
"claim": "ACPHOOK-ONRESPONSE-NEVER-CALLED",
|
|
145
|
+
"title": "someone 'completes' the hook contract by fabricating an HTTP 200 response on turn success — exactly the false evidence the exemption exists to forbid",
|
|
146
|
+
"subject": "pi-extensions/lib/acp/backend.ts",
|
|
147
|
+
"find": ["\tfunction finishSuccess(promptResult: { stopReason?: string }): void {"],
|
|
148
|
+
"replace": [
|
|
149
|
+
"\tfunction finishSuccess(promptResult: { stopReason?: string }): void {",
|
|
150
|
+
"\t\tvoid options?.onResponse?.({ status: 200, headers: {} }, model);"
|
|
151
|
+
],
|
|
152
|
+
"gate": ["bash", "run.sh", "check-acp-stream-hooks"],
|
|
153
|
+
"timeoutSeconds": 300,
|
|
154
|
+
"signature": "[QK:ACPHOOK-ONRESPONSE-NEVER-CALLED]",
|
|
155
|
+
"signatureSource": "scripts/check-acp-stream-hooks.ts"
|
|
156
|
+
}
|
|
157
|
+
]
|
|
158
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": 1,
|
|
3
|
+
"lane": "bridge-boot-resume",
|
|
4
|
+
"mutants": [
|
|
5
|
+
{
|
|
6
|
+
"claim": "BRIDGEBOOT-RESUME-CALL-REGISTERED",
|
|
7
|
+
"title": "the resume verb is imported but never registered — invisible to every host, while every import-level assertion stays green",
|
|
8
|
+
"subject": "mcp/entwurf-bridge/src/index.ts",
|
|
9
|
+
"find": ["server.tool(\n\t\"entwurf_resume_call\","],
|
|
10
|
+
"replace": [
|
|
11
|
+
"const _unregisteredResumeTool = (...a: unknown[]) => void a;\n_unregisteredResumeTool(\n\t\"entwurf_resume_call\","
|
|
12
|
+
],
|
|
13
|
+
"gate": ["bash", "run.sh", "check-entwurf-bridge-boot"],
|
|
14
|
+
"timeoutSeconds": 120,
|
|
15
|
+
"signature": "[QK:BRIDGEBOOT-RESUME-CALL-REGISTERED]",
|
|
16
|
+
"signatureSource": "scripts/check-entwurf-bridge-boot.ts"
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"claim": "BRIDGEBOOT-RESUME-CALL-TARGET-ONLY",
|
|
20
|
+
"title": "a model knob is added to the resume schema, giving the caller a second way to decide what a resumed citizen is after the record already decided",
|
|
21
|
+
"subject": "mcp/entwurf-bridge/src/index.ts",
|
|
22
|
+
"find": ["\t\t\t.describe(\"Garden id of the DORMANT pi citizen to reopen (discover with entwurf_peers).\"),"],
|
|
23
|
+
"replace": [
|
|
24
|
+
"\t\t\t.describe(\"Garden id of the DORMANT pi citizen to reopen (discover with entwurf_peers).\"),\n\t\tmodel: z.string().optional().describe(\"Override the recorded model.\"),"
|
|
25
|
+
],
|
|
26
|
+
"gate": ["bash", "run.sh", "check-entwurf-bridge-boot"],
|
|
27
|
+
"timeoutSeconds": 120,
|
|
28
|
+
"signature": "[QK:BRIDGEBOOT-RESUME-CALL-TARGET-ONLY]",
|
|
29
|
+
"signatureSource": "scripts/check-entwurf-bridge-boot.ts"
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"claim": "BRIDGEBOOT-PUBLIC-SURFACE-EXACT-SET",
|
|
33
|
+
"title": "a verb no assertion happens to name (entwurf_fresh_call) is imported but never registered — the public surface silently shrinks while every named check stays green",
|
|
34
|
+
"subject": "mcp/entwurf-bridge/src/index.ts",
|
|
35
|
+
"find": ["server.tool(\n\t\"entwurf_fresh_call\","],
|
|
36
|
+
"replace": [
|
|
37
|
+
"const _unregisteredFreshCallTool = (...a: unknown[]) => void a;\n_unregisteredFreshCallTool(\n\t\"entwurf_fresh_call\","
|
|
38
|
+
],
|
|
39
|
+
"gate": ["bash", "run.sh", "check-entwurf-bridge-boot"],
|
|
40
|
+
"timeoutSeconds": 120,
|
|
41
|
+
"signature": "[QK:BRIDGEBOOT-PUBLIC-SURFACE-EXACT-SET]",
|
|
42
|
+
"signatureSource": "scripts/check-entwurf-bridge-boot.ts"
|
|
43
|
+
}
|
|
44
|
+
]
|
|
45
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": 1,
|
|
3
|
+
"lane": "meta-facts",
|
|
4
|
+
"mutants": [
|
|
5
|
+
{
|
|
6
|
+
"claim": "MF-EXIT-UNREADABLE",
|
|
7
|
+
"title": "answering an unreadable store with exit 0 makes an unreadable host look like an empty one — the false-success class the exit contract exists to refuse (#65)",
|
|
8
|
+
"subject": "scripts/meta-facts.ts",
|
|
9
|
+
"find": ["\tprocess.exit(3); // see the EXIT CONTRACT above: 3 = unreadable store, never an empty one"],
|
|
10
|
+
"replace": ["\tprocess.exit(0); // see the EXIT CONTRACT above: 3 = unreadable store, never an empty one"],
|
|
11
|
+
"gate": ["bash", "run.sh", "check-meta-facts"],
|
|
12
|
+
"timeoutSeconds": 120,
|
|
13
|
+
"signature": "[QK:MF-EXIT-UNREADABLE]",
|
|
14
|
+
"signatureSource": "scripts/check-meta-facts.ts"
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"claim": "MF-FULL-V3-JOIN",
|
|
18
|
+
"title": "dropping the kept identities from the projection empties the join while still reporting a certified-looking store (#65 wrong fact)",
|
|
19
|
+
"subject": "scripts/meta-facts.ts",
|
|
20
|
+
"find": ["\t\t{ schemaVersion: 1, storeDir: facts.dir, citizens: facts.identities, defects: facts.errors },"],
|
|
21
|
+
"replace": ["\t\t{ schemaVersion: 1, storeDir: facts.dir, citizens: [], defects: facts.errors },"],
|
|
22
|
+
"gate": ["bash", "run.sh", "check-meta-facts"],
|
|
23
|
+
"timeoutSeconds": 120,
|
|
24
|
+
"signature": "[QK:MF-FULL-V3-JOIN]",
|
|
25
|
+
"signatureSource": "scripts/check-meta-facts.ts"
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"claim": "MF-DEFECTS-IN-BAND",
|
|
29
|
+
"title": "dropping the defect list shows a store the certification refuses as clean — the decayed-copy failure mode this surface exists to end (#65 wrong fact)",
|
|
30
|
+
"subject": "scripts/meta-facts.ts",
|
|
31
|
+
"find": ["\t\t{ schemaVersion: 1, storeDir: facts.dir, citizens: facts.identities, defects: facts.errors },"],
|
|
32
|
+
"replace": ["\t\t{ schemaVersion: 1, storeDir: facts.dir, citizens: facts.identities, defects: [] },"],
|
|
33
|
+
"gate": ["bash", "run.sh", "check-meta-facts"],
|
|
34
|
+
"timeoutSeconds": 120,
|
|
35
|
+
"signature": "[QK:MF-DEFECTS-IN-BAND]",
|
|
36
|
+
"signatureSource": "scripts/check-meta-facts.ts"
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"claim": "MF-USAGE",
|
|
40
|
+
"title": "disabling the argv guard makes extra positional args silently read the first as a store dir instead of refusing usage (#65)",
|
|
41
|
+
"subject": "scripts/meta-facts.ts",
|
|
42
|
+
"find": ["if (process.argv.length > 3 || (arg !== undefined && arg.startsWith(\"-\"))) {"],
|
|
43
|
+
"replace": ["if (false) {"],
|
|
44
|
+
"gate": ["bash", "run.sh", "check-meta-facts"],
|
|
45
|
+
"timeoutSeconds": 120,
|
|
46
|
+
"signature": "[QK:MF-USAGE]",
|
|
47
|
+
"signatureSource": "scripts/check-meta-facts.ts"
|
|
48
|
+
}
|
|
49
|
+
]
|
|
50
|
+
}
|