@junghanacs/entwurf 0.52.0 → 0.53.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +1 -1
- package/BASELINE.md +116 -397
- package/CHANGELOG.md +131 -2
- package/CONTRIBUTING.md +4 -4
- package/DELIVERY.md +2 -2
- package/ENTWURF-CLI.json +1 -1
- package/README.md +7 -4
- package/VERIFY.md +332 -142
- package/demo/demo-baseline.sh +1 -1
- package/demo/demo.sh +1 -1
- package/docs/acp-backend-rail.md +248 -34
- package/docs/setup-clean-host.md +50 -65
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/acp-client.js +1 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/backend-adapter.js +90 -14
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/config.js +4 -9
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/context.js +15 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/model-ids.js +9 -12
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/models.js +41 -56
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/tool-surface.js +33 -6
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/fresh-call-composition.js +34 -3
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/herdr-fresh-call.js +3 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +2 -1
- package/package.json +4 -4
- package/pi/settings.reference.json +0 -1
- package/pi-extensions/acp-provider.ts +12 -6
- package/pi-extensions/lib/acp/acp-client.ts +6 -7
- package/pi-extensions/lib/acp/backend-adapter.ts +139 -15
- package/pi-extensions/lib/acp/backend.ts +261 -32
- package/pi-extensions/lib/acp/config.ts +4 -9
- package/pi-extensions/lib/acp/context.ts +17 -0
- package/pi-extensions/lib/acp/event-mapper.ts +236 -17
- package/pi-extensions/lib/acp/model-ids.js +9 -12
- package/pi-extensions/lib/acp/models.ts +46 -57
- package/pi-extensions/lib/acp/tool-surface.ts +34 -6
- package/pi-extensions/lib/fresh-call-composition.ts +37 -3
- package/pi-extensions/lib/herdr-fresh-call.ts +3 -1
- package/pi-extensions/lib/mux-fresh-call.ts +3 -1
- package/run.sh +25 -86
- package/scripts/check-acp-carrier-augment.ts +3 -21
- package/scripts/check-acp-prompt-lifecycle.ts +24 -3
- package/scripts/check-acp-provider-surface.ts +17 -26
- package/scripts/check-acp-session-reuse.ts +23 -4
- package/scripts/check-acp-stop-reason.ts +22 -3
- package/scripts/check-acp-stream-hooks.ts +24 -3
- package/scripts/check-acp-usage-accounting.ts +53 -24
- package/scripts/check-copilot-receive-arm.ts +19 -2
- package/scripts/check-fresh-call-dispatch.ts +9 -3
- package/scripts/check-gate-qualification.ts +5 -5
- package/scripts/check-herdr-fresh-call.ts +1 -0
- package/scripts/check-install-surface.ts +6 -0
- package/scripts/check-probe-ordering.ts +34 -24
- package/scripts/check-release-gate-outcomes.ts +9 -23
- package/scripts/lib/probe-cli-target.ts +3 -2
- package/scripts/mutants/acp-augment.json +0 -26
- package/scripts/mutants/acp-cortex.json +10 -183
- package/scripts/mutants/acp-foreground-only.json +11 -0
- package/scripts/mutants/acp-launch-namespace.json +3 -1
- package/scripts/mutants/acp-prompt-lifecycle.json +2 -2
- package/scripts/mutants/acp-set-model-readback.json +488 -0
- package/scripts/mutants/mux-fresh-call.json +12 -1
- package/scripts/mutants/pi-floor.json +2 -2
- package/scripts/mutants/release-gate.json +3 -3
- package/scripts/raw-acp-compaction-measure/probe.ts +8 -2
- package/scripts/smoke-acp-cortex-live.ts +4 -408
- package/scripts/smoke-acp-ordering-probe-live.ts +7 -1
- package/scripts/smoke-acp-session-reuse-live.ts +11 -2
- package/scripts/smoke-herdr-raw-install-live.sh +4 -4
- package/scripts/check-acp-cortex.ts +0 -710
|
@@ -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
|
|
423
|
-
const
|
|
424
|
-
const
|
|
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
|
|
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
|
-
|
|
1349
|
-
|
|
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
|
-
|
|
1802
|
-
|
|
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
|
|
3379
|
-
# set
|
|
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 +
|
|
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
|
-
#
|
|
3713
|
-
#
|
|
3714
|
-
|
|
3715
|
-
|
|
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
|
|
5287
|
-
#
|
|
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
|
|
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:
|
|
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
|
|
7636
|
-
#
|
|
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,
|
|
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:
|
|
563
|
-
"tiny engraving-only carrier
|
|
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
|
-
|
|
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:
|
|
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
|
|
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
|
-
//
|
|
131
|
-
//
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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:
|
|
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
|
|
204
|
-
`${CURATED_ANCHOR_MODEL_ID}
|
|
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
|
-
|
|
56
|
-
|
|
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:
|
|
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
|
|
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
|
-
|
|
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:
|
|
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
|
|
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
|
-
|
|
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:
|
|
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
|
|
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
|
// ----------------------------------------------------------------------
|