@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
@@ -23,6 +23,7 @@
23
23
  */
24
24
 
25
25
  import { randomBytes } from "node:crypto";
26
+ import { PROVIDER_ID, SUPPORTED_ANTHROPIC_MODEL_IDS } from "./acp/model-ids.js";
26
27
 
27
28
  /** The backends this rail can open. Fixed set, not a profile — a further one is a decision,
28
29
  * not a config entry. `copilot` was added by #82 RAIL 9, `omp` by #87 Bundle C, `codex`
@@ -150,6 +151,34 @@ export const FRESH_CALL_TOOL_LOAD_HINT: Record<FreshCallBackend, readonly string
150
151
  "pi-durable": [],
151
152
  };
152
153
 
154
+ /**
155
+ * Which dialect table spells the bridge tools for THIS launch. The backend picks the runtime; the
156
+ * model can pick a different tool surface underneath it.
157
+ *
158
+ * `[측정 2026-11-13, D1]` a pi sibling opened on an ACP Claude model (`entwurf/claude-*`) does not
159
+ * see Pi's MCP names: its turns run inside the Claude Agent SDK, which mints the bridge tools as
160
+ * `mcp__entwurf-bridge__*` (the ACP Sonnet interview of 2026-11-13, n=1, called them by the schema
161
+ * it was given, not by the pi spelling its framing named). So the NAMES follow the claude-code
162
+ * table. The load hint stays the pi row (none): in that n=1 the bridge tools were directly callable
163
+ * without loading. Whether ToolSearch exists on this surface, or deferral can occur with more
164
+ * declared servers (`acp/tool-surface.ts` names the SDK's deferred tools), is unmeasured — not a
165
+ * claim that it cannot.
166
+ *
167
+ * Only an exact curated Anthropic id switches. A thinking suffix (`:high`) or fuzzy alias never
168
+ * reaches here through the public verb: the dispatch root refuses any `entwurf/` id that is not
169
+ * exactly curated (`isUncuratedEntwurfPiModel`, #128), and the test pins that alignment. Retired
170
+ * Cortex ids are refused at the dispatch root before framing. The ACP id list arrives from
171
+ * `acp/model-ids.js`, an import-free module.
172
+ */
173
+ export function freshCallToolDialect(backend: FreshCallBackend, model: string): FreshCallBackend {
174
+ if (backend !== "pi") return backend;
175
+ const trimmed = model.trim();
176
+ const slash = trimmed.indexOf("/");
177
+ if (slash < 0 || trimmed.slice(0, slash) !== PROVIDER_ID) return backend;
178
+ const id = trimmed.slice(slash + 1);
179
+ return (SUPPORTED_ANTHROPIC_MODEL_IDS as readonly string[]).includes(id) ? "claude-code" : backend;
180
+ }
181
+
153
182
  /**
154
183
  * What a launch has to say, in the two shapes the six backends need. pi, claude-code, copilot
155
184
  * and codex are handed the first-turn PROMPT as an argument; pi-durable is handed that same
@@ -413,15 +442,19 @@ export function composeBackendArgs(
413
442
  */
