@junghanacs/entwurf 0.51.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.
Files changed (88) hide show
  1. package/AGENTS.md +1 -1
  2. package/BASELINE.md +116 -397
  3. package/CHANGELOG.md +191 -2
  4. package/CONTRIBUTING.md +4 -4
  5. package/DELIVERY.md +2 -2
  6. package/ENTWURF-CLI.json +1 -1
  7. package/ENTWURF.json +10 -4
  8. package/README.md +11 -8
  9. package/VERIFY.md +333 -143
  10. package/demo/README.md +1 -1
  11. package/demo/demo-baseline.sh +1 -1
  12. package/demo/demo.sh +1 -1
  13. package/docs/acp-backend-rail.md +248 -34
  14. package/docs/setup-clean-host.md +75 -80
  15. package/mcp/entwurf-bridge/dist/ENTWURF.json +10 -4
  16. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/acp-client.js +1 -1
  17. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/backend-adapter.js +90 -14
  18. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/config.js +4 -9
  19. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/context.js +15 -0
  20. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/model-ids.js +9 -12
  21. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/models.js +41 -56
  22. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/tool-surface.js +33 -6
  23. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-control-rpc.js +4 -1
  24. package/mcp/entwurf-bridge/dist/pi-extensions/lib/fresh-call-composition.js +34 -3
  25. package/mcp/entwurf-bridge/dist/pi-extensions/lib/herdr-fresh-call.js +3 -1
  26. package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-mailbox-body.js +1 -1
  27. package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +2 -1
  28. package/package.json +14 -14
  29. package/pi/pi-durable/carrier/harness-setup.js +1 -1
  30. package/pi/pi-durable/carrier/subagent.js +1 -1
  31. package/pi/pi-durable/overlay/upstream-pin.json +15 -15
  32. package/pi/settings.reference.json +0 -1
  33. package/pi-extensions/acp-provider.ts +12 -6
  34. package/pi-extensions/lib/acp/acp-client.ts +6 -7
  35. package/pi-extensions/lib/acp/backend-adapter.ts +139 -15
  36. package/pi-extensions/lib/acp/backend.ts +261 -32
  37. package/pi-extensions/lib/acp/config.ts +4 -9
  38. package/pi-extensions/lib/acp/context.ts +17 -0
  39. package/pi-extensions/lib/acp/event-mapper.ts +236 -17
  40. package/pi-extensions/lib/acp/model-ids.js +9 -12
  41. package/pi-extensions/lib/acp/models.ts +46 -57
  42. package/pi-extensions/lib/acp/tool-surface.ts +34 -6
  43. package/pi-extensions/lib/entwurf-control-rpc.ts +5 -1
  44. package/pi-extensions/lib/fresh-call-composition.ts +37 -3
  45. package/pi-extensions/lib/herdr-fresh-call.ts +3 -1
  46. package/pi-extensions/lib/meta-mailbox-body.ts +1 -1
  47. package/pi-extensions/lib/mux-fresh-call.ts +3 -1
  48. package/pi-extensions/meta-bridge-pi-durable.ts +1 -1
  49. package/run.sh +62 -120
  50. package/scripts/check-acp-carrier-augment.ts +3 -21
  51. package/scripts/check-acp-prompt-lifecycle.ts +24 -3
  52. package/scripts/check-acp-provider-surface.ts +17 -26
  53. package/scripts/check-acp-session-reuse.ts +23 -4
  54. package/scripts/check-acp-stop-reason.ts +22 -3
  55. package/scripts/check-acp-stream-hooks.ts +24 -3
  56. package/scripts/check-acp-usage-accounting.ts +53 -24
  57. package/scripts/check-copilot-receive-arm.ts +19 -2
  58. package/scripts/check-entwurf-v2-surface.ts +7 -21
  59. package/scripts/check-fresh-call-dispatch.ts +9 -3
  60. package/scripts/check-gate-qualification.ts +6 -6
  61. package/scripts/check-herdr-fresh-call.ts +1 -0
  62. package/scripts/check-install-surface.ts +6 -0
  63. package/scripts/check-pi-durable-send.ts +4 -1
  64. package/scripts/check-probe-ordering.ts +34 -24
  65. package/scripts/check-release-gate-outcomes.ts +9 -23
  66. package/scripts/lib/codex-fresh-source-receipts.ts +12 -6
  67. package/scripts/lib/probe-cli-target.ts +3 -2
  68. package/scripts/mutants/acp-augment.json +0 -26
  69. package/scripts/mutants/acp-cortex.json +10 -183
  70. package/scripts/mutants/acp-foreground-only.json +11 -0
  71. package/scripts/mutants/acp-launch-namespace.json +3 -1
  72. package/scripts/mutants/acp-prompt-lifecycle.json +2 -2
  73. package/scripts/mutants/acp-set-model-readback.json +488 -0
  74. package/scripts/mutants/agent-guidance.json +2 -2
  75. package/scripts/mutants/codex-native.json +2 -2
  76. package/scripts/mutants/control-send-receipt.json +47 -7
  77. package/scripts/mutants/mux-fresh-call.json +12 -1
  78. package/scripts/mutants/pack-install.json +4 -4
  79. package/scripts/mutants/pi-floor.json +8 -10
  80. package/scripts/mutants/release-gate.json +3 -3
  81. package/scripts/mutants/v2-surface.json +4 -6
  82. package/scripts/raw-acp-compaction-measure/probe.ts +8 -2
  83. package/scripts/smoke-acp-cortex-live.ts +4 -408
  84. package/scripts/smoke-acp-ordering-probe-live.ts +7 -1
  85. package/scripts/smoke-acp-session-reuse-live.ts +11 -2
  86. package/scripts/smoke-acp-v2-send-live.ts +1 -1
  87. package/scripts/smoke-herdr-raw-install-live.sh +4 -4
  88. package/scripts/check-acp-cortex.ts +0 -710
