@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.
Files changed (68) hide show
  1. package/AGENTS.md +1 -1
  2. package/BASELINE.md +116 -397
  3. package/CHANGELOG.md +131 -2
  4. package/CONTRIBUTING.md +4 -4
  5. package/DELIVERY.md +2 -2
  6. package/ENTWURF-CLI.json +1 -1
  7. package/README.md +7 -4
  8. package/VERIFY.md +332 -142
  9. package/demo/demo-baseline.sh +1 -1
  10. package/demo/demo.sh +1 -1
  11. package/docs/acp-backend-rail.md +248 -34
  12. package/docs/setup-clean-host.md +50 -65
  13. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/acp-client.js +1 -1
  14. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/backend-adapter.js +90 -14
  15. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/config.js +4 -9
  16. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/context.js +15 -0
  17. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/model-ids.js +9 -12
  18. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/models.js +41 -56
  19. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/tool-surface.js +33 -6
  20. package/mcp/entwurf-bridge/dist/pi-extensions/lib/fresh-call-composition.js +34 -3
  21. package/mcp/entwurf-bridge/dist/pi-extensions/lib/herdr-fresh-call.js +3 -1
  22. package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +2 -1
  23. package/package.json +4 -4
  24. package/pi/settings.reference.json +0 -1
  25. package/pi-extensions/acp-provider.ts +12 -6
  26. package/pi-extensions/lib/acp/acp-client.ts +6 -7
  27. package/pi-extensions/lib/acp/backend-adapter.ts +139 -15
  28. package/pi-extensions/lib/acp/backend.ts +261 -32
  29. package/pi-extensions/lib/acp/config.ts +4 -9
  30. package/pi-extensions/lib/acp/context.ts +17 -0
  31. package/pi-extensions/lib/acp/event-mapper.ts +236 -17
  32. package/pi-extensions/lib/acp/model-ids.js +9 -12
  33. package/pi-extensions/lib/acp/models.ts +46 -57
  34. package/pi-extensions/lib/acp/tool-surface.ts +34 -6
  35. package/pi-extensions/lib/fresh-call-composition.ts +37 -3
  36. package/pi-extensions/lib/herdr-fresh-call.ts +3 -1
  37. package/pi-extensions/lib/mux-fresh-call.ts +3 -1
  38. package/run.sh +25 -86
  39. package/scripts/check-acp-carrier-augment.ts +3 -21
  40. package/scripts/check-acp-prompt-lifecycle.ts +24 -3
  41. package/scripts/check-acp-provider-surface.ts +17 -26
  42. package/scripts/check-acp-session-reuse.ts +23 -4
  43. package/scripts/check-acp-stop-reason.ts +22 -3
  44. package/scripts/check-acp-stream-hooks.ts +24 -3
  45. package/scripts/check-acp-usage-accounting.ts +53 -24
  46. package/scripts/check-copilot-receive-arm.ts +19 -2
  47. package/scripts/check-fresh-call-dispatch.ts +9 -3
  48. package/scripts/check-gate-qualification.ts +5 -5
  49. package/scripts/check-herdr-fresh-call.ts +1 -0
  50. package/scripts/check-install-surface.ts +6 -0
  51. package/scripts/check-probe-ordering.ts +34 -24
  52. package/scripts/check-release-gate-outcomes.ts +9 -23
  53. package/scripts/lib/probe-cli-target.ts +3 -2
  54. package/scripts/mutants/acp-augment.json +0 -26
  55. package/scripts/mutants/acp-cortex.json +10 -183
  56. package/scripts/mutants/acp-foreground-only.json +11 -0
  57. package/scripts/mutants/acp-launch-namespace.json +3 -1
  58. package/scripts/mutants/acp-prompt-lifecycle.json +2 -2
  59. package/scripts/mutants/acp-set-model-readback.json +488 -0
  60. package/scripts/mutants/mux-fresh-call.json +12 -1
  61. package/scripts/mutants/pi-floor.json +2 -2
  62. package/scripts/mutants/release-gate.json +3 -3
  63. package/scripts/raw-acp-compaction-measure/probe.ts +8 -2
  64. package/scripts/smoke-acp-cortex-live.ts +4 -408
  65. package/scripts/smoke-acp-ordering-probe-live.ts +7 -1
  66. package/scripts/smoke-acp-session-reuse-live.ts +11 -2
  67. package/scripts/smoke-herdr-raw-install-live.sh +4 -4
  68. 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":
