@junghanacs/entwurf 0.12.10 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/AGENTS.md +2 -1
  2. package/BASELINE.md +45 -6
  3. package/CHANGELOG.md +16 -0
  4. package/CONTRIBUTING.md +4 -2
  5. package/DELIVERY.md +1 -1
  6. package/README.md +20 -5
  7. package/VERIFY.md +7 -4
  8. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/backend-adapter.js +148 -5
  9. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/config.js +16 -4
  10. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/models.js +66 -7
  11. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/overlay.js +190 -5
  12. package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/tool-surface.js +12 -4
  13. package/package.json +4 -2
  14. package/pi/settings.reference.json +1 -1
  15. package/pi-extensions/acp-provider.ts +20 -10
  16. package/pi-extensions/lib/acp/augment.ts +60 -2
  17. package/pi-extensions/lib/acp/backend-adapter.ts +183 -8
  18. package/pi-extensions/lib/acp/backend.ts +5 -1
  19. package/pi-extensions/lib/acp/config.ts +19 -5
  20. package/pi-extensions/lib/acp/engraving.ts +3 -1
  21. package/pi-extensions/lib/acp/event-mapper.ts +10 -3
  22. package/pi-extensions/lib/acp/models.ts +69 -7
  23. package/pi-extensions/lib/acp/overlay.ts +234 -5
  24. package/pi-extensions/lib/acp/tool-surface.ts +12 -4
  25. package/run.sh +152 -21
  26. package/scripts/check-acp-cortex.ts +668 -0
  27. package/scripts/check-acp-provider-surface.ts +50 -6
  28. package/scripts/check-acp-session-reuse.ts +64 -1
  29. package/scripts/check-gate-qualification.ts +2 -0
  30. package/scripts/check-probe-cli-shim.ts +879 -0
  31. package/scripts/check-probe-ordering.ts +2450 -0
  32. package/scripts/check-shell-quote.ts +4 -4
  33. package/scripts/fixtures/probe-cli-shim +20 -0
  34. package/scripts/fixtures/probe-mcp-server.ts +168 -12
  35. package/scripts/lib/probe-acp-turn.ts +207 -0
  36. package/scripts/lib/probe-cli-shim.ts +464 -0
  37. package/scripts/lib/probe-cli-target.ts +165 -0
  38. package/scripts/lib/probe-event-log.ts +383 -0
  39. package/scripts/lib/probe-verdict.ts +1213 -0
  40. package/scripts/mutants/acp-cortex.json +196 -0
  41. package/scripts/mutants/probe-ordering.json +1032 -0
  42. package/scripts/smoke-acp-cortex-live.ts +392 -0
  43. package/scripts/smoke-acp-ordering-probe-live.ts +848 -0
package/AGENTS.md CHANGED
@@ -64,7 +64,7 @@ Detailed incident histories belong in CHANGELOG/issues/BASELINE and source-adjac
64
64
  - The common turn sequence stays backend-invariant: spawn → initialize → newSession → enforceModel → prompt → event map.
65
65
  - Rich operator/project context rides the **first user message augment**, not a large system prompt. The actual callable schema is the tool truth; prose never grants a tool.
66
66
  - A backend may have no carrier or use launch-time model pinning; those asymmetries stay inside its adapter.
67
- - Claude is the reference adapter. Cortex enters only after the current rail conditions in `docs/acp-backend-rail.md` and `NEXT.md` are satisfied.
67
+ - Claude is the reference adapter. Cortex is the second landed adapter (0.13.0): session-scoped dual-HOME containment, overlay-private `mcp.json` projection (its ACP server ignores the wire `mcpServers` param), `CORTEX_HOME` presence refusal, per-turn set-model. Audit record and contract: `docs/acp-backend-rail.md` §11-8.
68
68
  - entwurf never supplies, copies, proxies, decrypts, or bypasses vendor credentials/subscriptions. It uses the operator's existing local authenticated backend.
69
69
 
70
70
  ## Citizen Identity and Dispatch
@@ -114,6 +114,7 @@ LIVE=1 ./run.sh release-gate /path/to/scratch
114
114
  LIVE=1 ./run.sh smoke-acp-socket-citizen-live
115
115
  LIVE=1 ./run.sh smoke-acp-bundled-mcp-live
116
116
  LIVE=1 ./run.sh smoke-acp-v2-send-live
117
+ LIVE=1 ENTWURF_ACP_CORTEX_CONNECTION=<conn> ./run.sh smoke-acp-cortex-live # on-demand; outside the claude release floor
117
118
  LIVE=1 AGY_CONVERSATION_ID=<id> ./run.sh smoke-agy-native-push-live