@@ -52,8 +52,22 @@ function tctx(messages: Context["messages"]): TranscriptContext {
52
52
  return normalizeContext({ tools: PI_DECLARED_TOOLS, messages });
53
53
  }
54
54
 
55
- const sonnet = { id: "claude-sonnet-5-5" } as unknown as Model<Api>;
56
- const opus = { id: "claude-opus-5-5" } as unknown as Model<Api>;
55
+ // #142: a Claude turn carries pi's thinking level, which the backend sets as effort and reads back
56
+ // before the prompt. This fixture runs every turn at one fixed level on rows carrying the curated
57
+ // level map, and its fake driver reads back whatever config option it was asked to set. The effort
58
+ // contract itself is certified in pi-extensions/lib/acp/backend.thinking.test.ts.
59
+ const FIXTURE_THINKING_LEVEL = "medium";
60
+ const FIXTURE_THINKING_LEVEL_MAP = {
61
+ off: null,
62
+ minimal: null,
63
+ low: "low",
64
+ medium: "medium",
65
+ high: "high",
66
+ xhigh: "xhigh",
67
+ max: "max",
68
+ };
69
+ const sonnet = { id: "claude-sonnet-5-5", thinkingLevelMap: FIXTURE_THINKING_LEVEL_MAP } as unknown as Model<Api>;
70
+ const opus = { id: "claude-opus-5-5", thinkingLevelMap: FIXTURE_THINKING_LEVEL_MAP } as unknown as Model<Api>;
57
71
 
