@junghanacs/entwurf 0.35.0 → 0.35.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/CHANGELOG.md +129 -0
- package/VERIFY.md +5 -4
- package/docs/acp-backend-rail.md +28 -2
- package/docs/setup-clean-host.md +1 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/backend-adapter.js +46 -2
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/model-ids.js +67 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/models.js +9 -20
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-visible-resume.js +11 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/fresh-call-dispatch.js +27 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/herdr-fresh-call.js +3 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +2 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/resume-launch-identity.js +13 -4
- package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-pi-durable.js +119 -11
- package/package.json +2 -2
- package/pi-extensions/lib/acp/backend-adapter.ts +52 -2
- package/pi-extensions/lib/acp/model-ids.js +77 -0
- package/pi-extensions/lib/acp/models.ts +15 -22
- package/pi-extensions/lib/entwurf-v2-visible-resume.ts +12 -0
- package/pi-extensions/lib/fresh-call-dispatch.ts +25 -1
- package/pi-extensions/lib/herdr-fresh-call.ts +5 -0
- package/pi-extensions/lib/mux-fresh-call.ts +3 -0
- package/pi-extensions/lib/resume-launch-identity.ts +12 -4
- package/pi-extensions/meta-bridge-pi-durable.ts +153 -13
- package/run.sh +81 -20
- package/scripts/check-acp-backend-preflight.ts +10 -2
- package/scripts/check-acp-cortex.ts +7 -6
- package/scripts/check-acp-prompt-lifecycle.ts +2 -2
- package/scripts/check-acp-provider-surface.ts +1 -1
- package/scripts/check-acp-session-reuse.ts +5 -5
- package/scripts/check-acp-stop-reason.ts +2 -2
- package/scripts/check-acp-stream-hooks.ts +2 -2
- package/scripts/check-acp-usage-accounting.ts +2 -2
- package/scripts/check-fresh-call-dispatch.ts +47 -4
- package/scripts/check-gate-qualification.ts +6 -4
- package/scripts/check-probe-ordering.ts +13 -6
- package/scripts/check-resume-launch-identity.ts +38 -0
- package/scripts/mutants/acp-cortex.json +17 -8
- package/scripts/mutants/acp-set-model-readback.json +28 -0
- package/scripts/mutants/fresh-call-dispatch.json +11 -0
- package/scripts/mutants/herdr-dev-checkout.json +70 -0
- package/scripts/mutants/pi-durable.json +117 -0
- package/scripts/mutants/resume-launch-identity.json +15 -0
- package/scripts/mutants/v2-visible-resume.json +11 -0
- package/scripts/resolve-acp-bridge.ts +1 -1
- package/scripts/smoke-acp-bundled-mcp-live.ts +1 -1
- package/scripts/smoke-acp-carrier-augment-live.ts +1 -1
- package/scripts/smoke-acp-long-turn-live.ts +1 -1
- package/scripts/smoke-acp-mcp-live.ts +1 -1
- package/scripts/smoke-acp-memory-containment-live.ts +1 -1
- package/scripts/smoke-acp-ordering-probe-live.ts +1 -1
- package/scripts/smoke-acp-overlay-live.ts +1 -1
- package/scripts/smoke-acp-provider-live.ts +1 -1
- package/scripts/smoke-acp-raw-turn-live.ts +1 -1
- package/scripts/smoke-acp-session-reuse-live.ts +1 -1
- package/scripts/smoke-acp-skill-live.ts +1 -1
- package/scripts/smoke-acp-socket-citizen-live.ts +2 -2
- package/scripts/smoke-acp-v2-send-live.ts +1 -1
- package/scripts/smoke-entwurf-chain-live.ts +1 -1
- package/scripts/smoke-mux-lifecycle-live.ts +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,135 @@ All notable changes to this project will be documented here. Format follows [Kee
|
|
|
4
4
|
|
|
5
5
|
## Unreleased
|
|
6
6
|
|
|
7
|
+
## 0.35.1 - 2026-10-13
|
|
8
|
+
|
|
9
|
+
### ACP
|
|
10
|
+
|
|
11
|
+
- **Claude rows move to Opus/Sonnet 5.5 (#128).** The curated `entwurf` Claude ids are
|
|
12
|
+
now `claude-opus-5-5`, `claude-sonnet-5-5` and `claude-fable-5-1`; the Cortex rows keep
|
|
13
|
+
their native 5 ids. Driver `claude-agent-acp` stays 0.85.1, whose SDK already lists the
|
|
14
|
+
5.5 ids. Curation and launch checks alone do not certify a live 5.5 turn; the narrow
|
|
15
|
+
LIVE evidence is recorded under the release boundary below. The retired
|
|
16
|
+
`claude-opus-5` / `claude-sonnet-5` are not aliased: an Entwurf-built pi launch
|
|
17
|
+
(`entwurf_fresh_call` `entwurf/<id>`, or `entwurf_resume_call` of a session recorded on
|
|
18
|
+
one) refuses them as `acp-model-not-curated` before a pane opens, because Pi's substring
|
|
19
|
+
resolver would otherwise open them on the Cortex rows. Records and transcripts are not
|
|
20
|
+
rewritten.
|
|
21
|
+
- **Claude set-model answers are read back before the prompt (#128).** The driver answers
|
|
22
|
+
`claude-sonnet-5-5` and `claude-opus-5-5` with its alias rows `sonnet` / `opus`, named
|
|
23
|
+
`Sonnet 5.5` / `Opus 5.5`, and `claude-fable-5-1` with its own row (measured against
|
|
24
|
+
0.85.1 without a prompt). The claude adapter now accepts only that answer: a missing
|
|
25
|
+
model option, another row, or any row other than that exact value/name pair refuses the
|
|
26
|
+
turn before the prompt instead of letting a different model answer under the requested
|
|
27
|
+
id. Cortex answers are not read back; they are unmeasured.
|
|
28
|
+
|
|
29
|
+
### Developer checkout
|
|
30
|
+
|
|
31
|
+
- **A source-only menu returns a host to its checkout.** `./run.sh menu` offers a read-only
|
|
32
|
+
readiness check, or switches to the developer board: after that same check and the exact
|
|
33
|
+
confirmation `DEV`, it runs the existing `plugins/herdr/use-dev-checkout.sh --apply`, which
|
|
34
|
+
deactivates the currently owned installation and then links Pi / Claude Code wiring and
|
|
35
|
+
Herdr's local plugin to this checkout. It needs an interactive terminal, runs only from a
|
|
36
|
+
source checkout, and adds no installer, npm acquisition, retry or recovery. Its prompts
|
|
37
|
+
and messages are English.
|
|
38
|
+
|
|
39
|
+
### pi-durable
|
|
40
|
+
|
|
41
|
+
- **A read withdraws the inbox notices it made stale (#140).** A doorbell queues one native
|
|
42
|
+
follow-up per arriving message, while `entwurf_inbox_read` drains them all, so each
|
|
43
|
+
leftover notice started a run that read an empty inbox. After a read attempt, the
|
|
44
|
+
pi-durable host aborts (`Submission.abort`) its own queued notices whose announced mail is
|
|
45
|
+
no longer unread. Reads and rings share one gate. A failed inspection of the mailbox
|
|
46
|
+
retains the tracked notices; a failed abort releases its tracked handle without retry.
|
|
47
|
+
Both are named on stderr and leave the read's outcome unchanged. This does not claim the
|
|
48
|
+
native queued notice was removed after a failed abort, nor crash-safe receive.
|
|
49
|
+
|
|
50
|
+
### Release boundary and verification
|
|
51
|
+
|
|
52
|
+
- **Driver unchanged by operator instruction.** `claude-agent-acp` stays 0.85.1. The 0.89.1
|
|
53
|
+
line (ACP SDK 1.6 → 1.8, agent SDK 0.3.293) is a separate lane and is not read as
|
|
54
|
+
compatible here.
|
|
55
|
+
- **Local evidence is focused, not a whole pass.** Each slice ran its affected focused gates
|
|
56
|
+
and its affected qualification groups once as PARTIAL receipts, each with a green
|
|
57
|
+
control: the #128 launch cut killed 99/99 claims in 12 groups, the set-model readback
|
|
58
|
+
75/75 in 12 groups, and the menu 6/6 in one group. The set now holds 956 claims across
|
|
59
|
+
72 lanes. By operator instruction the FULL floor and the whole qualification BODY were
|
|
60
|
+
not repeated locally during development; both ran in the first prepared SHA's exact CI.
|
|
61
|
+
Push run 38258514409 at `b93bb37` first failed `check` at `check-herdr-sandbox`
|
|
62
|
+
`[QK:HS-NO-RESIDUE]`: a sandbox process still held the private HOME after
|
|
63
|
+
`herdr server stop` and the gate's fixed 2 s wait. A rerun of the failed job on the same
|
|
64
|
+
SHA passed that assertion, and the run concluded with all four required jobs green and
|
|
65
|
+
the qualification body at 956/956 claims killed. The first red stays on record as a
|
|
66
|
+
teardown-timing observation; it is not read as a pass.
|
|
67
|
+
- **LIVE release gate, native Codex excluded for this cut.** As in 0.35.0, the operator
|
|
68
|
+
excluded native Codex: its MUST step stays wired and declined for lack of the
|
|
69
|
+
operator-supplied app-server pid and model tiers, so no Codex app-server, sibling or
|
|
70
|
+
turn started. That SKIP is an unmeasured per-cut exception, not a PASS or a standing
|
|
71
|
+
waiver. The gate ran from fresh scratch at `b93bb37`, with HEAD, index and tracked tree
|
|
72
|
+
identical before and after: `/tmp/entwurf-release-gate-0.35.1.aXzo1A/release-gate.log`,
|
|
73
|
+
MUST PASS=23 FAIL=0 SKIP=1 (only native Codex), BEHAVIOR PASS=1 FAIL=0 SKIP=0, standard
|
|
74
|
+
`--cut` exit 1 / BLOCKED (MUST SKIP). An earlier attempt failed only its static step,
|
|
75
|
+
because the checkout's bridge `dist` was older than the source mtimes
|
|
76
|
+
(`check-bridge-delivery` staleness); the bridge was rebuilt and the whole gate re-run,
|
|
77
|
+
and only the re-run is the receipt.
|
|
78
|
+
- **What the 5.5 evidence covers.** The ACP steps drove `claude-sonnet-5-5` prompt turns,
|
|
79
|
+
the provider-path ones through the set-model readback above. `claude-opus-5-5` ran only
|
|
80
|
+
as a turn-free socket citizen whose record and `get_info` report
|
|
81
|
+
`entwurf/claude-opus-5-5`; neither it nor Fable 5.1 has prompt-turn evidence. Before the
|
|
82
|
+
gate, one no-prompt wire measurement against 0.85.1 recorded the set-model answers for
|
|
83
|
+
all three ids, and one operator-started ACP turn on `claude-sonnet-5-5` reported its own
|
|
84
|
+
exact model id; that is a model's self-report, not a gate.
|
|
85
|
+
- **Unpublished candidate carrier.** As in 0.35.0, `plugins/herdr/runtime-lock.json`
|
|
86
|
+
temporarily names the closed `herdr-checkout` source: an npm-source lock must agree with
|
|
87
|
+
the checkout's core version, and 0.35.1 has no published integrity yet. The Herdr
|
|
88
|
+
manifest stays 0.35.0. After the operator publishes core 0.35.1, a separate Herdr
|
|
89
|
+
snapshot re-pins the measured registry bytes.
|
|
90
|
+
- **Publication boundary.** This record amendment carries no source change and takes its
|
|
91
|
+
own exact CI, qualification body included, before the make artifact, core tag and GitHub
|
|
92
|
+
Release. npm publication belongs to the operator; the Herdr re-pin follows it.
|
|
93
|
+
|
|
94
|
+
## v2026.10.12-herdr.0.35.0 — Herdr Entwurf plugin 0.35.0
|
|
95
|
+
|
|
96
|
+
A separate Herdr plugin snapshot, not another core release: core package0.35.0 and
|
|
97
|
+
its `v0.35.0` tag remain unchanged. The operator has published the core to npm.
|
|
98
|
+
|
|
99
|
+
### Herdr plugin
|
|
100
|
+
|
|
101
|
+
- **Re-pin the published core0.35.0 and close the checkout window.**
|
|
102
|
+
`plugins/herdr/runtime-lock.json` returns to production npm source
|
|
103
|
+
`@junghanacs/entwurf@0.35.0` with the registry's measured sha512
|
|
104
|
+
(`sha512-ENivJbtr…umf2zA==`). The plugin manifest moves0.34.2→0.35.0;
|
|
105
|
+
admission remains Herdr>=0.9.0 and the one-build/one-pane/Linux shape is unchanged.
|
|
106
|
+
- **The same core capability through Herdr.** This snapshot follows the renewed demo
|
|
107
|
+
and source-tooling restoration, plus #137's conservative TOML-only qualification
|
|
108
|
+
refinement. It records no new demo or model turn and changes no rail, gate or mutant.
|
|
109
|
+
- **Upgrade by acquisition source, not just version.** A settled npm-source0.34.2
|
|
110
|
+
installation can rebind under the plugin README's ownership conditions. A host that
|
|
111
|
+
installed the intervening `herdr-checkout` candidate must explicitly deactivate by
|
|
112
|
+
its own absolute runtime path before reinstalling the npm-source snapshot.
|
|
113
|
+
No operator installation or active session is rewired by this release work.
|
|
114
|
+
|
|
115
|
+
### Source-specific verification
|
|
116
|
+
|
|
117
|
+
- Registry metadata reports version/latest0.35.0, repair0.12.8-repair.1. The downloaded
|
|
118
|
+
tarball is11083782 bytes, SHA256
|
|
119
|
+
`82ae9ab99b799c987fe0dac4bb76fc406afe42088a128a131af4d69a97592e71`, equal to the preserved
|
|
120
|
+
core release candidate. Independently computed sha512 equals `dist.integrity`;
|
|
121
|
+
the registry-shipped runtime helper is byte-equal to this checkout's.
|
|
122
|
+
- Required exact-registry-tarball Docker consumer passed without repacking. Real-npm
|
|
123
|
+
0.34.2→0.35.0 swap and harness-seeded torn-swap recovery passed: the product restored
|
|
124
|
+
the last-good tree before reacquisition, certified the new registry digest, then
|
|
125
|
+
performed an exact-request no-op and owned-fixture removal. Seven focused
|
|
126
|
+
Herdr/manifests gates passed; HEAD/index/work surface stayed unchanged during that run.
|
|
127
|
+
- At preparation time, public-ref install/use/deactivate/reinstall and the exact pushed
|
|
128
|
+
plugin SHA's ordinary four-job CI/#137 next-plugin BODY-SKIP observation are still
|
|
129
|
+
pending. Their actual outcomes must be recorded in the release checkpoint and #137
|
|
130
|
+
before tagging/closing; this paragraph is not their acceptance. Earlier npm or
|
|
131
|
+
checkout-carrier receipts cannot certify these source-specific axes.
|
|
132
|
+
- Core0.35.0's whole940 qualification and native Codex LIVE remain explicit per-cut
|
|
133
|
+
unmeasured exceptions, never PASS or standing waivers. This plugin-only metadata
|
|
134
|
+
change does not run the approximately95-minute BODY or another core release aggregate.
|
|
135
|
+
|
|
7
136
|
## 0.35.0 - 2026-10-12
|
|
8
137
|
|
|
9
138
|
### Collaboration and qualification
|
package/VERIFY.md
CHANGED
|
@@ -116,7 +116,7 @@ that moved out of `scripts/` and into a file beside its subject would otherwise
|
|
|
116
116
|
>
|
|
117
117
|
> A cut that touches the prompt-lifecycle contract (no wall clock on a running turn) owes one long-turn acceptance the aggregate floor is too short to carry: `LIVE=1 ./run.sh smoke-acp-long-turn-live` drives a real turn whose tool work outlasts the retired 600s cutoff and requires exactly one cold ACP bootstrap in the transcript. It takes >12 minutes by construction and is on-demand, not part of `release-gate`.
|
|
118
118
|
>
|
|
119
|
-
> **The integrated mux lifecycle IS a release-gate MUST.** `LIVE=1 ./run.sh smoke-mux-lifecycle-live` enters through a real MCP `tools/call` on the shipped bridge launcher and follows one citizen the whole way: fresh-call launch receipt, the sibling's own nonce callback whose SENDER ENVELOPE is the address, an `entwurf_v2` control-socket send that lands in the sibling's own transcript, `entwurf_resume_call` REFUSED while that citizen is live, a stable-handle close proving pane and socket gone with the record preserved, delivery to the dormant citizen refused honestly, then the public `entwurf_resume_call` — whose LAUNCH and OBSERVATION receipts stay apart — and finally an `entwurf_v2` send the resumed session answers with a fact it was told BEFORE the window closed. That last step is the point: a socket answering under the same garden id proves a process stood up at that address, and only the recall proves the same conversation came back. Both pi provider shapes are driven to it (native `openai-codex/gpt-6-luna` and recorded-ACP `entwurf/claude-sonnet-5`, the only shape that re-injects a bridge extension), and the Claude Code cell ends where its capability ends: its resume is refused as `target-not-pi` with no window opened and no lock residue (the per-gid coordination lock is released).
|
|
119
|
+
> **The integrated mux lifecycle IS a release-gate MUST.** `LIVE=1 ./run.sh smoke-mux-lifecycle-live` enters through a real MCP `tools/call` on the shipped bridge launcher and follows one citizen the whole way: fresh-call launch receipt, the sibling's own nonce callback whose SENDER ENVELOPE is the address, an `entwurf_v2` control-socket send that lands in the sibling's own transcript, `entwurf_resume_call` REFUSED while that citizen is live, a stable-handle close proving pane and socket gone with the record preserved, delivery to the dormant citizen refused honestly, then the public `entwurf_resume_call` — whose LAUNCH and OBSERVATION receipts stay apart — and finally an `entwurf_v2` send the resumed session answers with a fact it was told BEFORE the window closed. That last step is the point: a socket answering under the same garden id proves a process stood up at that address, and only the recall proves the same conversation came back. Both pi provider shapes are driven to it (native `openai-codex/gpt-6-luna` and recorded-ACP `entwurf/claude-sonnet-5-5`, the only shape that re-injects a bridge extension), and the Claude Code cell ends where its capability ends: its resume is refused as `target-not-pi` with no window opened and no lock residue (the per-gid coordination lock is released).
|
|
120
120
|
>
|
|
121
121
|
> One cell cannot be fully fenced, and that is recorded rather than glossed: the recorded-ACP pi cell runs under the operator's REAL home, because its provider spawns `claude` as a child that authenticates from there — measured, a fenced HOME kills that turn with `Authentication required` before the model is ever reached, and `CLAUDE_CONFIG_DIR` alone does not fix it. That is the same trade `smoke-acp-bundled-mcp-live` already makes. While that cell runs, its citizen's control socket and per-gid lock exist under the operator's own `~/.pi/entwurf-control` and `~/.pi/entwurf-v2-locks`; its meta store stays fixture, so no garden record is minted outside the fixture, and the run ends by proving all six real roots hold the entry sets they started with, with no fixture garden id and no leftover lock.
|
|
122
122
|
>
|
|
@@ -278,7 +278,7 @@ consumer image outside this standing protocol.
|
|
|
278
278
|
### Verifying the two capabilities a gate cannot fully judge
|
|
279
279
|
|
|
280
280
|
- **Garden-id delivery:** discover a target with `entwurf_peers`, then `entwurf_v2 fire-and-forget` — the intent axis is single-valued since the visible-first cut. It reaches a live pi target over its control socket, a deliverable self-fetch citizen through the mailbox, and a probe-alive native-push conversation by direct injection. A DORMANT in-domain citizen rejects as `dormant-fire-forget-unsupported` — delivery never wakes it; reopen it with the separate lifecycle verb `entwurf_resume_call {target}`, then dispatch again. A wrong target state is rejected, never auto-fixed and never launched into.
|
|
281
|
-
- **ACP continuity:** a direct `pi --provider entwurf --model claude-sonnet-5` turn, or the `smoke-acp-session-reuse-live` gate (process-scoped reuse + recall). Multi-turn reuse is proven by that gate, not by any v1 resume tool.
|
|
281
|
+
- **ACP continuity:** a direct `pi --provider entwurf --model claude-sonnet-5-5` turn, or the `smoke-acp-session-reuse-live` gate (process-scoped reuse + recall). Multi-turn reuse is proven by that gate, not by any v1 resume tool.
|
|
282
282
|
- **agy citizenship:** in a fresh agy conversation, the first `PreInvocation` must yield a garden id, `entwurf_self` must report `agentId=meta-session/antigravity` and `replyable:true` only while the native probe is alive, and a reply to that same garden id must direct-inject into the same conversation. No mailbox/receiver-marker evidence counts on this rail.
|
|
283
283
|
|
|
284
284
|
### What NOT to do — bypassing the operational path
|
|
@@ -393,7 +393,7 @@ strips Claude hooks (`hooks: {}` overlay). Strip those carriers:
|
|
|
393
393
|
cd /path/to/scratch
|
|
394
394
|
env -u CLAUDE_CONFIG_DIR -u PI_SESSION_ID -u PI_AGENT_ID \
|
|
395
395
|
LIVE=1 /path/to/entwurf/run.sh release-gate /path/to/scratch --cut
|
|
396
|
-
pi --provider entwurf --model claude-sonnet-5 -p "reply with ok only" # one-turn smoke
|
|
396
|
+
pi --provider entwurf --model claude-sonnet-5-5 -p "reply with ok only" # one-turn smoke
|
|
397
397
|
```
|
|
398
398
|
|
|
399
399
|
The one-turn smoke is a **provider**-surface check (auth + model routing + a real
|
|
@@ -421,7 +421,7 @@ frozen here.
|
|
|
421
421
|
|
|
422
422
|
## 1A. Main Agent Evaluation — Is `entwurf` Claude strong enough?
|
|
423
423
|
|
|
424
|
-
Separate from continuity gates. Gates prove "sessions continue"; this questionnaire examines tool self-awareness / native tool usability / MCP-boundary awareness / long-turn focus / quality vs direct Claude Code. Run it against `entwurf/claude-sonnet-5` via a direct `pi --provider entwurf` turn (or a live ACP session); accumulate turns by re-prompting the same target.
|
|
424
|
+
Separate from continuity gates. Gates prove "sessions continue"; this questionnaire examines tool self-awareness / native tool usability / MCP-boundary awareness / long-turn focus / quality vs direct Claude Code. Run it against `entwurf/claude-sonnet-5-5` via a direct `pi --provider entwurf` turn (or a live ACP session); accumulate turns by re-prompting the same target.
|
|
425
425
|
|
|
426
426
|
### 1A.0 Two carrier surfaces — engraving vs pi-context-augment (load-bearing)
|
|
427
427
|
|
|
@@ -459,6 +459,7 @@ The single-turn / multi-turn / cross-process / persistence-boundary / shutdown i
|
|
|
459
459
|
| Tool-call / event mapping | `check-acp-event-mapper`, `smoke-acp-provider-live` |
|
|
460
460
|
| Prompt lifecycle — no wall-clock cutoff on a running turn; abort ends it by ACP `session/cancel` with bounded cleanup; a child death is reported with exit status + stderr on new AND reuse turns; our prompt-phase error text is not classified transient by pi | `check-acp-prompt-lifecycle`, `check-probe-ordering` |
|
|
461
461
|
| Claude typed `incomplete_tool_call` prompt failure — real pi retry/overflow classifiers reject the fixed verdict, raw evidence stays in existing diagnostics, exactly one seal and drop on new/reuse; other failures retain the legacy stderr path, cancel stays separate. A deterministic receipt does not certify the target vendor runtime. No LIVE gate induces this rejection; the raw-turn and reuse smokes certify only their own rows above. | `check-acp-vendor-rejection` (four mutants) |
|
|
462
|
+
| Claude set-model answer read back before the prompt — an exact row, or the one alias row (value and name) measured for the requested id; a missing model option, another row or any other value/name pair refuses with non-transient text. Deterministic only: the measured 0.85.1 answer shapes are fixture literals, not a runtime certificate. | `check-acp-set-model-readback` (two mutants), `check-probe-ordering` |
|
|
462
463
|
| Operator mcpServers / skills reach the live session | `smoke-acp-mcp-live`, `smoke-acp-skill-live`, `check-acp-config` |
|
|
463
464
|
| Overlay isolation + memory containment | `check-acp-overlay`, `smoke-acp-memory-containment-live`, `check-acp-tool-surface` |
|
|
464
465
|
|
package/docs/acp-backend-rail.md
CHANGED
|
@@ -379,9 +379,9 @@ operator's repair named.
|
|
|
379
379
|
|
|
380
380
|
| Seam | Claude | Cortex Code |
|
|
381
381
|
|---|---|---|
|
|
382
|
-
| Model ids | unprefixed `claude-sonnet-5`, `claude-opus-5`, `claude-fable-5-1` | `cortex-auto`, `cortex-claude-opus-5`, `cortex-claude-sonnet-5`, `cortex-openai-gpt-5.4`; prefix stripped before set-model |
|
|
382
|
+
| Model ids | unprefixed `claude-sonnet-5-5`, `claude-opus-5-5`, `claude-fable-5-1` | `cortex-auto`, `cortex-claude-opus-5`, `cortex-claude-sonnet-5`, `cortex-openai-gpt-5.4`; prefix stripped before set-model |
|
|
383
383
|
| Launch | bundled `claude-agent-acp`; `CLAUDE_AGENT_ACP_COMMAND` override | `cortex acp serve`; optional connection; `CORTEX_ACP_COMMAND` override; never a launch-time `-m` |
|
|
384
|
-
| Model authority | per-turn ACP set-model | per-turn ACP set-model; an unavailable curated id fails before prompt |
|
|
384
|
+
| Model authority | per-turn ACP set-model, its answer read back before the prompt | per-turn ACP set-model; an unavailable curated id fails before prompt; the answer is not read back |
|
|
385
385
|
| Carrier | engraving in `_meta.systemPrompt` | no system-prompt carrier; engraving rides the first-user augment |
|
|
386
386
|
| Overlay | `CLAUDE_CONFIG_DIR` whitelist, configured-empty hooks, native memory hidden | session-scoped isolated HOME + `SNOWFLAKE_HOME`, private `cortex/mcp.json`, measured-minimum auth passthrough |
|
|
387
387
|
| MCP | explicit wire `mcpServers` | explicit declarations projected to private `mcp.json` because Cortex ignores the wire field |
|
|
@@ -390,6 +390,32 @@ operator's repair named.
|
|
|
390
390
|
|
|
391
391
|
### Claude
|
|
392
392
|
|
|
393
|
+
The unprefixed Claude curation uses exact Opus/Sonnet 5.5 ids; the ACP adapter does
|
|
394
|
+
not route the retired exact ids `claude-opus-5` or `claude-sonnet-5`, alias them to
|
|
395
|
+
a newer model, or rewrite existing session records. Cortex retains its separate
|
|
396
|
+
native 5 ids. Pi owns fuzzy CLI model selection before the adapter: with this
|
|
397
|
+
seven-row set, its 1.1.0 resolver matches the old bare 5 patterns to the Cortex rows
|
|
398
|
+
instead of refusing them (measured with the pure upstream resolver and synthetic rows,
|
|
399
|
+
not a vendor turn). So the launches Entwurf builds admit only an EXACT curated id
|
|
400
|
+
(`pi-extensions/lib/acp/model-ids.js`): `entwurf_fresh_call` with backend `pi` and an
|
|
401
|
+
`entwurf/<id>` model, and `entwurf_resume_call` of a session whose emitted provider is
|
|
402
|
+
`entwurf`, refuse anything else as `acp-model-not-curated` before a pane opens. A human
|
|
403
|
+
typing `pi --model …` keeps the upstream fuzzy behaviour, so use the full 5.5 ids there.
|
|
404
|
+
|
|
405
|
+
The adapter reads its set-model answer back before the prompt. The driver resolves the
|
|
406
|
+
requested id onto one of the rows it offers and answers with that row's `value` as the
|
|
407
|
+
model option's `currentValue` (0.85.1 `dist/acp-agent.js:5459-5499`). Measured on
|
|
408
|
+
2026-10-12 against 0.85.1 with no prompt sent: `claude-fable-5-1` answers its own row,
|
|
409
|
+
while `claude-sonnet-5-5` and `claude-opus-5-5` answer the alias rows `sonnet` and
|
|
410
|
+
`opus`, named `Sonnet 5.5` and `Opus 5.5`. The id an alias row resolves to is not on the
|
|
411
|
+
ACP wire, so `CLAUDE_SET_MODEL_ALIAS_ROWS` (`backend-adapter.ts`) declares, for each
|
|
412
|
+
alias-answered id, the one measured row it is accepted as, value and name together. A
|
|
413
|
+
missing model option, another row, or any other value/name pair refuses the turn before
|
|
414
|
+
the prompt, with text pi does not classify as transient. A curated id added later needs
|
|
415
|
+
its own measured answer before it ships. `check-acp-set-model-readback` holds the
|
|
416
|
+
contract offline; it does not certify the installed driver's runtime, and no 5.5 prompt
|
|
417
|
+
turn is claimed by this measurement.
|
|
418
|
+
|
|
393
419
|
The bundled adapter resolves before any PATH fallback. Its overlay retains only the
|
|
394
420
|
auth/runtime state required by the Claude Agent SDK and hides operator memory, hooks,
|
|
395
421
|
agents, history, and local settings. Rich project/operator context rides the first-user
|
package/docs/setup-clean-host.md
CHANGED
|
@@ -913,7 +913,7 @@ operator-installed Claude runtime. Resolution order:
|
|
|
913
913
|
2. `require.resolve(...)` against the bundled `@agentclientprotocol/claude-agent-acp`.
|
|
914
914
|
3. `PATH:claude-agent-acp` fallback if package resolution fails.
|
|
915
915
|
|
|
916
|
-
The curated Claude ids are `claude-opus-5`, `claude-sonnet-5`, and
|
|
916
|
+
The curated Claude ids are `claude-opus-5-5`, `claude-sonnet-5-5`, and
|
|
917
917
|
`claude-fable-5-1`. Cortex exposes `cortex-auto`, `cortex-claude-opus-5`,
|
|
918
918
|
`cortex-claude-sonnet-5`, and `cortex-openai-gpt-5.4`.
|
|
919
919
|
`entwurfProvider.cortexConnection` or `ENTWURF_ACP_CORTEX_CONNECTION` selects
|
|
@@ -110,6 +110,44 @@ function resolveClaudeLaunch() {
|
|
|
110
110
|
* ambient value cannot turn it off. Cortex declares nothing of the kind.
|
|
111
111
|
*/
|
|
112
112
|
export const CLAUDE_FOREGROUND_ONLY_ENV = { CLAUDE_CODE_DISABLE_BACKGROUND_TASKS: "1" };
|
|
113
|
+
/**
|
|
114
|
+
* The set-model answer is read back before the prompt (#128). claude-agent-acp resolves the
|
|
115
|
+
* requested id onto one of the rows it offers and answers with that row's `value` as the model
|
|
116
|
+
* option's `currentValue` (0.85.1 `dist/acp-agent.js:5459-5499`). `[측정 2026-10-12]` 0.85.1, no
|
|
117
|
+
* prompt: `claude-fable-5-1` answers its own row, while `claude-sonnet-5-5` and `claude-opus-5-5`
|
|
118
|
+
* answer the alias rows `sonnet` and `opus`, named `Sonnet 5.5` and `Opus 5.5`. The id an alias row
|
|
119
|
+
* resolves to (`ModelInfo.resolvedModel`) is not on the ACP wire, so an alias answer is accepted
|
|
120
|
+
* only as the exact measured row, value AND name, declared here for the requested id. No other
|
|
121
|
+
* spelling is matched.
|
|
122
|
+
*/
|
|
123
|
+
export const CLAUDE_SET_MODEL_ALIAS_ROWS = Object.freeze({
|
|
124
|
+
"claude-sonnet-5-5": Object.freeze({ value: "sonnet", name: "Sonnet 5.5" }),
|
|
125
|
+
"claude-opus-5-5": Object.freeze({ value: "opus", name: "Opus 5.5" }),
|
|
126
|
+
});
|
|
127
|
+
/** Refuse unless the set-model answer names the requested model. A missing model option, another
|
|
128
|
+
* row, or a row that is not the measured alias row would let a different model answer under this id. */
|
|
129
|
+
export function confirmClaudeSetModelAnswer(response, nativeModelId) {
|
|
130
|
+
const configOptions = response?.configOptions;
|
|
131
|
+
const modelOption = Array.isArray(configOptions)
|
|
132
|
+
? configOptions.find((option) => option?.id === "model")
|
|
133
|
+
: undefined;
|
|
134
|
+
const currentValue = modelOption?.currentValue;
|
|
135
|
+
if (typeof currentValue === "string" && currentValue === nativeModelId)
|
|
136
|
+
return;
|
|
137
|
+
const rows = Array.isArray(modelOption?.options)
|
|
138
|
+
? modelOption.options.flatMap((row) => Array.isArray(row?.options) ? row.options : [row])
|
|
139
|
+
: [];
|
|
140
|
+
const selected = typeof currentValue === "string" ? rows.find((row) => row?.value === currentValue) : undefined;
|
|
141
|
+
const alias = CLAUDE_SET_MODEL_ALIAS_ROWS[nativeModelId];
|
|
142
|
+
if (alias !== undefined && currentValue === alias.value && selected?.name === alias.name)
|
|
143
|
+
return;
|
|
144
|
+
const answered = modelOption === undefined
|
|
145
|
+
? "no model option"
|
|
146
|
+
: `model ${jsonText(currentValue)} named ${jsonText(selected?.name ?? null)}`;
|
|
147
|
+
throw new Error(`entwurf: claude-agent-acp answered set-model ${JSON.stringify(nativeModelId)} with ${answered}, which does not ` +
|
|
148
|
+
`name the requested model. Refusing before the prompt so a different model cannot answer under this id; ` +
|
|
149
|
+
`the installed driver and the curated id disagree, and repeating the turn cannot change that.`);
|
|
150
|
+
}
|
|
113
151
|
export const claudeAdapter = {
|
|
114
152
|
backend: "claude",
|
|
115
153
|
// Claude owns its UNPREFIXED curated ids only (rail “Adapter contract”). The native id
|
|
@@ -161,7 +199,12 @@ export const claudeAdapter = {
|
|
|
161
199
|
if (typeof setConfig !== "function") {
|
|
162
200
|
throw new Error(`setSessionConfigOption unsupported — cannot enforce model ${modelId}`);
|
|
163
201
|
}
|
|
164
|
-
await setConfig.call(connection, {
|
|
202
|
+
const answer = await setConfig.call(connection, {
|
|
203
|
+
sessionId: acpSessionId,
|
|
204
|
+
configId: "model",
|
|
205
|
+
value: nativeModelId,
|
|
206
|
+
});
|
|
207
|
+
confirmClaudeSetModelAnswer(answer, nativeModelId);
|
|
165
208
|
},
|
|
166
209
|
configSignatureFields(_adapterSettings) {
|
|
167
210
|
// Claude folds no extra backend-specific fields beyond backend + nativeModelId
|
|
@@ -302,7 +345,8 @@ export const cortexAdapter = {
|
|
|
302
345
|
// serves fails loud BEFORE the prompt (`Unsupported model: …`). PR #40's
|
|
303
346
|
// launch-time `-m` pin was retired for this: set-model is the single model
|
|
304
347
|
// authority (resolveLaunch never passes `-m`), and "auto" is set explicitly
|
|
305
|
-
// rather than treated as an unspoken default.
|
|
348
|
+
// rather than treated as an unspoken default. NO answer readback, unlike claude: nobody has
|
|
349
|
+
// measured what cortex's set_config_option response carries, so a check would be a guess.
|
|
306
350
|
async enforceModel({ connection, acpSessionId, nativeModelId, modelId }) {
|
|
307
351
|
const setConfig = connection.setSessionConfigOption;
|
|
308
352
|
if (typeof setConfig !== "function") {
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// ACP plugin — the curated model IDs, and nothing else (#128).
|
|
2
|
+
//
|
|
3
|
+
// This leaf imports NOTHING. `models.ts` builds the registry rows from the pi-ai
|
|
4
|
+
// catalog; the launch seams (`fresh-call-dispatch.ts`, `resume-launch-identity.ts`)
|
|
5
|
+
// only need to know which IDs exist, and they ride the pi-free bridge closure, so
|
|
6
|
+
// they read the IDs here rather than load a vendor catalog to learn them.
|
|
7
|
+
//
|
|
8
|
+
// WHY `.js`: the same reason as `protocol.js`. Both the emit-capable root program
|
|
9
|
+
// (allowJs) and Node --experimental-strip-types resolve an explicit `.js` import
|
|
10
|
+
// literally to this file, so `models.ts` (root fence, `.js` specifiers) and the
|
|
11
|
+
// strip-types gates that load `models.ts` directly reach one SSOT. A `.ts` leaf
|
|
12
|
+
// would need a `.ts` specifier the root config cannot allow.
|
|
13
|
+
//
|
|
14
|
+
// WHY THE LAUNCH SEAMS ASK AT ALL. Pi's `--model` resolver falls back to a
|
|
15
|
+
// case-insensitive substring match among the provider's rows and prefers the
|
|
16
|
+
// lexically later id. Under one `entwurf` provider a retired `claude-sonnet-5`
|
|
17
|
+
// is a substring of `cortex-claude-sonnet-5`, so a launch Entwurf builds would
|
|
18
|
+
// silently open on the Snowflake Cortex rail instead of the Claude subscription.
|
|
19
|
+
// An exact match against this list is the only admission; nothing is aliased,
|
|
20
|
+
// substituted or rewritten.
|
|
21
|
+
/** The single ACP provider id under which every curated row is registered. */
|
|
22
|
+
export const PROVIDER_ID = "entwurf";
|
|
23
|
+
// The curated Claude ids. Adding one here is a commitment to verify it across
|
|
24
|
+
// both axes (protocol smoke + agent interview) — do not extend casually.
|
|
25
|
+
export const SUPPORTED_ANTHROPIC_MODEL_IDS = /** @type {const} */ ([
|
|
26
|
+
"claude-sonnet-5-5",
|
|
27
|
+
"claude-opus-5-5",
|
|
28
|
+
"claude-fable-5-1",
|
|
29
|
+
]);
|
|
30
|
+
/** The anchor model whose absence is a hard registry regression, not a soft skip. */
|
|
31
|
+
export const CURATED_ANCHOR_MODEL_ID = "claude-opus-5-5";
|
|
32
|
+
// Cortex ids carry the reserved `cortex-` prefix; the rest of each id is the
|
|
33
|
+
// Snowflake-native model name, which is why Cortex still says `claude-opus-5`.
|
|
34
|
+
export const CORTEX_MODEL_PREFIX = "cortex-";
|
|
35
|
+
export const SUPPORTED_CORTEX_MODEL_IDS = /** @type {const} */ ([
|
|
36
|
+
"cortex-auto",
|
|
37
|
+
"cortex-claude-opus-5",
|
|
38
|
+
"cortex-claude-sonnet-5",
|
|
39
|
+
"cortex-openai-gpt-5.4",
|
|
40
|
+
]);
|
|
41
|
+
/** @type {ReadonlySet<string>} */
|
|
42
|
+
const CURATED_ACP_MODEL_IDS = new Set([...SUPPORTED_ANTHROPIC_MODEL_IDS, ...SUPPORTED_CORTEX_MODEL_IDS]);
|
|
43
|
+
/**
|
|
44
|
+
* Exact membership only — no trim, no case folding, no prefix.
|
|
45
|
+
* @param {string} model
|
|
46
|
+
* @returns {boolean}
|
|
47
|
+
*/
|
|
48
|
+
export function isCuratedAcpModelId(model) {
|
|
49
|
+
return CURATED_ACP_MODEL_IDS.has(model);
|
|
50
|
+
}
|
|
51
|
+
/** The named refusal both launch seams answer with. */
|
|
52
|
+
export const ACP_MODEL_NOT_CURATED = /** @type {const} */ ("acp-model-not-curated");
|
|
53
|
+
export const ACP_MODEL_NOT_CURATED_HINT = `an ${PROVIDER_ID} model must be one exact curated ACP id (${[...CURATED_ACP_MODEL_IDS].join(", ")}); ` +
|
|
54
|
+
"Pi would otherwise pick another row by substring — a retired claude-sonnet-5 lands on cortex-claude-sonnet-5 — " +
|
|
55
|
+
"so nothing was opened, and nothing is aliased or rewritten";
|
|
56
|
+
/** Carries `reason` so callers match a FIELD, never the wording. */
|
|
57
|
+
export class AcpModelNotCuratedError extends Error {
|
|
58
|
+
/** @param {string} model */
|
|
59
|
+
constructor(model) {
|
|
60
|
+
super(`${ACP_MODEL_NOT_CURATED}: ${JSON.stringify(model)} is not an exact curated ${PROVIDER_ID} model id`);
|
|
61
|
+
this.name = "AcpModelNotCuratedError";
|
|
62
|
+
/** @readonly */
|
|
63
|
+
this.reason = ACP_MODEL_NOT_CURATED;
|
|
64
|
+
/** @readonly */
|
|
65
|
+
this.model = model;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -32,8 +32,11 @@
|
|
|
32
32
|
// re-export of `getBuiltinModels`. When pi removes compat we migrate to whatever
|
|
33
33
|
// the loader then exposes.
|
|
34
34
|
import { getModels } from "@earendil-works/pi-ai/compat";
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
import { CURATED_ANCHOR_MODEL_ID, SUPPORTED_ANTHROPIC_MODEL_IDS } from "./model-ids.js";
|
|
36
|
+
// The provider id and the curated ID lists live in the dependency-free
|
|
37
|
+
// `model-ids.js` leaf, shared with the launch seams (#128); this file is the only
|
|
38
|
+
// one that loads the upstream catalog to turn them into registry rows.
|
|
39
|
+
export { CORTEX_MODEL_PREFIX, CURATED_ANCHOR_MODEL_ID, PROVIDER_ID, SUPPORTED_ANTHROPIC_MODEL_IDS, SUPPORTED_CORTEX_MODEL_IDS, } from "./model-ids.js";
|
|
37
40
|
// #26 auth-boundary sentinel. `pi.registerProvider` requires an apiKey when a
|
|
38
41
|
// provider defines custom models, but the ACP plugin consumes NO key: backend
|
|
39
42
|
// auth belongs to the operator's own Claude CLI child process (AGENTS
|
|
@@ -45,15 +48,8 @@ export const PROVIDER_ID = "entwurf";
|
|
|
45
48
|
// — that silences the warning but keeps the wrong auth-boundary shape. The
|
|
46
49
|
// check-auth-boundary gate pins this.
|
|
47
50
|
export const ENTWURF_ACP_NO_AUTH_SENTINEL = "entwurf-no-auth";
|
|
48
|
-
//
|
|
49
|
-
//
|
|
50
|
-
// Exported so the claude backend adapter (backend-adapter.ts) can answer
|
|
51
|
-
// `routeModel` without re-deriving the set from curatedClaudeModels().
|
|
52
|
-
export const SUPPORTED_ANTHROPIC_MODEL_IDS = ["claude-sonnet-5", "claude-opus-5", "claude-fable-5-1"];
|
|
53
|
-
/** The anchor model whose absence is a hard registry regression, not a soft skip. */
|
|
54
|
-
export const CURATED_ANCHOR_MODEL_ID = "claude-opus-5";
|
|
55
|
-
// Anthropic's registry reports 1M for Sonnet 5, Opus 5 and Fable 5.1, and the
|
|
56
|
-
// entwurf surface exposes the full 1M for all three. Sonnet 5's 1M window is the
|
|
51
|
+
// Anthropic's registry reports 1M for Sonnet 5.5, Opus 5.5 and Fable 5.1, and the
|
|
52
|
+
// entwurf surface exposes the full 1M for all three. Sonnet 5's 1M window was the
|
|
57
53
|
// whole point of the 0.12.3 bump — it is the compact-free long-context floor the
|
|
58
54
|
// earlier 200K Sonnet cap could not provide. We still clamp to a 1M ceiling so a
|
|
59
55
|
// future registry value can't silently inflate the surface past what we verify.
|
|
@@ -82,7 +78,7 @@ function claudeContextWindow(model) {
|
|
|
82
78
|
// carries the reserved `cortex-` prefix: the prefix is the SINGLE routing
|
|
83
79
|
// authority (backend-adapter.ts `routeModel`) and keeps the ids off the Claude
|
|
84
80
|
// native ids Cortex routes to (`cortex-claude-sonnet-5` vs the unprefixed
|
|
85
|
-
// `claude-sonnet-5` the claude adapter owns). `routeModel` strips the prefix to
|
|
81
|
+
// `claude-sonnet-5-5` the claude adapter owns). `routeModel` strips the prefix to
|
|
86
82
|
// recover the native id, which is enforced per-turn via
|
|
87
83
|
// session/set_config_option — never a launch `-m` pin (CP0-M).
|
|
88
84
|
//
|
|
@@ -91,14 +87,7 @@ function claudeContextWindow(model) {
|
|
|
91
87
|
// the running cortex no longer serves fails LOUD at set-model, before the
|
|
92
88
|
// prompt (measured: `Unsupported model: …` names the live set). Adding an id
|
|
93
89
|
// here is the same verify-both-axes commitment as the Claude set — do not
|
|
94
|
-
// extend casually.
|
|
95
|
-
export const CORTEX_MODEL_PREFIX = "cortex-";
|
|
96
|
-
export const SUPPORTED_CORTEX_MODEL_IDS = [
|
|
97
|
-
"cortex-auto",
|
|
98
|
-
"cortex-claude-opus-5",
|
|
99
|
-
"cortex-claude-sonnet-5",
|
|
100
|
-
"cortex-openai-gpt-5.4",
|
|
101
|
-
];
|
|
90
|
+
// extend casually. The prefix and the four ids are declared in model-ids.js.
|
|
102
91
|
// Hand-set conservative context window. Cortex reports the LIVE window via its
|
|
103
92
|
// own ACP session config, not this curated metadata — this is a floor for the
|
|
104
93
|
// registry surface, deliberately not inflated to the Claude 1M.
|
|
@@ -46,6 +46,7 @@
|
|
|
46
46
|
* a resume runs no turn (measured — the transcript stayed byte-identical), so there is no turn
|
|
47
47
|
* outcome for this module to claim.
|
|
48
48
|
*/
|
|
49
|
+
import { ACP_MODEL_NOT_CURATED_HINT, AcpModelNotCuratedError } from "./acp/model-ids.js";
|
|
49
50
|
import { buildResumePiArgs } from "./entwurf-resume-args.js";
|
|
50
51
|
import { ENTWURF_V2_LOCK_DIR, acquireLock as realAcquireLock, releaseLock as realReleaseLock, } from "./entwurf-v2-lock.js";
|
|
51
52
|
import { ResumeBackendUnsupportedError, resolveResumeLaunchIdentity, } from "./resume-launch-identity.js";
|
|
@@ -129,6 +130,15 @@ export async function visibleResume(targetGardenId, deps) {
|
|
|
129
130
|
identity = deps.resolveIdentity(targetGardenId);
|
|
130
131
|
}
|
|
131
132
|
catch (err) {
|
|
133
|
+
// #128: the recorded model is not an exact curated ACP id, so Pi would pick another row
|
|
134
|
+
// by substring. Same shape as `target-not-pi`: a named refusal matched on its FIELD.
|
|
135
|
+
if (err instanceof AcpModelNotCuratedError) {
|
|
136
|
+
return {
|
|
137
|
+
ok: false,
|
|
138
|
+
reason: err.reason,
|
|
139
|
+
detail: `${targetGardenId} would launch model ${JSON.stringify(err.model)} under the entwurf provider (the resolved launch pair, which may differ from the recorded provider)`,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
132
142
|
if (err instanceof ResumeBackendUnsupportedError) {
|
|
133
143
|
return {
|
|
134
144
|
ok: false,
|
|
@@ -210,6 +220,7 @@ export async function observeSameGidSocket(targetGardenId, socketPath, deps, tim
|
|
|
210
220
|
const REJECT_HINT = {
|
|
211
221
|
"target-invalid": "no citizen has that id; discover targets with entwurf_peers",
|
|
212
222
|
"target-not-pi": "that citizen's backend has no same-id resume — reach it with entwurf_v2, or open a fresh sibling",
|
|
223
|
+
"acp-model-not-curated": ACP_MODEL_NOT_CURATED_HINT,
|
|
213
224
|
"target-locked": "another resume or dispatch holds this citizen's lock right now",
|
|
214
225
|
"target-live": "that citizen is already running — this verb only reopens dormant ones",
|
|
215
226
|
"target-indeterminate": "the socket probe could not prove the citizen is dormant, so nothing was started",
|
|
@@ -24,8 +24,9 @@
|
|
|
24
24
|
* the one address path, on both rails.
|
|
25
25
|
*/
|
|
26
26
|
import { spawn as spawnChildProcess } from "node:child_process";
|
|
27
|
+
import { ACP_MODEL_NOT_CURATED, isCuratedAcpModelId, PROVIDER_ID } from "./acp/model-ids.js";
|
|
27
28
|
import { codexCallerFreshPreflight, codexFreshPreflight } from "./codex-fresh-preflight.js";
|
|
28
|
-
import { mintNonce } from "./fresh-call-composition.js";
|
|
29
|
+
import { isSafeFreshCallModel, mintNonce } from "./fresh-call-composition.js";
|
|
29
30
|
import { createHerdrRunner, herdrFreshCall, renderHerdrFreshCall, resolveHerdrContext, } from "./herdr-fresh-call.js";
|
|
30
31
|
import { freshCall, renderFreshCall } from "./mux-fresh-call.js";
|
|
31
32
|
/** herdr's own marker, read for its EXACT value. `[측정 2026-09-14]` herdr exports `HERDR_ENV=1`
|
|
@@ -33,6 +34,25 @@ import { freshCall, renderFreshCall } from "./mux-fresh-call.js";
|
|
|
33
34
|
export function selectFreshCallRail(env) {
|
|
34
35
|
return env.HERDR_ENV === "1" ? "herdr" : "tmux";
|
|
35
36
|
}
|
|
37
|
+
/**
|
|
38
|
+
* #128: a pi sibling asked for under the `entwurf` provider must name an EXACT curated ACP id. Pi
|
|
39
|
+
* resolves `--model` by substring among that provider's rows, so a retired `entwurf/claude-sonnet-5`
|
|
40
|
+
* would open on `cortex-claude-sonnet-5` — another vendor and billing rail — with a receipt that
|
|
41
|
+
* still names the requested id. Asked here, at the one root both rails share, so neither rail can
|
|
42
|
+
* forget it. It reads the model the way the rails will (trimmed), and only a model that already
|
|
43
|
+
* passes their grammar: a malformed one keeps its own `model-invalid` answer from the rail.
|
|
44
|
+
*/
|
|
45
|
+
export function isUncuratedEntwurfPiModel(backend, model) {
|
|
46
|
+
if (backend !== "pi")
|
|
47
|
+
return false;
|
|
48
|
+
const trimmed = model.trim();
|
|
49
|
+
if (!isSafeFreshCallModel(trimmed))
|
|
50
|
+
return false;
|
|
51
|
+
const slash = trimmed.indexOf("/");
|
|
52
|
+
if (slash < 0 || trimmed.slice(0, slash) !== PROVIDER_ID)
|
|
53
|
+
return false;
|
|
54
|
+
return !isCuratedAcpModelId(trimmed.slice(slash + 1));
|
|
55
|
+
}
|
|
36
56
|
/**
|
|
37
57
|
* Open a sibling on whichever rail this process is actually standing in.
|
|
38
58
|
*
|
|
@@ -49,6 +69,12 @@ export function selectFreshCallRail(env) {
|
|
|
49
69
|
* them rather than inheriting a rule from a topology it does not share.
|
|
50
70
|
*/
|
|
51
71
|
export async function dispatchFreshCall(request, env = process.env, spawn, nonce = mintNonce()) {
|
|
72
|
+
// Before either rail is asked anything: both would open a pane on the wrong row.
|
|
73
|
+
if (isUncuratedEntwurfPiModel(request.backend, request.model)) {
|
|
74
|
+
return selectFreshCallRail(env) === "herdr"
|
|
75
|
+
? { rail: "herdr", result: { ok: false, reason: ACP_MODEL_NOT_CURATED } }
|
|
76
|
+
: { rail: "tmux", result: { ok: false, reason: ACP_MODEL_NOT_CURATED } };
|
|
77
|
+
}
|
|
52
78
|
if (selectFreshCallRail(env) === "herdr") {
|
|
53
79
|
const context = resolveHerdrContext(env);
|
|
54
80
|
if (!context.ok) {
|
|
@@ -789,6 +789,9 @@ const HERDR_REJECT_HINT = {
|
|
|
789
789
|
"caller-identity-unavailable": "this surface has no record-backed identity, so a sibling would have nowhere to call home",
|
|
790
790
|
"model-empty": "pass the model you want the sibling opened with — it is never inherited from this process",
|
|
791
791
|
"model-invalid": "the model is not in the accepted grammar (no whitespace, no leading dash)",
|
|
792
|
+
// #128: decided at the dispatch root (`fresh-call-dispatch.ts`), worded here because this rail's
|
|
793
|
+
// import fence admits no ACP module. The curated list itself lives in `acp/model-ids.js`.
|
|
794
|
+
"acp-model-not-curated": "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",
|
|
792
795
|
"task-empty": "a sibling opened with nothing to do is a window nobody asked for",
|
|
793
796
|
"task-too-long": `the task is over the ${TASK_MAX_CHARS}-character interface bound this verb shares with entwurf_v2`,
|
|
794
797
|
"herdr-context-missing": "HERDR_ENV/HERDR_BIN_PATH are absent, so this process is not inside herdr and this rail does not exist here",
|
|
@@ -85,6 +85,7 @@
|
|
|
85
85
|
* the session the window is actually in. It still reports no `pane_current_path`, and
|
|
86
86
|
* there is no "session created" field because nothing here creates one.
|
|
87
87
|
*/
|
|
88
|
+
import { ACP_MODEL_NOT_CURATED_HINT } from "./acp/model-ids.js";
|
|
88
89
|
import { callbackEnvAssignments } from "./callback-env.js";
|
|
89
90
|
import { classifyTmuxCwd } from "./classify-tmux-cwd.js";
|
|
90
91
|
import { CODEX_CALLER_SEAT_HINT, resolveCodexCallerSeat, } from "./codex-caller-seat.js";
|
|
@@ -490,6 +491,7 @@ const REJECT_HINT = {
|
|
|
490
491
|
"tmux-session-missing": "no session with that exact name answers on this agent's tmux server (or that server stopped answering) — nothing was created, so open the session yourself and call again",
|
|
491
492
|
"model-empty": "model is empty after trimming; fresh calls require an explicit model",
|
|
492
493
|
"model-invalid": `model must be one ${MODEL_MAX_CHARS}-character argv-safe id/alias without whitespace or tmux syntax`,
|
|
494
|
+
"acp-model-not-curated": ACP_MODEL_NOT_CURATED_HINT,
|
|
493
495
|
"task-empty": "task is empty after trimming",
|
|
494
496
|
"task-too-long": `task exceeds ${TASK_MAX_CHARS} characters`,
|
|
495
497
|
"runtime-unresolved": "the backend's runtime is not installed on PATH",
|
|
@@ -19,11 +19,12 @@
|
|
|
19
19
|
*/
|
|
20
20
|
import { existsSync } from "node:fs";
|
|
21
21
|
import path from "node:path";
|
|
22
|
+
import { AcpModelNotCuratedError, isCuratedAcpModelId, PROVIDER_ID } from "./acp/model-ids.js";
|
|
22
23
|
import { getEntwurfExplicitExtensions, readSessionIdentity } from "./entwurf-core.js";
|
|
23
24
|
import { readAddressableMetaIdentity } from "./meta-session.js";
|
|
24
25
|
/**
|
|
25
|
-
*
|
|
26
|
-
* same-id resume at all.
|
|
26
|
+
* One of the TWO expected refusals on this leaf: the target is a citizen of a backend that has no
|
|
27
|
+
* same-id resume at all. The other is `AcpModelNotCuratedError` (#128, `acp/model-ids.js`).
|
|
27
28
|
*
|
|
28
29
|
* Everything else this function throws is a defect in the record, the transcript or the
|
|
29
30
|
* environment — a stale path, a foreign session file, an unrecorded model, an unresolvable
|
|
@@ -126,11 +127,19 @@ export function resolveResumeLaunchIdentity(gardenId) {
|
|
|
126
127
|
if (!identity.cwd) {
|
|
127
128
|
throw new Error(`resume-launch-identity: ${gardenId} header has no cwd (the cold-resume authority, #9).`);
|
|
128
129
|
}
|
|
130
|
+
// #128: the pair checked is the one the launch will EMIT, not the recorded provider alone —
|
|
131
|
+
// `getEntwurfExplicitExtensions` also routes a bare Claude model id to `entwurf`. Under that
|
|
132
|
+
// provider Pi resolves `--model` by substring, so a retired `claude-sonnet-5` would reopen on
|
|
133
|
+
// `cortex-claude-sonnet-5`. The record and transcript stay as they are; the resume is refused.
|
|
134
|
+
const provider = explicitExtensions.provider ?? identity.provider;
|
|
135
|
+
const model = explicitExtensions.modelOverride ?? resumeModel;
|
|
136
|
+
if (provider === PROVIDER_ID && !isCuratedAcpModelId(model))
|
|
137
|
+
throw new AcpModelNotCuratedError(model);
|
|
129
138
|
return {
|
|
130
139
|
sessionFile,
|
|
131
140
|
cwd: identity.cwd,
|
|
132
141
|
explicitExtensionArgs: explicitExtensions.args,
|
|
133
|
-
provider
|
|
134
|
-
model
|
|
142
|
+
provider,
|
|
143
|
+
model,
|
|
135
144
|
};
|
|
136
145
|
}
|