118
119
  ```
119
120
 
package/BASELINE.md CHANGED
@@ -6,12 +6,16 @@ silently drifted into a different identity / context surface. Questions
6
6
  are deliberately open-ended — they probe what the agent actually sees,
7
7
  not what it was told to claim.
8
8
 
9
- The 0.12 shipped ACP backend is **Claude**; the main question bank below is the
10
- Claude ACP baseline. Antigravity (`agy`) is also shipped, but as a native-push
11
- garden citizen rather than an ACP backend, so it has a separate citizen/round-trip
12
- baseline below instead of being forced into Claude's overlay questions. Codex
13
- (pi-native / delivery probe) and Gemini (historical non-goal ACP probe) remain
14
- reference axes, not the shipped ACP baseline.
9
+ The released 0.12 ACP backend is **Claude**, and the main question bank below is the
10
+ Claude ACP baseline. The prepared **0.13.0** cut adds **Snowflake Cortex Code** as a
11
+ second ACP backend; it has no question bank of its own yet (deferred to 0.13.1), and the
12
+ Claude bank must not be run against it verbatim cortex is
13
+ system-prompt-carrier-less and runs its own native tool surface, so the overlay/engraving
14
+ questions below would grade it against a surface it does not have. Antigravity (`agy`)
15
+ is also shipped, but as a native-push garden citizen rather than an ACP backend, so it
16
+ has a separate citizen/round-trip baseline below instead of being forced into Claude's
17
+ overlay questions. Codex (pi-native / delivery probe) and Gemini (historical non-goal
18
+ ACP probe) remain reference axes, not a shipped ACP baseline.
15
19
 
16
20
  ## Release-host baseline — #51 repair cut
17
21
 
@@ -274,6 +278,41 @@ prompt, and if so quote the visible text exactly:
274
278
 
275
279
  # HISTORY (pointer)
276
280
 
281
+ 2026-07-30 Cortex 0.13.0 acceptance + landing checkpoint (Linux/thinkpad, Cortex Code
282
+ v1.1.52). Pre-version landing HEAD `9f1c7dc9e1fa77103e29a6f1884af7759e1595eb` passed
283
+ exact-SHA GitHub Actions run [30505001694](https://github.com/junghan0611/entwurf/actions/runs/30505001694):
284
+ `check`, `install-surface`, and `artifact-consumer` all success. The non-interference reading rests on what the
285
+ sources prove, not on an inspection of the runner image: the external Cortex CLI is not an npm
286
+ dependency, nothing in install or provider registration looks for the executable,
287
+ `.github/workflows/ci.yml` provisions only checkout, pnpm, and Node 24 (no Cortex install step,
288
+ no Snowflake auth), and the required Linux `artifact-consumer` job ran the candidate inside a
289
+ clean `node:24-bookworm` container and passed. Cells at that tree: `ENTWURF_REQUIRE_DOCKER=1 ./run.sh
290
+ check-install-container` EXIT=0 (40 ok) — candidate `junghanacs-entwurf-0.12.10.tgz`
291
+ **sha256=63342aa8a144011dee86ebea8f0b778c7860e54dc0f1c710438983c679e8af87**, image
292
+ `node:24-bookworm` `id=sha256:fcd0f74fb415c752…`,
293
+ `repoDigest=node@sha256:5711a0d445a1af54…`; `./run.sh check-pack-install` EXIT=0 with the
294
+ installed package's own model list enumerating the **exact six curated rows (claude 2 +
295
+ cortex 4)**; `LIVE=1 ./run.sh smoke-acp-cortex-live` **PASS 23/23**, deliberately rerun
296
+ after the `resolveSessionKey` overlay-scope repair so the claim does not rest on the earlier
297
+ CP2 code — overlay scope dir + `autoUpdate:false` + overlay-private `mcp.json` projection +
298
+ real-operator-HOME restore on the bridge entry, the model's own `entwurf_v2` landing exactly
299
+ one `.msg` on a seeded peer with the gid only in the envelope, and process-group teardown
300
+ leaving nothing alive in that run's overlay. `pnpm check` on the prepared tree includes
301
+ `check-gate-qualification` **111/111 killed** (lane `acp-cortex` twelve), and
302
+ `LIVE=1 ./run.sh release-gate /tmp/entwurf-release-gate-0.13.0.drnRyR` was all green —
303
+ **MUST PASS=17 FAIL=0 SKIP=0**, **BEHAVIOR PASS=1 FAIL=0**, EXIT=0. Operator-session (not
304
+ gate) evidence from 2026-07-29 on the same implementation set: a Cortex resident used the
305
+ host pi citizen's garden id instead of inventing one (`agentId=entwurf/cortex-claude-sonnet-5`,
306
+ `backend=pi`, `origin=pi-session`, `replyable=true`) and exchanged real turns across three
307
+ rails — native Claude Code mailbox drain, pi native control socket, and pi ACP Claude Sonnet
308
+ control socket; record `20260729T204101-7aaa6f` remains on disk. **Still pending as a
309
+ release-blocking `make` tag gate:** the preserved exact 0.13.0 candidate installed into a
310
+ fresh temporary root must drive one cold `entwurf/cortex-claude-sonnet-5` turn from those
311
+ installed bytes (unique nonce, exit 0, candidate sha256 unchanged before/after, resolved
312
+ installed root recorded). Installed-artifact evidence and real-Cortex evidence have not yet
313
+ met in one execution; a RED there stops the cut before the tag. Second machine, macOS, and
314
+ WSL2 remain unclaimed for this cut.
315
+
277
316
  2026-07-25 repair.1 installed-native host acceptance (stable-promotion prerequisite):
278
317
  the published registry artifact `@junghanacs/entwurf@0.12.8-repair.1` was installed
279
318
  fresh on two Linux hosts (maintainer + secondary) through the package's stable bins.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,22 @@ All notable changes to this project will be documented here. Format follows [Kee
4
4
 
5
5
  ## Unreleased
6
6
 
7
+ ## 0.13.0 — 2026-07-30
8
+
9
+ ### Added
10
+
11
+ - **Snowflake Cortex Code joins the ACP rail as the first non-claude backend** — the `cortexAdapter` contributed by hvkiefer (PR #40), landed together with the revisions the CP0 live audit of 2026-07-29 measured against Cortex Code v1.1.52 (`docs/acp-backend-rail.md` §11-8 records the ten defects D1–D10 and the agreed contract; the deltas are drift repair, not contributor error). As landed: the curated surface is the GLG-decided 4-row set (`cortex-auto`, `cortex-claude-opus-5`, `cortex-claude-sonnet-5`, `cortex-openai-gpt-5.4`) riding real pi-ai registry bases behind the reserved `cortex-` routing prefix; launch is `cortex acp serve` from PATH (+ `-c <connection>` via `entwurfProvider.cortexConnection` / `ENTWURF_ACP_CORTEX_CONNECTION`) with **no `-m` pin** — the model is enforced per turn through `session/set_config_option("model", <native id>)`, the same wire call as claude, so a curated id the running cortex no longer serves fails loud before the prompt. Containment is a **session-scoped dual-HOME overlay**: cortex's config/skill/hook surface is `homedir()`-anchored and `CORTEX_HOME` outranks `SNOWFLAKE_HOME`, so the adapter refuses an ambient `CORTEX_HOME` outright (empty string included), runs the child under an isolated HOME (global-scope leak measured to zero; explicit cwd project scope retained), passes through only the measured-minimum auth (`connections.toml`, optional `config.toml`, `cortex/cache/credential_cache` — symlink-through, never copied), authors `autoUpdate: false`, and — because cortex's ACP `newSession` ignores the wire `mcpServers` param — **projects** the envelope-enriched explicit `entwurfProvider.mcpServers` into the overlay-private `cortex/mcp.json`, restoring the real operator HOME on the `entwurf-bridge` entry alone so a cortex sibling sees the real garden (an isolated-HOME bridge measured an empty citizen roster); non-stdio entries fail loud before spawn. Cortex is system-prompt-carrier-less: the operator engraving rides the first-user augment. Coverage: the deterministic `check-acp-cortex` gate (in `pnpm check`) plus the `acp-cortex` mutant lane's twelve kill-qualified claims — ten on that gate, and two cross-gate: the production overlay call site passing `resolveSessionKey`'s authoritative value (`check-acp-session-reuse`) and the real compiled provider entry registering both adapters' curated rows (`check-acp-provider-surface`, so an entry that silently dropped every `cortex-` row can no longer go green) — and the on-demand CP2 live smoke `LIVE=1 ENTWURF_ACP_CORTEX_CONNECTION=<conn> ./run.sh smoke-acp-cortex-live` — a real cortex model turn delivering outbound `entwurf_v2` to a seeded peer as itself, plus overlay disk facts and process-group reclaim (an MCP-configured cortex child ignores stdin EOF) — deliberately outside the claude-only **aggregate** LIVE floor, so a host that runs no cortex cannot redden a claude release; that is a wiring decision, and a cut shipping cortex still owes a deliberate run of it.
12
+ - **A host that does not run Cortex is unaffected by this release.** The external `cortex` CLI is not an npm dependency, and neither package install nor provider registration looks for the executable: the four curated rows are enumerated deterministically from the compiled provider entry, and `cortex acp serve` is spawned only when a turn actually selects a `cortex-` model — so selecting one on a host without Cortex fails loud at that turn rather than degrading install or the rest of the provider. The deterministic `check-acp-cortex` gate runs on fake/injected seams and needs neither the CLI nor Snowflake auth; the aggregate release floor contains no Cortex LIVE step; and the dedicated smoke honest-skips without `LIVE=1`, without `cortex` on `PATH`, or without a connection on the adapter seam. Rows are deliberately **not** hidden by PATH detection — that would make the provider surface differ per host and dissolve the exact-six-row fence — so an explicit operator enable/disable control remains a separate future contract, not a silent behaviour of this cut.
13
+ - **The ACP ordering probe lands as a gated instrument, and the readiness question it exists to answer stays open.** The rail's open question — whether MCP tool availability precedes what `newSession` returns — had no observable signal, so the probe injects a controlled fixture startup delay and reads control + D1 + D2 paired runs off one shared NDJSON axis instead of reconstructing order from separate logs with drifting clocks. A raw client may stand in for the backend only while a gate holds it to the same calls, arguments, and order, so `check-probe-ordering` pins the sequence against `backend.ts` source, attributes every failure to its wire phase, drives the probe-mode fixture as a real child process, types the event log at its door, and replays the paired-verdict truth table through the pure classifier. The log's door is a gate too: the envelope belongs to the writer, `ts` derives from a single clock read, and per `(runId, pid)` writer the raw append order must have strictly increasing `seq` and a non-regressing clock — a JSON-valid line with an unknown marker, a broken sort axis, or a payload the classifier cannot judge takes the INVALIDATED path instead of moving a verdict. The runner now holds the child open past the turn until the marker lands, the child exits, or a deadline anchored on the fixture's own delay markers passes, and only a deadline close lets a missing marker read as evidence; every intervention reports ordering and callability as two axes so a settled comparison is not buried by an unsettled verdict, and each ordering value is named for the comparison it is (`prompt-request-ahead-of-wire` says we issued the request first, never that the server failed to wait). The delta-B name oracle cannot be the model, so that seam is a CLI shim at `CLAUDE_CODE_EXECUTABLE`: the consumer half (target preconditions by key presence, absolute/regular/executable/no-script-suffix assertions, path+sha256 roster pinning with a post-pair re-hash, snapshot channel doors, receive-axis single-prompt binding, structural-vs-reading severity split) and then the producer shim itself both land kill-qualified. **No readiness finding is claimed.** The first paired run under the door contract produced 57 events with zero malformed lines, so the parser is calibrated rather than merely strict, and rail §11-7-b carries the numbers — but the verdict is inconclusive, and an inconclusive verdict is not "nothing wrong". Artifacts predating the window protocol re-parse as INVALIDATED and remain forensic records only.
14
+
15
+ ### Verification
16
+
17
+ - Pre-version landing HEAD `9f1c7dc9e1fa77103e29a6f1884af7759e1595eb` passed exact-SHA GitHub Actions run [30505001694](https://github.com/junghan0611/entwurf/actions/runs/30505001694): `check`, `install-surface`, and `artifact-consumer` all success. That run is also the third-party half of the optionality claim above, at the strength its sources actually carry: `.github/workflows/ci.yml` provisions only checkout, pnpm, and Node 24 — it installs no Cortex CLI and supplies no Snowflake auth — and the required Linux `artifact-consumer` job ran the candidate inside a clean `node:24-bookworm` container and passed. That is a workflow-configuration fact plus a passing consumer run; it is not a probe of the runner image's contents.
18
+ - The prepared tree passed an independent `pnpm check`, including `check-gate-qualification` at **111/111 committed mutants killed** (the `acp-cortex` lane contributing twelve). Then `LIVE=1 ./run.sh release-gate /tmp/entwurf-release-gate-0.13.0.drnRyR` completed all green: **MUST PASS=17 FAIL=0 SKIP=0** and **BEHAVIOR PASS=1 FAIL=0**, EXIT=0. Full log: `/tmp/entwurf-release-gate-0.13.0.drnRyR/release-gate.log`; per-step artifacts are preserved inside it.
19
+ - Cortex acceptance on the landing tree (2026-07-30, Linux/thinkpad, Cortex Code v1.1.52, connection on the adapter seam): the on-demand `smoke-acp-cortex-live` passed **23/23** — a deliberate rerun *after* the `resolveSessionKey` overlay-scope repair, so the shipped claim no longer rests on the earlier CP2 run's code. `check-pack-install` enumerated the **exact six curated rows (claude 2 + cortex 4)** out of the installed package's own model list, and `ENTWURF_REQUIRE_DOCKER=1 ./run.sh check-install-container` consumed candidate `sha256=63342aa8a144011dee86ebea8f0b778c7860e54dc0f1c710438983c679e8af87` on `node:24-bookworm` (`id=sha256:fcd0f74fb415c752…`, `repoDigest=node@sha256:5711a0d445a1af54…`).
20
+ - Cross-rail live round trips are **operator-session evidence, not a gate** (2026-07-29, same implementation set): a Cortex resident used the host pi citizen's garden id rather than inventing one — `agentId=entwurf/cortex-claude-sonnet-5`, `backend=pi`, reply envelope `origin=pi-session`, `replyable=true` — and exchanged real turns with native Claude Code (mailbox drain), pi native (control socket), and pi ACP Claude Sonnet (control socket). The record `20260729T204101-7aaa6f` remains on disk as corroboration.
21
+ - **Deferred to `make` as a release-blocking tag gate:** the preserved exact 0.13.0 candidate must be installed into a fresh temporary root and drive one cold `entwurf/cortex-claude-sonnet-5` turn from *those installed bytes* — unique nonce, exit 0, candidate SHA-256 unchanged before and after, resolved installed package root recorded. Until it runs, installed-artifact evidence and real-Cortex evidence have never met in one execution; a RED there stops the cut before the tag. Prepared-HEAD exact-SHA CI and preserved-candidate container acceptance remain deferred to `make` as usual.
22
+
7
23
  ## 0.12.10 — 2026-07-27
8
24
 
9
25
  ### Changed
package/CONTRIBUTING.md CHANGED
@@ -4,7 +4,9 @@ This is a daily-driver bridge. Correctness beats feature breadth. Read this befo
4
4
 
5
5
  ## What this repo is
6
6
 
7
- `entwurf` is a **garden-citizen dispatch bridge** — entwurf-core (v2 dispatch) + a meta-bridge + a pi adapter + a **Claude-first ACP plugin** — that lets already-running harnesses address one another by garden id; pi is one adapter, not the subject. The ACP plugin borrows the backend's identity (system prompt preset, model behavior, tool implementations) and shapes the *operating surface* — what tools, MCP, skills, and permissions are visible — to match pi's own policy. Claude is the shipped ACP backend, Codex is a native garden citizen, and the Gemini path is deprecated. That is the entire scope.
7
+ `entwurf` is a **garden-citizen dispatch bridge** — entwurf-core (v2 dispatch) + a meta-bridge + a pi adapter + an **ACP plugin on a two-backend adapter rail** — that lets already-running harnesses address one another by garden id; pi is one adapter, not the subject. The ACP plugin borrows the backend's identity (system prompt preset, model behavior, tool implementations) and shapes the *operating surface* — tools, MCP, skills, permissions — to match pi's own policy **wherever that backend exposes a knob for it**. Where it does not, the plugin does not fake one: a cortex session keeps its own native tool surface and receives MCP through an overlay projection, so "shaped to pi's policy" is a Claude-strength claim, not a universal one. Claude is the reference ACP backend and Snowflake Cortex Code is the second (landed 0.13.0, `cortex-` prefixed ids — [docs/acp-backend-rail.md](./docs/acp-backend-rail.md) §6/§11-8); Codex is a native garden citizen, and the Gemini path is deprecated. That is the entire scope.
8
+
9
+ **How a backend joins the rail** (the shape a PR must take): one adapter object in `pi-extensions/lib/acp/backend-adapter.ts` + its own curated rows/overlay modules + its own `check-acp-*` gate and mutant lane. Backend-specific *behavior* must stay behind the adapter, and backend-specific settings ride the opaque `adapterSettings` seam rather than growing the common config. That is not a ban on ever touching the common layer (`backend.ts`'s turn loop, `acp-client.ts`, `event-mapper.ts`, `session-store.ts`, `config.ts`) — cortex's landing did change `backend.ts` to pass the authoritative session key through the generic `ensureOverlay` seam. The rule is narrower and stricter: a common-layer change must be **backend-invariant** (it reads no backend name and branches on no backend) and **separately gated**. A common file that grows an `if (backend === …)` is the thing to reject.
8
10
 
9
11
  If a change moves the bridge toward "second harness" — prompt reconstruction, transcript hydration, ambient discovery, silent fallback — it does not belong here.
10
12
 
@@ -16,7 +18,7 @@ These are enforced by code, gates, and review. Do not weaken them in a PR; if yo
16
18
  2. **Session persistence**: only `pi:<sessionId>` is persisted. `cwd:<cwd>` is never persisted.
17
19
  3. **MCP injection**: only via `entwurfProvider.mcpServers`. No ambient `~/.mcp.json` scanning, no `~/.claude/settings.json` MCP inheritance.
18
20
  4. **Operating surface, not config inheritance**: the user's filesystem Claude Code config (`~/.claude/settings.json` hooks, env, plugins, `permissions.defaultMode`) is intentionally *not* inherited. Skills come from `skillPlugins`, permissions from `permissionAllow`, deferred-tool surface from `disallowedTools`. The `CLAUDE_CONFIG_DIR` overlay enforces this even where the SDK reads filesystem independently of `settingSources`.
19
- 5. **Backend-specific knobs stay explicit and namespaced**: Codex/Gemini-era ACP knobs are not part of the current Claude-first shipped path. If a future backend lane reintroduces a sandbox or mode knob, it must use the `ENTWURF_ACP_*` namespace and invalid values must throw, never fall back.
21
+ 5. **Backend-specific knobs stay explicit and namespaced**: retired Codex/Gemini-era ACP knobs are not carried on the current path. A backend that needs its own knob uses the `ENTWURF_ACP_*` namespace, and invalid values must throw, never fall back — cortex followed this with `ENTWURF_ACP_CORTEX_CONNECTION` (the renamed `PI_SHELL_ACP*` legacy var), and its settings key rides `adapterSettings`, not the common config.
20
22
  6. **Bridge does not implement compaction**: When a backend compacts natively, the pi session and mapping survive that. Pi-side JSONL compaction must not be presented as backend-transcript reduction, and backend-specific compaction controls belong to the backend's own native interface. Legacy `PI_SHELL_ACP_*` compaction knobs must not reappear.
21
23
  7. **Backend coverage honesty**: changes to operating surface, session lifecycle, or persistence must state which shipped/probed backend surfaces they cover. A claim that silently drops a covered backend is a regression; if one backend is genuinely not covered, record that carve-out explicitly.
22
24
  8. **This bridge is not a second harness**: no prompt reconstruction, no transcript hydration, no tool result ledger, no Claude Code emulation.
package/DELIVERY.md CHANGED
@@ -115,7 +115,7 @@ the `D0–D8` capability level:
115
115
  | **Codex app-server-backed TUI 0.136.0** | verified-probe | D6, D7 (status) | WebSocket-over-UDS `turn/start` into the live `threadId` | **Demonstrated, no managed standalone, no cloud.** `codex app-server --listen unix://<owned 0700 dir>` + plain `codex` auto-attach (or `--remote unix://`). Full message injection (agy-like, not a doorbell); `thread/status/changed` gives completion observation. D8 robustness (dedupe / crash recovery / ordering policy) is not tested. `turn/steer` is active-turn steering, not idle wake. |
