@junghanacs/entwurf 0.34.2 → 0.35.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/CHANGELOG.md +244 -0
- package/README.md +7 -0
- package/VERIFY.md +16 -5
- package/demo/README.md +109 -175
- package/demo/demo-baseline.sh +5 -1
- package/demo/demo.sh +6 -2
- package/demo/herdr-pi-cc-20261011.provenance.json +64 -0
- package/docs/acp-backend-rail.md +28 -2
- package/docs/assets/entwurf-demo.gif +0 -0
- package/docs/setup-clean-host.md +1 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/backend-adapter.js +46 -2
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/model-ids.js +67 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/models.js +9 -20
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-visible-resume.js +11 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/fresh-call-dispatch.js +27 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/herdr-fresh-call.js +3 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +2 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/resume-launch-identity.js +13 -4
- package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-pi-durable.js +119 -11
- package/package.json +4 -2
- package/pi-extensions/lib/acp/backend-adapter.ts +52 -2
- package/pi-extensions/lib/acp/model-ids.js +77 -0
- package/pi-extensions/lib/acp/models.ts +15 -22
- package/pi-extensions/lib/entwurf-v2-visible-resume.ts +12 -0
- package/pi-extensions/lib/fresh-call-dispatch.ts +25 -1
- package/pi-extensions/lib/herdr-fresh-call.ts +5 -0
- package/pi-extensions/lib/mux-fresh-call.ts +3 -0
- package/pi-extensions/lib/resume-launch-identity.ts +12 -4
- package/pi-extensions/meta-bridge-pi-durable.ts +153 -13
- package/run.sh +118 -20
- package/scripts/check-acp-backend-preflight.ts +10 -2
- package/scripts/check-acp-cortex.ts +7 -6
- package/scripts/check-acp-prompt-lifecycle.ts +2 -2
- package/scripts/check-acp-provider-surface.ts +1 -1
- package/scripts/check-acp-session-reuse.ts +5 -5
- package/scripts/check-acp-stop-reason.ts +2 -2
- package/scripts/check-acp-stream-hooks.ts +2 -2
- package/scripts/check-acp-usage-accounting.ts +2 -2
- package/scripts/check-fresh-call-dispatch.ts +47 -4
- package/scripts/check-gate-qualification.ts +8 -5
- package/scripts/check-probe-ordering.ts +13 -6
- package/scripts/check-release-gate-outcomes.ts +159 -1
- package/scripts/check-resume-launch-identity.ts +38 -0
- package/scripts/ci-qualify-decide.sh +114 -17
- package/scripts/mutants/acp-cortex.json +17 -8
- package/scripts/mutants/acp-set-model-readback.json +28 -0
- package/scripts/mutants/demo-reproducibility.json +155 -0
- package/scripts/mutants/fresh-call-dispatch.json +11 -0
- package/scripts/mutants/herdr-dev-checkout.json +70 -0
- package/scripts/mutants/pi-durable.json +117 -0
- package/scripts/mutants/release-gate.json +28 -2
- package/scripts/mutants/resume-launch-identity.json +15 -0
- package/scripts/mutants/v2-visible-resume.json +11 -0
- package/scripts/resolve-acp-bridge.ts +1 -1
- package/scripts/smoke-acp-bundled-mcp-live.ts +1 -1
- package/scripts/smoke-acp-carrier-augment-live.ts +1 -1
- package/scripts/smoke-acp-long-turn-live.ts +1 -1
- package/scripts/smoke-acp-mcp-live.ts +1 -1
- package/scripts/smoke-acp-memory-containment-live.ts +1 -1
- package/scripts/smoke-acp-ordering-probe-live.ts +1 -1
- package/scripts/smoke-acp-overlay-live.ts +1 -1
- package/scripts/smoke-acp-provider-live.ts +1 -1
- package/scripts/smoke-acp-raw-turn-live.ts +1 -1
- package/scripts/smoke-acp-session-reuse-live.ts +1 -1
- package/scripts/smoke-acp-skill-live.ts +1 -1
- package/scripts/smoke-acp-socket-citizen-live.ts +2 -2
- package/scripts/smoke-acp-v2-send-live.ts +1 -1
- package/scripts/smoke-entwurf-chain-live.ts +1 -1
- package/scripts/smoke-mux-lifecycle-live.ts +1 -1
- package/scripts/tsconfig.json +1 -0
|
@@ -3441,6 +3441,7 @@ let manifestCount: number;
|
|
|
3441
3441
|
"acp-launch-namespace": 2,
|
|
3442
3442
|
"acp-overlay": 1,
|
|
3443
3443
|
"acp-prompt-lifecycle": 15,
|
|
3444
|
+
"acp-set-model-readback": 2,
|
|
3444
3445
|
"acp-stop-reason": 6,
|
|
3445
3446
|
"acp-stream-hooks": 10,
|
|
3446
3447
|
"acp-usage-accounting": 12,
|
|
@@ -3455,6 +3456,7 @@ let manifestCount: number;
|
|
|
3455
3456
|
"codex-native": 82,
|
|
3456
3457
|
"compaction-send-guard": 7,
|
|
3457
3458
|
"control-socket-disconnect": 4,
|
|
3459
|
+
"demo-reproducibility": 12,
|
|
3458
3460
|
"copilot-birth": 19,
|
|
3459
3461
|
"copilot-launch": 14,
|
|
3460
3462
|
"pi-launch": 9,
|
|
@@ -3462,10 +3464,10 @@ let manifestCount: number;
|
|
|
3462
3464
|
"pi-bridge-sender": 4,
|
|
3463
3465
|
"pi-mcp-bridge": 2,
|
|
3464
3466
|
"pi-mcp-register": 3,
|
|
3465
|
-
"pi-durable":
|
|
3467
|
+
"pi-durable": 65,
|
|
3466
3468
|
"copilot-receive": 20,
|
|
3467
3469
|
"entwurf-peers": 1,
|
|
3468
|
-
"fresh-call-dispatch":
|
|
3470
|
+
"fresh-call-dispatch": 12,
|
|
3469
3471
|
"fresh-cut": 3,
|
|
3470
3472
|
"gate-qualification": 10,
|
|
3471
3473
|
"herdr-placement": 13,
|
|
@@ -3473,6 +3475,7 @@ let manifestCount: number;
|
|
|
3473
3475
|
"herdr-plugin-profile": 14,
|
|
3474
3476
|
"herdr-activation": 34,
|
|
3475
3477
|
"herdr-plugin-build": 20,
|
|
3478
|
+
"herdr-dev-checkout": 4,
|
|
3476
3479
|
"herdr-runtime-bootstrap": 31,
|
|
3477
3480
|
"herdr-fresh-call": 38,
|
|
3478
3481
|
"herdr-supply": 11,
|
|
@@ -3493,16 +3496,16 @@ let manifestCount: number;
|
|
|
3493
3496
|
"omp-fresh": 24,
|
|
3494
3497
|
"omp-receive": 11,
|
|
3495
3498
|
"probe-ordering": 1,
|
|
3496
|
-
"release-gate":
|
|
3499
|
+
"release-gate": 24,
|
|
3497
3500
|
"retired-sent-surface": 2,
|
|
3498
3501
|
"resume-args": 6,
|
|
3499
|
-
"resume-launch-identity":
|
|
3502
|
+
"resume-launch-identity": 7,
|
|
3500
3503
|
"self-address": 8,
|
|
3501
3504
|
"setup-verdict": 14,
|
|
3502
3505
|
"source-install": 2,
|
|
3503
3506
|
"typing-call-fence": 1,
|
|
3504
3507
|
"v2-surface": 13,
|
|
3505
|
-
"v2-visible-resume":
|
|
3508
|
+
"v2-visible-resume": 18,
|
|
3506
3509
|
};
|
|
3507
3510
|
const laneTally: Record<string, number> = {};
|
|
3508
3511
|
for (const man of manifests) laneTally[man.lane] = (laneTally[man.lane] ?? 0) + man.mutants.length;
|
|
@@ -174,7 +174,7 @@ function makeRecordingConnection(calls: RecordedCall[]): AcpConnectionLike {
|
|
|
174
174
|
},
|
|
175
175
|
setSessionConfigOption: async (params) => {
|
|
176
176
|
calls.push({ method: "setSessionConfigOption", params });
|
|
177
|
-
return {};
|
|
177
|
+
return { configOptions: [{ id: "model", currentValue: (params as { value?: unknown }).value }] };
|
|
178
178
|
},
|
|
179
179
|
prompt: async (params) => {
|
|
180
180
|
calls.push({ method: "prompt", params });
|
|
@@ -381,12 +381,19 @@ function makeRecordingConnection(calls: RecordedCall[]): AcpConnectionLike {
|
|
|
381
381
|
"backend.ts still sends the clientInfo the probe mirrors",
|
|
382
382
|
);
|
|
383
383
|
// The claude adapter's enforceModel wire shape — the LIVE probe executes THIS
|
|
384
|
-
// method (emitted twin), so its shape is pinned where it lives.
|
|
384
|
+
// method (emitted twin), so its shape is pinned where it lives. Sliced to the claude
|
|
385
|
+
// adapter: the cortex adapter makes the same call, and must not satisfy this pin.
|
|
386
|
+
const claudeAdapterSrc = ADAPTER_SRC.slice(
|
|
387
|
+
ADAPTER_SRC.indexOf("export const claudeAdapter"),
|
|
388
|
+
ADAPTER_SRC.indexOf("export const cortexAdapter"),
|
|
389
|
+
);
|
|
385
390
|
assert.ok(
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
391
|
+
claudeAdapterSrc.length > 0 &&
|
|
392
|
+
claudeAdapterSrc.includes(
|
|
393
|
+
'await setConfig.call(connection, {\n\t\t\tsessionId: acpSessionId,\n\t\t\tconfigId: "model",\n\t\t\tvalue: nativeModelId,\n\t\t});',
|
|
394
|
+
) &&
|
|
395
|
+
claudeAdapterSrc.includes("confirmClaudeSetModelAnswer(answer, nativeModelId);"),
|
|
396
|
+
'claudeAdapter.enforceModel still sends setSessionConfigOption({configId:"model"}) and reads its answer back',
|
|
390
397
|
);
|
|
391
398
|
|
|
392
399
|
// Permission policy: the runner's copy must mirror backend.ts's private
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
// Pure + subprocess, no network/model — IN pnpm run check:full.
|
|
44
44
|
|
|
45
45
|
import { strict as assert } from "node:assert";
|
|
46
|
-
import { execFileSync } from "node:child_process";
|
|
46
|
+
import { execFileSync, spawnSync } from "node:child_process";
|
|
47
47
|
import { copyFileSync, globSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
48
48
|
import { tmpdir } from "node:os";
|
|
49
49
|
import { basename, join } from "node:path";
|
|
@@ -650,6 +650,164 @@ function runSubcommand(sub: string, env: Record<string, string | undefined>): {
|
|
|
650
650
|
}
|
|
651
651
|
}
|
|
652
652
|
|
|
653
|
+
// ===========================================================================
|
|
654
|
+
// 8g. #137: TOML data-subject hunks, not path identity; Markdown/JSON data and
|
|
655
|
+
// code/signatures/machinery remain file-level. Hermetic like 8f: real history is NOT
|
|
656
|
+
// in a qualification snapshot.
|
|
657
|
+
// ==========================================================================
|
|
658
|
+
{
|
|
659
|
+
const tmp = mkdtempSync(join(tmpdir(), "entwurf-qualify-hunks-"));
|
|
660
|
+
const git = (...args: string[]) =>
|
|
661
|
+
execFileSync("git", ["-c", "core.hooksPath=/dev/null", "-c", "user.email=g@e", "-c", "user.name=g", ...args], {
|
|
662
|
+
cwd: tmp,
|
|
663
|
+
stdio: "ignore",
|
|
664
|
+
});
|
|
665
|
+
const head = () => execFileSync("git", ["rev-parse", "HEAD"], { cwd: tmp, encoding: "utf8" }).trim();
|
|
666
|
+
const data = 'version = "1"\n[[panes]]\ncommand = ["node", "build.mjs"]\n# outside\n';
|
|
667
|
+
const wrong: string[] = [];
|
|
668
|
+
try {
|
|
669
|
+
git("init", "-q", "-b", "main");
|
|
670
|
+
mkdirSync(join(tmp, "scripts/mutants"), { recursive: true });
|
|
671
|
+
mkdirSync(join(tmp, "fixture"));
|
|
672
|
+
mkdirSync(join(tmp, "scripts/fixtures"));
|
|
673
|
+
copyFileSync(join(REPO_DIR, "scripts/ci-qualify-decide.sh"), join(tmp, "scripts/ci-qualify-decide.sh"));
|
|
674
|
+
const manifest = JSON.stringify({
|
|
675
|
+
mutants: [
|
|
676
|
+
{ subject: "fixture/data.toml", find: ["[[panes]]"] },
|
|
677
|
+
{ subject: "fixture/data.toml", find: ['command = ["node", "build.mjs"]'] },
|
|
678
|
+
{ subject: "fixture/data.toml", find: ["[[panes]]", 'command = ["node", "build.mjs"]'] },
|
|
679
|
+
{ subject: "fixture/code.ts", find: ["return 1"], signatureSource: "fixture/signature.md" },
|
|
680
|
+
{ subject: "fixture/signature.md", find: ["signature"] },
|
|
681
|
+
{ subject: "fixture/joined.toml", find: ["alpha = 1", "beta = 2"] },
|
|
682
|
+
{ subject: "fixture/doc.md", find: ["Legacy rows are documented: kept for history."] },
|
|
683
|
+
{ subject: "fixture/reg.json", find: ['"mode": "self-fetch",'] },
|
|
684
|
+
{ subject: "scripts/fixtures/pin.json", find: ['"pinned": 1'] },
|
|
685
|
+
{ subject: "scripts/ci-qualify-decide.sh", find: ["set -euo pipefail"] },
|
|
686
|
+
],
|
|
687
|
+
});
|
|
688
|
+
writeFileSync(join(tmp, "scripts/mutants/fixture.json"), manifest);
|
|
689
|
+
writeFileSync(join(tmp, "fixture/data.toml"), data);
|
|
690
|
+
writeFileSync(join(tmp, "fixture/code.ts"), "// outside\nfunction f() { return 1; }\n");
|
|
691
|
+
writeFileSync(join(tmp, "fixture/signature.md"), "signature\n# outside\n");
|
|
692
|
+
writeFileSync(join(tmp, "fixture/joined.toml"), "version = 1\nalpha = 1\nbeta = 2\n");
|
|
693
|
+
const doc = "# Doc\nLegacy rows are documented: kept for history.\n# outside\n";
|
|
694
|
+
const reg = '{\n"copilot": {\n"mode": "self-fetch",\n"level": 6\n}\n}\n';
|
|
695
|
+
writeFileSync(join(tmp, "fixture/doc.md"), doc);
|
|
696
|
+
writeFileSync(join(tmp, "fixture/reg.json"), reg);
|
|
697
|
+
writeFileSync(join(tmp, "scripts/fixtures/pin.json"), '{\n"version": 1,\n"pinned": 1\n}\n');
|
|
698
|
+
git("add", "-A");
|
|
699
|
+
git("commit", "-qm", "seed");
|
|
700
|
+
const base = head();
|
|
701
|
+
const nonToml: string[] = [];
|
|
702
|
+
const check = (name: string, expected: boolean, reason?: string, before = base, into = wrong) => {
|
|
703
|
+
const r = spawnSync("bash", [join(tmp, "scripts/ci-qualify-decide.sh"), before, head()], {
|
|
704
|
+
cwd: tmp,
|
|
705
|
+
encoding: "utf8",
|
|
706
|
+
env: { ...process.env, CI_EVENT_NAME: "push", CI_FORCED: "false" },
|
|
707
|
+
});
|
|
708
|
+
if (r.status !== 0 || r.stdout.trim() !== `run_body=${expected}` || (reason && !r.stderr.includes(reason)))
|
|
709
|
+
into.push(`${name}: status=${r.status}, out=${r.stdout.trim()}, err=${r.stderr.trim()}`);
|
|
710
|
+
};
|
|
711
|
+
const commit = () => {
|
|
712
|
+
git("add", "-A");
|
|
713
|
+
git("commit", "-qm", "cell");
|
|
714
|
+
};
|
|
715
|
+
const reset = () => git("reset", "--hard", base); // ONLY this test-owned throwaway repository.
|
|
716
|
+
writeFileSync(join(tmp, "fixture/data.toml"), data.replace('version = "1"', 'version = "2"'));
|
|
717
|
+
commit();
|
|
718
|
+
check("outside", false, "fixture/data.toml: no find range touched");
|
|
719
|
+
// A replay must read HEAD's manifest, not an unrelated dirty worktree's find.
|
|
720
|
+
writeFileSync(
|
|
721
|
+
join(tmp, "scripts/mutants/fixture.json"),
|
|
722
|
+
'{"mutants":[{"subject":"fixture/data.toml","find":["version"]}]}',
|
|
723
|
+
);
|
|
724
|
+
check("HEAD manifest, not dirty worktree", false);
|
|
725
|
+
reset();
|
|
726
|
+
for (const [name, content] of [
|
|
727
|
+
["panes line", data.replace("[[panes]]", "[[panes]] # changed")],
|
|
728
|
+
["command line", data.replace('command = ["node", "build.mjs"]', 'command = ["node", "build.mjs"] # changed')],
|
|
729
|
+
["moved", `# inserted above\n${data}`],
|
|
730
|
+
["ambiguous", `${data}[[panes]]\n`],
|
|
731
|
+
["missing find", data.replace("[[panes]]", "[[other]]")],
|
|
732
|
+
] as const) {
|
|
733
|
+
writeFileSync(join(tmp, "fixture/data.toml"), content);
|
|
734
|
+
commit();
|
|
735
|
+
check(name, true);
|
|
736
|
+
reset();
|
|
737
|
+
}
|
|
738
|
+
git("rm", "fixture/data.toml");
|
|
739
|
+
commit();
|
|
740
|
+
check("deleted subject", true);
|
|
741
|
+
reset();
|
|
742
|
+
git("mv", "fixture/data.toml", "fixture/renamed.toml");
|
|
743
|
+
commit();
|
|
744
|
+
check("renamed subject", true);
|
|
745
|
+
reset();
|
|
746
|
+
writeFileSync(join(tmp, "fixture/code.ts"), "// outside changed\nfunction f() { return 1; }\n");
|
|
747
|
+
commit();
|
|
748
|
+
check("code outside find remains file-level", true);
|
|
749
|
+
reset();
|
|
750
|
+
writeFileSync(join(tmp, "fixture/signature.md"), "signature\n# outside changed\n");
|
|
751
|
+
commit();
|
|
752
|
+
check("signature also data subject remains file-level", true);
|
|
753
|
+
reset();
|
|
754
|
+
writeFileSync(join(tmp, "fixture/joined.toml"), "version = 1\nalpha = 1\nbeta = 2 # changed\n");
|
|
755
|
+
commit();
|
|
756
|
+
check("only multiline find, second line touched", true);
|
|
757
|
+
reset();
|
|
758
|
+
writeFileSync(join(tmp, "scripts/fixtures/pin.json"), '{\n"version": 2,\n"pinned": 1\n}\n');
|
|
759
|
+
commit();
|
|
760
|
+
check("machinery also data subject remains file-level", true);
|
|
761
|
+
reset();
|
|
762
|
+
writeFileSync(
|
|
763
|
+
join(tmp, "scripts/ci-qualify-decide.sh"),
|
|
764
|
+
`${readFileSync(join(tmp, "scripts/ci-qualify-decide.sh"), "utf8")}\n# outside find\n`,
|
|
765
|
+
);
|
|
766
|
+
commit();
|
|
767
|
+
check("machinery also subject remains file-level", true);
|
|
768
|
+
reset();
|
|
769
|
+
// Outside an untouched exact-once span, a shorter presence literal (measured on DELIVERY.md)
|
|
770
|
+
// or a later JSON duplicate key (JSON.parse: last wins) can re-satisfy the oracle.
|
|
771
|
+
writeFileSync(join(tmp, "fixture/doc.md"), `${doc}Legacy rows are documented\n`);
|
|
772
|
+
commit();
|
|
773
|
+
check("markdown restatement outside find", true, "conservative:", base, nonToml);
|
|
774
|
+
reset();
|
|
775
|
+
writeFileSync(
|
|
776
|
+
join(tmp, "fixture/reg.json"),
|
|
777
|
+
reg.replace("6\n}\n}\n", '6\n},\n"copilot": { "level": 6, "mode": "self-fetch" }\n}\n'),
|
|
778
|
+
);
|
|
779
|
+
commit();
|
|
780
|
+
check("json duplicate key outside find", true, "conservative:", base, nonToml);
|
|
781
|
+
reset();
|
|
782
|
+
// The first quoted name must still fail open after a >pipe-buffer list:
|
|
783
|
+
// printf | grep -q can lose its match to SIGPIPE under pipefail.
|
|
784
|
+
writeFileSync(join(tmp, '000"quoted'), "x\n");
|
|
785
|
+
for (let i = 0; i < 768; i++) writeFileSync(join(tmp, "fixture", `${i}_${"x".repeat(180)}.txt`), "x\n");
|
|
786
|
+
commit();
|
|
787
|
+
check("large quoted changed-name list", true, "quoted changed path cannot be read honestly");
|
|
788
|
+
reset();
|
|
789
|
+
check("unreadable range", true, "fail-open 4", "f".repeat(40));
|
|
790
|
+
git("rm", "scripts/mutants/fixture.json");
|
|
791
|
+
commit();
|
|
792
|
+
check("missing manifests", true);
|
|
793
|
+
assert.ok(
|
|
794
|
+
wrong.length === 0,
|
|
795
|
+
"[QK:QUALIFY-FILTER-SUBJECT-HUNKS] only untouched, unmoved, exact-once TOML find ranges may skip; " +
|
|
796
|
+
"unknown ranges run, and code/signature/machinery retain file-level protection. " +
|
|
797
|
+
wrong.join("; "),
|
|
798
|
+
);
|
|
799
|
+
assert.ok(
|
|
800
|
+
nonToml.length === 0,
|
|
801
|
+
"[QK:QUALIFY-FILTER-TOML-ONLY-REFINEMENT] Markdown and JSON subjects stay file-level: an untouched exact-once " +
|
|
802
|
+
"find does not pin kill-power when a shorter presence literal or a later duplicate JSON key outside it can " +
|
|
803
|
+
"re-satisfy the oracle. " +
|
|
804
|
+
nonToml.join("; "),
|
|
805
|
+
);
|
|
806
|
+
} finally {
|
|
807
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
|
|
653
811
|
// ===========================================================================
|
|
654
812
|
// 9. Every gate a mutant manifest NAMES runs inside `pnpm run check:full`
|
|
655
813
|
//
|
|
@@ -42,6 +42,7 @@ import assert from "node:assert/strict";
|
|
|
42
42
|
import * as fs from "node:fs";
|
|
43
43
|
import * as os from "node:os";
|
|
44
44
|
import * as path from "node:path";
|
|
45
|
+
import { AcpModelNotCuratedError } from "../pi-extensions/lib/acp/model-ids.js";
|
|
45
46
|
import { serializeMetaIdentity, upsertMetaSession } from "../pi-extensions/lib/meta-session.ts";
|
|
46
47
|
import {
|
|
47
48
|
ResumeBackendUnsupportedError,
|
|
@@ -162,6 +163,43 @@ function main(): void {
|
|
|
162
163
|
);
|
|
163
164
|
}
|
|
164
165
|
|
|
166
|
+
// 4d. #128 — the second EXPECTED refusal. Under the `entwurf` provider Pi resolves `--model`
|
|
167
|
+
// by substring, so a retired Claude 5 id would reopen on a Cortex row. The pair judged is the
|
|
168
|
+
// one the launch would EMIT: a bare Claude id recorded under another provider is routed to
|
|
169
|
+
// `entwurf` too, so judging the recorded provider alone would let that one through.
|
|
170
|
+
{
|
|
171
|
+
const acpModelLine = (provider: string, modelId: string): string =>
|
|
172
|
+
`${JSON.stringify({ type: "model_change", provider, modelId })}\n`;
|
|
173
|
+
const settle = (gid: string): { identity?: ReturnType<typeof resolveResumeLaunchIdentity>; err?: unknown } => {
|
|
174
|
+
try {
|
|
175
|
+
return { identity: resolveResumeLaunchIdentity(gid) };
|
|
176
|
+
} catch (err) {
|
|
177
|
+
return { err };
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
const minted = (native: string, name: string, provider: string, modelId: string): string =>
|
|
181
|
+
mintRecord(native, writeTranscript(name, sessionLine(native) + acpModelLine(provider, modelId)));
|
|
182
|
+
const retired = settle(
|
|
183
|
+
minted("0199a5a5-1111-4222-8333-444455556666", "acp-retired.jsonl", "entwurf", "claude-sonnet-5"),
|
|
184
|
+
);
|
|
185
|
+
const routed = settle(
|
|
186
|
+
minted("0199b5b5-1111-4222-8333-444455556666", "acp-routed.jsonl", "anthropic", "claude-opus-5"),
|
|
187
|
+
);
|
|
188
|
+
const current = settle(
|
|
189
|
+
minted("0199c5c5-1111-4222-8333-444455556666", "acp-current.jsonl", "entwurf", "claude-sonnet-5-5"),
|
|
190
|
+
);
|
|
191
|
+
ok(
|
|
192
|
+
"4d: an entwurf-provider resume of a non-curated id is a TYPED refusal — recorded under entwurf, or a bare Claude id the launch would route to entwurf — while the exact 5.5 id resolves unchanged [QK:RESUME-ID-ACP-EXACT-CURATION]",
|
|
193
|
+
retired.err instanceof AcpModelNotCuratedError &&
|
|
194
|
+
retired.err.reason === "acp-model-not-curated" &&
|
|
195
|
+
retired.err.model === "claude-sonnet-5" &&
|
|
196
|
+
routed.err instanceof AcpModelNotCuratedError &&
|
|
197
|
+
routed.err.model === "claude-opus-5" &&
|
|
198
|
+
current.identity?.provider === "entwurf" &&
|
|
199
|
+
current.identity.model === "claude-sonnet-5-5",
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
|
|
165
203
|
const gidNoFile = mintRecord("0199dddd-1111-4222-8333-444455556666", null);
|
|
166
204
|
rejects(gidNoFile, "no recorded transcriptPath", "4: transcriptPath null → nothing to resume");
|
|
167
205
|
|
|
@@ -28,37 +28,117 @@ fail_open() {
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
collect_paths_from_manifests() {
|
|
31
|
-
python3 - "$REPO_DIR" <<'PY'
|
|
31
|
+
python3 - "$REPO_DIR" "${1:-}" <<'PY'
|
|
32
32
|
import glob
|
|
33
33
|
import json
|
|
34
34
|
import os
|
|
35
35
|
import sys
|
|
36
|
+
import subprocess
|
|
36
37
|
|
|
37
|
-
repo = sys.argv[1]
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
38
|
+
repo, rev = sys.argv[1:]
|
|
39
|
+
def git(*args):
|
|
40
|
+
return subprocess.check_output(["git", "-C", repo, *args])
|
|
41
|
+
paths = (git("ls-tree", "-r", "--name-only", rev, "scripts/mutants").decode().splitlines()
|
|
42
|
+
if rev else sorted(glob.glob(os.path.join(repo, "scripts/mutants/*.json"))))
|
|
43
|
+
paths = [p for p in paths if p.endswith(".json")]
|
|
44
|
+
if not paths:
|
|
45
|
+
raise ValueError("no mutant manifests")
|
|
46
|
+
out, signatures, subjects = set(), set(), {}
|
|
47
|
+
for path in paths:
|
|
48
|
+
if rev:
|
|
49
|
+
manifest = json.loads(git("show", f"{rev}:{path}"))
|
|
50
|
+
else:
|
|
51
|
+
with open(path, encoding="utf-8") as handle:
|
|
52
|
+
manifest = json.load(handle)
|
|
42
53
|
for mutant in manifest.get("mutants", []):
|
|
43
54
|
subject = mutant.get("subject")
|
|
44
55
|
if subject:
|
|
45
56
|
out.add(subject)
|
|
57
|
+
find = mutant.get("find")
|
|
58
|
+
# The runner joins a find ARRAY into ONE multiline occurrence.
|
|
59
|
+
needle = "\n".join(find) if isinstance(find, list) and all(isinstance(s, str) for s in find) else None
|
|
60
|
+
subjects.setdefault(subject, []).append(needle)
|
|
46
61
|
signature_source = mutant.get("signatureSource")
|
|
47
62
|
if signature_source:
|
|
48
63
|
out.add(signature_source)
|
|
49
|
-
|
|
50
|
-
|
|
64
|
+
signatures.add(signature_source)
|
|
65
|
+
if not out or any(any(c in p for c in '\n\r\t"\\') for p in out):
|
|
66
|
+
raise ValueError("empty or unrepresentable manifest paths")
|
|
67
|
+
print(json.dumps({"paths": sorted(out), "signatures": sorted(signatures), "subjects": subjects}))
|
|
51
68
|
PY
|
|
52
69
|
}
|
|
53
70
|
|
|
54
|
-
|
|
55
|
-
|
|
71
|
+
load_manifest_surface() {
|
|
72
|
+
MANIFEST_DATA="$(collect_paths_from_manifests "${1:-}")" || fail_open 6 "missing/unreadable mutant manifests"
|
|
73
|
+
MANIFEST_PATHS="$(printf '%s' "$MANIFEST_DATA" | python3 -c 'import json,sys;print("\n".join(json.load(sys.stdin)["paths"]))')" || fail_open 6 "unreadable manifest paths"
|
|
74
|
+
SIGNATURE_PATHS="$(printf '%s' "$MANIFEST_DATA" | python3 -c 'import json,sys;print("\n".join(json.load(sys.stdin)["signatures"]))')" || fail_open 6 "unreadable signature paths"
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
# Return success ONLY when every exact-once TOML find span is demonstrably
|
|
78
|
+
# untouched and unmoved on BOTH blobs. Any uncertainty leaves the body armed.
|
|
79
|
+
subject_find_untouched() {
|
|
80
|
+
python3 - "$REPO_DIR" "$BEFORE" "$HEAD_SHA" "$1" 3<<< "$MANIFEST_DATA" <<'PY'
|
|
81
|
+
import json
|
|
82
|
+
import os
|
|
83
|
+
import re
|
|
84
|
+
import subprocess
|
|
85
|
+
import sys
|
|
86
|
+
|
|
87
|
+
repo, before, head, path = sys.argv[1:]
|
|
88
|
+
def git(*args):
|
|
89
|
+
return subprocess.check_output(["git", "-C", repo, *args], stderr=subprocess.DEVNULL).decode("utf-8")
|
|
90
|
+
def span(source, needle):
|
|
91
|
+
if not needle or source.count(needle) != 1:
|
|
92
|
+
raise ValueError("find absent or ambiguous")
|
|
93
|
+
start = source[:source.index(needle)].count("\n") + 1
|
|
94
|
+
return start, start + needle.count("\n") - int(needle.endswith("\n"))
|
|
95
|
+
try:
|
|
96
|
+
finds = json.load(os.fdopen(3))["subjects"].get(path)
|
|
97
|
+
if not finds or any(not needle for needle in finds):
|
|
98
|
+
raise ValueError("missing find spans")
|
|
99
|
+
old, new = git("show", f"{before}:{path}"), git("show", f"{head}:{path}")
|
|
100
|
+
if "\0" in old or "\0" in new:
|
|
101
|
+
raise ValueError("binary subject")
|
|
102
|
+
diff = git("diff", "--no-renames", "--no-ext-diff", "--no-textconv", "--no-color", "-U0", before, head, "--", path)
|
|
103
|
+
if "Binary files " in diff or "GIT binary patch" in diff:
|
|
104
|
+
raise ValueError("binary diff")
|
|
105
|
+
hunks = []
|
|
106
|
+
for line in diff.splitlines():
|
|
107
|
+
if not line.startswith("@@"):
|
|
108
|
+
continue
|
|
109
|
+
match = re.fullmatch(r"@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@(?: .*)?", line)
|
|
110
|
+
if not match:
|
|
111
|
+
raise ValueError("unreadable hunk")
|
|
112
|
+
a, b, c, d = match.groups()
|
|
113
|
+
hunks.append((int(a), int(b or 1), int(c), int(d or 1)))
|
|
114
|
+
if not hunks:
|
|
115
|
+
raise ValueError("no readable text hunks")
|
|
116
|
+
for needle in set(finds):
|
|
117
|
+
os_, oe = span(old, needle)
|
|
118
|
+
ns, ne = span(new, needle)
|
|
119
|
+
if (os_, oe) != (ns, ne):
|
|
120
|
+
raise ValueError("find range moved")
|
|
121
|
+
for a, b, c, d in hunks:
|
|
122
|
+
if (b > 0 and a <= oe and a + b - 1 >= os_) or (d > 0 and c <= ne and c + d - 1 >= ns):
|
|
123
|
+
raise ValueError("find range touched")
|
|
124
|
+
except (ValueError, KeyError, TypeError, OSError, subprocess.SubprocessError) as error:
|
|
125
|
+
print(f"ci-qualify-decide: fail-open 7 {path}: {error} -> run_body=true", file=sys.stderr)
|
|
126
|
+
sys.exit(1)
|
|
127
|
+
print(f"ci-qualify-decide: {path}: no find range touched (all exact-once spans unmoved)", file=sys.stderr)
|
|
128
|
+
PY
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
# Signature sources and machinery MUST precede subjects: many paths are BOTH.
|
|
132
|
+
# Code/unknown subjects retain file-level protection; only TOML data gets hunk
|
|
133
|
+
# refinement (#137 non-goal: no skipping code/gate/mutant/matrix changes). TOML
|
|
134
|
+
# refuses a redefined key, but a shorter Markdown presence literal or a later JSON
|
|
135
|
+
# duplicate key outside an untouched find can still re-satisfy an oracle.
|
|
56
136
|
matches_qualification_surface() {
|
|
57
137
|
local file="$1"
|
|
58
138
|
local subject
|
|
59
139
|
while IFS= read -r subject; do
|
|
60
|
-
[ "$file" = "$subject" ] && { echo "manifest-
|
|
61
|
-
done <<< "$
|
|
140
|
+
[ "$file" = "$subject" ] && { echo "manifest-signature-source"; return 0; }
|
|
141
|
+
done <<< "$SIGNATURE_PATHS"
|
|
62
142
|
case "$file" in
|
|
63
143
|
scripts/mutants/*) echo "glob:scripts/mutants/**"; return 0 ;;
|
|
64
144
|
scripts/check-*) echo "glob:scripts/check-*"; return 0 ;;
|
|
@@ -73,6 +153,20 @@ matches_qualification_surface() {
|
|
|
73
153
|
# that stopped covering something cannot hide behind its own change.
|
|
74
154
|
scripts/ci-qualify-decide.sh) echo "exact:scripts/ci-qualify-decide.sh"; return 0 ;;
|
|
75
155
|
esac
|
|
156
|
+
while IFS= read -r subject; do
|
|
157
|
+
if [ "$file" = "$subject" ]; then
|
|
158
|
+
if [ "${MATCH_MODE:-files}" = range ]; then
|
|
159
|
+
case "$file" in
|
|
160
|
+
*.toml) ;;
|
|
161
|
+
*) echo "conservative:non-toml-subject"; return 0 ;;
|
|
162
|
+
esac
|
|
163
|
+
if subject_find_untouched "$file"; then
|
|
164
|
+
return 1
|
|
165
|
+
fi
|
|
166
|
+
fi
|
|
167
|
+
echo "manifest-subject"; return 0
|
|
168
|
+
fi
|
|
169
|
+
done <<< "$MANIFEST_PATHS"
|
|
76
170
|
return 1
|
|
77
171
|
}
|
|
78
172
|
|
|
@@ -98,13 +192,12 @@ decide_from_files() {
|
|
|
98
192
|
fi
|
|
99
193
|
}
|
|
100
194
|
|
|
101
|
-
MANIFEST_PATHS="$(collect_paths_from_manifests)"
|
|
102
|
-
[ -n "$MANIFEST_PATHS" ] || { echo "ABORT: no mutant manifests read from $REPO_DIR/scripts/mutants" >&2; exit 1; }
|
|
103
|
-
|
|
104
195
|
if [ "${1:-}" = "--files-from" ]; then
|
|
196
|
+
MATCH_MODE=files
|
|
197
|
+
load_manifest_surface
|
|
105
198
|
SRC="${2:-}"
|
|
106
199
|
[ -n "$SRC" ] || { echo "ABORT: --files-from needs a path or -" >&2; exit 1; }
|
|
107
|
-
if [ "$SRC" = "-" ]; then FILES="$(cat)"; else FILES="$(cat "$SRC")"; fi
|
|
200
|
+
if [ "$SRC" = "-" ]; then FILES="$(cat)"; else FILES="$(cat "$SRC")"; fi || fail_open 4 "unreadable files-from input"
|
|
108
201
|
decide_from_files "$FILES"
|
|
109
202
|
exit 0
|
|
110
203
|
fi
|
|
@@ -154,6 +247,10 @@ esac
|
|
|
154
247
|
git -C "$REPO_DIR" cat-file -e "$BEFORE^{commit}" 2>/dev/null || fail_open 4 "before=$BEFORE is not a commit object in this checkout"
|
|
155
248
|
git -C "$REPO_DIR" cat-file -e "$HEAD_SHA^{commit}" 2>/dev/null || fail_open 4 "head=$HEAD_SHA is not a commit object in this checkout"
|
|
156
249
|
|
|
157
|
-
|
|
250
|
+
MATCH_MODE=range
|
|
251
|
+
load_manifest_surface "$HEAD_SHA"
|
|
252
|
+
CHANGED="$(git -C "$REPO_DIR" diff --no-renames --no-ext-diff --no-textconv --name-only "$BEFORE".."$HEAD_SHA" 2>/dev/null)" || fail_open 4 "git diff $BEFORE..$HEAD_SHA failed"
|
|
253
|
+
# Git quotes unrepresentable changed names; never interpret those as absent paths.
|
|
254
|
+
if grep -q '^"' <<< "$CHANGED"; then fail_open 4 "quoted changed path cannot be read honestly"; fi
|
|
158
255
|
|
|
159
256
|
decide_from_files "$CHANGED"
|
|
@@ -5,21 +5,21 @@
|
|
|
5
5
|
{
|
|
6
6
|
"claim": "CORTEX-CURATED-FOUR-ROWS",
|
|
7
7
|
"title": "dropping a curated row silently shrinks the GLG-decided 4-row cortex surface",
|
|
8
|
-
"subject": "pi-extensions/lib/acp/
|
|
8
|
+
"subject": "pi-extensions/lib/acp/model-ids.js",
|
|
9
9
|
"find": [
|
|
10
|
-
"export const SUPPORTED_CORTEX_MODEL_IDS = [",
|
|
10
|
+
"export const SUPPORTED_CORTEX_MODEL_IDS = /** @type {const} */ ([",
|
|
11
11
|
"\t\"cortex-auto\",",
|
|
12
12
|
"\t\"cortex-claude-opus-5\",",
|
|
13
13
|
"\t\"cortex-claude-sonnet-5\",",
|
|
14
14
|
"\t\"cortex-openai-gpt-5.4\",",
|
|
15
|
-
"]
|
|
15
|
+
"]);"
|
|
16
16
|
],
|
|
17
17
|
"replace": [
|
|
18
|
-
"export const SUPPORTED_CORTEX_MODEL_IDS = [",
|
|
18
|
+
"export const SUPPORTED_CORTEX_MODEL_IDS = /** @type {const} */ ([",
|
|
19
19
|
"\t\"cortex-auto\",",
|
|
20
20
|
"\t\"cortex-claude-sonnet-5\",",
|
|
21
21
|
"\t\"cortex-openai-gpt-5.4\",",
|
|
22
|
-
"]
|
|
22
|
+
"]);"
|
|
23
23
|
],
|
|
24
24
|
"gate": ["bash", "run.sh", "check-acp-cortex"],
|
|
25
25
|
"timeoutSeconds": 300,
|
|
@@ -29,11 +29,20 @@
|
|
|
29
29
|
{
|
|
30
30
|
"claim": "CLAUDE-CURATED-THREE-ROWS",
|
|
31
31
|
"title": "dropping a curated CLAUDE row shrinks the operator's model list while the exact-set comparison stays green — expected derives from the same constant, so only the independent count catches it",
|
|
32
|
-
"subject": "pi-extensions/lib/acp/
|
|
32
|
+
"subject": "pi-extensions/lib/acp/model-ids.js",
|
|
33
33
|
"find": [
|
|
34
|
-
"export const SUPPORTED_ANTHROPIC_MODEL_IDS =
|
|
34
|
+
"export const SUPPORTED_ANTHROPIC_MODEL_IDS = /** @type {const} */ ([",
|
|
35
|
+
"\t\"claude-sonnet-5-5\",",
|
|
36
|
+
"\t\"claude-opus-5-5\",",
|
|
37
|
+
"\t\"claude-fable-5-1\",",
|
|
38
|
+
"]);"
|
|
39
|
+
],
|
|
40
|
+
"replace": [
|
|
41
|
+
"export const SUPPORTED_ANTHROPIC_MODEL_IDS = /** @type {const} */ ([",
|
|
42
|
+
"\t\"claude-sonnet-5-5\",",
|
|
43
|
+
"\t\"claude-opus-5-5\",",
|
|
44
|
+
"]);"
|
|
35
45
|
],
|
|
36
|
-
"replace": ["export const SUPPORTED_ANTHROPIC_MODEL_IDS = [\"claude-sonnet-5\", \"claude-opus-5\"] as const;"],
|
|
37
46
|
"gate": ["bash", "run.sh", "check-acp-provider-surface"],
|
|
38
47
|
"timeoutSeconds": 300,
|
|
39
48
|
"signature": "[QK:CLAUDE-CURATED-THREE-ROWS]",
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": 1,
|
|
3
|
+
"lane": "acp-set-model-readback",
|
|
4
|
+
"mutants": [
|
|
5
|
+
{
|
|
6
|
+
"claim": "ACP-CLAUDE-SET-MODEL-READBACK",
|
|
7
|
+
"title": "the claude set-model answer is no longer read back — a missing model option or another row reaches the prompt, so a different model answers under the requested id",
|
|
8
|
+
"subject": "pi-extensions/lib/acp/backend-adapter.ts",
|
|
9
|
+
"find": ["\t\tconfirmClaudeSetModelAnswer(answer, nativeModelId);"],
|
|
10
|
+
"replace": ["\t\tvoid answer;"],
|
|
11
|
+
"gate": ["bash", "run.sh", "check-acp-set-model-readback"],
|
|
12
|
+
"timeoutSeconds": 60,
|
|
13
|
+
"signature": "[QK:ACP-CLAUDE-SET-MODEL-READBACK]",
|
|
14
|
+
"signatureSource": "pi-extensions/lib/acp/backend-adapter.test.ts"
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"claim": "ACP-CLAUDE-SET-MODEL-ALIAS-EXACT",
|
|
18
|
+
"title": "an alias answer is matched by row name alone — the `default` row or a retired id's row carrying the 5.5 name answers silently under the 5.5 id",
|
|
19
|
+
"subject": "pi-extensions/lib/acp/backend-adapter.ts",
|
|
20
|
+
"find": ["\tif (alias !== undefined && currentValue === alias.value && selected?.name === alias.name) return;"],
|
|
21
|
+
"replace": ["\tif (alias !== undefined && selected?.name === alias.name) return;"],
|
|
22
|
+
"gate": ["bash", "run.sh", "check-acp-set-model-readback"],
|
|
23
|
+
"timeoutSeconds": 60,
|
|
24
|
+
"signature": "[QK:ACP-CLAUDE-SET-MODEL-ALIAS-EXACT]",
|
|
25
|
+
"signatureSource": "pi-extensions/lib/acp/backend-adapter.test.ts"
|
|
26
|
+
}
|
|
27
|
+
]
|
|
28
|
+
}
|