414
443
  export function composeFreshCallFraming(params: {
415
444
  backend: FreshCallBackend;
445
+ /** The launch model, as the caller wrote it. It chooses the tool-name dialect
446
+ * (`freshCallToolDialect`) and is never echoed into the framing. */
447
+ model: string;
416
448
  callerGardenId: string;
417
449
  nonce: string;
418
450
  /** The rail's own first sentence — see `PLACEMENT SENTENCE` above. Empty is a crash, not a
419
451
  * default: a sibling told nothing about where it is would be told something false by silence. */
420
452
  openingLine: string;
421
453
  }): string[] {
422
- const birthTool = FRESH_CALL_CALLBACK_TOOL[params.backend];
423
- const deliveryTool = FRESH_CALL_DELIVERY_TOOL[params.backend];
424
- const peersTool = FRESH_CALL_PEERS_TOOL[params.backend];
454
+ const dialect = freshCallToolDialect(params.backend, params.model);
455
+ const birthTool = FRESH_CALL_CALLBACK_TOOL[dialect];
456
+ const deliveryTool = FRESH_CALL_DELIVERY_TOOL[dialect];
457
+ const peersTool = FRESH_CALL_PEERS_TOOL[dialect];
425
458
  // The hint is a per-backend TEMPLATE so the two tool dialects stay the single source of their
426
459
  // own spelling here too — a hint that hard-coded Claude's name would go stale the day a dialect
427
460
  // moves, and it would go stale silently, in the one sentence a stuck child depends on.
@@ -490,6 +523,7 @@ export function composeFreshCallFraming(params: {
490
523
  */
491
524
  export function composeFreshCallPrompt(params: {
492
525
  backend: FreshCallBackend;
526
+ model: string;
493
527
  task: string;
494
528
  callerGardenId: string;
495
529
  nonce: string;
@@ -962,6 +962,7 @@ export async function herdrFreshCall(
962
962
 
963
963
  const framing = composeFreshCallFraming({
964
964
  backend,
965
+ model,
965
966
  callerGardenId,
966
967
  nonce,
967
968
  openingLine: HERDR_FRESH_CALL_OPENING_LINE,
@@ -1796,6 +1797,7 @@ async function herdrLayoutFreshCall(
1796
1797
 
1797
1798
  const prompt = composeFreshCallPrompt({
1798
1799
  backend: "pi-durable",
1800
+ model,
1799
1801
  task,
1800
1802
  callerGardenId,
1801
1803
  nonce,
@@ -1876,7 +1878,7 @@ export const HERDR_REJECT_HINT: Record<
1876
1878
  // #128: decided at the dispatch root (`fresh-call-dispatch.ts`), worded here because this rail's
1877
1879
  // import fence admits no ACP module. The curated list itself lives in `acp/model-ids.js`.
1878
1880
  "acp-model-not-curated":
1879
- "an entwurf model must be one exact curated ACP id — Pi would otherwise pick another row by substring (a retired claude-sonnet-5 lands on cortex-claude-sonnet-5), so nothing was opened and nothing was aliased",
1881
+ "an entwurf model must be one exact curated ACP id; Cortex ACP is disabled. Pi may otherwise pick another row by substring, so nothing was opened or aliased",
1880
1882
  "task-empty": "a sibling opened with nothing to do is a window nobody asked for",
1881
1883
  "task-too-long": `the task is over the ${TASK_MAX_CHARS}-character interface bound this verb shares with entwurf_v2`,
1882
1884
  "herdr-context-missing":
@@ -79,7 +79,7 @@ export function formatMetaMailboxBody(sender: MailboxSenderEnvelope, message: st
79
79
  // SENDING host, so it is neither abbreviated against this host's HOME nor shown as a local path.
80
80
  const remote = !replyable ? sender.remote : undefined;
81
81
  const sessionLine = replyable
82
- ? `${sender.sessionId} (${kind}replyable — reply via entwurf_v2 to this sessionId, intent=fire-and-forget)`
82
+ ? `${sender.sessionId} (${kind}replyable — when a reply is requested, send the result via entwurf_v2 (intent=fire-and-forget) to the requested recipient, defaulting to this sessionId; a local final is not delivery. Honor routing and no-send instructions; an absent or false wants_reply does not cancel a request. A replyable address alone needs no acknowledgment)`
83
83
  : remote
84
84
  ? `${sender.sessionId} (remote via ${remote.via}; ${remote.replyRoute === "declared" ? "reply route declared, not probed" : "no reply route declared"})`
85
85
  : isMeta
@@ -214,6 +214,7 @@ export function buildBackendArgs(
214
214
  /** The first turn for THIS rail: the neutral framing under this rail's placement sentence. */
215
215
  export function buildFreshCallPrompt(params: {
216
216
  backend: FreshCallBackend;
217
+ model: string;
217
218
  task: string;
218
219
  callerGardenId: string;
219
220
  nonce: string;
@@ -510,7 +511,7 @@ export function freshCall(
510
511
  const runtime = layout === null ? null : checkPiDurableRuntime(layout);
511
512
  const missing = piDurableFreshPreflight({
512
513
  model,
513
- firstInput: buildFreshCallPrompt({ backend: params.backend, task, callerGardenId, nonce }),
514
+ firstInput: buildFreshCallPrompt({ backend: params.backend, model, task, callerGardenId, nonce }),
514
515
  layout,
515
516
  runtime: runtime === null || runtime.ok ? null : runtime.reason,
516
517
  });
@@ -549,6 +550,7 @@ export function freshCall(
549
550
  const composition: FreshCallComposition = {
550
551
  prompt: buildFreshCallPrompt({
551
552
  backend: params.backend,
553
+ model,
552
554
  task,
553
555
  callerGardenId,
554
556
  nonce,
@@ -349,7 +349,7 @@ export interface PiDurableRoots {
349
349
  // ---------------------------------------------------------------------------
350
350
  // Durable surface, typed NARROWLY and locally — read at pi-durable 1.0.2 @cd32f77 (#129,
351
351
  // `harness/types.ts`; historical coordinates, not re-anchored here). The current supply is the
352
- // carrier emitted from Entwurf's owned source (pi/pi-durable/src, #141) against the 1.5.0 SDK set
352
+ // carrier emitted from Entwurf's owned source (pi/pi-durable/src, #141) against the 1.5.2 SDK set
353
353
  // `pi/pi-durable/overlay/upstream-pin.json` names (origin `abe508e`). `[read 2026-10-13]` at
354
354
  // v1.2.0 one shape typed below moved: `ToolExecutionResult.content` is now `output`, beside the new
355
355
  // `structuredOutput` (`harness/types.ts`, its CHANGELOG's breaking changes). An omitted `output`
package/run.sh CHANGED
@@ -1156,8 +1156,10 @@ check_control_send_receipt() {
1156
1156
  # test titles `run_vitest` emits. Narrow file filter, same reason as check-entwurf-v2-send.
1157
1157
  # It stays inside check:contracts even though the discovery door also runs this file — see
1158
1158
  # check-release-gate-outcomes: a gate a mutant names must be reachable outside qualification.
1159
- section "control-socket acceptance boundary (mutant execution coordinate)"
1160
- run_vitest pi-extensions/lib/control-send-receipt.test.ts
1159
+ # #159 adds the three delivery-input seams here as a shared mutant coordinate; discovery remains
1160
+ # the beside glob. This reuses the existing gate instead of adding a second help/catalog surface.
1161
+ section "control-socket acceptance and received guidance (mutant execution coordinate)"
1162
+ run_vitest pi-extensions/lib/control-send-receipt.test.ts pi-extensions/entwurf-control.test.ts pi-extensions/lib/entwurf-v2-mailbox.test.ts pi-extensions/lib/entwurf-v2-native-push.test.ts
1161
1163
  }
1162
1164
 
1163
1165
  check_control_retire() {
@@ -1342,9 +1344,13 @@ check_fresh_call_provider() {
1342
1344
  check_acp_set_model_readback() {
1343
1345
  # NOT a discovery path — check-tests-beside-behavior finds the test by glob. This case is the
1344
1346
  # MUTANT EXECUTION COORDINATE for scripts/mutants/acp-set-model-readback.json (#128): the claude
1345
- # adapter refuses a set-model answer that does not name the requested model, before the prompt.
1346
- section "claude ACP set-model readback (mutant execution coordinate)"
1347
- run_vitest pi-extensions/lib/acp/backend-adapter.test.ts
1347
+ # adapter refuses a set-model answer that does not name the requested model, before the prompt;
1348
+ # and (#142) pi's thinking level reaches Claude effort, set and read back before the prompt;
1349
+ # and (#142) what a cancel leaves: the operator notice and the aborted-turn status in a rebuild;
1350
+ # and (#163) Pi's summary request as a one-shot tool-less turn, and the child closed on compaction;
1351
+ # and (#163) the display-only tool title, output preview and compaction lines in the event mapper.
1352
+ section "claude ACP set-model + effort readback (mutant execution coordinate)"
1353
+ run_vitest pi-extensions/lib/acp/backend-adapter.test.ts pi-extensions/lib/acp/backend.thinking.test.ts pi-extensions/lib/acp/context.test.ts pi-extensions/lib/acp/cortex-admission.test.ts pi-extensions/lib/acp/backend.summary-turn.test.ts pi-extensions/lib/acp/event-mapper.test.ts
1348
1354
  }
1349
1355
 
1350
1356
  check_acp_vendor_rejection() {
@@ -1796,38 +1802,8 @@ smoke_acp_rgg_live() {
1796
1802
  }
1797
1803
 
1798
1804
  smoke_acp_cortex_live() {
1799
- # On-demand LIVE smoke for the Cortex (Snowflake Cortex Code) ACP backend — the
1800
- # first NON-claude adapter on the rail (docs/acp-backend-rail.md, Cortex audit). OUT of
1801
- # `pnpm check` AND OUTSIDE the claude-only aggregate LIVE floor (capability
1802
- # dignity, invariant #7): a host with no cortex install / no Snowflake auth must
1803
- # not redden a release for a backend it does not run, so this is deliberately
1804
- # absent from release-gate's 11-smoke ACP acceptance floor. That is a WIRING
1805
- # decision, not a lower evidence bar — a cut that SHIPS cortex still owes a
1806
- # deliberate run of this smoke, and the aggregate gate's silence is not a pass
1807
- # (ACP rail Cortex verification boundary). Drives one real
1808
- # cortex ACP turn through the entwurf provider path (outbound entwurf_v2 +
1809
- # dual-HOME overlay facts + process-group reclaim — CP2).
1810
- # HONEST-SKIP (protocol exit 97, never 0) when LIVE!=1 OR `cortex` is not on PATH OR no
1811
- # connection is pinned — the live turn needs `cortex` installed with the
1812
- # operator's own web-login auth already present (there is no `cortex auth`
1813
- # subcommand — CP0 D6), reached through the overlay's narrow credential
1814
- # symlinks — narrow in WHICH paths are reachable (D5), not read-only: a symlink
1815
- # carries no write protection (AGENTS §ACP Plugin Boundary: entwurf never
1816
- # provides/proxies the Snowflake credential). The connection must ride the adapter seam
1817
- # (ENTWURF_ACP_CORTEX_CONNECTION) because the dual-HOME overlay denies the
1818
- # operator settings.json where a default would live. PR #40 called this
1819
- # `smoke-cortex`; the canonical family name is smoke-acp-cortex-live.
1820
- # LIVE=1 ENTWURF_ACP_CORTEX_CONNECTION=<conn> ./run.sh smoke-acp-cortex-live
1821
- entwurf_require_step_outcome
1822
- if [ "${LIVE:-}" != "1" ]; then
1823
- echo "[entwurf:skip] smoke-acp-cortex-live — set LIVE=1 (+ ENTWURF_ACP_CORTEX_CONNECTION) to run."
1824
- return "$ENTWURF_STEP_SKIP_EXIT"
1825
- fi
1826
- if ! command -v cortex >/dev/null 2>&1; then
1827
- echo "[entwurf:skip] smoke-acp-cortex-live — cortex not on PATH (install Snowflake Cortex Code and complete its own login flow)."
1828
- return "$ENTWURF_STEP_SKIP_EXIT"
1829
- fi
1830
- run_ts scripts/smoke-acp-cortex-live.ts
1805
+ echo "[entwurf:cortex-acp-disabled] Cortex ACP is not supported; no live probe was started." >&2
1806
+ return 2
1831
1807
  }
1832
1808
 
1833
1809
  smoke_entwurf_v2_matrix_live() {
@@ -2502,15 +2478,16 @@ assert.deepEqual(floorGaps, [],
2502
2478
  // The published-scope regions' own oracle (see PUBLISHED-SCOPE REGIONS above). Checked after the
2503
2479
  // four surfaces so a stranded closure row is still attributed to the claim above.
2504
2480
  // PUBLISHED_ARTIFACT is the one published pair a region may name, copied from the artifact, not from
2505
- // this checkout. `[측정 2026-11-03]` the global install of @junghanacs/entwurf@0.50.0 whose bytes #155
2506
- // HOP1 accepted against the registry tarball declares exactly these nine @earendil-works dependencies
2507
- // in its package.json (sha256 20c572534fcfc03e8f89890a0853a25048166f7c998e433945289a5d20ee1234).
2481
+ // this checkout. Read 2026-11-11 from package/package.json in the preserved registry-accepted
2482
+ // 0.51.0 release TGZ (TGZ SHA256 04fcce26d0a6dec8b8bb9a7f14a843eee0defb313aedafa34080600853d36e4d;
2483
+ // raw manifest SHA256 b5043ba8edde6f2b9d13052a98c3cd808a3b30f1023da41b22ee022470a96120).
2484
+ // Its ten @earendil-works production dependencies are exact 1.5.0, independent of this checkout.
2508
2485
  const PUBLISHED_ARTIFACT = {
2509
- version: '0.50.0',
2486
+ version: '0.51.0',
2510
2487
  dependencies: {
2511
- '@earendil-works/chord': '1.4.0', '@earendil-works/pi-agent-core': '1.4.0', '@earendil-works/pi-ai': '1.4.0',
2512
- '@earendil-works/pi-codemode': '1.4.0', '@earendil-works/pi-coding-agent': '1.4.0', '@earendil-works/pi-durable': '1.4.0',
2513
- '@earendil-works/pi-mcp': '1.4.0', '@earendil-works/pi-telemetry': '1.4.0', '@earendil-works/pi-tui': '1.4.0',
2488
+ '@earendil-works/chord': '1.5.0', '@earendil-works/pi-agent-core': '1.5.0', '@earendil-works/pi-ai': '1.5.0',
2489
+ '@earendil-works/pi-codemode': '1.5.0', '@earendil-works/pi-coding-agent': '1.5.0', '@earendil-works/pi-durable': '1.5.0',
2490
+ '@earendil-works/pi-mcp': '1.5.0', '@earendil-works/pi-schema': '1.5.0', '@earendil-works/pi-telemetry': '1.5.0', '@earendil-works/pi-tui': '1.5.0',
2514
2491
  },
2515
2492
  };
2516
2493
  const publishedSdk = [...new Set(Object.values(PUBLISHED_ARTIFACT.dependencies))];
@@ -3372,13 +3349,11 @@ check_acp_provider_surface() {
3372
3349
  # Deterministic gate for the ACP provider registration surface. Loads the REAL
3373
3350
  # provider lib modules AND compiles + drives the REAL entry against a fake pi,
3374
3351
  # asserting: one surface name, the no-auth sentinel shape, full
3375
- # ProviderModelConfig rows on the curated claude anchor, the EXACT curated model
3376
- # set both adapters contribute (claude 3 + cortex 4 —
3377
- # [QK:CORTEX-PROVIDER-EXACT-ROW-SURFACE], the claim that catches an entry which
3378
- # silently drops a whole backend), and that streamSimple is the real
3352
+ # ProviderModelConfig rows on the curated Claude anchor, the exact Claude-only
3353
+ # set [QK:ACP-PROVIDER-CLAUDE-ONLY], and that streamSimple is the real
3379
3354
  # streamShellAcp backend (checked BY NAME, never invoked — invoking spawns an
3380
3355
  # ACP child). Pure, no pi runtime, no API.
3381
- section "ACP provider surface (registration + both-adapter model set)"
3356
+ section "ACP provider surface (registration + Claude-only model set)"
3382
3357
  run_ts scripts/check-acp-provider-surface.ts
3383
3358
  }
3384
3359
 
@@ -3706,30 +3681,10 @@ check_acp_carrier_augment() {
3706
3681
  }
3707
3682
 
3708
3683
  check_acp_cortex() {
3709
- # Deterministic gate for the Cortex (Snowflake Cortex Code) ACP backend — the
3710
- # first non-claude adapter on the rail, landed 0.13.0 on the CP0-measured
3711
- # contract (docs/acp-backend-rail.md, Cortex audit D1–D10). The cortex source is one
3712
- # `cortexAdapter` object (backend-adapter.ts) + its curated surface (models.ts)
3713
- # + the dual-HOME overlay (overlay.ts); the 결합 규칙 requires the gate to land
3714
- # WITH it, extending the check-acp-* family, so cortex's whole
3715
- # deterministic axis lives here. Locks (each Cortex-audit D-pinned): the GLG-decided
3716
- # 4-row curation rides real registry bases and the `cortex-` prefix routes to
3717
- # cortexAdapter; launch is `cortex acp serve` with NO -m — the model is
3718
- # enforced per turn via session/set_config_option with the native id (E);
3719
- # CORTEX_HOME presence (empty included) refuses the spawn (D3); the dual-HOME
3720
- # overlay isolates HOME+SNOWFLAKE_HOME per session key (P0-1: scope =
3721
- # backend-passed resolveSessionKey, never ambient), passes through only
3722
- # connections.toml / optional config.toml / credential_cache (D5/F — never the
3723
- # whole cache, never skills; symlink-through, no copies — AGENTS §ACP Plugin
3724
- # Boundary), authors autoUpdate:false (D4), and projects envelope-enriched
3725
- # explicit servers into cortex/mcp.json with the real HOME restored on
3726
- # entwurf-bridge alone (D9/D10), non-stdio failing loud; the
3727
- # CORTEX_ACP_COMMAND override single-quotes shell-metachar connection tokens.
3728
- # The gate self-manages the tsc-emit layer for the .js-suffixed backend-adapter
3729
- # imports; no auth, no spawn, no live cortex needed. Kill-qualified via the
3730
- # `acp-cortex` mutant lane under check-gate-qualification.
3731
- section "ACP cortex backend (Snowflake Cortex Code — 1st non-claude adapter)"
3732
- run_ts scripts/check-acp-cortex.ts
3684
+ # The old positive Cortex adapter contract is retired. Exercise the production
3685
+ # admission and direct adapter refusal without launching a vendor process.
3686
+ section "ACP Cortex disabled admission"
3687
+ run_vitest pi-extensions/lib/acp/cortex-admission.test.ts
3733
3688
  }
3734
3689
 
3735
3690
  check_pack() {
@@ -4129,7 +4084,7 @@ check_pack_win32_carrier() {
4129
4084
  # which is exactly what property (1)'s `(_|$)` absorbs; all three shapes are fixtures
4130
4085
  # below so a future suffix change cannot pass vacuously.)
4131
4086
  pack_install_leaked_pi() {
4132
- grep '^@earendil-works+' | grep -Ev '@1\.5\.0(_|$)' || true
4087
+ grep '^@earendil-works+' | grep -Ev '@1\.5\.2(_|$)' || true
4133
4088
  }
4134
4089
 
4135
4090
  # Matcher self-test on SYNTHETIC lookalikes: a healthy install tree cannot exercise either
@@ -4146,18 +4101,18 @@ check_pack_pin_matcher() {
4146
4101
  # Cell 1 — the version boundary. Every measured peer-hash shape is a fixture: the
4147
4102
  # 0.84.4-era `_@modelcontextprotocol+sdk@…`, the `_ws@8.21.3` observed on a real
4148
4103
  # 0.85.1 tree, and `[측정 2026-09-20]` the combined suffix a 0.86.0 install carries.
4149
- # None may leak; the two lookalikes must — a PREFIX-EXTENDED version (`1.5.0-beta.1`,
4104
+ # None may leak; the two lookalikes must — a PREFIX-EXTENDED version (`1.5.2-beta.1`,
4150
4105
  # the prerelease shape that an unanchored match would bless) and an off-pin version.
4151
4106
  matcher_probe=$(printf '%s\n' \
4152
- '@earendil-works+pi-ai@1.5.0' \
4153
- '@earendil-works+pi-ai@1.5.0_@modelcontextprotocol+sdk@1.29.0_zod@4.3.6' \
4154
- '@earendil-works+pi-ai@1.5.0_ws@8.21.3' \
4155
- '@earendil-works+pi-ai@1.5.0_@modelcontextprotocol+sdk@1.29.0_zod@4.3.6__ws@8.21.3_zod@4.3.6' \
4156
- '@earendil-works+pi-ai@1.5.0-beta.1' \
4107
+ '@earendil-works+pi-ai@1.5.2' \
4108
+ '@earendil-works+pi-ai@1.5.2_@modelcontextprotocol+sdk@1.29.0_zod@4.3.6' \
4109
+ '@earendil-works+pi-ai@1.5.2_ws@8.21.3' \
4110
+ '@earendil-works+pi-ai@1.5.2_@modelcontextprotocol+sdk@1.29.0_zod@4.3.6__ws@8.21.3_zod@4.3.6' \
4111
+ '@earendil-works+pi-ai@1.5.2-beta.1' \
4157
4112
  '@earendil-works+pi-agent-core@0.87.1' | pack_install_leaked_pi)
4158
- if [ "$matcher_probe" != '@earendil-works+pi-ai@1.5.0-beta.1
4113
+ if [ "$matcher_probe" != '@earendil-works+pi-ai@1.5.2-beta.1
4159
4114
  @earendil-works+pi-agent-core@0.87.1' ]; then
4160
- fail "[QK:PACK-INSTALL-PIN-MATCHER-BOUNDED] the pin-leak matcher must flag the prefix-extended 1.5.0-beta.1 and the off-pin 0.87.1 lookalikes, and pass 1.5.0 bare or with any measured peer-hash — got: ${matcher_probe:-<nothing leaked>}"
4115
+ fail "[QK:PACK-INSTALL-PIN-MATCHER-BOUNDED] the pin-leak matcher must flag the prefix-extended 1.5.2-beta.1 and the off-pin 0.87.1 lookalikes, and pass 1.5.2 bare or with any measured peer-hash — got: ${matcher_probe:-<nothing leaked>}"
4161
4116
  return 1
4162
4117
  fi
4163
4118
 
@@ -4165,9 +4120,9 @@ check_pack_pin_matcher() {
4165
4120
  # (0.85.0 onward, still true at 0.99.1). An off-pin chord MUST leak; the pinned one must not. A matcher narrowed
4166
4121
  # back to `^@earendil-works+pi-` sees nothing here and dies at this signature.
4167
4122
  matcher_probe=$(printf '%s\n' \
4168
- '@earendil-works+chord@1.5.0' \
4123
+ '@earendil-works+chord@1.5.2' \
4169
4124
  '@earendil-works+chord@0.87.1' \
4170
- '@earendil-works+pi-ai@1.5.0' | pack_install_leaked_pi)
4125
+ '@earendil-works+pi-ai@1.5.2' | pack_install_leaked_pi)
4171
4126
  if [ "$matcher_probe" != '@earendil-works+chord@0.87.1' ]; then
4172
4127
  fail "[QK:PACK-INSTALL-PIN-MATCHER-COVERS-CLOSURE] the pin-leak matcher must cover every @earendil-works closure member, not just the pi-* families — an off-pin @earendil-works/chord has to leak (it was a runtime dependency of pi-coding-agent, pi-agent-core, pi-client and pi-protocol — `[측정 2026-09-20]` at 0.86.0, re-measured `[측정 2026-09-22]` at 0.87.0, `[측정 2026-09-23]` at 0.87.1 and `[측정 2026-09-30]` at 0.99.1, where pi-coding-agent adds pi-codemode and pi-mcp beside them; `[측정 2026-10-13]` at 1.2.0 pi-durable alone declares it) — got: ${matcher_probe:-<nothing leaked>}"
4173
4128
  return 1
@@ -5089,16 +5044,16 @@ _check_pack_install_impl() {
5089
5044
  local install_log
5090
5045
  install_log=$(cd "$tmp" && pnpm add \
5091
5046
  "$tgz_path" \
5092
- "@earendil-works/pi-ai@1.5.0" \
5093
- "@earendil-works/pi-coding-agent@1.5.0" \
5094
- "@earendil-works/pi-tui@1.5.0" \
5095
- "@earendil-works/pi-agent-core@1.5.0" \
5096
- "@earendil-works/pi-telemetry@1.5.0" \
5097
- "@earendil-works/chord@1.5.0" \
5098
- "@earendil-works/pi-codemode@1.5.0" \
5099
- "@earendil-works/pi-mcp@1.5.0" \
5100
- "@earendil-works/pi-durable@1.5.0" \
5101
- "@earendil-works/pi-schema@1.5.0" \
5047
+ "@earendil-works/pi-ai@1.5.2" \
5048
+ "@earendil-works/pi-coding-agent@1.5.2" \
5049
+ "@earendil-works/pi-tui@1.5.2" \
5050
+ "@earendil-works/pi-agent-core@1.5.2" \
5051
+ "@earendil-works/pi-telemetry@1.5.2" \
5052
+ "@earendil-works/chord@1.5.2" \
5053
+ "@earendil-works/pi-codemode@1.5.2" \
5054
+ "@earendil-works/pi-mcp@1.5.2" \
5055
+ "@earendil-works/pi-durable@1.5.2" \
5056
+ "@earendil-works/pi-schema@1.5.2" \
5102
5057
  --ignore-workspace --ignore-scripts 2>&1) || {
5103
5058
  fail "[check-pack-install] pnpm add failed:"
5104
5059
  echo "$install_log" | tail -10 | sed 's/^/ /' >&2
@@ -5107,17 +5062,17 @@ _check_pack_install_impl() {
5107
5062
 
5108
5063
  # A pin is a wish until the resolved tree is read back. Assert it: EVERY
5109
5064
  # @earendil-works package present — direct or transitive, top level or nested,
5110
- # chord included — must be the pinned 0.86.0. Anything else means an unpinned caret
5065
+ # chord included — must be the pinned 1.5.2. Anything else means an unpinned caret
5111
5066
  # floated and the rest of this gate would be exercising a runtime nobody verified,
5112
- # while still printing "pinned pi 0.86.0". Fail loud instead of proving the wrong floor.
5067
+ # while still printing a pinned runtime. Fail loud instead of proving the wrong floor.
5113
5068
  local leaked_pi
5114
5069
  leaked_pi=$(ls "$tmp/node_modules/.pnpm" 2>/dev/null | pack_install_leaked_pi)
5115
5070
  if [ -n "$leaked_pi" ]; then
5116
- fail "[check-pack-install] UNVERIFIED pi runtime resolved into the install tree (expected only 1.5.0):"
5071
+ fail "[check-pack-install] UNVERIFIED pi runtime resolved into the install tree (expected only 1.5.2):"
5117
5072
  printf '%s\n' "$leaked_pi" | sed 's/^/ /' >&2
5118
5073
  return 1
5119
5074
  fi
5120
- echo "[check-pack-install] pi runtime tree pin verified: every @earendil-works package is 1.5.0 (chord, pi-durable and pi-schema included)"
5075
+ echo "[check-pack-install] pi runtime tree pin verified: every @earendil-works package is 1.5.2 (chord, pi-durable and pi-schema included)"
5121
5076
 
5122
5077
  # Resolve the installed package.json and confirm pi.extensions
5123
5078
  # arrived intact. If pi.extensions is empty or missing, the
@@ -5280,17 +5235,9 @@ _check_pack_install_impl() {
5280
5235
  echo "$loader_out" | tail -10 | sed 's/^/ /' >&2
5281
5236
  return 1
5282
5237
  fi
5283
- # Verify the EXACT curated surface the installed package registers — both
5284
- # backends (claude 2 rows + cortex 4 rows = 6), as a SET, not as anchors.
5285
- #
5286
- # Why exact-set and not `grep -q <id>`: the cortex ids CONTAIN the claude ids
5287
- # as substrings (`cortex-claude-sonnet-5` matches a bare `claude-sonnet-5`
5288
- # grep), so a substring probe could go green on a surface that dropped every
5289
- # unprefixed Claude row. And an anchor-only probe cannot see a MISSING cortex
5290
- # row at all — that is exactly the installed-consumer gap this assertion
5291
- # closes: the source-side sets are pinned by check-acp-cortex /
5292
- # check-acp-provider-surface, but only THIS gate proves the ids survive the
5293
- # pack → install → pi-loader path into a real consumer's model list.
5238
+ # Verify the EXACT supported surface the installed package registers: four
5239
+ # Claude rows, no retired Cortex row. A substring probe cannot prove absence.
5240
+ # This gate joins the source admission to pack → install → real Pi loader.
5294
5241
  #
5295
5242
  # Parse rows whose FIRST field is the provider `entwurf` (pi prints a
5296
5243
  # `provider model context …` table; the header row's first field is
@@ -5299,14 +5246,11 @@ _check_pack_install_impl() {
5299
5246
  loader_ids=$(awk '$1 == "entwurf" { print $2 }' <<<"$loader_out" | LC_ALL=C sort)
5300
5247
  loader_expected=$(printf '%s\n' \
5301
5248
  claude-fable-5-1 \
5249
+ claude-fable-5-2 \
5302
5250
  claude-opus-5-5 \
5303
- claude-sonnet-5-5 \
5304
- cortex-auto \
5305
- cortex-claude-opus-5 \
5306
- cortex-claude-sonnet-5 \
5307
- cortex-openai-gpt-5.4 | LC_ALL=C sort)
5251
+ claude-sonnet-5-5 | LC_ALL=C sort)
5308
5252
  if [ "$loader_ids" != "$loader_expected" ]; then
5309
- fail "[check-pack-install] installed provider model set drifted — expected exactly 7 curated ids (claude 3 + cortex 4):"
5253
+ fail "[check-pack-install] installed provider model set drifted — expected exactly 4 Claude ids and no Cortex ids:"
5310
5254
  echo " expected:" >&2
5311
5255
  echo "$loader_expected" | sed 's/^/ /' >&2
5312
5256
  echo " actual (entwurf rows):" >&2
@@ -5318,7 +5262,7 @@ _check_pack_install_impl() {
5318
5262
  # Name the set on SUCCESS too, not only in the failure branch — same principle as
5319
5263
  # the pinned-pi version assert above: a gate that cannot name what it proved has
5320
5264
  # proved nothing, and a row COUNT without the ids cannot be audited from a CI log.
5321
- echo "[check-pack-install] pi loader smoke pass (entwurf registered; exact curated set: claude 3 + cortex 4):"
5265
+ echo "[check-pack-install] pi loader smoke pass (entwurf registered; exact curated set: 4 Claude ids, no Cortex):"
5322
5266
  echo "$loader_ids" | sed 's/^/ /'
5323
5267
 
5324
5268
  # npm-managed neutral install regression — the README's PRIMARY install path is
@@ -7629,10 +7573,8 @@ release_gate() {
7629
7573
  # green cut said nothing about them. Under the P1 outcome protocol wiring
7630
7574
  # them costs nothing on a host that cannot run them — they SKIP honestly and
7631
7575
  # `--cut` names the missing prerequisite — while a host that CAN run them now
7632
- # must. Cortex is deliberately NOT here: its rail needs an external Snowflake
7633
- # connection the HOST owns, so wiring it would block every cut taken without
7634
- # that account; it stays a required DIRECT call for a Cortex-rail cut instead
7635
- # (VERIFY 「Cortex is an on-demand axis」). Same for `smoke-acp-long-turn-live`,
7576
+ # must. Cortex ACP is disabled; its former direct smoke now refuses without
7577
+ # starting a probe. The excluded `smoke-acp-long-turn-live`,
7636
7578
  # `smoke-agy-native-push-live` and `smoke-acp-ordering-probe-live`: >12min by
7637
7579
  # construction, no aggregate-owned agy conversation id, and an opt-in instrument
7638
7580
  # rather than acceptance. check-release-gate-outcomes pins that every LIVE smoke
@@ -15,7 +15,7 @@
15
15
  // Pure + temp-dir fs, no spawn/child — IN pnpm check.
16
16
 
17
17
  import { strict as assert } from "node:assert";
18
- import { mkdtempSync, readFileSync, writeFileSync } from "node:fs";
18
+ import { mkdtempSync, writeFileSync } from "node:fs";
19
19
  import { tmpdir } from "node:os";
20
20
  import { join } from "node:path";
21
21
  import { fileURLToPath } from "node:url";
@@ -379,23 +379,6 @@ function ctxWith(firstUser: string) {
379
379
  "[QK:CARRIER-FRAME-NAMES-CLAUDE-CARRIER] the claude frame must name the real carrier AND scope it to the engraving — " +
380
380
  `saying only "this is not your system prompt" leaves the model to guess what the system prompt then is. Got: ${JSON.stringify(claudeAug.slice(0, 500))}`,
381
381
  );
382
- assert.ok(
383
- cortexAug.includes("carries no system-prompt carrier at all") &&
384
- !cortexAug.includes("does have a system-prompt carrier"),
385
- "[QK:CARRIER-FRAME-DENIES-CORTEX-CARRIER] the cortex frame must deny the carrier — cortex's buildSessionMeta returns " +
386
- `undefined so no _meta is sent; claiming a tiny carrier there would be a lie. Got: ${JSON.stringify(cortexAug.slice(0, 500))}`,
387
- );
388
-
389
- // The rail difference is not a doc claim — it is in the adapter source.
390
- const adapterSrc = readFileSync(join(REPO_DIR, "pi-extensions/lib/acp/backend-adapter.ts"), "utf8");
391
- const cortexAt = adapterSrc.indexOf('backend: "cortex"');
392
- assert.ok(cortexAt > 0, "backend-adapter.ts still declares the cortex adapter");
393
- assert.match(
394
- adapterSrc.slice(cortexAt),
395
- /buildSessionMeta\(\)\s*\{\s*return undefined;/,
396
- "[QK:CARRIER-RAIL-DIFF-IS-SOURCE-PINNED] cortex's buildSessionMeta must still return undefined — the source fact the " +
397
- "cortex frame's 'no carrier at all' sentence rests on. If this ever grows a carrier, the frame becomes a lie.",
398
- );
399
382
 
400
383
  // DISJOINT SURFACES: the thing the model is told is its system prompt (the
401
384
  // engraving) must not contain the narrative it is told is user text. If the
@@ -559,9 +542,8 @@ console.log(
559
542
  "signature change (stable carrier → stable signature); augment: prepended on `new` only (reuse delta has none), " +
560
543
  "wire-only so it never enters contextMessageSignatures, entwurf cwd/AGENTS.md de-dup (present → drop only that " +
561
544
  "section, home kept; absent → kept), day-granularity date, 50KB truncation marker, shipped AGENTS budget; " +
562
- "provenance: both rails state the augment is first-user-message text and not the system prompt, claude names its " +
563
- "tiny engraving-only carrier while cortex denies having one (pinned against cortexAdapter.buildSessionMeta → " +
564
- "undefined), and carrier/augment stay disjoint; A-join: the carrier opens its own block after the SDK's fixed " +
545
+ "provenance: the augment is first-user-message text and not the system prompt, Claude names its " +
546
+ "tiny engraving-only carrier, and carrier/augment stay disjoint; A-join: the carrier opens its own block after the SDK's fixed " +
565
547
  "sentence, the boundary is a loader constant (not the template's whitespace, which trim eats) and does not " +
566
548
  "resurrect an opted-out carrier, the shipped carrier stays under the tiny budget, and the meta hop sends the " +
567
549
  "loader string byte-for-byte so the wire and the reuse signature cannot desynchronize",
@@ -41,7 +41,21 @@ import { normalizeContext } from "@earendil-works/pi-ai";
41
41
  import { isRetryableAssistantError } from "@earendil-works/pi-ai/compat";
42
42
  import { assertOverlayInSandbox, sandboxAcpGateHome } from "./lib/acp-gate-home.ts";
43
43
 
44
- const sonnet = { id: "claude-sonnet-5-5" } as unknown as Model<Api>;
44
+ // #142: a Claude turn carries pi's thinking level, which the backend sets as effort and reads back
45
+ // before the prompt. This fixture runs every turn at one fixed level on rows carrying the curated
46
+ // level map, and its fake driver reads back whatever config option it was asked to set. The effort
47
+ // contract itself is certified in pi-extensions/lib/acp/backend.thinking.test.ts.
48
+ const FIXTURE_THINKING_LEVEL = "medium";
49
+ const FIXTURE_THINKING_LEVEL_MAP = {
50
+ off: null,
51
+ minimal: null,
52
+ low: "low",
53
+ medium: "medium",
54
+ high: "high",
55
+ xhigh: "xhigh",
56
+ max: "max",
57
+ };
58
+ const sonnet = { id: "claude-sonnet-5-5", thinkingLevelMap: FIXTURE_THINKING_LEVEL_MAP } as unknown as Model<Api>;
45
59
 
46
60
  type Stream = AsyncIterable<AssistantMessageEvent>;
47
61
 
@@ -152,7 +166,7 @@ function makeHarness(recordDir: string) {
152
166
  const makeConnection = (handlers: any) => ({
153
167
  initialize: async () => ({ agentCapabilities: {} }),
154
168
  newSession: async () => ({ sessionId: "ACP-1" }),
155
- setSessionConfigOption: async (p: any) => ({ configOptions: [{ id: "model", currentValue: p.value }] }),
169
+ setSessionConfigOption: async (p: any) => ({ configOptions: [{ id: p.configId, currentValue: p.value }] }),
156
170
  prompt: ({ sessionId }: any) => {
157
171
  promptCount++;
158
172
  notifier = (update) => handlers.sessionUpdate({ update, sessionId });
@@ -287,7 +301,14 @@ try {
287
301
  const promptsOut = resolve(TMP_EMIT, "pi-extensions/lib/acp/prompts");
288
302
  mkdirSync(promptsOut, { recursive: true });
289
303
  copyFileSync("pi-extensions/lib/acp/prompts/engraving.md", resolve(promptsOut, "engraving.md"));
290
- const backend = (await import(pathToFileURL(resolve(TMP_EMIT, "pi-extensions/lib/acp/backend.js")).href)) as any;
304
+ const backendModule = (await import(
305
+ pathToFileURL(resolve(TMP_EMIT, "pi-extensions/lib/acp/backend.js")).href
306
+ )) as any;
307
+ const backend = {
308
+ ...backendModule,
309
+ streamAcpTurn: (model: unknown, context: unknown, options: Record<string, unknown> | undefined, deps: unknown) =>
310
+ backendModule.streamAcpTurn(model, context, { reasoning: FIXTURE_THINKING_LEVEL, ...options }, deps),
311
+ };
291
312
  await assertOverlayInSandbox(TMP_EMIT, gateHome);
292
313
 
293
314
  // ----------------------------------------------------------------------
@@ -35,7 +35,6 @@ import {
35
35
  ENTWURF_ACP_NO_AUTH_SENTINEL,
36
36
  PROVIDER_ID,
37
37
  SUPPORTED_ANTHROPIC_MODEL_IDS,
38
- SUPPORTED_CORTEX_MODEL_IDS,
39
38
  } from "../pi-extensions/lib/acp/models.ts";
40
39
 
41
40
  // ---------------------------------------------------------------------------
@@ -103,10 +102,16 @@ try {
103
102
  const mod = await import(entryUrl);
104
103
 
105
104
  const calls: Array<{ id: string; cfg: CapturedCfg }> = [];
105
+ const events: string[] = [];
106
106
  const fakePi = {
107
107
  registerProvider(id: string, cfg: CapturedCfg) {
108
108
  calls.push({ id, cfg });
109
109
  },
110
+ // #163: the entry closes a session's Claude child on compaction. Its behaviour is proven in
111
+ // pi-extensions/lib/acp/backend.summary-turn.test.ts; here only the registration count.
112
+ on(event: string) {
113
+ events.push(event);
114
+ },
110
115
  };
111
116
 
112
117
  // Drive the REAL default export twice — the second call must be a no-op.
@@ -118,6 +123,7 @@ try {
118
123
  1,
119
124
  `entry must register exactly once across two calls (idempotency) — got ${calls.length}`,
120
125
  );
126
+ assert.deepEqual(events, ["session_compact"], `entry must subscribe to session_compact exactly once — got ${events}`);
121
127
  const cap = calls[0];
122
128
  assert.equal(cap.id, PROVIDER_ID, `entry registered the wrong provider id: ${cap.id}`);
123
129
  assert.equal(cap.cfg.apiKey, ENTWURF_ACP_NO_AUTH_SENTINEL, "entry apiKey is not the no-auth sentinel");
@@ -127,33 +133,18 @@ try {
127
133
  assert.ok(capIds.includes(want), `entry model surface missing ${want} (got: ${capIds.join(", ") || "none"})`);
128
134
  }
129
135
 
130
- // The REAL entry must register the curated rows of EVERY adapter on the rail —
131
- // claude's 2 unprefixed ids AND cortex's 4 `cortex-` ids. This is the JOIN
132
- // claim, distinct from the per-backend data claims: check-acp-cortex pins the
133
- // 4-row cortex curation in models.ts, but nothing there proves those rows
134
- // actually reach `pi.registerProvider`. An entry that filtered `cortex-` out
135
- // of `allCuratedModels()` would leave models.ts and check-acp-cortex fully
136
- // green while the operator's model list silently lost the whole backend.
137
- //
138
- // Set comparison (sorted, exact) — not `includes`: a substring/anchor probe
139
- // cannot see a row that VANISHED, and the cortex ids contain the claude ids as
140
- // substrings. Expected is derived from the two source id constants so there is
141
- // one SSOT; the literal 7 below is the independent floor that catches a
142
- // curation list that SHRANK — expected derives from the same constants, so a
143
- // dropped row moves expected and captured together and the set comparison
144
- // stays green. That is why the count is asserted separately and carries its
145
- // own claim: the cortex list has `CORTEX-CURATED-FOUR-ROWS` for exactly this
146
- // failure, and the claude list needs its own oracle rather than borrowing it.
147
- const expectedIds = [...SUPPORTED_ANTHROPIC_MODEL_IDS, ...SUPPORTED_CORTEX_MODEL_IDS].slice().sort();
148
- assert.equal(
149
- expectedIds.length,
150
- 7,
151
- `[QK:CLAUDE-CURATED-THREE-ROWS] curated id constants must total 7 rows (claude 3 + cortex 4) — got ${expectedIds.length}: ${expectedIds.join(", ")}`,
136
+ // Independent literal oracle: the compiled provider exposes the four Claude
137
+ // rows and no retired Cortex row, even if a source list drifts with the SUT.
138
+ const expectedIds = ["claude-fable-5-1", "claude-fable-5-2", "claude-opus-5-5", "claude-sonnet-5-5"];
139
+ assert.deepEqual(
140
+ [...SUPPORTED_ANTHROPIC_MODEL_IDS].sort(),
141
+ expectedIds,
142
+ "[QK:CLAUDE-CURATED-FOUR-ROWS] the four Claude IDs are an independent literal oracle",
152
143
  );
153
144
  assert.deepEqual(
154
145
  capIds.slice().sort(),
155
146
  expectedIds,
156
- `[QK:CORTEX-PROVIDER-EXACT-ROW-SURFACE] compiled entry must register the EXACT curated set of both adapters (claude 3 + cortex 4) — got: ${capIds.join(", ") || "none"}`,
147
+ `[QK:ACP-PROVIDER-CLAUDE-ONLY] compiled provider must expose exactly the four Claude rows — got: ${capIds.join(", ") || "none"}`,
157
148
  );
158
149
  assert.equal(typeof cap.cfg.streamSimple, "function", "entry streamSimple must be a function");
159
150
  // Regression guard: the entry must wire the REAL backend (streamShellAcp),
@@ -200,7 +191,7 @@ assert.ok(!/backend-stub/.test(entrySrc), "entry must not import the removed S0
200
191
 
201
192
  console.log(
202
193
  `[check-acp-provider-surface] ok — compiled entry registers ${PROVIDER_ID} once (idempotent) with the no-auth ` +
203
- `sentinel + the EXACT curated set of both adapters (claude ${SUPPORTED_ANTHROPIC_MODEL_IDS.length} incl. ` +
204
- `${CURATED_ANCHOR_MODEL_ID} + cortex ${SUPPORTED_CORTEX_MODEL_IDS.length}) + real streamShellAcp backend (S2c); ` +
194
+ `sentinel + the EXACT Claude-only curated set (${SUPPORTED_ANTHROPIC_MODEL_IDS.length} incl. ` +
195
+ `${CURATED_ANCHOR_MODEL_ID}) + real streamShellAcp backend (S2c); ` +
205
196
  `claude lib surface verified live (${models.length} model(s))`,
206
197
  );