116
116
  | **Codex embedded TUI 0.136.0** | deferred | D0 partial | Native state DB / rollout transcript only | Standalone Embedded TUI binds no socket; no `FileChanged`/`asyncRewake` in Codex hooks; not retrofittable. Identify-only via state DB / rollout. |
117
117
  | **Codex managed-daemon / remote-control 0.136.0** | deferred | D4–D6 conditional | `app-server proxy` newline JSON-RPC over the daemon control socket | Needs the managed standalone install; `remote-control` also enables the **cloud** bridge. Use the bare `--listen` path above for a purely-local setup. |
118
- | **ACP Claude / Cortex (candidate)** | deferred | — | ACP (via entwurf's pi adapter) | Shipped as an ACP *runtime* lane (you can run Claude through ACPsee VERIFY/README), but **not a native-async-*delivery* target**: ACP sessions are bridge-spawned children, not already-running native sessions to wake, so they fall outside this doc's delivery question. `deferred` here means "no async-delivery lane," not "unsupported." Cortex is the candidate that would land on the ACP path next. |
118
+ | **ACP Claude / Cortex (runtime lane)** | shipped as runtime; deferred as delivery target | — | ACP (via entwurf's pi adapter) | Both ACP backends are **landed runtime lanes** (Claude the reference; Cortex since 0.13.0 under the dual-HOME containment `docs/acp-backend-rail.md` §11-8), but **not native-async-*delivery* targets**: ACP sessions are bridge-spawned children, not already-running native sessions to wake, so they fall outside this doc's delivery question. `deferred` here means "no async-delivery lane," not "unsupported." |
119
119
 
120
120
  ## Backend notes
121
121
 
package/README.md CHANGED
@@ -10,7 +10,7 @@ npm package: <https://www.npmjs.com/package/@junghanacs/entwurf>
10
10
 
11
11
  Legacy package: [`@junghanacs/pi-shell-acp`](https://www.npmjs.com/package/@junghanacs/pi-shell-acp). `entwurf` is its 0.12+ successor line: the same work renamed around the garden-citizen dispatch substrate rather than the pi adapter.
12
12
 
13
- > **Repository shape.** This repo is **entwurf-core (v2 dispatch) + native-harness bridges + a pi adapter + an ACP plugin**. Pi is one supported harness adapter — important because it supplies control sockets and hosts the ACP plugin today — but it is not the project subject. Claude Code is shipped as a mailbox-backed meta-session; Antigravity (`agy`) is shipped as a native-push citizen with automatic `PreInvocation` birth, ambient garden-id status, and a managed MCP/permission install surface. Codex has a launch-mode-specific verified delivery probe documented in [DELIVERY.md](./DELIVERY.md), but no managed native-citizen install lane yet. The ACP plugin is Claude-first; Cortex/vendor-governed ACP backends are future lanes.
13
+ > **Repository shape.** This repo is **entwurf-core (v2 dispatch) + native-harness bridges + a pi adapter + an ACP plugin**. Pi is one supported harness adapter — important because it supplies control sockets and hosts the ACP plugin today — but it is not the project subject. Claude Code is shipped as a mailbox-backed meta-session; Antigravity (`agy`) is shipped as a native-push citizen with automatic `PreInvocation` birth, ambient garden-id status, and a managed MCP/permission install surface. Codex has a launch-mode-specific verified delivery probe documented in [DELIVERY.md](./DELIVERY.md), but no managed native-citizen install lane yet. The ACP plugin ships two backends through one adapter rail: Claude (the reference) and Snowflake Cortex Code (landed in 0.13.0 under a session-scoped dual-HOME containment [docs/acp-backend-rail.md](./docs/acp-backend-rail.md) §11-8).
14
14
 
15
15
  <details>
16
16
  <summary>Watch archived pre-0.12 demo (2131×1142 GIF, click to expand)</summary>
@@ -28,7 +28,7 @@ Claude Code / Codex / agy / pi
28
28
  → control-socket | spawn-bg resume | meta-mailbox | native-push
29
29
  ```
30
30
 
31
- [`entwurf_v2`](#entwurf_v2--canonical-dispatch-verb) is the canonical dispatch surface over *existing* garden citizens — live control-socket send, spawn-bg resume, meta-mailbox enqueue, and native-push into a live Antigravity conversation. The meta-record is the sole address authority (#50 C4): a record-less control socket is refused as a `record-less-socket` diagnostic, never dispatched. The v1 entwurf verbs are gone. Fresh sibling minting and non-Claude ACP backends are deferred lanes.
31
+ [`entwurf_v2`](#entwurf_v2--canonical-dispatch-verb) is the canonical dispatch surface over *existing* garden citizens — live control-socket send, spawn-bg resume, meta-mailbox enqueue, and native-push into a live Antigravity conversation. The meta-record is the sole address authority (#50 C4): a record-less control socket is refused as a `record-less-socket` diagnostic, never dispatched. The v1 entwurf verbs are gone. Fresh sibling minting is still a deferred lane; the non-Claude ACP lane is not — Snowflake Cortex Code landed as the second backend in 0.13.0.
32
32
 
33
33
  **Garden id is deliberate vocabulary.** It is not a decorative synonym for session id, worker, delegate, or subagent. The unfamiliar word is a guard: each harness keeps its own identity and transcript, while `entwurf` supplies a narrow addressable surface between siblings.
34
34
 
@@ -462,13 +462,26 @@ plain MCP registrations.
462
462
 
463
463
  ### Backend prerequisites
464
464
 
465
- The ACP plugin is **Claude-first**. The Claude ACP server package (`@agentclientprotocol/claude-agent-acp`, pinned with `@agentclientprotocol/sdk`) ships as a pinned `dependency` of `entwurf`; backend authentication still belongs to the operator's local `claude` CLI / runtime. Once the bridge is installed, the resolver picks the ACP server in this order:
465
+ **Claude is the reference ACP backend.** The Claude ACP server package (`@agentclientprotocol/claude-agent-acp`, pinned with `@agentclientprotocol/sdk`) ships as a pinned `dependency` of `entwurf`; backend authentication still belongs to the operator's local `claude` CLI / runtime. Once the bridge is installed, the resolver picks the ACP server in this order:
466
466
 
467
467
  1. **`CLAUDE_AGENT_ACP_COMMAND` env override** — explicit override for an alternative binary or a wrapper command.
468
468
  2. **`require.resolve(...)` against the bundled package dependency** (`@agentclientprotocol/claude-agent-acp`). This is the default path; no extra global install needed.
469
469
  3. **`PATH:claude-agent-acp` fallback** — used when the package resolution fails (e.g. a hand-edited `node_modules`).
470
470
 
471
- The curated model registry exposes Claude models only, so the ACP backend is Claude. Codex is *not* an ACP backend here — a native Codex session is already a first-class garden citizen via direct injection, so it needs no ACP plugin (see [AGENTS.md](./AGENTS.md)). Vendor / governed CLIs (e.g. Cortex) are a later ACP backend lane.
471
+ The curated model registry exposes the unprefixed Claude ids plus the `cortex-` prefixed Cortex rows (below). Codex is *not* an ACP backend here — a native Codex session is already a first-class garden citizen via direct injection, so it needs no ACP plugin (see [AGENTS.md](./AGENTS.md)).
472
+
473
+ **Snowflake Cortex Code is the second landed ACP backend** (0.13.0; audit record and contract: [docs/acp-backend-rail.md](./docs/acp-backend-rail.md) §11-8). Operator surface:
474
+
475
+ - **Curated ids (4):** `cortex-auto`, `cortex-claude-opus-5`, `cortex-claude-sonnet-5`, `cortex-openai-gpt-5.4`. The `cortex-` prefix routes; the model is enforced per turn via ACP set-model, so an id the running cortex no longer serves fails loud before the prompt.
476
+ - **CLI + auth:** `cortex` must be on PATH and already authenticated through Cortex's **own web-login flow** (there is no `cortex auth` subcommand; entwurf never supplies or proxies the Snowflake credential — the overlay reaches it by **symlinking through** exactly `connections.toml`, optional `config.toml`, and `cortex/cache/credential_cache`, and nothing else. The narrowing is *which paths are reachable* (§11-8 D5), not a read-only mount: a symlink carries no write protection, so the child holds the same access to those three paths that it would have with the operator's own HOME. What the overlay removes is the rest of the operator's **home** surface — the rest of `cortex/cache`, `~/.claude`/`~/.cortex` skills, home-anchored hooks and settings, and the operator's `cortex/mcp.json`. It does **not** reach cortex's *bundled* plugin/hook surface, which fires from the CLI's own install directory and is outside any HOME the overlay controls — that stays a host fact, not a containment claim (§11-8 D1).)
477
+ - **Connection:** pin a Snowflake connection with `entwurfProvider.cortexConnection` in settings or per-shell via `ENTWURF_ACP_CORTEX_CONNECTION` (env wins). `entwurfProvider.backend: "cortex"` is an optional diagnostic guard, never the router.
478
+ - **`CORTEX_HOME` must be unset.** It outranks `SNOWFLAKE_HOME` inside cortex and would bypass the dual-HOME containment, so the adapter refuses the spawn when it is present at all (empty string included).
479
+ - **Containment:** each session runs under an isolated HOME (operator-global `~/.claude`/`~/.cortex` skills, hooks and settings are structurally unreachable; explicit `<cwd>/.claude` project scope is retained), with the explicit `entwurfProvider.mcpServers` projected into an overlay-private `cortex/mcp.json` — cortex's ACP server ignores the wire `mcpServers` parameter, so this projection is how tools reach a cortex session. Only the `entwurf-bridge` entry gets the real operator HOME back (the garden store axis).
480
+ - **Live check:** `LIVE=1 ENTWURF_ACP_CORTEX_CONNECTION=<conn> entwurf smoke-acp-cortex-live` (or `./run.sh …` from a clone) drives one real cortex turn: outbound `entwurf_v2` delivery as `entwurf/<cortex model>`, overlay disk facts, and process-group reclaim. It is deliberately **on-demand** — which is a statement about *wiring*, not about *whether cortex needs live evidence*:
481
+
482
+ - The aggregate floor (`LIVE=1 ./run.sh release-gate`) stays claude-only, so a host with no cortex install or no Snowflake auth cannot redden a release for a backend it does not run.
483
+ - That is **not** a claim that cortex ships on deterministic evidence alone. Accepting a cut that ships cortex means running this smoke deliberately, and reading its result — the aggregate gate will not run it for you, and its silence is not a pass.
484
+ - Cortex's always-on axis is the deterministic `check-acp-cortex`, which does ride `pnpm check`.
472
485
 
473
486
  ### Emacs frontends
474
487
 
@@ -523,6 +536,8 @@ Reference shape lives in [`pi/settings.reference.json`](./pi/settings.reference.
523
536
 
524
537
  `appendSystemPrompt: false` is intentional. Pi / AGENTS context rides the first-user augment; putting it into the Claude `_meta.systemPrompt` carrier can route OAuth sessions to metered "extra usage" billing.
525
538
 
539
+ **Which keys reach which backend.** `entwurfProvider` is one block for both backends, but its keys are not universal. `tools` / `permissionAllow` / `disallowedTools` / `settingSources` / `skillPlugins` / `appendSystemPrompt` are Claude's declaration surface: they do not shape a cortex session, which runs its own native tools and reaches MCP through the overlay-private `mcp.json` projection instead. They are not inert, though — the bridge still reads `tools` for its backend-invariant exclude-tools preflight and folds all of them into the config signature, so editing one still forces a fresh cortex session. `cortexConnection` is cortex-only. `mcpServers` is the one declaration surface that reaches both, each through its own transport. (`compaction` is a *top-level* pi key, not an `entwurfProvider` one — it is pi's own transcript policy and is not a backend knob at all; see §Compaction.) Unknown and retired keys are ignored rather than rejected, so a key aimed at the wrong backend fails silently: if a cortex session does not show the surface you configured, check that the key is one cortex actually consumes before suspecting the bridge.
540
+
526
541
  ### Wiring `entwurf-bridge` into an external MCP host
527
542
 
528
543
  `entwurf-bridge` can also be registered in a separate MCP-aware harness (Claude Code, Codex CLI, Antigravity/`agy`, …). That host does **not** become a pi session and does **not** need to be ACP-backed. There are now two honest cases:
@@ -651,7 +666,7 @@ The Claude ACP backend keeps its native model / API / tools; entwurf shapes only
651
666
 
652
667
  **Claude** uses `_meta.systemPrompt` for the engraving carrier (kept short and pure — billing-safe; rich operator context rides the first user message instead, see [Context carriers](#context-carriers)) and `CLAUDE_CONFIG_DIR` for a whitelist overlay so auth/runtime entries stay available while operator memory, hooks, agents, history, local settings, and project memory remain hidden. The overlay writes an explicit empty `hooks: {}` because Claude SDK organic compaction needs the configured-empty shape; no operator hook definitions are inherited. The four-tool baseline (`Read`, `Bash`, `Edit`, `Write`) is enforced through `tools` + `permissionAllow`; `Skill` is added automatically when `skillPlugins` is non-empty. Operator context cap override: `ENTWURF_ACP_CLAUDE_CONTEXT=<int>`.
653
668
 
654
- (Codex is *not* an ACP backend here — it reaches the garden natively. Vendor / governed CLIs are a later ACP backend lane.)
669
+ (Codex is *not* an ACP backend here — it reaches the garden natively. The governed-CLI lane is open, not later: Snowflake Cortex Code landed on it in 0.13.0, and a third governed backend would join the same adapter rail.)
655
670
 
656
671
  Antigravity is also not an ACP backend. It is a native-push citizen: `PreInvocation` supplies birth/sender identity, `entwurf_v2` probes and direct-injects replies into the live conversation, and no mailbox/receiver marker is involved.
657
672
 
package/VERIFY.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # VERIFY.md
2
2
 
3
- Agent-driven verification guide for the current `entwurf` 0.12.x surface.
3
+ Agent-driven verification guide for the `entwurf` surface as it stands in the working tree — the last **released** line is 0.12.x, and the tree is the **prepared 0.13.0 cut** (its tag, GitHub release, and publication are separate authorizations that have not run). The cut carries more than one change (the probe/ordering lane sits in it too); the change relevant to *this guide* is the second ACP backend. Rows below marked 0.12 are the released baseline; where 0.13.0 differs, it says so.
4
4
 
5
- > **Current surface.** The bundled MCP server, `entwurf-bridge`, exposes five tools: `entwurf_v2`, `entwurf_peers`, `entwurf_self`, `entwurf_inbox_read`, and `entwurf_register_native` (an explicit/manual fallback for binding an already-running native conversation). The shipped ACP backend is **Claude**. Antigravity (`agy`) is a separate shipped **native-push citizen** lane, not an ACP backend: automatic `PreInvocation` birth + sender identity + live probe/direct injection. Codex is pi-native by default and has native delivery probe evidence; Gemini is a non-goal/historical ACP probe on 0.12. The 0.4.x `session-bridge` adapter, the 0.11.0 fat-bridge (`acp-bridge.ts` / `ensureBridgeSession`), and the v1 `entwurf` / `entwurf_resume` / `entwurf_send` verbs are **retired** — rows mentioning them survive in CHANGELOG/git as historical baseline, never as a runnable recipe.
5
+ > **Current surface.** The bundled MCP server, `entwurf-bridge`, exposes five tools: `entwurf_v2`, `entwurf_peers`, `entwurf_self`, `entwurf_inbox_read`, and `entwurf_register_native` (an explicit/manual fallback for binding an already-running native conversation). The ACP backends are **Claude** (the reference, 0.12) and, as of **0.13.0**, **Snowflake Cortex Code** (`cortex-` prefixed ids; deterministic axis `check-acp-cortex` in `pnpm check`, LIVE axis `smoke-acp-cortex-live` on demand — see [docs/acp-backend-rail.md](./docs/acp-backend-rail.md) §11-8 for the measured contract and its limits). Antigravity (`agy`) is a separate shipped **native-push citizen** lane, not an ACP backend: automatic `PreInvocation` birth + sender identity + live probe/direct injection. Codex is pi-native by default and has native delivery probe evidence; Gemini is a non-goal/historical ACP probe on 0.12. The 0.4.x `session-bridge` adapter, the 0.11.0 fat-bridge (`acp-bridge.ts` / `ensureBridgeSession`), and the v1 `entwurf` / `entwurf_resume` / `entwurf_send` verbs are **retired** — rows mentioning them survive in CHANGELOG/git as historical baseline, never as a runnable recipe.
6
6
 
7
7
  This is a **working document, not a metrics document**. The deterministic and live gates carry the machine-checkable invariants; this file carries only what a gate cannot judge — the human/agent reading of *whether the bridge is honestly itself*. Where a former manual procedure is now a gate, it is named as a pointer rather than re-spelled as a runnable script.
8
8
 
@@ -43,7 +43,7 @@ Verification here is not a benchmark. In production we exchange short turns and
43
43
 
44
44
  > The aggregate release gate does not own a live agy conversation id, so agy's real native-push round trip is a separate acceptance axis: three fail-loud doctors plus `LIVE=1 AGY_CONVERSATION_ID=<id> ./run.sh smoke-agy-native-push-live`, followed by a fresh-conversation sender/reply check after package install. Its deterministic install/sender gates are already inside `pnpm check`; do not misreport the aggregate gate as live agy evidence.
45
45
  >
46
- > The authoritative per-cut counts live in BASELINE.md's HISTORY and CHANGELOG/git, not inline here (they drift against `run.sh`). Most recent recorded aggregate: **2026-07-24 (night) — MUST 16/1/0 + BEHAVIOR 1/0, EXIT=1** at `cbda097` — the single FAIL is the known bundled-MCP readiness race (ROADMAP 「🔴 OPEN」, observe-don't-fix; isolated re-runs pass). Most recent all-green floor: **2026-07-24 — MUST 17/0/0 + BEHAVIOR 1/0, EXIT=0** at `7cbeb29b6afcfbaf4fc28da3b7929037c339113d` (the dependency-uplift HEAD: pi 0.82.0 + claude-agent-acp 0.61.0 / ACP SDK 1.3.0). The step count moved 16→17 with `smoke-acp-v2-send-live`; earlier that day it had moved 17→16 with the v2-cutover smoke retirements. A release cut requires the aggregate red to be resolved or explicitly ruled by GLG.
46
+ > The authoritative per-cut counts live in BASELINE.md's HISTORY and CHANGELOG/git, not inline here (they drift against `run.sh`). Most recent recorded aggregate: **2026-07-24 (night) — MUST 16/1/0 + BEHAVIOR 1/0, EXIT=1** at `cbda097` — the single FAIL is the known bundled-MCP readiness race (ROADMAP 「🔴 OPEN」, observe-don't-fix; isolated re-runs pass). Most recent all-green floor: **2026-07-30 — MUST 17/0/0 + BEHAVIOR 1/0, EXIT=0** on the prepared 0.13.0 tree (parent landing HEAD `9f1c7dc`, scratch `/tmp/entwurf-release-gate-0.13.0.drnRyR`) — the first aggregate completion carrying the cortex adapter and the probe lane; the step count did not move, because the cortex LIVE axis is deliberately outside this aggregate and is run on demand. Before it: **2026-07-24 — MUST 17/0/0 + BEHAVIOR 1/0, EXIT=0** at `7cbeb29b6afcfbaf4fc28da3b7929037c339113d` (the dependency-uplift HEAD: pi 0.82.0 + claude-agent-acp 0.61.0 / ACP SDK 1.3.0). The step count moved 16→17 with `smoke-acp-v2-send-live`; earlier that day it had moved 17→16 with the v2-cutover smoke retirements. A release cut requires the aggregate red to be resolved or explicitly ruled by GLG.
47
47
 
48
48
  ### Artifact / host certification matrix — #51 repair line → 0.12.8 stable
49
49
 
@@ -203,6 +203,8 @@ mailbox with `origin=pi-session`, `replyable=true`).
203
203
 
204
204
  Compare a fresh self-awareness report across axes: (1) same backend, different install path — answer must be path-invariant; (2) same backend, different machine — identical native tool list + MCP server/tool set; (3) different backend, same bridge — same garden capability but **different** native tool surface (a Claude session reporting another backend's native tools is a fail); (4) native pi routing vs ACP-bridged, same model — the native target reports no `entwurf-bridge` MCP (capability via pi's extension surface), while the ACP target reports it as the single MCP server. Honest "native: I cannot tell" hedging is PASS on the native side. Claude axes 1–4 are closed. agy is graded by the separate native-citizen checklist above, not by pretending it has Claude's ACP overlay. Gemini remains probe-only on 0.12.
205
205
 
206
+ **Cortex (0.13.0) on axis 3.** Cortex is the first real *different backend, same bridge* subject, and it is the sharpest form of this axis: a cortex session must report the garden capability (`entwurf-bridge` reachable, record-backed peers) while its native tool surface is cortex's own, never Claude's — a cortex session listing Claude's built-ins is a fail. Two contract facts to grade against rather than re-derive: the callable identifier shape is the same `mcp__<server>__<tool>` convention as Claude (**measured**, §11-8), and cortex is **system-prompt-carrier-less**, so the operator engraving arrives at the head of the first-user augment instead of in a system prompt — a cortex session claiming a system-prompt engraving is reporting something it does not have. Axis 1 (install-path invariance) is split as of 0.13.0: the **deterministic** half is closed — `check-pack-install` enumerates the exact six curated rows out of the installed package's own model list — while the **live** half is a release-blocking `make` tag gate (the preserved exact candidate installed into a fresh temporary root driving one cold `entwurf/cortex-claude-sonnet-5` turn from those installed bytes). Until that gate runs, installed-artifact evidence and real-Cortex evidence have never met in one execution. Axis 2 (second machine) is unrun.
207
+
206
208
  ---
207
209
 
208
210
  ## 1A. Main Agent Evaluation — Is `entwurf` Claude strong enough?
@@ -253,10 +255,11 @@ The literal callable identifier differs per backend — probe by asking the agen
253
255
  | Backend | Literal identifier | Outer sep | Inner server name |
254
256
  |---|---|---|---|
255
257
  | Claude | `mcp__entwurf-bridge__entwurf_v2` | `__` | `entwurf-bridge` (hyphen) |
258
+ | Cortex *(0.13.0)* | `mcp__entwurf-bridge__entwurf_v2` | `__` | `entwurf-bridge` (hyphen) — same shape as Claude, **measured** (§11-8) |
256
259
  | Codex | `mcp__entwurf_bridge__.entwurf_v2` | `__` | `entwurf_bridge` (underscore) + **literal dot** |
257
260
  | Gemini *(probe)* | `mcp_entwurf-bridge_entwurf_v2` | `_` (single) | `entwurf-bridge`, no dot |
258
261
 
259
- A Claude session reporting the underscore form, or any cross-shape leak, is a backend-identification leak. Shipped 0.12 baseline is Claude; the Codex/Gemini rows are reference for the probe lanes.
262
+ A Claude session reporting the underscore form, or any cross-shape leak, is a backend-identification leak. Released 0.12 baseline is Claude; cortex joins it in 0.13.0 and is the one row whose shape had to be *measured* rather than assumed (identical to Claude's — so this row cannot discriminate Claude from Cortex, and the tool-surface axis in §1.4 is what does). The Codex/Gemini rows are reference for the probe lanes.
260
263
 
261
264
  ### 2.2 MCP injection visibility — equal across resume/load/new
262
265
 
@@ -18,11 +18,22 @@
18
18
  // lib/acp modules — no new strip-types fence.
19
19
  import { readFileSync } from "node:fs";
20
20
  import { createRequire } from "node:module";
21
+ import { homedir } from "node:os";
21
22
  import { dirname, join } from "node:path";
23
+ import { enrichMcpServersWithEnvelope } from "./config.js";
22
24
  import { loadEngraving } from "./engraving.js";
23
- import { curatedClaudeModels, SUPPORTED_ANTHROPIC_MODEL_IDS } from "./models.js";
24
- import { claudeLaunchEnvDefaults, ensureClaudeConfigOverlay } from "./overlay.js";
25
+ import { CORTEX_MODEL_PREFIX, curatedClaudeModels, curatedCortexModels, SUPPORTED_ANTHROPIC_MODEL_IDS, SUPPORTED_CORTEX_MODEL_IDS, } from "./models.js";
26
+ import { claudeLaunchEnvDefaults, ensureClaudeConfigOverlay, ensureCortexDualHomeOverlay } from "./overlay.js";
25
27
  import { buildClaudeSessionMeta } from "./tool-surface.js";
28
+ // POSIX-safe single-quote wrapper for shell arg interpolation. Byte-for-byte
29
+ // identical to the reference in entwurf-core.ts; PARITY-PINNED by
30
+ // scripts/check-shell-quote.ts (SOURCE_SITES). Used only by the cortex override
31
+ // path below, where operator-configured connection/model tokens are appended to
32
+ // an operator `bash -lc` string — quoting keeps a connection name with shell
33
+ // metacharacters from being reinterpreted by the shell.
34
+ function shellQuote(value) {
35
+ return `'${value.replace(/'/g, `'\\''`)}'`;
36
+ }
26
37
  // ---------------------------------------------------------------------------