@@ -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,
package/run.sh CHANGED
@@ -1344,9 +1344,13 @@ check_fresh_call_provider() {
1344
1344
  check_acp_set_model_readback() {
1345
1345
  # NOT a discovery path — check-tests-beside-behavior finds the test by glob. This case is the
1346
1346
  # MUTANT EXECUTION COORDINATE for scripts/mutants/acp-set-model-readback.json (#128): the claude
1347
- # adapter refuses a set-model answer that does not name the requested model, before the prompt.
1348
- section "claude ACP set-model readback (mutant execution coordinate)"
1349
- 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
1350
1354
  }
1351
1355
 
1352
1356
  check_acp_vendor_rejection() {
@@ -1798,38 +1802,8 @@ smoke_acp_rgg_live() {
1798
1802
  }
1799
1803
 
1800
1804
  smoke_acp_cortex_live() {
1801
- # On-demand LIVE smoke for the Cortex (Snowflake Cortex Code) ACP backend — the
1802
- # first NON-claude adapter on the rail (docs/acp-backend-rail.md, Cortex audit). OUT of
1803
- # `pnpm check` AND OUTSIDE the claude-only aggregate LIVE floor (capability
1804
- # dignity, invariant #7): a host with no cortex install / no Snowflake auth must
1805
- # not redden a release for a backend it does not run, so this is deliberately
1806
- # absent from release-gate's 11-smoke ACP acceptance floor. That is a WIRING
1807
- # decision, not a lower evidence bar — a cut that SHIPS cortex still owes a
1808
- # deliberate run of this smoke, and the aggregate gate's silence is not a pass
1809
- # (ACP rail Cortex verification boundary). Drives one real
1810
- # cortex ACP turn through the entwurf provider path (outbound entwurf_v2 +
1811
- # dual-HOME overlay facts + process-group reclaim — CP2).
1812
- # HONEST-SKIP (protocol exit 97, never 0) when LIVE!=1 OR `cortex` is not on PATH OR no
1813
- # connection is pinned — the live turn needs `cortex` installed with the
1814
- # operator's own web-login auth already present (there is no `cortex auth`
1815
- # subcommand — CP0 D6), reached through the overlay's narrow credential
1816
- # symlinks — narrow in WHICH paths are reachable (D5), not read-only: a symlink
1817
- # carries no write protection (AGENTS §ACP Plugin Boundary: entwurf never
1818
- # provides/proxies the Snowflake credential). The connection must ride the adapter seam
1819
- # (ENTWURF_ACP_CORTEX_CONNECTION) because the dual-HOME overlay denies the
1820
- # operator settings.json where a default would live. PR #40 called this
1821
- # `smoke-cortex`; the canonical family name is smoke-acp-cortex-live.
1822
- # LIVE=1 ENTWURF_ACP_CORTEX_CONNECTION=<conn> ./run.sh smoke-acp-cortex-live
1823
- entwurf_require_step_outcome
1824
- if [ "${LIVE:-}" != "1" ]; then
1825
- echo "[entwurf:skip] smoke-acp-cortex-live — set LIVE=1 (+ ENTWURF_ACP_CORTEX_CONNECTION) to run."
1826
- return "$ENTWURF_STEP_SKIP_EXIT"
1827
- fi
1828
- if ! command -v cortex >/dev/null 2>&1; then
1829
- echo "[entwurf:skip] smoke-acp-cortex-live — cortex not on PATH (install Snowflake Cortex Code and complete its own login flow)."
1830
- return "$ENTWURF_STEP_SKIP_EXIT"
1831
- fi
1832
- 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
1833
1807
  }
1834
1808
 
1835
1809
  smoke_entwurf_v2_matrix_live() {
@@ -3375,13 +3349,11 @@ check_acp_provider_surface() {
3375
3349
  # Deterministic gate for the ACP provider registration surface. Loads the REAL
3376
3350
  # provider lib modules AND compiles + drives the REAL entry against a fake pi,
3377
3351
  # asserting: one surface name, the no-auth sentinel shape, full
3378
- # ProviderModelConfig rows on the curated claude anchor, the EXACT curated model
3379
- # set both adapters contribute (claude 3 + cortex 4 —
3380
- # [QK:CORTEX-PROVIDER-EXACT-ROW-SURFACE], the claim that catches an entry which
3381
- # 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
3382
3354
  # streamShellAcp backend (checked BY NAME, never invoked — invoking spawns an
3383
3355
  # ACP child). Pure, no pi runtime, no API.
3384
- section "ACP provider surface (registration + both-adapter model set)"
3356
+ section "ACP provider surface (registration + Claude-only model set)"
3385
3357
  run_ts scripts/check-acp-provider-surface.ts
3386
3358
  }
3387
3359
 
@@ -3709,30 +3681,10 @@ check_acp_carrier_augment() {
3709
3681
  }
3710
3682
 
3711
3683
  check_acp_cortex() {
3712
- # Deterministic gate for the Cortex (Snowflake Cortex Code) ACP backend — the
3713
- # first non-claude adapter on the rail, landed 0.13.0 on the CP0-measured
3714
- # contract (docs/acp-backend-rail.md, Cortex audit D1–D10). The cortex source is one
3715
- # `cortexAdapter` object (backend-adapter.ts) + its curated surface (models.ts)
3716
- # + the dual-HOME overlay (overlay.ts); the 결합 규칙 requires the gate to land
3717
- # WITH it, extending the check-acp-* family, so cortex's whole
3718
- # deterministic axis lives here. Locks (each Cortex-audit D-pinned): the GLG-decided
3719
- # 4-row curation rides real registry bases and the `cortex-` prefix routes to
3720
- # cortexAdapter; launch is `cortex acp serve` with NO -m — the model is
3721
- # enforced per turn via session/set_config_option with the native id (E);
3722
- # CORTEX_HOME presence (empty included) refuses the spawn (D3); the dual-HOME
3723
- # overlay isolates HOME+SNOWFLAKE_HOME per session key (P0-1: scope =
3724
- # backend-passed resolveSessionKey, never ambient), passes through only
3725
- # connections.toml / optional config.toml / credential_cache (D5/F — never the
3726
- # whole cache, never skills; symlink-through, no copies — AGENTS §ACP Plugin
3727
- # Boundary), authors autoUpdate:false (D4), and projects envelope-enriched
3728
- # explicit servers into cortex/mcp.json with the real HOME restored on
3729
- # entwurf-bridge alone (D9/D10), non-stdio failing loud; the
3730
- # CORTEX_ACP_COMMAND override single-quotes shell-metachar connection tokens.
3731
- # The gate self-manages the tsc-emit layer for the .js-suffixed backend-adapter
3732
- # imports; no auth, no spawn, no live cortex needed. Kill-qualified via the
3733
- # `acp-cortex` mutant lane under check-gate-qualification.
3734
- section "ACP cortex backend (Snowflake Cortex Code — 1st non-claude adapter)"
3735
- 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
3736
3688
  }
3737
3689
 
3738
3690
  check_pack() {
@@ -5283,17 +5235,9 @@ _check_pack_install_impl() {
5283
5235
  echo "$loader_out" | tail -10 | sed 's/^/ /' >&2
5284
5236
  return 1
5285
5237
  fi
5286
- # Verify the EXACT curated surface the installed package registers — both
5287
- # backends (claude 2 rows + cortex 4 rows = 6), as a SET, not as anchors.
5288
- #
5289
- # Why exact-set and not `grep -q <id>`: the cortex ids CONTAIN the claude ids
5290
- # as substrings (`cortex-claude-sonnet-5` matches a bare `claude-sonnet-5`
5291
- # grep), so a substring probe could go green on a surface that dropped every
5292
- # unprefixed Claude row. And an anchor-only probe cannot see a MISSING cortex
5293
- # row at all — that is exactly the installed-consumer gap this assertion
5294
- # closes: the source-side sets are pinned by check-acp-cortex /
5295
- # check-acp-provider-surface, but only THIS gate proves the ids survive the
5296
- # 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.
5297
5241
  #
5298
5242
  # Parse rows whose FIRST field is the provider `entwurf` (pi prints a
5299
5243
  # `provider model context …` table; the header row's first field is
@@ -5302,14 +5246,11 @@ _check_pack_install_impl() {
5302
5246
  loader_ids=$(awk '$1 == "entwurf" { print $2 }' <<<"$loader_out" | LC_ALL=C sort)
5303
5247
  loader_expected=$(printf '%s\n' \
5304
5248
  claude-fable-5-1 \
5249
+ claude-fable-5-2 \
5305
5250
  claude-opus-5-5 \
5306
- claude-sonnet-5-5 \
5307
- cortex-auto \
5308
- cortex-claude-opus-5 \
5309
- cortex-claude-sonnet-5 \
5310
- cortex-openai-gpt-5.4 | LC_ALL=C sort)
5251
+ claude-sonnet-5-5 | LC_ALL=C sort)
5311
5252
  if [ "$loader_ids" != "$loader_expected" ]; then
5312
- 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:"
5313
5254
  echo " expected:" >&2
5314
5255
  echo "$loader_expected" | sed 's/^/ /' >&2
5315
5256
  echo " actual (entwurf rows):" >&2
@@ -5321,7 +5262,7 @@ _check_pack_install_impl() {
5321
5262
  # Name the set on SUCCESS too, not only in the failure branch — same principle as
5322
5263
  # the pinned-pi version assert above: a gate that cannot name what it proved has
5323
5264
  # proved nothing, and a row COUNT without the ids cannot be audited from a CI log.
5324
- 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):"
5325
5266
  echo "$loader_ids" | sed 's/^/ /'
5326
5267
 
5327
5268
  # npm-managed neutral install regression — the README's PRIMARY install path is
@@ -7632,10 +7573,8 @@ release_gate() {
7632
7573
  # green cut said nothing about them. Under the P1 outcome protocol wiring
7633
7574
  # them costs nothing on a host that cannot run them — they SKIP honestly and
7634
7575
  # `--cut` names the missing prerequisite — while a host that CAN run them now
7635
- # must. Cortex is deliberately NOT here: its rail needs an external Snowflake
7636
- # connection the HOST owns, so wiring it would block every cut taken without
7637
- # that account; it stays a required DIRECT call for a Cortex-rail cut instead
7638
- # (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`,
7639
7578
  # `smoke-agy-native-push-live` and `smoke-acp-ordering-probe-live`: >12min by
7640
7579
  # construction, no aggregate-owned agy conversation id, and an opt-in instrument
7641
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
  );
@@ -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
  // ----------------------------------------------------------------------