@junghanacs/entwurf 0.52.0 → 0.53.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 +1 -1
- package/BASELINE.md +116 -397
- package/CHANGELOG.md +131 -2
- package/CONTRIBUTING.md +4 -4
- package/DELIVERY.md +2 -2
- package/ENTWURF-CLI.json +1 -1
- package/README.md +7 -4
- package/VERIFY.md +332 -142
- package/demo/demo-baseline.sh +1 -1
- package/demo/demo.sh +1 -1
- package/docs/acp-backend-rail.md +248 -34
- package/docs/setup-clean-host.md +50 -65
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/acp-client.js +1 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/backend-adapter.js +90 -14
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/config.js +4 -9
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/context.js +15 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/model-ids.js +9 -12
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/models.js +41 -56
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/tool-surface.js +33 -6
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/fresh-call-composition.js +34 -3
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/herdr-fresh-call.js +3 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +2 -1
- package/package.json +4 -4
- package/pi/settings.reference.json +0 -1
- package/pi-extensions/acp-provider.ts +12 -6
- package/pi-extensions/lib/acp/acp-client.ts +6 -7
- package/pi-extensions/lib/acp/backend-adapter.ts +139 -15
- package/pi-extensions/lib/acp/backend.ts +261 -32
- package/pi-extensions/lib/acp/config.ts +4 -9
- package/pi-extensions/lib/acp/context.ts +17 -0
- package/pi-extensions/lib/acp/event-mapper.ts +236 -17
- package/pi-extensions/lib/acp/model-ids.js +9 -12
- package/pi-extensions/lib/acp/models.ts +46 -57
- package/pi-extensions/lib/acp/tool-surface.ts +34 -6
- package/pi-extensions/lib/fresh-call-composition.ts +37 -3
- package/pi-extensions/lib/herdr-fresh-call.ts +3 -1
- package/pi-extensions/lib/mux-fresh-call.ts +3 -1
- package/run.sh +25 -86
- package/scripts/check-acp-carrier-augment.ts +3 -21
- package/scripts/check-acp-prompt-lifecycle.ts +24 -3
- package/scripts/check-acp-provider-surface.ts +17 -26
- package/scripts/check-acp-session-reuse.ts +23 -4
- package/scripts/check-acp-stop-reason.ts +22 -3
- package/scripts/check-acp-stream-hooks.ts +24 -3
- package/scripts/check-acp-usage-accounting.ts +53 -24
- package/scripts/check-copilot-receive-arm.ts +19 -2
- package/scripts/check-fresh-call-dispatch.ts +9 -3
- package/scripts/check-gate-qualification.ts +5 -5
- package/scripts/check-herdr-fresh-call.ts +1 -0
- package/scripts/check-install-surface.ts +6 -0
- package/scripts/check-probe-ordering.ts +34 -24
- package/scripts/check-release-gate-outcomes.ts +9 -23
- package/scripts/lib/probe-cli-target.ts +3 -2
- package/scripts/mutants/acp-augment.json +0 -26
- package/scripts/mutants/acp-cortex.json +10 -183
- package/scripts/mutants/acp-foreground-only.json +11 -0
- package/scripts/mutants/acp-launch-namespace.json +3 -1
- package/scripts/mutants/acp-prompt-lifecycle.json +2 -2
- package/scripts/mutants/acp-set-model-readback.json +488 -0
- package/scripts/mutants/mux-fresh-call.json +12 -1
- package/scripts/mutants/pi-floor.json +2 -2
- package/scripts/mutants/release-gate.json +3 -3
- package/scripts/raw-acp-compaction-measure/probe.ts +8 -2
- package/scripts/smoke-acp-cortex-live.ts +4 -408
- package/scripts/smoke-acp-ordering-probe-live.ts +7 -1
- package/scripts/smoke-acp-session-reuse-live.ts +11 -2
- package/scripts/smoke-herdr-raw-install-live.sh +4 -4
- package/scripts/check-acp-cortex.ts +0 -710
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
// totalTokens is CONTEXT OCCUPANCY, not a turn total, and is never overwritten
|
|
28
28
|
// with the turn aggregate — pi reads that field as the context gauge.
|
|
29
29
|
// a backend with no measured semantics (no sealsTurnAccounting) is sealed NOT AT
|
|
30
|
-
// ALL:
|
|
30
|
+
// ALL: the no-capability fixture preserves the mapper's output.
|
|
31
31
|
//
|
|
32
32
|
// ORACLES, each independent of the subject:
|
|
33
33
|
// - the cost sum: THIS FILE constructs the cumulative series and asserts the
|
|
@@ -62,8 +62,21 @@ import { isContextOverflow, normalizeContext } from "@earendil-works/pi-ai";
|
|
|
62
62
|
import { calculateContextTokens } from "@earendil-works/pi-coding-agent";
|
|
63
63
|
import { assertOverlayInSandbox, sandboxAcpGateHome } from "./lib/acp-gate-home.ts";
|
|
64
64
|
|
|
65
|
-
|
|
66
|
-
|
|
65
|
+
// #142: a Claude turn carries pi's thinking level, which the backend sets as effort and reads back
|
|
66
|
+
// before the prompt. This fixture runs every turn at one fixed level on rows carrying the curated
|
|
67
|
+
// level map, and its fake driver reads back whatever config option it was asked to set. The effort
|
|
68
|
+
// contract itself is certified in pi-extensions/lib/acp/backend.thinking.test.ts.
|
|
69
|
+
const FIXTURE_THINKING_LEVEL = "medium";
|
|
70
|
+
const FIXTURE_THINKING_LEVEL_MAP = {
|
|
71
|
+
off: null,
|
|
72
|
+
minimal: null,
|
|
73
|
+
low: "low",
|
|
74
|
+
medium: "medium",
|
|
75
|
+
high: "high",
|
|
76
|
+
xhigh: "xhigh",
|
|
77
|
+
max: "max",
|
|
78
|
+
};
|
|
79
|
+
const claude = { id: "claude-sonnet-5-5", thinkingLevelMap: FIXTURE_THINKING_LEVEL_MAP } as unknown as Model<Api>;
|
|
67
80
|
|
|
68
81
|
type Stream = AsyncIterable<AssistantMessageEvent>;
|
|
69
82
|
|
|
@@ -161,7 +174,7 @@ function makeHarness(recordDir: string, scripts: TurnScript[]) {
|
|
|
161
174
|
newSessionCalls++;
|
|
162
175
|
return { sessionId: "ACP-1" };
|
|
163
176
|
},
|
|
164
|
-
setSessionConfigOption: async (p: any) => ({ configOptions: [{ id:
|
|
177
|
+
setSessionConfigOption: async (p: any) => ({ configOptions: [{ id: p.configId, currentValue: p.value }] }),
|
|
165
178
|
prompt: async ({ sessionId }: any) => {
|
|
166
179
|
const script = scripts[turnIndex++] ?? {};
|
|
167
180
|
const update: Record<string, unknown> = { sessionUpdate: "usage_update" };
|
|
@@ -305,7 +318,14 @@ try {
|
|
|
305
318
|
const promptsOut = resolve(TMP_EMIT, "pi-extensions/lib/acp/prompts");
|
|
306
319
|
mkdirSync(promptsOut, { recursive: true });
|
|
307
320
|
copyFileSync("pi-extensions/lib/acp/prompts/engraving.md", resolve(promptsOut, "engraving.md"));
|
|
308
|
-
const
|
|
321
|
+
const backendModule = (await import(
|
|
322
|
+
pathToFileURL(resolve(TMP_EMIT, "pi-extensions/lib/acp/backend.js")).href
|
|
323
|
+
)) as any;
|
|
324
|
+
const backend = {
|
|
325
|
+
...backendModule,
|
|
326
|
+
streamAcpTurn: (model: unknown, context: unknown, options: Record<string, unknown> | undefined, deps: unknown) =>
|
|
327
|
+
backendModule.streamAcpTurn(model, context, { reasoning: FIXTURE_THINKING_LEVEL, ...options }, deps),
|
|
328
|
+
};
|
|
309
329
|
await assertOverlayInSandbox(TMP_EMIT, gateHome);
|
|
310
330
|
|
|
311
331
|
// ----------------------------------------------------------------------
|
|
@@ -915,13 +935,10 @@ try {
|
|
|
915
935
|
// ----------------------------------------------------------------------
|
|
916
936
|
// CELL 5 — a backend with no MEASURED semantics is sealed NOT AT ALL.
|
|
917
937
|
//
|
|
918
|
-
//
|
|
919
|
-
//
|
|
920
|
-
//
|
|
921
|
-
//
|
|
922
|
-
// turn", the field comments say "across all turns/session". So cortex must
|
|
923
|
-
// keep exactly its pre-#93 output: the coarse mapper assignment, and no token
|
|
924
|
-
// projection, no baseline, no diff.
|
|
938
|
+
// The common loop must not invent accounting semantics for an adapter that
|
|
939
|
+
// declares none. Cortex is disabled, so this isolated fixture temporarily
|
|
940
|
+
// removes the capability from the compiled Claude adapter; it changes no
|
|
941
|
+
// production routing and restores the capability before the next cell.
|
|
925
942
|
// ----------------------------------------------------------------------
|
|
926
943
|
// TWO turns, and the second one is what carries the claim: on turn ONE a
|
|
927
944
|
// diff-against-an-empty-baseline and the raw cumulative are the SAME number, so
|
|
@@ -946,18 +963,30 @@ try {
|
|
|
946
963
|
occupancyTokens: 140_000,
|
|
947
964
|
},
|
|
948
965
|
]);
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
await collect(
|
|
952
|
-
backend.streamAcpTurn(
|
|
953
|
-
cortex,
|
|
954
|
-
reuseCtx("cortex NONCE-1", "cortex NONCE-2"),
|
|
955
|
-
{ sessionId: "usage-E" },
|
|
956
|
-
h.deps,
|
|
957
|
-
) as Stream,
|
|
958
|
-
),
|
|
966
|
+
const { claudeAdapter } = await import(
|
|
967
|
+
pathToFileURL(resolve(TMP_EMIT, "pi-extensions/lib/acp/backend-adapter.js")).href
|
|
959
968
|
);
|
|
960
|
-
|
|
969
|
+
const oldCapability = claudeAdapter.sealsTurnAccounting;
|
|
970
|
+
claudeAdapter.sealsTurnAccounting = false;
|
|
971
|
+
let msg: AssistantMessage;
|
|
972
|
+
try {
|
|
973
|
+
await collect(
|
|
974
|
+
backend.streamAcpTurn(claude, userCtx("fixture NONCE-1"), { sessionId: "usage-E" }, h.deps) as Stream,
|
|
975
|
+
);
|
|
976
|
+
msg = sealedMessage(
|
|
977
|
+
await collect(
|
|
978
|
+
backend.streamAcpTurn(
|
|
979
|
+
claude,
|
|
980
|
+
reuseCtx("fixture NONCE-1", "fixture NONCE-2"),
|
|
981
|
+
{ sessionId: "usage-E" },
|
|
982
|
+
h.deps,
|
|
983
|
+
) as Stream,
|
|
984
|
+
),
|
|
985
|
+
);
|
|
986
|
+
} finally {
|
|
987
|
+
claudeAdapter.sealsTurnAccounting = oldCapability;
|
|
988
|
+
}
|
|
989
|
+
assert.equal(h.children.length, 1, "both synthetic no-accounting turns ran on ONE reused child");
|
|
961
990
|
assert.deepEqual(
|
|
962
991
|
{
|
|
963
992
|
input: msg.usage.input,
|
|
@@ -1094,7 +1123,7 @@ console.log(
|
|
|
1094
1123
|
"a reused session; a turn with no cost notification attributes $0 while HOLDING the baseline so the session sum " +
|
|
1095
1124
|
"reproduces the backend's own cumulative ESTIMATE exactly (agreement with that carrier, never with a bill); a DECREASING total rebaselines, attributes $0 and says so to the operator with both numbers; " +
|
|
1096
1125
|
"totalTokens stays CONTEXT OCCUPANCY (verified through pi's own calculateContextTokens) and is carried forward " +
|
|
1097
|
-
"when a turn reports none;
|
|
1126
|
+
"when a turn reports none; an injected no-accounting capability fixture is sealed not at all; and a next turn " +
|
|
1098
1127
|
"started from INSIDE the previous turn's terminal event still reuses the one session, so the cost baseline " +
|
|
1099
1128
|
"survives the turn boundary",
|
|
1100
1129
|
);
|
|
@@ -705,12 +705,29 @@ ok(
|
|
|
705
705
|
"python3",
|
|
706
706
|
[nonDumpable, path.join(root, "node_modules", "@github", "copilot", "npm-loader.js")],
|
|
707
707
|
{
|
|
708
|
-
stdio: "ignore",
|
|
708
|
+
stdio: ["ignore", "ignore", "pipe"],
|
|
709
709
|
},
|
|
710
710
|
);
|
|
711
711
|
running.push(opaque);
|
|
712
712
|
try {
|
|
713
|
-
|
|
713
|
+
// Wait for the child's own "ready" (written after prctl succeeded), not a fixed delay: on a
|
|
714
|
+
// loaded host 400 ms was not enough for python to start and drop dumpability, so the
|
|
715
|
+
// environ was still readable and this cell failed for a timing reason (0.53.0 release gate).
|
|
716
|
+
await new Promise<void>((resolve, reject) => {
|
|
717
|
+
let seen = "";
|
|
718
|
+
const timer = setTimeout(() => reject(new Error("non-dumpable fixture never reported ready")), 15_000);
|
|
719
|
+
opaque.stderr?.on("data", (b: Buffer) => {
|
|
720
|
+
seen += b.toString();
|
|
721
|
+
if (seen.includes("ready")) {
|
|
722
|
+
clearTimeout(timer);
|
|
723
|
+
resolve();
|
|
724
|
+
}
|
|
725
|
+
});
|
|
726
|
+
opaque.once("exit", (code) => {
|
|
727
|
+
clearTimeout(timer);
|
|
728
|
+
reject(new Error(`non-dumpable fixture exited early (${code})`));
|
|
729
|
+
});
|
|
730
|
+
});
|
|
714
731
|
let environReadable = true;
|
|
715
732
|
try {
|
|
716
733
|
readFileSync(`/proc/${opaque.pid}/environ`);
|
|
@@ -150,7 +150,10 @@ async function main(): Promise<void> {
|
|
|
150
150
|
const tmuxRetired = await dispatchFreshCall(pi(" entwurf/claude-opus-5 "), {}, recordingSpawn().spawn);
|
|
151
151
|
const current = recordingSpawn();
|
|
152
152
|
const herdrCurrent = await dispatchFreshCall(pi("entwurf/claude-opus-5-5"), HERDR_ENV, current.spawn);
|
|
153
|
-
const
|
|
153
|
+
const cortexSpawn = recordingSpawn();
|
|
154
|
+
const cortex = await dispatchFreshCall(pi("entwurf/cortex-claude-sonnet-5"), {}, cortexSpawn.spawn);
|
|
155
|
+
const cortexHerdrSpawn = recordingSpawn();
|
|
156
|
+
const cortexHerdr = await dispatchFreshCall(pi("entwurf/cortex-auto"), HERDR_ENV, cortexHerdrSpawn.spawn);
|
|
154
157
|
const otherBackend = await dispatchFreshCall(
|
|
155
158
|
{ ...REQUEST, backend: "claude-code", model: "entwurf/claude-sonnet-5" },
|
|
156
159
|
{},
|
|
@@ -158,7 +161,7 @@ async function main(): Promise<void> {
|
|
|
158
161
|
);
|
|
159
162
|
const reasonOf = (d: Awaited<ReturnType<typeof dispatchFreshCall>>) => (d.result.ok ? null : d.result.reason);
|
|
160
163
|
ok(
|
|
161
|
-
"[QK:FCD-ACP-MODEL-EXACT] a pi sibling under the entwurf provider
|
|
164
|
+
"[QK:FCD-ACP-MODEL-EXACT] a pi sibling under the entwurf provider names an exact supported Claude id: retired Claude and Cortex ids refuse before launch, while a current Claude id passes and another backend is not judged",
|
|
162
165
|
herdrRetired.rail === "herdr" &&
|
|
163
166
|
reasonOf(herdrRetired) === "acp-model-not-curated" &&
|
|
164
167
|
herdrSpawn.calls.length === 0 &&
|
|
@@ -166,7 +169,10 @@ async function main(): Promise<void> {
|
|
|
166
169
|
reasonOf(tmuxRetired) === "acp-model-not-curated" &&
|
|
167
170
|
reasonOf(herdrCurrent) !== "acp-model-not-curated" &&
|
|
168
171
|
current.calls.length > 0 &&
|
|
169
|
-
reasonOf(cortex)
|
|
172
|
+
reasonOf(cortex) === "acp-model-not-curated" &&
|
|
173
|
+
cortexSpawn.calls.length === 0 &&
|
|
174
|
+
reasonOf(cortexHerdr) === "acp-model-not-curated" &&
|
|
175
|
+
cortexHerdrSpawn.calls.length === 0 &&
|
|
170
176
|
reasonOf(otherBackend) !== "acp-model-not-curated" &&
|
|
171
177
|
renderDispatchedFreshCall(herdrRetired).text.includes("exact curated ACP id") &&
|
|
172
178
|
renderDispatchedFreshCall(tmuxRetired).text.includes("exact curated ACP id"),
|
|
@@ -3435,13 +3435,13 @@ let manifestCount: number;
|
|
|
3435
3435
|
// Extend this inventory and the manifests TOGETHER, never silently.
|
|
3436
3436
|
const EXPECTED_LANE_MUTANTS: Record<string, number> = {
|
|
3437
3437
|
"acp-backend-preflight": 1,
|
|
3438
|
-
"acp-augment":
|
|
3439
|
-
"acp-cortex":
|
|
3440
|
-
"acp-foreground-only":
|
|
3438
|
+
"acp-augment": 8,
|
|
3439
|
+
"acp-cortex": 3,
|
|
3440
|
+
"acp-foreground-only": 2,
|
|
3441
3441
|
"acp-launch-namespace": 2,
|
|
3442
3442
|
"acp-overlay": 1,
|
|
3443
3443
|
"acp-prompt-lifecycle": 15,
|
|
3444
|
-
"acp-set-model-readback":
|
|
3444
|
+
"acp-set-model-readback": 44,
|
|
3445
3445
|
"acp-stop-reason": 7,
|
|
3446
3446
|
"acp-stream-hooks": 10,
|
|
3447
3447
|
"acp-usage-accounting": 12,
|
|
@@ -3500,7 +3500,7 @@ let manifestCount: number;
|
|
|
3500
3500
|
"meta-identity": 9,
|
|
3501
3501
|
"meta-retire": 4,
|
|
3502
3502
|
"mux-boundary": 16,
|
|
3503
|
-
"mux-fresh-call":
|
|
3503
|
+
"mux-fresh-call": 68,
|
|
3504
3504
|
"mux-launcher-fence": 8,
|
|
3505
3505
|
"mux-parent-artifact": 3,
|
|
3506
3506
|
"pack-install": 9,
|
|
@@ -413,6 +413,7 @@ async function main(): Promise<void> {
|
|
|
413
413
|
// bytes somebody wrote — while the framing gets its own claim about surviving the fold verbatim.
|
|
414
414
|
const FRAMING = composeFreshCallFraming({
|
|
415
415
|
backend: "claude-code",
|
|
416
|
+
model: "claude-sonnet-5-5",
|
|
416
417
|
callerGardenId: "20260101T010101-aaaaaa",
|
|
417
418
|
nonce: "mux-fresh-call-deadbeefdeadbeefdeadbeef",
|
|
418
419
|
openingLine: HERDR_FRESH_CALL_OPENING_LINE,
|
|
@@ -516,6 +516,12 @@ const operatorCmds = [...targets].filter(([cmd, ts]) => !isDevGate(cmd) && ts.le
|
|
|
516
516
|
l.replace(/^ {2}\.\/run\.sh (\S.*?)( +)# /, (_m, syn, gap) => ` entwurf ${syn}${gap} # `);
|
|
517
517
|
const toolCountWord = ({ 9: "nine" } as Record<number, string>)[EXPECTED_TOOLS.length];
|
|
518
518
|
const literalAmendments: { old: string; next: string; receipt: string }[] = [
|
|
519
|
+
{
|
|
520
|
+
old: "claude-native-resume; Cortex stays a documented on-demand direct call",
|
|
521
|
+
next: "claude-native-resume; Cortex ACP is disabled",
|
|
522
|
+
receipt:
|
|
523
|
+
"run.sh smoke_acp_cortex_live refuses with cortex-acp-disabled before a probe; the other MUST axes are unchanged",
|
|
524
|
+
},
|
|
519
525
|
{
|
|
520
526
|
old: "EXACTLY the eight shipped garden verbs",
|
|
521
527
|
next: `EXACTLY the ${toolCountWord} shipped garden verbs`,
|
|
@@ -1892,20 +1892,24 @@ function intervention(
|
|
|
1892
1892
|
// a mutant subject (§11-7-c), so kill-power here is the synthetic
|
|
1893
1893
|
// negatives, not a manifest entry.
|
|
1894
1894
|
{
|
|
1895
|
-
|
|
1895
|
+
// claude-agent-acp 0.103.0+ split the resolver out of the agent: `claudeCliPath` lives in
|
|
1896
|
+
// `dist/claude-cli.js` (`cliSrc`) and the query-options literal in `dist/v1/acp-agent.js`
|
|
1897
|
+
// (`acpSrc`), which now passes `await claudeCliPath()` alone — the env check moved inside
|
|
1898
|
+
// the resolver, so the verbatim-override premise is judged there.
|
|
1899
|
+
const inspect = (cliSrc: string, acpSrc: string, sdkSrc: string): string[] => {
|
|
1896
1900
|
const violations: string[] = [];
|
|
1897
|
-
if (!
|
|
1898
|
-
violations.push("claudeCliPath export missing from
|
|
1901
|
+
if (!cliSrc.includes("export async function claudeCliPath()")) {
|
|
1902
|
+
violations.push("claudeCliPath export missing from claude-cli.js");
|
|
1899
1903
|
}
|
|
1900
|
-
if (!/if \(process\.env\.CLAUDE_CODE_EXECUTABLE\) \{\s*return process\.env\.CLAUDE_CODE_EXECUTABLE;/.test(
|
|
1904
|
+
if (!/if \(process\.env\.CLAUDE_CODE_EXECUTABLE\) \{\s*return process\.env\.CLAUDE_CODE_EXECUTABLE;/.test(cliSrc)) {
|
|
1901
1905
|
violations.push(
|
|
1902
1906
|
"claudeCliPath no longer returns the ambient override VERBATIM — the refusal precondition's premise moved",
|
|
1903
1907
|
);
|
|
1904
1908
|
}
|
|
1905
|
-
const overrideLine = "pathToClaudeCodeExecutable:
|
|
1909
|
+
const overrideLine = "pathToClaudeCodeExecutable: await claudeCliPath(),";
|
|
1906
1910
|
const iOverride = acpSrc.indexOf(overrideLine);
|
|
1907
1911
|
if (iOverride === -1) {
|
|
1908
|
-
violations.push("the
|
|
1912
|
+
violations.push("the claudeCliPath resolution at the query options literal is gone");
|
|
1909
1913
|
} else {
|
|
1910
1914
|
const iSpread = acpSrc.indexOf("...userProvidedOptions,");
|
|
1911
1915
|
if (iSpread === -1 || iSpread > iOverride) {
|
|
@@ -1944,49 +1948,55 @@ function intervention(
|
|
|
1944
1948
|
|
|
1945
1949
|
// Synthetic fixtures FIRST — an inspector that cannot see the defect it
|
|
1946
1950
|
// exists for is no inspector.
|
|
1947
|
-
const
|
|
1951
|
+
const goodCli =
|
|
1948
1952
|
"export async function claudeCliPath() {\n" +
|
|
1949
|
-
" if (process.env.CLAUDE_CODE_EXECUTABLE) {\n return process.env.CLAUDE_CODE_EXECUTABLE;\n }\n}\n"
|
|
1953
|
+
" if (process.env.CLAUDE_CODE_EXECUTABLE) {\n return process.env.CLAUDE_CODE_EXECUTABLE;\n }\n}\n";
|
|
1954
|
+
const goodAcp =
|
|
1950
1955
|
"const options = {\n ...userProvidedOptions,\n" +
|
|
1951
|
-
" pathToClaudeCodeExecutable:
|
|
1956
|
+
" pathToClaudeCodeExecutable: await claudeCliPath(),\n};\n";
|
|
1952
1957
|
const goodSdk = `x=![".js",".mjs",".tsx",".ts",".jsx"].some((r)=>e.endsWith(r));y=Cs()?"bun":"node";spawnLocalProcess(e){let{command:t,args:r,cwd:n,env:o,signal:i}=e,s=cxe(t,r,{cwd:n,stdio:["pipe","pipe","pipe"],signal:i,env:o,windowsHide:!0})}`;
|
|
1953
|
-
assert.deepEqual(inspect(goodAcp, goodSdk), [], "inspector passes the correct synthetic fixture");
|
|
1958
|
+
assert.deepEqual(inspect(goodCli, goodAcp, goodSdk), [], "inspector passes the correct synthetic fixture");
|
|
1954
1959
|
const invertedAcp = goodAcp.replace(
|
|
1955
|
-
" ...userProvidedOptions,\n pathToClaudeCodeExecutable:
|
|
1956
|
-
" pathToClaudeCodeExecutable:
|
|
1960
|
+
" ...userProvidedOptions,\n pathToClaudeCodeExecutable: await claudeCliPath(),\n",
|
|
1961
|
+
" pathToClaudeCodeExecutable: await claudeCliPath(),\n ...userProvidedOptions,\n",
|
|
1957
1962
|
);
|
|
1958
1963
|
assert.ok(
|
|
1959
|
-
inspect(invertedAcp, goodSdk).some((v) => v.includes("key-order")),
|
|
1964
|
+
inspect(goodCli, invertedAcp, goodSdk).some((v) => v.includes("key-order")),
|
|
1960
1965
|
"inspector catches userProvidedOptions inverted to win over the explicit override key",
|
|
1961
1966
|
);
|
|
1962
1967
|
assert.ok(
|
|
1963
|
-
inspect(
|
|
1964
|
-
(
|
|
1965
|
-
|
|
1968
|
+
inspect(
|
|
1969
|
+
goodCli.replace("export async function claudeCliPath()", "async function claudeCliPath()"),
|
|
1970
|
+
goodAcp,
|
|
1971
|
+
goodSdk,
|
|
1972
|
+
).some((v) => v.includes("export missing")),
|
|
1966
1973
|
"inspector catches the claudeCliPath export disappearing",
|
|
1967
1974
|
);
|
|
1968
1975
|
assert.ok(
|
|
1969
|
-
inspect(goodAcp, goodSdk.replace('".mjs",', "")).some((v) => v.includes("discriminator")),
|
|
1976
|
+
inspect(goodCli, goodAcp, goodSdk.replace('".mjs",', "")).some((v) => v.includes("discriminator")),
|
|
1970
1977
|
"inspector catches the SDK suffix list drifting from the pinned SDK_SCRIPT_SUFFIXES",
|
|
1971
1978
|
);
|
|
1972
1979
|
// The no-shell negatives must be caught IN THE LEAF WINDOW, in both the
|
|
1973
1980
|
// minified and the plain spelling — the global one-spelling absence check
|
|
1974
1981
|
// this replaced passed them all.
|
|
1975
1982
|
assert.ok(
|
|
1976
|
-
inspect(goodAcp, goodSdk.replace("windowsHide:!0", "windowsHide:!0,shell:!0")).some((v) =>
|
|
1983
|
+
inspect(goodCli, goodAcp, goodSdk.replace("windowsHide:!0", "windowsHide:!0,shell:!0")).some((v) =>
|
|
1977
1984
|
v.includes("no-shell"),
|
|
1978
1985
|
) &&
|
|
1979
|
-
inspect(goodAcp, goodSdk.replace("windowsHide:!0", "windowsHide:!0,shell:true")).some((v) =>
|
|
1986
|
+
inspect(goodCli, goodAcp, goodSdk.replace("windowsHide:!0", "windowsHide:!0,shell:true")).some((v) =>
|
|
1980
1987
|
v.includes("no-shell"),
|
|
1981
1988
|
) &&
|
|
1982
|
-
inspect(goodAcp, goodSdk.replace("spawnLocalProcess(", "spawnElsewhere(")).some((v) =>
|
|
1989
|
+
inspect(goodCli, goodAcp, goodSdk.replace("spawnLocalProcess(", "spawnElsewhere(")).some((v) =>
|
|
1990
|
+
v.includes("anchor"),
|
|
1991
|
+
),
|
|
1983
1992
|
"inspector catches shell:!0 AND shell:true inside the spawn leaf, and the leaf disappearing entirely",
|
|
1984
1993
|
);
|
|
1985
1994
|
|
|
1986
1995
|
// Installed dists — the actual §11-7-c load-bearing assumptions.
|
|
1987
1996
|
const rootRequire = createRequire(resolve(REPO_ROOT, "package.json"));
|
|
1988
1997
|
const acpPkgJson = rootRequire.resolve("@agentclientprotocol/claude-agent-acp/package.json");
|
|
1989
|
-
const
|
|
1998
|
+
const cliDist = readFileSync(join(dirname(acpPkgJson), "dist", "claude-cli.js"), "utf8");
|
|
1999
|
+
const acpDist = readFileSync(join(dirname(acpPkgJson), "dist", "v1", "acp-agent.js"), "utf8");
|
|
1990
2000
|
const acpRequire = createRequire(acpPkgJson);
|
|
1991
2001
|
const sdkEntry = acpRequire.resolve("@anthropic-ai/claude-agent-sdk");
|
|
1992
2002
|
let sdkDist = readFileSync(sdkEntry, "utf8");
|
|
@@ -1995,9 +2005,9 @@ function intervention(
|
|
|
1995
2005
|
if (existsSync(sibling)) sdkDist = readFileSync(sibling, "utf8");
|
|
1996
2006
|
}
|
|
1997
2007
|
assert.deepEqual(
|
|
1998
|
-
inspect(acpDist, sdkDist),
|
|
2008
|
+
inspect(cliDist, acpDist, sdkDist),
|
|
1999
2009
|
[],
|
|
2000
|
-
"installed acp-agent.js + sdk.mjs still carry every §11-7-c launch-semantics assumption (claudeCliPath verbatim env, override key order, suffix discriminator, node|bun choice, piped no-shell spawn)",
|
|
2010
|
+
"installed claude-cli.js + v1/acp-agent.js + sdk.mjs still carry every §11-7-c launch-semantics assumption (claudeCliPath verbatim env, override key order, suffix discriminator, node|bun choice, piped no-shell spawn)",
|
|
2001
2011
|
);
|
|
2002
2012
|
}
|
|
2003
2013
|
|
|
@@ -2050,7 +2060,7 @@ function intervention(
|
|
|
2050
2060
|
);
|
|
2051
2061
|
assert.ok(
|
|
2052
2062
|
RUNNER_SRC.includes("CLI_TARGET = await resolveProbeCliTarget({") &&
|
|
2053
|
-
RUNNER_SRC.includes('"
|
|
2063
|
+
RUNNER_SRC.includes('join(dirname(pkgJson), "dist", "claude-cli.js")') &&
|
|
2054
2064
|
RUNNER_SRC.includes("`precondition-${err.reason}`"),
|
|
2055
2065
|
"the runner resolves the target through upstream claudeCliPath BEFORE any run, and a precondition refusal writes a NAMED classification on the artifact (not stderr alone)",
|
|
2056
2066
|
);
|
|
@@ -260,30 +260,18 @@ function runSubcommand(sub: string, env: Record<string, string | undefined>): {
|
|
|
260
260
|
// is the second skip surface: some smokes never reach their .ts file at all.
|
|
261
261
|
// ===========================================================================
|
|
262
262
|
{
|
|
263
|
-
const viaWrapper = runSubcommand("smoke-
|
|
263
|
+
const viaWrapper = runSubcommand("smoke-entwurf-v2-matrix-live", { LIVE: undefined });
|
|
264
264
|
assert.equal(
|
|
265
265
|
viaWrapper.code,
|
|
266
266
|
LIVE_SKIP_EXIT,
|
|
267
267
|
`[QK:WRAPPER-SKIP-IS-PROTOCOL-EXIT] a run.sh smoke WRAPPER that declines a prerequisite must return the protocol ` +
|
|
268
|
-
`SKIP code, not 0 — got exit ${viaWrapper.code}. The wrapper
|
|
269
|
-
"both decline before their .ts is ever reached), so fixing only the TypeScript " +
|
|
268
|
+
`SKIP code, not 0 — got exit ${viaWrapper.code}. The matrix wrapper declines before its .ts is reached, so fixing only the TypeScript ` +
|
|
270
269
|
`half would leave the aggregate counting those as PASS. Output: ${JSON.stringify(viaWrapper.output.slice(-300))}`,
|
|
271
270
|
);
|
|
272
271
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
const missingPrereq = runSubcommand("smoke-acp-cortex-live", {
|
|
277
|
-
LIVE: "1",
|
|
278
|
-
ENTWURF_ACP_CORTEX_CONNECTION: undefined,
|
|
279
|
-
});
|
|
280
|
-
assert.equal(
|
|
281
|
-
missingPrereq.code,
|
|
282
|
-
LIVE_SKIP_EXIT,
|
|
283
|
-
"LIVE=1 with no ENTWURF_ACP_CORTEX_CONNECTION must be a SKIP, not a PASS — this is the exact cell that made a " +
|
|
284
|
-
"cortex-less host look like cortex acceptance. Output: " +
|
|
285
|
-
JSON.stringify(missingPrereq.output.slice(-300)),
|
|
286
|
-
);
|
|
272
|
+
const disabled = runSubcommand("smoke-acp-cortex-live", { LIVE: undefined });
|
|
273
|
+
assert.equal(disabled.code, 2, "disabled Cortex smoke must refuse before a vendor probe");
|
|
274
|
+
assert.match(disabled.output, /cortex-acp-disabled/);
|
|
287
275
|
}
|
|
288
276
|
|
|
289
277
|
// ===========================================================================
|
|
@@ -312,11 +300,9 @@ function runSubcommand(sub: string, env: Record<string, string | undefined>): {
|
|
|
312
300
|
"smoke-agy-native-push-live": ["VERIFY.md", "Aggregate release-gate does not own an agy conversation id"],
|
|
313
301
|
"smoke-codex-native-push-live": ["VERIFY.md", "Aggregate release-gate does not own a loaded Codex thread"],
|
|
314
302
|
"smoke-acp-ordering-probe-live": ["docs/acp-backend-rail.md", "opt-in paired observation"],
|
|
315
|
-
// Cortex
|
|
316
|
-
//
|
|
317
|
-
|
|
318
|
-
// it without the connection still reports protocol SKIP rather than a pass.
|
|
319
|
-
"smoke-acp-cortex-live": ["VERIFY.md", "The release aggregate does not re-certify Cortex"],
|
|
303
|
+
// Cortex ACP is disabled. Its former direct LIVE requirement is historical;
|
|
304
|
+
// the retained command refuses by name without starting a vendor probe.
|
|
305
|
+
"smoke-acp-cortex-live": ["VERIFY.md", "Cortex ACP is disabled"],
|
|
320
306
|
// #116 C4: first evidence precedes promotion. The herdr rail's LIVE acceptance exists and
|
|
321
307
|
// runs on demand, but a rail whose first real acceptance run is days old has not earned a
|
|
322
308
|
// place in the aggregate that blocks every cut — and herdr is an OPTIONAL rail, so a cut
|
|
@@ -1066,7 +1052,7 @@ console.log(
|
|
|
1066
1052
|
"everything else→FAIL (never rounding a skip up), `--cut` refuses a MUST SKIP while a bare diagnostic run stays " +
|
|
1067
1053
|
"exit 0, no LIVE smoke still carries the pre-P1 exit-0 skip shape, and both real skip surfaces were INVOKED and " +
|
|
1068
1054
|
"observed to propagate the code: a smoke with LIVE unset (through run_ts, with its operator-readable marker) and " +
|
|
1069
|
-
"a run.sh wrapper declining its own prerequisite
|
|
1055
|
+
"a run.sh wrapper declining its own prerequisite; the retired Cortex smoke refuses by name; and every " +
|
|
1070
1056
|
"LIVE smoke is either wired into release_gate or excluded by a sentence the docs still carry; and the moved " +
|
|
1071
1057
|
"check-gate-qualification stays reachable only as the schedule/dispatch drift diagnostic (absent from the " +
|
|
1072
1058
|
"default chain, from release_gate, from every push and from the release skill; exactly once in CI) over the " +
|
|
@@ -92,8 +92,9 @@ export function hashFileSha256(path: string): string {
|
|
|
92
92
|
|
|
93
93
|
/** Refuse if the given env carries the upstream override — KEY PRESENCE is the
|
|
94
94
|
* predicate, an empty string included. Upstream consumers disagree about
|
|
95
|
-
* empty (`??` at acp-agent.js:4083
|
|
96
|
-
* truthy check treats it as unset), and
|
|
95
|
+
* empty (0.85.1's `??` at acp-agent.js:4083 treated "" as set and passed it
|
|
96
|
+
* on; 0.106.0's truthy check at dist/claude-cli.js:20 treats it as unset), and
|
|
97
|
+
* the probe refuses the ambiguity
|
|
97
98
|
* instead of picking a side. Used twice: on the runner's own process.env
|
|
98
99
|
* before resolving, and on the COMPOSED spawn env of every ACP child (adapter
|
|
99
100
|
* launch defaults / overlay overrides could inject what process.env did not
|
|
@@ -52,32 +52,6 @@
|
|
|
52
52
|
"signature": "[QK:CARRIER-FRAME-NAMES-CLAUDE-CARRIER]",
|
|
53
53
|
"signatureSource": "scripts/check-acp-carrier-augment.ts"
|
|
54
54
|
},
|
|
55
|
-
{
|
|
56
|
-
"claim": "CARRIER-FRAME-DENIES-CORTEX-CARRIER",
|
|
57
|
-
"title": "the carrier-less rail is told it has a tiny system-prompt carrier — a flat lie, since cortex's buildSessionMeta sends no _meta at all",
|
|
58
|
-
"subject": "pi-extensions/lib/acp/augment.ts",
|
|
59
|
-
"find": [
|
|
60
|
-
"\t\t\t\t\t`The ${params.backend} rail carries no system-prompt carrier at all — entwurf sends no \\`_meta.systemPrompt\\`, so everything entwurf tells you, including any operator engraving above, arrives here as user-message text.`,"
|
|
61
|
-
],
|
|
62
|
-
"replace": [
|
|
63
|
-
"\t\t\t\t\t`The ${params.backend} rail does have a system-prompt carrier (\\`_meta.systemPrompt\\`), and entwurf keeps it deliberately tiny: it carries the operator engraving only.`,"
|
|
64
|
-
],
|
|
65
|
-
"gate": ["bash", "run.sh", "check-acp-carrier-augment"],
|
|
66
|
-
"timeoutSeconds": 60,
|
|
67
|
-
"signature": "[QK:CARRIER-FRAME-DENIES-CORTEX-CARRIER]",
|
|
68
|
-
"signatureSource": "scripts/check-acp-carrier-augment.ts"
|
|
69
|
-
},
|
|
70
|
-
{
|
|
71
|
-
"claim": "CARRIER-RAIL-DIFF-IS-SOURCE-PINNED",
|
|
72
|
-
"title": "cortex grows a session carrier while the augment still tells it there is none — the frame becomes a lie and no prose check would notice",
|
|
73
|
-
"subject": "pi-extensions/lib/acp/backend-adapter.ts",
|
|
74
|
-
"find": ["\tbuildSessionMeta() {\n\t\treturn undefined;\n\t},"],
|
|
75
|
-
"replace": ["\tbuildSessionMeta() {\n\t\treturn { systemPrompt: \"x\" };\n\t},"],
|
|
76
|
-
"gate": ["bash", "run.sh", "check-acp-carrier-augment"],
|
|
77
|
-
"timeoutSeconds": 60,
|
|
78
|
-
"signature": "[QK:CARRIER-RAIL-DIFF-IS-SOURCE-PINNED]",
|
|
79
|
-
"signatureSource": "scripts/check-acp-carrier-augment.ts"
|
|
80
|
-
},
|
|
81
55
|
{
|
|
82
56
|
"claim": "CARRIER-LEADS-ITS-OWN-BLOCK",
|
|
83
57
|
"title": "the carrier loses its leading boundary — the Claude SDK's fixed sentence swallows the operator engraving again (the measured 2026-07-31 A-join)",
|