27
38
  // claude adapter — the first implementation (the rail's reference backend)
28
39
  // ---------------------------------------------------------------------------
@@ -105,9 +116,141 @@ export const claudeAdapter = {
105
116
  // Registry — modelId → adapter
106
117
  // ---------------------------------------------------------------------------
107
118
  /** Registered adapters. Order carries NO routing authority — routeModel decides.
108
- * Step A: claude only. A second backend appends here with its reserved prefix
109
- * (e.g. `<backend>-*`), and the fail-fast below proves no two adapters claim one id. */
110
- const ADAPTERS = [claudeAdapter];
119
+ * claude (unprefixed ids) + cortex (the `cortex-` prefix). A further backend
120
+ * appends here with its own reserved prefix; the fail-fast below proves no two
121
+ * adapters claim one id. */
122
+ // ---------------------------------------------------------------------------
123
+ // cortex adapter — Snowflake Cortex Code, the first non-claude backend on the
124
+ // rail (docs/acp-backend-rail.md §4/§6). It adds ZERO to the common layer:
125
+ // everything cortex-specific lives here + models.ts + overlay.ts + the gates.
126
+ // ---------------------------------------------------------------------------
127
+ const SUPPORTED_CORTEX_IDS = new Set(SUPPORTED_CORTEX_MODEL_IDS);
128
+ /** The env var an operator sets to pin a Snowflake connection per-shell without
129
+ * editing settings.json. Wins over `entwurfProvider.cortexConnection`. The
130
+ * ENTWURF_ACP_* convention (PR #40's legacy PI_SHELL_ACP* cortex-connection var renamed). */
131
+ export const CORTEX_CONNECTION_ENV = "ENTWURF_ACP_CORTEX_CONNECTION";
132
+ export const cortexAdapter = {
133
+ backend: "cortex",
134
+ // Cortex owns the reserved `cortex-` prefix (§9-1). routeModel strips it to the
135
+ // native id: `cortex-auto` → "auto", `cortex-claude-sonnet-5` → "claude-sonnet-5".
136
+ routeModel(modelId) {
137
+ if (!SUPPORTED_CORTEX_IDS.has(modelId))
138
+ return undefined;
139
+ return { nativeModelId: modelId.slice(CORTEX_MODEL_PREFIX.length) };
140
+ },
141
+ curatedModels() {
142
+ return curatedCortexModels();
143
+ },
144
+ // Cortex's ONLY own setting is the connection name. env override wins over
145
+ // settings (per-shell pin); empty/whitespace → null (Cortex falls back to its
146
+ // own default connection). A non-string settings value fails loud.
147
+ resolveAdapterSettings({ mergedBlock, projectBlock, globalPath, projectPath }) {
148
+ const raw = mergedBlock.cortexConnection;
149
+ if (raw !== undefined && typeof raw !== "string") {
150
+ const offending = projectBlock.cortexConnection !== undefined ? projectPath : globalPath;
151
+ throw new Error(`${offending}: invalid entwurfProvider settings: cortexConnection must be a string`);
152
+ }
153
+ const envConn = process.env[CORTEX_CONNECTION_ENV]?.trim();
154
+ const settingsConn = raw?.trim();
155
+ const cortexConnection = envConn || settingsConn || null;
156
+ return { cortexConnection };
157
+ },
158
+ // `cortex acp serve` resolved from PATH (the CLI itself IS the ACP server — no
159
+ // `*-acp` npm package, unlike claude). `-c <conn>` appended when a connection
160
+ // is pinned. NO `-m`: the model is enforced per-turn via
161
+ // session/set_config_option (enforceModel below, CP0-M measured GO) and a
162
+ // launch pin would be a SECOND model authority that drifts from it.
163
+ // CORTEX_ACP_COMMAND override runs via `bash -lc` with the selection flags
164
+ // appended so the bridge's choice wins (later yargs args override earlier ones).
165
+ resolveLaunch({ config }) {
166
+ const settings = config.adapterSettings;
167
+ const connection = settings?.cortexConnection?.trim() || undefined;
168
+ const selectionArgs = [];
169
+ if (connection)
170
+ selectionArgs.push("-c", connection);
171
+ const override = process.env.CORTEX_ACP_COMMAND?.trim();
172
+ if (override) {
173
+ const command = selectionArgs.length > 0 ? `${override} ${selectionArgs.map(shellQuote).join(" ")}` : override;
174
+ return { command: "bash", args: ["-lc", command] };
175
+ }
176
+ return { command: "cortex", args: ["acp", "serve", ...selectionArgs] };
177
+ },
178
+ // The overlay location is SESSION-SCOPED (never static), so the spawn env
179
+ // rides ensureOverlay(...).envOverrides; there is no static launch env. The
180
+ // v1.1.8-era CORTEX_DISABLE_AUTO_APPLY_PROFILES knob was retired with the
181
+ // dual-HOME redesign: profiles now live inside the overlay-owned isolated
182
+ // home (empty by construction), and the knob is unmeasured on v1.1.52.
183
+ launchEnvDefaults() {
184
+ return {};
185
+ },
186
+ // Dual-HOME containment (CP0 D2/D3/D9/D10 — see the overlay module header):
187
+ // refuse an ambient CORTEX_HOME outright, then materialize the session-scoped
188
+ // isolated HOME with auth symlinks, `autoUpdate:false`, and the mcp.json
189
+ // projection of the envelope-enriched explicit servers (cortex ignores the
190
+ // wire mcpServers param, so this file IS how tools reach a cortex session).
191
+ ensureOverlay({ modelId, config, sessionKey }) {
192
+ // D3 — presence refusal, empty string included: upstream's resolver treats
193
+ // a set-but-empty CORTEX_HOME differently from unset, and one ambient value
194
+ // would silently bypass SNOWFLAKE_HOME (the probe's CLAUDE_CODE_EXECUTABLE
195
+ // precondition is the same family). Refuse the ambiguity; never pick a side.
196
+ if ("CORTEX_HOME" in process.env) {
197
+ throw new Error("entwurf: CORTEX_HOME is present in the environment (empty string included) — it overrides " +
198
+ "SNOWFLAKE_HOME inside cortex and would bypass the dual-HOME overlay entirely (CP0 D3). " +
199
+ "Unset it to run a cortex ACP turn.");
200
+ }
201
+ // The scope authority is the AUTHORITATIVE params.sessionKey backend.ts
202
+ // computed — never an ambient re-derivation, which would drop opts.sessionId
203
+ // and alias two same-process/cwd sessions onto one overlay (P0-1). The
204
+ // envelope below still reads PI_SESSION_ID: that is the identity CARRIER for
205
+ // the bridge child (the same source the turn loop's wire enrichment uses),
206
+ // a different axis from overlay-dir scoping.
207
+ const piSessionId = process.env.PI_SESSION_ID?.trim() || undefined;
208
+ const enriched = enrichMcpServersWithEnvelope(config.mcpServers, { modelId, piSessionId });
209
+ const overlay = ensureCortexDualHomeOverlay({
210
+ scopeKey: sessionKey,
211
+ mcpServers: enriched,
212
+ realHome: homedir(),
213
+ });
214
+ return { envOverrides: { HOME: overlay.home, SNOWFLAKE_HOME: overlay.snowflakeHome } };
215
+ },
216
+ // System-prompt-carrier-less (§9-4/§11-8): Cortex ACP exposes no
217
+ // `_meta.systemPrompt` and has no developer_instructions / GEMINI_SYSTEM_MD
218
+ // equivalent. (It does READ `_meta` — a caller-session-id seam, measured but
219
+ // unexplored and deliberately not part of this contract.) loadCarrier returns
220
+ // null WITHOUT calling loadEngraving, so the cortex turn never touches the
221
+ // shipped-engraving / appendSystemPrompt signature; buildSessionMeta returns
222
+ // undefined so backend.ts omits the `_meta` key entirely. The operator
223
+ // engraving instead rides the first-user augment (augment.ts).
224
+ loadCarrier() {
225
+ return null;
226
+ },
227
+ buildSessionMeta() {
228
+ return undefined;
229
+ },
230
+ // Per-turn enforcement via session/set_config_option — the SAME wire call the
231
+ // claude adapter makes, measured live against cortex v1.1.52 (CP0-M): the
232
+ // option id is "model", accepted values are the NATIVE ids (`auto`,
233
+ // `claude-sonnet-5`, `openai-gpt-5.4`, …), and a value cortex no longer
234
+ // serves fails loud BEFORE the prompt (`Unsupported model: …`). PR #40's
235
+ // launch-time `-m` pin was retired for this: set-model is the single model
236
+ // authority (resolveLaunch never passes `-m`), and "auto" is set explicitly
237
+ // rather than treated as an unspoken default.
238
+ async enforceModel({ connection, acpSessionId, nativeModelId, modelId }) {
239
+ const setConfig = connection.setSessionConfigOption;
240
+ if (typeof setConfig !== "function") {
241
+ throw new Error(`setSessionConfigOption unsupported — cannot enforce model ${modelId}`);
242
+ }
243
+ await setConfig.call(connection, { sessionId: acpSessionId, configId: "model", value: nativeModelId });
244
+ },
245
+ // A connection change must invalidate a reused session (§4/§7). Flat,
246
+ // sorted-stable primitive map; reads ONLY the opaque adapterSettings. `backend`
247
+ // + `nativeModelId` are added by backend.ts.
248
+ configSignatureFields(adapterSettings) {
249
+ const settings = adapterSettings;
250
+ return { cortexConnection: settings?.cortexConnection ?? null };
251
+ },
252
+ };
253
+ const ADAPTERS = [claudeAdapter, cortexAdapter];
111
254
  /**
112
255
  * Resolve the backend adapter that owns `modelId`.
113
256
  *
@@ -8,11 +8,23 @@
8
8
  // config and hands it to the backend so the documented passthrough actually
9
9
  // reaches `newSession`.
10
10
  //
11
- // Claude-only scope (NEXT §스코프). Ported from the v0.11.0 behavior oracle
12
- // (index.ts `loadProviderSettings`/`readSettingsFile` + acp-bridge.ts
11
+ // Scope. Ported from the v0.11.0 behavior oracle (index.ts
12
+ // `loadProviderSettings`/`readSettingsFile` + acp-bridge.ts
13
13
  // `normalizeMcpServers`/`enrichMcpServersWithEnvelope`) — structure new, behavior
14
- // preserved. Codex/Gemini fields (codexDisabledFeatures, …) are out of scope and
15
- // deliberately NOT carried.
14
+ // preserved. Retired Codex/Gemini-era fields (codexDisabledFeatures, …) are NOT
15
+ // carried and are silently ignored if present.
16
+ //
17
+ // The COMMON keys parsed here are Claude-SHAPED by origin (tools / permissionAllow /
18
+ // disallowedTools / settingSources / skillPlugins are claude-agent-acp's surface).
19
+ // What varies per backend is whether they are PROJECTED ONTO THE BACKEND SESSION —
20
+ // not whether they are read at all. Cortex runs native tools and reaches MCP through
21
+ // its own overlay mcp.json projection, so none of those keys shape a cortex session;
22
+ // they nonetheless stay live on the common path, where backend.ts reads `tools` for
23
+ // the backend-invariant exclude-tools preflight and folds every one of them into
24
+ // `bridgeConfigSignature` (so changing one still invalidates a reused cortex
25
+ // session). Backend-OWNED settings never land here: they ride the opaque
26
+ // `adapterSettings` seam (§10 B), which is what keeps a backend-named key like
27
+ // `cortexConnection` off this common shape.
16
28
  //
17
29
  // PURITY / SIGNATURE contract (NEXT oracle C / 핀1 / GPT `…2f9325` boost):
18
30
  // - `normalizeMcpServers` is pure: a SORTED, validated server list + a sha256
@@ -1,14 +1,17 @@
1
- // ACP plugin — curated Claude model surface (S0 loader/fence slice).
1
+ // ACP plugin — curated model surface (S0 loader/fence slice; cortex rows added
2
+ // in 0.13).
2
3
  //
3
4
  // entwurf is an ACP *plugin* on the v2 core, NOT a general-purpose
4
5
  // Anthropic provider. It deliberately does not expose the full pi-ai model
5
- // registry — the surface is curated to the Claude anchor the plugin commits to
6
- // driving through a real ACP backend (the backend lands in S2; S0 only stands
7
- // the provider/model surface up).
6
+ // registry — the surface is curated to the ids the plugin commits to driving
7
+ // through a real ACP backend.
8
8
  //
9
- // Claude-first scope (NEXT §스코프): Codex is already a native garden citizen
10
- // and Gemini / major tools use native, so the curated surface is Claude-only on
11
- // this lane. A second governed backend would EXTEND this set — it does not change
9
+ // Two curated sets live here, one per adapter on the rail
10
+ // (backend-adapter.ts): the unprefixed Claude ids (the reference backend) and
11
+ // the `cortex-` prefixed Snowflake Cortex Code ids (the second backend, landed
12
+ // 0.13.0 — docs/acp-backend-rail.md §6/§11-8). Codex is already a native garden
13
+ // citizen and Gemini/major tools use native, so neither is an ACP backend here.
14
+ // A third governed backend EXTENDS this file the same way — it does not change
12
15
  // the pattern.
13
16
  // pi 0.80 migration: the standalone root `getModels()` moved to the deprecated
14
17
  // `@earendil-works/pi-ai/compat` entrypoint (the global-API churn). We import it
@@ -71,6 +74,62 @@ function requireRegistryModel(models, id) {
71
74
  function claudeContextWindow(model) {
72
75
  return Math.min(model.contextWindow, CLAUDE_CONTEXT_DEFAULT);
73
76
  }
77
+ // ── Cortex (Snowflake Cortex Code) curated surface ──────────────────────────
78
+ //
79
+ // entwurf's ACP rail is backend-extensible: Cortex is the first NON-claude
80
+ // backend to land through it (docs/acp-backend-rail.md §6). pi-ai carries no
81
+ // snowflake/cortex model source, so this surface is HAND-CURATED. Every id
82
+ // carries the reserved `cortex-` prefix (§9-1): the prefix is the SINGLE routing
83
+ // authority (backend-adapter.ts `routeModel`) and keeps the ids off the Claude
84
+ // 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
86
+ // recover the native id, which is enforced per-turn via
87
+ // session/set_config_option — never a launch `-m` pin (CP0-M).
88
+ //
89
+ // The 4-row set is the GLG-decided curation (2026-07-29) — a subset of the 11
90
+ // native ids cortex v1.1.52 exposed live when authenticated. A curated id that
91
+ // the running cortex no longer serves fails LOUD at set-model, before the
92
+ // prompt (measured: `Unsupported model: …` names the live set). Adding an id
93
+ // 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
+ ];
102
+ // Hand-set conservative context window. Cortex reports the LIVE window via its
103
+ // own ACP session config, not this curated metadata — this is a floor for the
104
+ // registry surface, deliberately not inflated to the Claude 1M.
105
+ const CORTEX_CONTEXT_DEFAULT = 200_000;
106
+ /**
107
+ * The curated Cortex model rows handed to the single `entwurf` provider via
108
+ * `allCuratedModels()`. The two Claude rows ride their OWN registry base
109
+ * (opus-5 / sonnet-5); `auto` (default family Claude) and the GPT row (no pi-ai
110
+ * source) ride the sonnet-5 base as a metadata floor. id / name / contextWindow
111
+ * are overridden; the `cortex-` prefix keeps them from colliding with the
112
+ * Claude curated ids. Same row shape as `curatedClaudeModels()` (AcpModelRow).
113
+ */
114
+ export function curatedCortexModels() {
115
+ const sonnetBase = requireRegistryModel(ANTHROPIC_MODELS_ALL, "claude-sonnet-5");
116
+ const opusBase = requireRegistryModel(ANTHROPIC_MODELS_ALL, "claude-opus-5");
117
+ const row = (id, name, base) => ({
118
+ id,
119
+ name,
120
+ reasoning: base.reasoning,
121
+ input: base.input,
122
+ cost: base.cost,
123
+ contextWindow: CORTEX_CONTEXT_DEFAULT,
124
+ maxTokens: base.maxTokens,
125
+ });
126
+ return [
127
+ row("cortex-auto", "Cortex · Auto", sonnetBase),
128
+ row("cortex-claude-opus-5", "Cortex · Claude Opus 5", opusBase),
129
+ row("cortex-claude-sonnet-5", "Cortex · Claude Sonnet 5", sonnetBase),
130
+ row("cortex-openai-gpt-5.4", "Cortex · OpenAI GPT-5.4", sonnetBase),
131
+ ];
132
+ }
74
133
  /**
75
134
  * The curated Claude model rows handed to `pi.registerProvider({ models })`.
76
135
  * Fail-loud if the anchor is absent from the pi-ai registry.