58
72
  type Stream = AsyncIterable<AssistantMessageEvent> & {
59
73
  result: () => Promise<{ stopReason: string; errorMessage?: string }>;
@@ -155,7 +169,7 @@ function makeHarness(recordDir: string, config: any = DEFAULT_RESOLVED_CONFIG, e
155
169
  newSessionSeq++;
156
170
  return { sessionId: `ACP-${newSessionSeq}` };
157
171
  },
158
- setSessionConfigOption: async (p: any) => ({ configOptions: [{ id: "model", currentValue: p.value }] }),
172
+ setSessionConfigOption: async (p: any) => ({ configOptions: [{ id: p.configId, currentValue: p.value }] }),
159
173
  prompt: async ({ sessionId, prompt }: any) => {
160
174
  promptCalls.push({ sessionId, text: prompt.map((b: { text: string }) => b.text).join("\n") });
161
175
  if (block) await block;
@@ -282,7 +296,12 @@ try {
282
296
  copyFileSync("pi-extensions/lib/acp/prompts/engraving.md", resolve(promptsOut, "engraving.md"));
283
297
  const backendUrl = pathToFileURL(resolve(TMP_EMIT, "pi-extensions/lib/acp/backend.js")).href;
284
298
  const storeUrl = pathToFileURL(resolve(TMP_EMIT, "pi-extensions/lib/acp/session-store.js")).href;
285
- const backend = (await import(backendUrl)) as any;
299
+ const backendModule = (await import(backendUrl)) as any;
300
+ const backend = {
301
+ ...backendModule,
302
+ streamAcpTurn: (model: unknown, context: unknown, options: Record<string, unknown> | undefined, deps: unknown) =>
303
+ backendModule.streamAcpTurn(model, context, { reasoning: FIXTURE_THINKING_LEVEL, ...options }, deps),
304
+ };
286
305
  await assertOverlayInSandbox(TMP_EMIT, gateHome);
287
306
  const store = (await import(storeUrl)) as any;
288
307
  const configUrl = pathToFileURL(resolve(TMP_EMIT, "pi-extensions/lib/acp/config.js")).href;
@@ -56,7 +56,21 @@ import type { Api, AssistantMessageEvent, Model } from "@earendil-works/pi-ai";
56
56
  import { normalizeContext } from "@earendil-works/pi-ai";
57
57
  import { assertOverlayInSandbox, sandboxAcpGateHome } from "./lib/acp-gate-home.ts";
58
58
 
59
- const sonnet = { id: "claude-sonnet-5-5" } as unknown as Model<Api>;
59
+ // #142: a Claude turn carries pi's thinking level, which the backend sets as effort and reads back
60
+ // before the prompt. This fixture runs every turn at one fixed level on rows carrying the curated
61
+ // level map, and its fake driver reads back whatever config option it was asked to set. The effort
62
+ // contract itself is certified in pi-extensions/lib/acp/backend.thinking.test.ts.
63
+ const FIXTURE_THINKING_LEVEL = "medium";
64
+ const FIXTURE_THINKING_LEVEL_MAP = {
65
+ off: null,
66
+ minimal: null,
67
+ low: "low",
68
+ medium: "medium",
69
+ high: "high",
70
+ xhigh: "xhigh",
71
+ max: "max",
72
+ };
73
+ const sonnet = { id: "claude-sonnet-5-5", thinkingLevelMap: FIXTURE_THINKING_LEVEL_MAP } as unknown as Model<Api>;
60
74
 
61
75
  type Stream = AsyncIterable<AssistantMessageEvent> & {
62
76
  result: () => Promise<{ stopReason: string; errorMessage?: string }>;
@@ -123,7 +137,7 @@ function makeHarness(recordDir: string, wireReason: string | null) {
123
137
  createConnection: (_child: any, handlers: any) => ({
124
138
  initialize: async () => ({ agentCapabilities: {} }),
125
139
  newSession: async () => ({ sessionId: "ACP-1" }),
126
- setSessionConfigOption: async (p: any) => ({ configOptions: [{ id: "model", currentValue: p.value }] }),
140
+ setSessionConfigOption: async (p: any) => ({ configOptions: [{ id: p.configId, currentValue: p.value }] }),
127
141
  prompt: async ({ sessionId }: any) => {
128
142
  await handlers.sessionUpdate({
129
143
  update: { sessionUpdate: "agent_message_chunk", content: { type: "text", text: "PARTIAL" } },
@@ -249,7 +263,12 @@ try {
249
263
 
250
264
  const backendUrl = pathToFileURL(resolve(TMP_EMIT, "pi-extensions/lib/acp/backend.js")).href;
251
265
  const eventUrl = pathToFileURL(resolve(TMP_EMIT, "pi-extensions/lib/acp/event-mapper.js")).href;
252
- const backend = (await import(backendUrl)) as any;
266
+ const backendModule = (await import(backendUrl)) as any;
267
+ const backend = {
268
+ ...backendModule,
269
+ streamAcpTurn: (model: unknown, context: unknown, options: Record<string, unknown> | undefined, deps: unknown) =>
270
+ backendModule.streamAcpTurn(model, context, { reasoning: FIXTURE_THINKING_LEVEL, ...options }, deps),
271
+ };
253
272
  await assertOverlayInSandbox(TMP_EMIT, gateHome);
254
273
  const eventMod = (await import(eventUrl)) as any;
255
274
 
@@ -36,7 +36,21 @@ import type { Api, AssistantMessageEvent, Message, Model, TranscriptContext } fr
36
36
  import { normalizeContext } from "@earendil-works/pi-ai";
37
37
  import { assertOverlayInSandbox, sandboxAcpGateHome } from "./lib/acp-gate-home.ts";
38
38
 
39
- const sonnet = { id: "claude-sonnet-5-5" } as unknown as Model<Api>;
39
+ // #142: a Claude turn carries pi's thinking level, which the backend sets as effort and reads back
40
+ // before the prompt. This fixture runs every turn at one fixed level on rows carrying the curated
41
+ // level map, and its fake driver reads back whatever config option it was asked to set. The effort
42
+ // contract itself is certified in pi-extensions/lib/acp/backend.thinking.test.ts.
43
+ const FIXTURE_THINKING_LEVEL = "medium";
44
+ const FIXTURE_THINKING_LEVEL_MAP = {
45
+ off: null,
46
+ minimal: null,
47
+ low: "low",
48
+ medium: "medium",
49
+ high: "high",
50
+ xhigh: "xhigh",
51
+ max: "max",
52
+ };
53
+ const sonnet = { id: "claude-sonnet-5-5", thinkingLevelMap: FIXTURE_THINKING_LEVEL_MAP } as unknown as Model<Api>;
40
54
 
41
55
  type Stream = AsyncIterable<AssistantMessageEvent>;
42
56
 
@@ -94,7 +108,7 @@ function makeHarness(recordDir: string) {
94
108
  const makeConnection = (_handlers: any) => ({
95
109
  initialize: async () => ({ agentCapabilities: {} }),
96
110
  newSession: async () => ({ sessionId: "ACP-1" }),
97
- setSessionConfigOption: async (p: any) => ({ configOptions: [{ id: "model", currentValue: p.value }] }),
111
+ setSessionConfigOption: async (p: any) => ({ configOptions: [{ id: p.configId, currentValue: p.value }] }),
98
112
  prompt: async (params: any) => {
99
113
  // Deep-cloned: the capture must be the bytes that crossed the seam, not a
100
114
  // live reference a later mutation could rewrite.
@@ -195,7 +209,14 @@ try {
195
209
  const promptsOut = resolve(TMP_EMIT, "pi-extensions/lib/acp/prompts");
196
210
  mkdirSync(promptsOut, { recursive: true });
197
211
  copyFileSync("pi-extensions/lib/acp/prompts/engraving.md", resolve(promptsOut, "engraving.md"));
198
- const backend = (await import(pathToFileURL(resolve(TMP_EMIT, "pi-extensions/lib/acp/backend.js")).href)) as any;
212
+ const backendModule = (await import(
213
+ pathToFileURL(resolve(TMP_EMIT, "pi-extensions/lib/acp/backend.js")).href
214
+ )) as any;
215
+ const backend = {
216
+ ...backendModule,
217
+ streamAcpTurn: (model: unknown, context: unknown, options: Record<string, unknown> | undefined, deps: unknown) =>
218
+ backendModule.streamAcpTurn(model, context, { reasoning: FIXTURE_THINKING_LEVEL, ...options }, deps),
219
+ };
199
220
  await assertOverlayInSandbox(TMP_EMIT, gateHome);
200
221
 
201
222
  // ----------------------------------------------------------------------
@@ -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: cortex's emitted usage must be byte-identical to pre-#93.
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
- const claude = { id: "claude-sonnet-5-5" } as unknown as Model<Api>;
66
- const cortex = { id: "cortex-auto" } as unknown as Model<Api>;
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: "model", currentValue: p.value }] }),
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 backend = (await import(pathToFileURL(resolve(TMP_EMIT, "pi-extensions/lib/acp/backend.js")).href)) as any;
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
- // backend.ts / event-mapper.ts are COMMON layer: cortex drives the same turn
919
- // loop and the same mapper. Nobody has measured what cortex's ACP `usage`
920
- // means (turn delta or session total? against which price table?), and ACP's
921
- // own type is self-contradictory about it — the outer comment says "for this
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
- await collect(backend.streamAcpTurn(cortex, userCtx("cortex NONCE-1"), { sessionId: "usage-E" }, h.deps) as Stream);
950
- const msg = sealedMessage(
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
- assert.equal(h.children.length, 1, "both cortex turns ran on ONE reused child");
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; a backend that declares no measured semantics — cortex — is sealed not at all; and a next turn " +
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
- await sleep(400);
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`);
@@ -92,7 +92,7 @@ function assertRailSemantics(tag: string, longDescRaw: string, intentDescRaw: st
92
92
  );
93
93
  ok(
94
94
  `${tag} — long description names the THIRD result for an unsupported citizen`,
95
- /mailbox-undeliverable/.test(longDesc) && /THIRD RESULT/.test(longDesc),
95
+ /mailbox-undeliverable/.test(longDesc) && /target without a live socket, the THIRD RESULT/.test(longDesc),
96
96
  );
97
97
  ok(
98
98
  `${tag} — intent param names a rail's reject, not only its allow cell`,
@@ -139,30 +139,16 @@ function assertRailSemantics(tag: string, longDescRaw: string, intentDescRaw: st
139
139
  * whose subject (two distinct OWNED rejects) left with the `owned-outcome` intent.
140
140
  *
141
141
  * After the visible-first cut a dormant socket-domain citizen rejects delivery by v2, and
142
- * these four strings are the only place a caller learns that BEFORE spending a dispatch. So each
143
- * one must, on its own:
144
- * - name the reason it will actually get back (`dormant-fire-forget-unsupported`);
145
- * - name the intent that used to answer there (`owned-outcome`) — silently deleting it leaves
146
- * an operator who read last week's docs with no way to find out what happened;
147
- * - say it was `withdrawn`, ADJACENTLY to that name. Presence alone is not enough: naming a
148
- * retired intent without the retirement verb beside it reads as an offer, which is exactly
149
- * the "advertised as selectable" failure this claim exists to forbid;
150
- * - carry NEITHER retired advertisement form (`owned-outcome = …` enum styling, or the old
151
- * "DORMANT socket-domain citizen … never auto-converted" sentence that described picking it).
152
- *
153
- * ORDER IS LOAD-BEARING, and not for readability. `ok` throws on the FIRST failure, and the kill
154
- * signature is read off the failing line — so the `[QK:…]` label must sit on the assertion this
155
- * claim's mutant reaches FIRST. That mutant drops the reason AND the withdrawal in one edit; with
156
- * the reason check leading, an UNLABELLED line would throw first and the kill would read as
157
- * WRONG-REASON. The adjacency check therefore goes first and carries the only token; the other two
158
- * run unlabelled behind it — still real assertions, just not this claim's signature.
142
+ * both caller-facing strings must independently name the actual dormant rejection and say that
143
+ * delivery never launches or resumes a process. Neither may advertise the retired offer. History
144
+ * belongs in the issue and literal amendment, not in the current caller contract. The labelled
145
+ * assertion comes first because qualification reads the first failing line as the kill signature.
159
146
  */
160
147
  function assertDormantHonesty(tag: string, rawText: string): void {
161
148
  const text = rawText;
162
- const at = text.indexOf("owned-outcome");
163
149
  ok(
164
- `${tag} — names the withdrawn intent owned-outcome and says "withdrawn" beside it (not as an offer) [QK:V2SURF-DORMANT-HONESTY]`,
165
- at >= 0 && /withdrawn/.test(text.slice(at, at + 200)),
150
+ `${tag} — delivery never launches or resumes a process and names the dormant rejection [QK:V2SURF-DORMANT-HONESTY]`,
151
+ /This delivery verb never launches or resumes a process/.test(text) && /dormant-fire-forget-unsupported/.test(text),
166
152
  );
167
153
  ok(`${tag} — names the reason a dormant target actually returns`, /dormant-fire-forget-unsupported/.test(text));
168
154
  ok(
@@ -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 cortex = await dispatchFreshCall(pi("entwurf/cortex-claude-sonnet-5"), {}, recordingSpawn().spawn);
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 must name an EXACT curated ACP id: a retired Claude 5 id refuses as acp-model-not-curated on BOTH rails (trimmed like the rails read it) before herdr is asked anything, while the 5.5 id and a Cortex id pass through and another backend is not judged — Pi would otherwise open the retired id on a Cortex row by substring",
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) !== "acp-model-not-curated" &&
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": 10,
3439
- "acp-cortex": 14,
3440
- "acp-foreground-only": 1,
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": 2,
3444
+ "acp-set-model-readback": 44,
3445
3445
  "acp-stop-reason": 7,
3446
3446
  "acp-stream-hooks": 10,
3447
3447
  "acp-usage-accounting": 12,
@@ -3460,7 +3460,7 @@ let manifestCount: number;
3460
3460
  "pi-retire-request": 6,
3461
3461
  "retirement-event": 16,
3462
3462
  "control-retire": 9,
3463
- "control-send-receipt": 20,
3463
+ "control-send-receipt": 23,
3464
3464
  "codex-native": 95,
3465
3465
  "native-push-paused": 1,
3466
3466
  "chain-terminus-publication": 1,
@@ -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": 67,
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`,
@@ -900,7 +900,10 @@ async function cell(): Promise<void> {
900
900
  hostReceiver.ownerKind === "pi-durable-host";
901
901
  ok(
902
902
  `[QK:${QK}-SENDER-REPLYABILITY] the host's receiver marker is armed and owned by the spawned host, and the landed body says what that predicts: replyable (armed ${hostArmed})`,
903
- hostArmed && body.includes(`session: ${hostGid} (meta-session, replyable — reply via entwurf_v2`),
903
+ hostArmed &&
904
+ body.includes(
905
+ `session: ${hostGid} (meta-session, replyable — when a reply is requested, send the result via entwurf_v2`,
906
+ ),
904
907
  );
905
908
  const hostRecords = fs
906
909
  .readdirSync(dirs.sessions)
@@ -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
- const inspect = (acpSrc: string, sdkSrc: string): string[] => {
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 (!acpSrc.includes("export async function claudeCliPath()")) {
1898
- violations.push("claudeCliPath export missing from acp-agent.js");
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(acpSrc)) {
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: process.env.CLAUDE_CODE_EXECUTABLE ?? (await claudeCliPath())";
1909
+ const overrideLine = "pathToClaudeCodeExecutable: await claudeCliPath(),";
1906
1910
  const iOverride = acpSrc.indexOf(overrideLine);
1907
1911
  if (iOverride === -1) {
1908
- violations.push("the env-??-claudeCliPath resolution at the query options literal is gone");
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 goodAcp =
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: process.env.CLAUDE_CODE_EXECUTABLE ?? (await claudeCliPath()),\n};\n";
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: process.env.CLAUDE_CODE_EXECUTABLE ?? (await claudeCliPath()),\n",
1956
- " pathToClaudeCodeExecutable: process.env.CLAUDE_CODE_EXECUTABLE ?? (await claudeCliPath()),\n ...userProvidedOptions,\n",
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(goodAcp.replace("export async function claudeCliPath()", "async function claudeCliPath()"), goodSdk).some(
1964
- (v) => v.includes("export missing"),
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) => v.includes("anchor")),
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 acpDist = readFileSync(join(dirname(acpPkgJson), "dist", "acp-agent.js"), "utf8");
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('"@agentclientprotocol/claude-agent-acp/dist/acp-agent.js"') &&
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
  );