Skip to content

feat: the plugin owns the opencode and opencode-go routes - #113

Merged
viztor merged 182 commits into
mainfrom
claude/vigilant-mendel-bctz9n-p-r1
Nov 13, 2026
Merged

viztor merged 182 commits into
mainfrom
claude/vigilant-mendel-bctz9n-p-r1

Conversation

@viztor

@viztor viztor commented Oct 24, 2026 •

Copy link
Copy Markdown
Owner

Phase P-R1 of docs/improvement-plan.md. Its parent, P-H (#112), is merged, and this PR targets main.

The plugin now serves the opencode and opencode-go routes itself (the D13 design), using only supported host paths. The hidden-route workaround is gone.

Steps aside for a route another plugin owns (D27)

The official OpenCode Console plugin (@opencode/console) writes its own opencode / opencode-go entries into pi-ai's config. Its setup gives pi-ai a private credentials realm whose only token is OPENCODE_CONSOLE_TOKEN, and marks each entry with an x-opencode-plugin header. Before this change, the route claim took those entries over and served them without the realm's token. That would have failed every Console turn with MISSING_CREDENTIAL, and our headers and key capture would have touched Console traffic.

Detection. It reads the host's own data, never a vendor name or a version. A plane is left to pi-ai when either rule holds:

  • Realm (primary): pi-ai's row resolves credentials in a realm the plugin's own row does not share. The realm is resolved the way the loader resolves it, including a realm inherited from an enclosing group.
  • Marker (secondary): the entry's headers carry x-opencode-plugin, in any case.

What steps aside:

  • the claim's strip, adapter registration and restart accounting;
  • the save validation, so pi-ai's own check judges such an entry;
  • the settings.describe graft;
  • the stream hook (no turn state, session id or cost);
  • the fetch patch (no headers, no key fill, no key capture);
  • the meter's key discovery;
  • Settings → Models → Fetch enrichment;
  • the purge action.

Live edits. A live edit that adds or removes the marker, or moves a realm on pi-ai's row, the plugin's own row or an enclosing group, moves the plane with at most one pi-ai restart. Each stepped-aside entry is logged once per row, plane and reason, at info level.

New pure module: src/utils/plane-ownership.ts, held at 100% coverage.

Commit Subject
5ecc037 fix: the route claim steps aside for a plane another plugin owns
e21f99d docs: D27, and the routes OpenCode Console serves
6527780 fix: keep the plugin off every stepped-aside plane, by realm or marker (review round 1)
d63a24c docs: D27 names the realm by what each context resolves, and covers the meter and live saves (round 1)
a1db1d2 fix: a live change of pi-ai's realm alone moves its planes, with one restart (round 2)
51f4626 fix: the purge never rewrites a plane entry another plugin owns (round 2)
7052ab0 test: pin the step-aside's multi-row branches and the save check on a real host (round 2)
574604e docs: D27 covers a realm-only live edit and the purge (round 2)
140c9c9 fix: a live realm change on the plugin's own row or a group moves the planes (round 3)
3c85d97 fix: a marker saved beside a second pi-ai row releases the plane in the commit run (round 3)
d58994b test: perform the discovery and meter step-aside through the applied plugin (round 3)
b2d5a86 docs: D27 covers every row's realm edit and the second-row release; true 1.x history (round 3)

Tests.

  • A new real-host file, test/host-planes-foreign.test.ts, uses tier B's new realm option. It writes the Console's patch shape with our own fixture plugin; no vendor file is read or run.
  • Known answer: pi-ai alone serves both planes with the realm's token.
  • In both apply orders, the realm and marked planes stay with pi-ai's adapter, with zero restarts.
  • A Console turn carries the realm token and the same header names as pi-ai alone, none of ours, and nothing is captured.
  • Each of the following is covered: the realm alone, the marker alone, a marked/unmarked split, live realm moves, the save check, the purge, discovery and the meter.

Red first. With src/ at 33925ef and the new tests in place, 14 tests failed: 9 in host-planes-foreign (including the tier-A marker case), 3 in fetch-patch, 1 in stream-hook and 1 in plane-profile. Mutation checks: removing the stream-hook skip fails 2 tests, removing the fetch passthrough fails 4, removing the discovery wiring fails 2, and removing the meter's realm skip fails 1.

Review. Three rounds:

Round Found Upheld Fixed
1 9 9 9
2 6 5 5
3 6 6 6

Gate on b2d5a86:

  • vp check: 0 errors, 11 warnings.
  • Unit suite: 110 files, 1721 passed, 1 skipped, on both channels.
  • Mock suite: 68 passed.
  • Coverage: every floor met (total 97.79 / 94.47 / 98.26 / 97.78).
  • Build and bundle gate: passed.
  • Binary e2e, npm cells: rc.2 and alpha.3 each 13 passed, 2 skipped (the two HOST_LIMITATION rows).
  • No absolute local paths in the diff.

Behaviour change, in both READMEs: a new section, "With OpenCode Console installed", and a "Changed in 1.x" bullet.

Known limit: a live edit that adds or removes the marker on a plane pi-ai has already registered takes effect at pi-ai's next restart, because pi-ai registers its routes when it activates (F11, F15).

Follow-up for P3 (#115): its discovery complement must leave out stepped-aside planes. That goes into the cascade.

Merged main (#170, #159)

33925ef merges main 7504fe2. That brings in:

There were six conflicts: the README pair, three docs and test/catalog.test.ts. Each was resolved by keeping this PR's structure and taking #159's numbers. The merge's review found 0 findings, and the gate passed on both channels.

Rebased onto main 1.9.2 and the latest hosts

This section supersedes "Restacked onto main 1.8.0" below wherever they disagree.

(Planned as main 1.9.1; by the time the rebase ran, main had cut 1.9.2, and the branch sits on main 1b4ebf0, v1.9.2 plus four commits, among them #110 (P-T1b) and #112 (P-H).)

The PR is now its own change on its parent. The branch is one rebase commit plus its adaptation and review-fix commits on top of main 1b4ebf0:

Commit Subject
d90f066 feat: the plugin owns the opencode and opencode-go routes (the rebase commit: this PR's whole earlier change, as one commit)
0ab3fa7 fix: the route claim speaks both internal/config call shapes
9ba2290 fix: plane profiles carry pi-ai's image bounds where pi-ai defines them
d4f6557 test: the binary e2e expects the claim on both host cores
5f82bd1 test: the route claim through a pi-ai row the loader removed first
bbd924d docs: record the new host core and the claim on both shapes
aa70a43 fix: the route claim's re-append never throws into pi-ai's status emit (review round 1)
1f80a51 docs: mark P-T1b and P-H merged, and put P-R1 on main 1.9.2 (review round 2)
0b6fc99 docs: the plan's summaries say P0 to P-H are merged (review round 3)
974b43a history-only merge of the pre-rebase tip 26d1e74. Its tree is 0b6fc99's, so the branch moved forward without a force-push

The rebase had no conflicts.

Dropped because main already has it: nothing. Every hunk of the earlier branch that main now carries (P-T1b, P-H) went away with the parent's history, and none of this PR's own change was already on main.

Host adaptation. Both channels' newest builds, rc 0.2.2-rc.2 and alpha 0.2.2-alpha.3, run on the new cordis core. That core calls an internal/config listener as (fiber, next) with this null, where the old core bound the fiber as this and passed the raw value.

  • Claim. The route claim is one appended listener that reads either call shape (src/utils/config-run.ts, held at 100%). It calls next() exactly once and strips what next() answers.
    • It re-appends itself when pi-ai reaches ACTIVE. It registers the new listener before it releases the old one, so a refusal never throws into pi-ai's status emit.
    • A run counts as a commit when its raw value is the fiber's raw config or the loader's stored row config, or when the fiber is not ACTIVE. A commit never throws.
    • Every read of a fiber's config goes through the feature-detected accessors. No version is read anywhere.
    • The old core's waterfall (cordis 4.0.5, loader 1.0.6, pi-ai rc.5) also passes through next() in every listener, so the same listener serves both cores.
  • Contracts. Plane profiles carry maxRequestImages, and imageOffloadCountQuantum (min(10, bound) unless the entry sets one), with pi-ai's own defaults and messages. They carry these only where pi-ai's Config answers the bound (the 0.2.2 bridges), and neither where it does not.
  • Records. The binary e2e's [KNOWN_DEFECT] row (pi-ai keeps the planes on the new core) is now a [BASELINE] row: both planes are the plugin's PlaneAdapter on either core. The probe counts every internal/config call by shape (configHooks), and the row requires one known shape throughout.
    • A tier-B case removes pi-ai's row through the loader's own group.remove (loader 1.1.0 deletes the store entry before it emits loader/partial-dispose). It pins that every route is withdrawn, the plugin's row stays ACTIVE, nothing restarts, and nothing logs at warn or error.
    • The plan, both READMEs, CONTRIBUTING, redirect-planes, the routing and engineering notes and the AGENTS.md map now describe the claim on both cores.

Measured cells (binary e2e, built from the head tree):

Cell Host Result internal/config shape
rc npm 0.2.2-rc.2 (cordis 4.1.0, pi 1.5.2) 13 passed, 2 skipped fiber 307, config 0, neither 0
alpha npm 0.2.2-alpha.3 (cordis 4.0.6-alpha.2, pi 1.5.2) 13 passed, 2 skipped fiber 302
rc floor 0.2.2-rc.2, pi 1.3.0 13 passed, 2 skipped fiber 307
alpha floor 0.2.2-alpha.3, pi 1.3.0 13 passed, 2 skipped fiber 302
legacy, old core 0.2.1-rc.5 (cordis 4.0.5, pi 1.5.0), run against the rc expectations 12 passed, 3 skipped (with a recorded override) config 302, fiber 0
  • All four current cells. opencode and opencode-go are both PlaneAdapter, the planes settled, and the control edit was accepted. Each floor cell asserted that every floor override was applied.
  • Skips in the four current cells. Both are skipped by detection: [HOST_LIMITATION] "the host's pi sends the Go Gemini level model a thinking budget", and [HOST_LIMITATION] "no config-editor save succeeds, because the host's own code throws". Every host did save in practice.
  • The legacy cell is a one-off measurement, not a standing cell. Unmodified, it stops in beforeAll because the prefix holds another channel's host. It ran with a temporary, uncommitted override behind DSH_HOST_LEGACY_OLD_CORE=1 that skips only the dsh-version and declared-range record pins.
    • Both planes were PlaneAdapter, and the save was accepted.
    • The extra skip is the per-model rows, because pi 1.5.0 is not the record's 1.5.2.
    • So the one appended listener serves the old shape on a published binary.
  • Gate on the head tree:
    • vp check: 0 errors, 11 warnings (limit 18).
    • Unit suite: 108 files, 1641 passed, 1 skipped. Shuffled with seeds 1 and 42, and on Node 24 with seed 7, the result was the same.
    • Mock suite: 68 passed.
    • Coverage: 97.77 / 94.45 / 98.22 / 97.76 against the ratchet 97.5 / 94 / 97.4 / 97.5.
    • Build and bundle gate: check passed, and the consumer install succeeded.
    • Floor files on rc and on alpha: 268 passed, 1 skipped.
    • The local-path sweep over the diff, the new files and lib/ found nothing, and planted canaries confirmed that it read its input.

What remains.

  • The live re-measures listed under "Restacked onto main 1.8.0" still need CI: keyless-matrix.e2e, protocol-routing.e2e and the e2e-host job for both channels' npm and floor cells. The alpha npm row is now expected to be [BASELINE], not [KNOWN_DEFECT].
  • The two [HOST_LIMITATION] rows stay until the host fixes them upstream (D26).

Restacked onto main 1.8.0

This branch now carries main 1.8.0 through its parent (P-H) and the two-channel host: rc 0.2.1-rc.5 and alpha 0.2.2-alpha.2, with pi unpatched. Some sections below describe the branch before the restack; where they disagree, this section wins.

Scope now. The plugin owns the opencode and opencode-go routes: the route claim, PlaneAdapter, the describe graft and the host modules. One characterization table serves every host cell (S0_TODAY_BY_CELL, built per channel by s0PluginFor). Rows differ only where the host decides.

Dropped because main or the host now does it (D26).

  • The Gemini level wrapper (src/host/google-levels.ts) is deleted, with its wiring in plane-provider, plane-profile, host-modules and cordis-context, its tests, its 100% coverage floor and its doc lines. The thinking control is pi's own. The catalog level rule now lives only in test/support/thinking-levels.ts, where it picks test subjects. The "Behaviour changes" bullet about thinkingLevel above no longer holds.
  • pi's catalog entry is no longer a protocol or base source in src/utils/plane-model.ts, including the legacy branch that took pi's own /zen base (D16). The protocol order is catalog SDK, then the route's api, then completions. Bases come only from the endpoint table.
  • plane-profile no longer restates pi-ai's default window and output tokens (D17).
  • main fix: send Gemini 4 thinking levels on older host runtimes #130's test/google-thinking.test.ts imported the deleted bridge. Its intents moved to the mock suite and to the HOST_LIMITATION rows.
  • main feat(test): gate the language pairs, and lift the live-run rules out of the e2e #140's decoratePlaneAdapters case and its stream-hook "cannot be served" case are both obsolete after this PR.
  • P-T1b's per-cell machinery from before this PR is gone: the per-channel floor tables, withMoved, the keyless/keyed listings, saveRejectedUnknown, SAVE_BLOCKED_BY_ANY_UNDESCRIBED, the pi-ai-first override and NEXT_DISPATCH. Every host cell now reaches TODAY_DISPATCH.
  • The patched "pinned" binary cell was not built, because no pi patch remains. The npm and floor cells of both channels ran instead.

Decisions applied.

  • D14': the route claim and the plane adapter are built from the host's own pi on each channel, and nothing is bundled or patched.

  • D22': a channel bump re-measures F8/F12/F14/F20 per channel against that channel's own expectation, and the route claim adopts a moved internal/config contract by detecting the shape the host dispatches, never its version.

  • D24: debug / debugFile follow main's wording in cordis.patch.yml, and the lifecycle test uses apply(ctx, { debugFile }).

  • D25: host limitations are recorded and not fixed:

    • the rc desktop app's pi 1.1.0 sends the level-only Gemini 4 model a thinking budget;
    • alpha 0.2.2-alpha.2 installed by npm cannot save.

    Both are detected from the host's own answers, and both READMEs list them.

Known defect (resolved by the rebase above). On cordis 4.0.6-alpha.2 (alpha's npm cell) the route claim did not engage, so pi-ai served the planes. This was a [KNOWN_DEFECT] row; the rebase's claim on both call shapes turned it into [BASELINE].

Live re-measures CI must confirm.

  • protocol-routing.e2e: the Google level case on Go with a key, on the newest pi. It must skip as HOST_LIMITATION on the rc floor.
  • That the gateway still refuses a thinkingBudget for the Go level-only model. The HOST_LIMITATION rows rest on this, and Zen is expected to refuse too but is not yet measured.
  • keyless-matrix.e2e: TODAY_DISPATCH, all four protocols on the modern family on both planes.
  • protocol-routing.e2e: the keyed adapter walks through PlaneAdapter on tier A for both planes.
  • e2e-host for both channels' npm and floor cells. It records configHook per cell and confirms the rc floor HOST_LIMITATION rows.
  • Inherited from P-R2: the x-opencode-request, x-opencode-project and Retry-After rows, and the combined Google User-Agent.

What changed

Item Change
Root plugin Declares inject: [].
src/host/route-claim.ts (new) An internal/config listener gives pi-ai's fiber a copy of its config with the planes the plugin takes removed (both, unless D27 steps aside). On a commit it schedules reconcile. On a Settings candidate it strictly validates only the taken plane entries that changed. The served pi-ai row is sticky: it stays the same while it is live and still declares a plane.
src/host/plane-provider.ts (new) reconcile builds one PlaneAdapter, a subclass of the host's PiAiAdapter, and registers only the taken planes through registerAdapter / handle.replace. pi-ai keeps every other route.
src/utils/plane-profile.ts, plane-model.ts, model-metadata.ts, protocols.ts, plane-ownership.ts (new) Pure profiles the adapter is built from, and the D27 ownership rules. The utils/ purity gate covers them.
src/host/describe-graft.ts (new) A read-only settings.describe wrapper, built on patchMethod, that grafts the committed plane entries back (D18).
src/host/host-modules.ts (new) The host-module loaders, moved here. The seam stays in cordis-context.ts. pi-ai's module comes from pi-ai's own loader entry during its commit run, so nothing waits on a timer.
Deleted responses-provider.ts, responses-routes.ts, google-bridge.ts, pi-provider.ts, utils/google-replay.ts (removed after a mock-gateway replay test showed a single provider label); from models-discovery.ts: the listing hide, decoratePlaneAdapters and decorateModelInfo; the stream redirect; the D19 settings; the hidden ids in DEFAULT_PROVIDERS and cordis.patch.yml.
Tests New unit, real-host (host-planes, host-planes-foreign), contract (describe-graft, catalog-change) and mock (plane-dispatch) suites. Characterization rows flipped to new [BASELINE] values: 14 S0_TODAY facets, 14 named host-characterization cases and 5 binary-e2e rows, each named under "Characterization rows this PR flips" below. Every other row stays green unchanged.
Docs README (both languages), AGENTS.md, CONTRIBUTING, credentials, deep-dive, protocol routing and redirect planes now describe the new wiring. In the plan, P-R1 is set to in review.

Some details refine the plan text on purpose, and the plan's P-R1 entry records each one:

  • D20 follows pi-ai's exact rule: it uses the credentials service when one is mounted, and the launch environment only when none is.
  • The fixture-sanity case derives info the way D17 does, so the INFO_NOT_FROM_CATALOG BASELINE guard stays intact.

Behaviour changes

After this phase:

  • Any saved id is accepted and served (D15). An id that no catalog knows dispatches as completions.
  • The protocol comes from the catalog (D16). Model info follows the user's own row first (D17).
  • A model whose reasoning can only be switched on or off offers Off / On (D1). Off is offered only where the protocol can disable reasoning.
  • The default endpoint is modern: the per-protocol inference family. Go Gemini dispatches on /inference/go/google/v1beta.
  • Keys resolve by pi-ai's own rule (D20).
  • A Go entry without apiKeyEnv now sends the Zen key. pi's own lookup reads OPENCODE_API_KEY on both planes, so such an entry authenticates every Go protocol with it. Before, Go's Responses, Messages and Google turns defaulted to OPENCODE_GO_API_KEY (completions already took pi's rule). The meter follows: it prefers the key a Go turn carried, so after the first such turn it sends the Zen key to the Go usage endpoint, is refused, and falls back to the Zen pill or "subscription required". The fix for a user is apiKeyEnv: OPENCODE_GO_API_KEY on the Go entry; README step 3 and "Changed in 1.x" (both languages) say so, and test/host-planes.test.ts pins both cases.
  • There is no keyless listing filter (D21). A plane lists its declared ids even when no key resolves.
  • A route another plugin owns (a foreign credentials realm, or the x-opencode-plugin marker) is left to pi-ai and untouched by this plugin (D27, above).

Two changes from the last review round before the rebase:

  • When several pi-ai rows declare OpenCode planes, the row whose planes the plugin serves stays the same while it is live and still declares a plane. If another row adds a plane later, the plugin does not take it over: that row keeps and serves its own plane, and the plugin reports this once. Before, the plugin could switch rows and leave the old row's planes served by nobody, without a warning.
  • If the host's pi-ai module cannot be read, the plugin now steps aside even when no OpenCode plane is declared yet. A plane added later in Settings then goes to pi-ai's own save check. Before, it was refused for good with "try the save again".

Characterization rows this PR flips

Every row below was [KNOWN_DEFECT] or [INTENDED_CHANGE] on the base and is [BASELINE] here. No [BASELINE] row on the base moved.

S0_TODAY facets (test/support/host.ts; facet order info / callConfig / wire), 14:

Row Facet(s) Base Now
opencode/deepseek-v4-pro wire [INTENDED_CHANGE] /zen/v1/chat/completions /inference/openai/v1/chat/completions
opencode/gpt-5.3-codex-spark info, callConfig [KNOWN_DEFECT] user row ignored (128000), INVALID_CONFIG the user row's 400000 (D17), ok
opencode/not-in-any-catalog info, callConfig, wire [KNOWN_DEFECT] INVALID_CONFIG, nothing sent 262144 / no efforts, ok, sent as completions on /inference/openai/v1 (D15, D16)
opencode-go/deepseek-v4-pro wire [INTENDED_CHANGE] /zen/go/v1/chat/completions /inference/go/openai/v1/chat/completions
opencode-go/minimax-m3 info [KNOWN_DEFECT] toggle-only model offered Minimal…Max Off / On (D1)
opencode-go/gemini-4-argon callConfig [KNOWN_DEFECT] INVALID_CONFIG ok
opencode-go/minimax-m3.1-flash callConfig, wire [KNOWN_DEFECT] INVALID_CONFIG, nothing sent ok, sent on /inference/go/openai/v1
opencode-go/not-in-any-catalog info, callConfig, wire [KNOWN_DEFECT] INVALID_CONFIG, nothing sent 262144 / no efforts, ok, sent on /inference/go/openai/v1

The listing rows keylessListing / keyedListing are replaced by listingBeforeTurn / listingAfterTurn (every declared id, D21), and saveRejectedUnknown is gone (D15).

Named host-characterization cases, 14 (base title → head title):

  1. [INTENDED_CHANGE] registry: both planes are pi-ai's, six hidden routes → [BASELINE] registry: both planes are the plugin's adapter, no hidden route (D13)
  2. [INTENDED_CHANGE] keyless listing hides every paid model → [BASELINE] listing before any turn: every declared id (D21)
  3. [KNOWN_DEFECT] keyed listing: only pi-resolved ids → [BASELINE] listing after a turn: gap ids and the unknown id included (G1)
  4. [KNOWN_DEFECT] toggle-only model offered Minimal/Xhigh/Max → [BASELINE] offered Off / On, an untaken level refused (D1)
  5. [INTENDED_CHANGE] completions dispatch to pi 1.1.0's legacy bases → [BASELINE] completions dispatch to the modern family on both planes
  6. [KNOWN_DEFECT] the adapter decoration never runs in this order → [BASELINE] the plugin patches nothing on its own PlaneAdapter
  7. [KNOWN_DEFECT] Zen gpt-5.3-codex-spark: user row ignored, resolveCallConfig refuses → [BASELINE] user row reaches model info, sent on Responses (D17)
  8. [KNOWN_DEFECT] Go minimax-m3.1-flash INVALID_CONFIG → [BASELINE] validates, sent once as completions on Go's modern base (G1)
  9. [KNOWN_DEFECT] Go gemini-4-argon refused while served through the bridge → [BASELINE] validates, sent on Go's Google base with the Go key (F10)
  10. [KNOWN_DEFECT] a stored gap id blocks every save of its plane → [BASELINE] a stored gap id blocks no save; the unknown id is listed, validates and is sent (D15)
  11. [KNOWN_DEFECT] the probe's save sequence needs the gap ids dropped first → [BASELINE] the first append of the unknown id is accepted
  12. [INTENDED_CHANGE] the adapter decoration runs in pi-ai-first order → [BASELINE] pi-ai held the planes first, so the claim restarted it exactly once
  13. [KNOWN_DEFECT] the apply order decides one facet (Go gemini-4-argon) → [BASELINE] no facet depends on the apply order (todayRowsFor's override dropped)
  14. [KNOWN_DEFECT] the unknown id is what the save check names → [BASELINE] the save check accepts it and the file holds it (D15)

Binary-e2e rows (test/e2e/host-binary.e2e.ts), 5:

  1. [INTENDED_CHANGE] registry: the planes are pi-ai's, hidden routes registered → [BASELINE] both planes are the plugin's PlaneAdapter, no hidden route (D13)
  2. [INTENDED_CHANGE] keyless listing hides every paid model → [BASELINE] listing before any turn: every declared id (D21)
  3. [KNOWN_DEFECT] keyed listing: the gap ids are missing → [BASELINE] listing after a turn on each plane: every declared id (G1)
  4. [KNOWN_DEFECT] a stored gap id blocks saving any new id → [BASELINE] a stored gap id blocks no save (D15)
  5. [KNOWN_DEFECT] a catalog-unknown id cannot be saved → [BASELINE] it saves, is listed, validates and is sent as completions on the modern base (D15, D16)

The binary e2e also gained rows with no base counterpart: the plane provider is built from the host's own pi, and the Settings graft (below).

Records the plan asks for

docs/improvement-plan.md (Test plan, P-R1) asks this PR body to record four things.

  • The measured endpoints. Default (auto = modern) dispatch: Zen https://opencode.ai/inference/{openai/v1, anthropic/v1, google/v1beta}, Go https://opencode.ai/inference/go/{openai/v1, anthropic/v1} and https://opencode.ai/inference/go/google/v1beta (the catalog's family map, else MODERN_FALLBACK in src/utils/protocols.ts). legacy: /zen/v1 and /zen/go/v1 (/zen, /zen/go for Messages), with Go's Google staying on /inference/go/google/v1beta under every family. The keyless matrix (every PR in CI) asserts that no cell the default endpoint dispatches to answers 404; the binary e2e records the URL each characterized model was sent to through the shipped bundle (the wire facets above).
  • The describe graft. src/host/describe-graft.ts grafts the committed plane entries back into pi-ai's settings.describe value (D18). Held by the binary-e2e row "the Settings graft shows each plane's own entry, and the Models page reads both planes configured under their apiKeyEnv", by the [BASELINE] hasProviderApiKey row in both apply orders, and by test/describe-graft.test.ts.
  • The handback. Disabling the plugin's row hands both planes back to pi-ai (PiAiAdapter owns them again), and pi-ai's own save check is back in force; dispose then apply hands them to the plugin again (test/host-planes.test.ts, tiers B and A). When the host's modules cannot be read the plugin stands down and says so; a failing handback restart is reported, not thrown (test/route-claim.test.ts).
  • The save of a catalog-unknown id. On the first try, with a gap id already stored: accepted, written to the file, listed, validated, and sent as completions on the plane's modern base (D15, D16) — in tier B and on the binary.

Review

Round Found Upheld Fixed
1 31 31 31
2 8 8 8
3 5 5 5
audit 8 8 8
D27 (3 rounds, above) 21 20 20

In rounds 2 and 3, reviewers reported some of the same defects (the catalog-listener unsubscribe; the store-opt-in case reading the shell's OPENCODE_API_KEY; the modules-unreadable refusal). Round 3's fixes:

  • the sticky served row;
  • the stand-down when the modules cannot be read;
  • a launch environment that answers nothing, for the store-opt-in case.

Three new route-claim cases fail under the old behaviour and pass with the fix.

The audit round's eight findings repeat four defects. The README.zh-CN.md credential wording, the live adapter walk (a withdrawn model now moves to the protocol's next candidate, a withdrawal is confirmed by a raw probe, and a protocol no candidate reached skips by name) and docs/deep-dive.md's key-isolation claim were already fixed on this branch; a source comment in src/go-discovery.ts still claimed the isolation and is corrected; the description findings are this body's flipped-row and record sections and the D20 Go entry above.

Before each commit, the full diff and the new files were checked for absolute local paths and anything shaped like a credential. Nothing was found, and the pattern was confirmed to match a known positive.

Note: the catalog freshness check gates a pull request only when its own diff touches that check's generator inputs (D23). This PR touches them, so a vendor catalog change since the last refresh turns it red until the nightly refresh lands on main and is merged in.

🤖 Generated with Claude Code

https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

claude added 26 commits October 22, 2026 22:24
- Stop the run on a Node below engines.node, before collecting a file.
  Below the floor, failures came from the engine (node:sqlite,
  Intl.DurationFormat, ICU strings) and looked like code regressions.
  .node-version names the Node CI uses.
- Add a CI job that runs the unit and mock suites on Node 24, the floor
  engines.node promises but no job ran.
- The zh fallback duration unit is now 分钟, the platform's own narrow
  output, so the meter row reads the same with or without
  Intl.DurationFormat. The DurationFormat tests restore the engine as they
  found it, rather than leaving a stand-in installed or asserting the
  engine ships the constructor.
- Move the node:sqlite store fixture out of the helpers that the jsdom
  tests share, into test/support/opencode-store-fixture.ts.
- Add an architecture test that walks the client bundle's runtime import
  graph from settings-page.tsx. It forbids node built-ins, the cordis
  runtime, pi, and process.env. Checked by planting a violation.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
…n discovery

- A rejected internal-route mount ran from a `void` IIFE with no catch, so
  a Google bridge failure became an unhandled rejection, which Node's
  default mode answers by exiting the process. mountInternalRoutes now
  owns the mount and its withdrawal, and reports a rejection.
- Three early exits in registerResponsesProvider (internalRoutes off,
  every route declared, no llm-pi-ai) answered `undefined` after the
  Gemini bridges had already registered. That left them with no
  disposer, and the next reload failed DUPLICATE_ADAPTER. They now hand
  back the bridges' disposer.
- A mount that failed to start, or that captured nothing, kept its fiber
  and every route it had registered. Both paths now release the routes
  and dispose the fiber.
- Model discovery was registered on every activation, and the host's
  duplicate refusal was swallowed, so the first activation's settings
  answered for the life of the process. It is now registered once per
  root and answers from the latest activation's settings. Each namespace
  registers on its own, so one refusal no longer skips the rest.
- Move three doc blocks back above the functions they describe.

Each fix has a regression test. The two mount tests fail without the fix.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
…e the suite

Request path and meter:
- Price a turn on its route's plane. Go-first lookups priced every Zen
  turn on deepseek-v4-pro at 0.66/1.98 instead of 1.74/3.48.
- The listing and model-info patches now act only on the routes this
  plugin claims. Before, a direct anthropic or openai route lost rows the
  OpenCode routing called "unservable", and a refused foreign id was
  answered with OpenCode's catalog.
- A relay's Go /models is answered with the Go catalog. The enrichment
  re-checked the URL against the default gateway markers and fell back to
  Zen.
- The free-model check reads a Request-carried body. That is how DSH
  sends a turn, and every keyless free turn was reported as a missing
  credential. The body is now read once for both uses.
- The meter's session fallback is the session that most recently ran a
  turn, not the most recently created one.
- A captured key's tier comes from the observed URL, then the route, and
  never the key's prefix. OpenCode issues oc_sk_ keys for Go, so a
  working Go key was filed under Zen. An OpenAI sk- key on a claimed
  relay was filed under Go.

Suite isolation:
- The unit and mock suites refuse non-loopback requests. Each unit run
  sent 54 requests to models.opencode.ai through the catalog's background
  revalidation, so on a networked runner a result could depend on the
  vendor's live catalog.
- Captured keys, session spend and the live catalog are reset after every
  test (resetCatalogState is new). Shuffled runs failed catalog cases on
  seeds 42 and 1234. test:shuffle runs the suite in random order, and the
  Node 24 CI job uses it.

Each behaviour change has a test that fails without it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
…egration

- The card's store-inspection row called remote.opencodeStore.inspect, but
  no client contribution carried that namespace's descriptor. The row
  reported "unavailable" on a host that had the service. storeRemote now
  carries it, with a whitelist codec, and the client mounts it.
- The host/client parity test missed this because it matched only a
  string literal in super(ctx, "name"), and the store passes a constant.
  It now resolves constants, and checks every declared Remote method
  against a client descriptor, not just every namespace.
- inspectStore parses through the whitelist instead of a cast, so a field
  the host did not mean to send (a token) is dropped. An ok:false
  envelope reports the remote's own message.
- The credential reader's legacy-row fallback was uncorrelated: one
  flagged opencode-go row hid every legacy opencode row, and the Zen key
  disappeared. The fallback is now per integration.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
… keeps

- AGENTS.md's repo map described nine modules this build does not have
  (the Console login, token store, org cache and sign-in row). They are
  replaced with one line saying sign-in is v2 work. The go-limits
  duplication note is gone, because attachAllowance calls allowanceFor.
  adapter-replacement.ts is marked as not wired. The client-bundle line
  names the boundary test.
- CONTRIBUTING.md documents the engine floor, test:shuffle and test:mock,
  the offline guard, the per-test state reset, where host-only fixtures
  live, and that a regression test must fail without its fix.
- The no-credential warning no longer tells the reader to "sign in
  through the settings card", which this build does not have. It points
  at readOpencodeStore instead, and a test asserts the advice.
- CLIENT_BUDGET is raised from 128 to 132 KB for the store descriptor
  and its parser (about 1.2 KB of source, not a dependency), following
  the gate's own instruction.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
docs/improvement-plan.md groups the review's findings by cause and orders
the fixes in nine phases after this one. Each phase lists its problems,
its steps, the test that has to fail first, its acceptance criteria and
any decision the owner needs to make. Findings are marked "measured"
(reproduced) or "traced" (read in the code). Among the measured ones:
the picker and dispatch derive reasoning levels differently for 9 of 95
catalog models.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
Replace docs/improvement-plan.md with the plan of record. It now holds:
- the owner's decisions (D1-D11);
- what the DeepSeek Harness host supports, with references into
  packages/llm, llm-pi-ai, session-controller, gateway and
  ui-settings-models;
- the defects this audit found that are not yet fixed;
- the target architecture and the phases, one PR each, with a status
  table that each PR updates.

The decisions that change the code:
- Settings offers the whole catalog, and the composer offers only the
  declared models.
- The plugin never writes the user's profile and follows the user's
  configuration at dispatch.
- The adapter patch exists to merge provider modes and nothing else. It
  is generalized to every mode, and its catalog fill is removed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
Investigation of the host's source and of the pi catalog it pins (1.1.0)
changed the routing design.

- llm-pi-ai resolves `api` and `baseUrl` per model, and pi's installed
  `opencode` catalog already spans Anthropic, Google, Completions and
  Responses with per-protocol endpoints. One route serves every mode for
  every model pi knows.
- The plugin fills only three gaps: models newer than pi's catalog
  (including all Gemini on Go), endpoint drift, and profile-level
  `api`/`baseURL` collapsing the modes.
- The new design is one unregistered catalog adapter per plane, keyed by
  the user's route id, and the adapter patch (merge-modes) delegates only
  gap models to it. That deletes the hidden routes, the listing hide,
  the stream redirect, the child mount and the Gemini bridge
  registration.
- A real-host test harness (P-T1) comes before any of it.
- One upstream change (per-entry `api`/`baseURL` in llm-pi-ai, U1)
  would remove the routing layer entirely. It would also let Settings
  save a model pi doesn't know, which pi-ai's strict write validation
  rejects today.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
pi-ai registers exactly the providers in its resolved config, and the
fiber keeps whatever the internal/config waterfall returns, while the
config editor writes the raw value. A prepended listener that resolves
pi-ai's config without opencode/opencode-go therefore keeps pi-ai from
claiming them, without touching the user's file, and the plugin registers
its own PiAiAdapter under the same ids through the public API.

Measured on the real host with pi 1.1.0 and the harness's patch: both
routes held by the plugin, pi-ai's other routes unchanged, every protocol
on both planes (Gemini on Go included) sent to its own endpoint, a
catalog-unknown model id saved, listed and dispatched, and both routes
handed back to pi-ai on release.

Replaces the upstream asks (no upstream changes) and the save-gate
design, which is kept only as the fallback.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
A nine-lens review of the routing design, with two independent skeptics
per finding (78 of 82 upheld), changed five things before any code moves:

- Settings reads pi-ai's resolved config, so a read-only wrapper on
  settings.describe puts the plane entries back into the displayed value;
  writes were never affected, the Models page edits by path.
- The route claim gets an explicit lifecycle: commits are told from
  validations by identity, reconcile runs on commit, mount and teardown
  are ordered, and pi-ai restarts at most once per transition.
- The plugin's adapter is built from public pieces only, with pi-ai's
  credential rule and no captured keys on the adapter path.
- The protocol comes from the catalog map, never from a guess at the
  model's name; metadata and levels are catalog first.
- Releases stay on 1.x, phases land as stacked pull requests, and a real
  host harness plus a binary e2e job pin the behaviour first.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
- The deprecated @viztor/dsh-opencode wrapper re-exported a `default` the
  barrel does not have, an ESM link error on every host load. It now
  forwards `export *` only, written by one shared helper that a test links
  against the built barrel in a real Node process.
- isZenProvider("opencode-google") answered false: "go" inside "google"
  read as the Go plane. It now uses the plane rule the catalog already has.
- enrichModelsResponse answered a 401, 403, 429 or 5xx with a 200 catalog.
  Those statuses now pass through; a 404 is still filled from the catalog,
  because the Go plane's inference base has no /models of its own.
- The warning colour named a token the host does not define, so warnings
  rendered as caption text. They now use --dsw-alias-state-warn-label.
- The settings spinner only animated while the meter was mounted; the
  settings surface now carries its own keyframes.
- The stream hook wrote debugFile with debug off. debug is the switch and
  debugFile only where it writes, the same gate the fetch patch uses.
- The purge row reported the host's own { ok: false } failure as "not a
  purge result" and cast the answer; it now shows the host's message and
  parses through the contract's whitelisting parser.
- The endpoint copy described modern as /inference/v1, which the plugin
  never sends. It now names the per-protocol inference family, and auto as
  its alias, which is what the code already did.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
- Delete adapter-replacement.ts and its test. Nothing imported it, and
  deleting another plugin's registry entry is not a host API; the
  engineering notes record why, and what the plugin does instead.
  A gate fails the build if src/ writes the llm registry map directly.
- Delete the stream hook's `prepared` handling: the host passes the
  prepared dispatch as its own argument and never reads options.prepared.
- Untrack .freebuff/ and ignore it.
- The settings partition test walks Config.dict, which is how debugFile
  went missing from it; a manifest test checks cordis.patch.yml against
  the schema and the field registry, and the manifest's phantom
  showUsagePrice, wrong marker text and incomplete lists are fixed.
- utils/retry.ts takes its timer and logger as parameters, and the utils
  purity gate now also rejects console and global timers, walking the
  folder recursively.
- tool-fallback reads free-tier ids from the request's own plane (Go iff
  opencode-go or opencode-go-*) and loses its unreachable Anthropic
  branch. A Zen-only free model is no longer treated as free on Go.
- test/support/src.ts is the one door tests use into src/, so later
  module moves touch one file; CONTRIBUTING.md states the rule.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
The barrel is the host's contract and nothing more: apply, Config,
inject, name and the config types. Everything else is internal, and
test/public-api.test.ts pins the list, so widening it is a reviewed
change. The 1.x line keeps its version scheme: nobody consumes the
removed exports (D6).

- Every test reaches src through test/support/src.ts, importing each
  name from the module that defines it; a gate rejects a static src
  import anywhere else outside its named exemptions.
- package.json no longer exports ./src/*, and check.ts fails when an
  exports target is outside the published files.
- resolveRoutedKey is deleted: it was exported and never called.
- The internalRoutes setting is deleted; the per-protocol routes always
  mount until the plugin owns the plane routes. A row that still carries
  it validates and the key is ignored.
- @viztor/dsh-opencode is no longer published. The deprecation message
  already on npm stays as its final word, and check.ts asserts that the
  release workflow verifies exactly what publish-scoped.ts publishes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
The test install now runs the host users run.

- pi is pinned to 1.1.0, the version the harness's own lockfile pins,
  with the harness's patch applied verbatim (patches/, hash recorded by
  pnpm). A pnpm override holds every copy in the tree to it, not just the
  direct devDependency.
- The @deepseek-ai packages move from 0.2.1-alpha.5 to 0.2.1-rc.2, the
  version a registry install of the published dsh resolves (cordis 4.0.5,
  loader 1.0.6, schemastery 3.18.5). The boot-tier packages the real-host
  harness needs next (dsh-llm, dsh-app-boot, dsh-config-editor,
  dsh-settings, cordis-plugin-loader) are added as devDependencies.
- test/support/host-versions.ts names those versions once.
  test/host-versions.test.ts checks that the pi the plugin loads at
  runtime is 1.1.0 and the same copy the tests import, that the patch is
  applied, and that the installed cluster matches.
- mount-protocols now checks that every catalog SDK maps to a protocol
  pi's exported api modules implement, and records as a characterization
  row that pi 1.1.0's opencode-go provider has no Google API.

No src/ behaviour changes: the rc.2 builds of llm-pi-ai, cordis and
schemastery are code-identical to alpha.5.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
Add an in-process harness that runs the plugin inside the real host,
so the routing work that follows can be measured against today's
behaviour rather than against fakes. Additive only: no src/ change.

test/support/host.ts gives two tiers. Tier A is a bare cordis Context
with LlmRuntime and llm-pi-ai mounted with { providers }, for the
config waterfall and pi-ai's credential paths. Tier B is dsh-app-boot's
boot() with a temporary HOME, a test bundle, a fake credentials service
and a launch-environment snapshot, for the config editor,
settings.describe and the volatile commit. Tier B's rows carry package
names through a stub module loader, and it fixes the apply order,
because the loader starts rows in parallel.

On top of it:

- contract tests for the waterfall, the profile and Config shapes,
  credentials, and the config editor (host-waterfall, host-shapes,
  host-credentials, host-editor-contracts), plus a tier A smoke test;
- a characterization of today's behaviour on pi 1.1.0, each row tagged
  where P-R1 is expected to flip it, with S0_TODAY shared by the
  binary e2e;
- the keyless 401-vs-404 matrix: a pure classifier, its unit test and
  a gated live run;
- a binary e2e gated on DSH_HOST_E2E=1 with its probe bundle, the
  e2e-host CI job (a pinned, patched cell on PRs that touch the host
  seams or vite.config.ts, and a floating cell nightly and on
  dispatch), and the test:e2e:host script.

The binary e2e now reports why a run failed: a timeout only when its
own deadline killed the host, the signal for a crash, and the probe's
fatal message whenever there is one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
Add src/host/patch-method.ts (patchMethod), now the only way the
plugin wraps a method on a host object, and move the four instance
patch sites in src/models-discovery.ts onto it: the three listings
hidden by hideResponsesRoute, decorateModelDiscovery,
decorateModelInfo and decoratePlaneAdapters.

On the real host the three llm-level disposers never restored:
ctx.llm is cordis's traceable proxy, every method read returns a new
shadow proxy, so the `===` identity checks were never true and each
reload stacked a wrapper over the last. The helper reads identity
from own descriptors instead. A dispose restores only while its
wrapper is still current (otherwise it becomes a pass-through),
deletes the own property when the original was inherited, and a
same-site reinstall wraps the unpatched method rather than the
earlier install, so either disposal order ends at the exact
original.

decoratePlaneAdapters becomes one patch per (adapter, method) that
composes the adapter's plane routes in registry order, which keeps
today's nesting on pi-ai's shared adapter. rewrapOnAdaptersUpdated is
built and tested but wired to no site yet.

test/reload.test.ts runs apply A, apply B, dispose A, dispose B (and
the reverse order) for every site on own and inherited originals, on
the real host (tier A) and through apply(); test/patch-method.test.ts
covers the helper's contract. The existing per-site pins in
test/models-discovery.test.ts stay unchanged; five new cases there
record the deliberate edge changes on hosts that refuse the patch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
The vendor added kimi-k3.1-pro to the Go plane, so `catalog freshness`
went red on every branch of this stack. This takes the two generated
files exactly as the nightly refresh produced them (#111), with no
hand edits, and updates the counts that hard-code the catalog's size:
the Go shim test (28 -> 29), the plane table in redirect-planes.md
(Go served 28 -> 29, the no-SDK Go cell 17 -> 18), and the Go
completions list in both READMEs. The listed half of "29 served / 33
listed" is models.dev's count, inferred from the one added entry,
because models.dev cannot be reached from here.

It no-ops once main carries #111. P-T2 turns these counts into
invariants so a refresh stops needing this.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
The plugin now serves the opencode and opencode-go routes itself,
through supported host paths only, and the hidden-route workaround is
gone.

A prepended internal/config listener hands pi-ai a copy of its config
without the two plane entries, so pi-ai keeps every other route. On a
commit the plugin reconciles: it builds one plane adapter, a subclass
of the host's PiAiAdapter, over pure profiles (utils/plane-profile,
plane-model, model-metadata and protocols) and registers only the
planes that are declared. A Settings candidate has the plane entries
it changes validated strictly. A read-only settings.describe graft
puts the committed plane entries back, so the host still sees them.

Deleted: responses-provider, responses-routes, google-bridge,
pi-provider and utils/google-replay; the listing hide and the
discovery decoration; the stream redirect; the D19 settings; and the
hidden ids in DEFAULT_PROVIDERS and cordis.patch.yml.

What users see: any saved id is accepted and served, and an id no
catalog knows dispatches as completions. The protocol comes from the
catalog and model info follows the user's own row first. A model
whose reasoning is only on or off offers Off/On. The default endpoint
is the per-protocol inference family. Keys resolve by pi-ai's own
rule, and a plane lists its declared ids even when no key resolves.

When several pi-ai rows declare planes, the row the plugin serves
stays the same while it is live and still declares one, so no row is
left stripped with nobody serving it. If the host's pi-ai module
cannot be read, the plugin steps aside even before any plane is
declared, so a later Settings save goes to pi-ai's own check.

The characterization rows this changes are flipped to new baselines;
every other row stays green unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
# Conflicts:
#	README.md
#	README.zh-CN.md
#	docs/redirect-planes.md
The vendor renamed the Go plane's one Gemini model (dropping its
contributor suffix, halving its price and publishing an allowance for
it where the limits table had none) and added a free model with no
SDK, so `catalog freshness` went red on every branch of this stack.
This takes the two generated files exactly as the nightly refresh
produced them (#111), with no hand edits, and follows them:

- the Go Gemini fixtures in six test files (attachment-services,
  catalog, go-google, go-google-prepare, models-discovery,
  responses-routes) now name the renamed id, since each means "the
  Go Gemini model the catalog serves";
- the usage panel test built its unknown allowance from the table's
  one unpriced row, which the vendor has now priced, so it builds the
  `Number.NaN` allowance the generator emits itself;
- the Go shim count in catalog.test.ts (29 -> 30);
- redirect-planes.md: Go 29 -> 30 served, the no-SDK Go cell
  18 -> 19, and "0 of 29" -> "0 of 30" in the prose;
- both READMEs: the renamed Gemini id, and the new model in the Go
  completions list, since it declares no SDK.

The listed half of "30 served / 34 listed" is models.dev's count,
inferred from one rename (no change) and one addition (+1), because
models.dev cannot be reached from here. The dated S0 measurement row
in improvement-plan.md keeps the id it was measured with.

It no-ops once main carries #111.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
The live routing suite picks its free-tier subjects from the catalog by
asking the gateway, but it counted "400 Upstream request failed: Model
is unavailable" as an observable answer. The upstream withdrew
nemotron-3-ultra-free while the catalog still lists it, so the picker
chose it and two cases asserted on a model the gateway no longer
serves; the reachability case also failed on a 429 throttle.

A withdrawn model (that 400, or a 410) is now skipped like a throttled
one. The free-tier routing case only accepts a candidate whose answer
is the free-tier gate it asserts, so an unmetered model cannot become
its subject, and the reachability walk moves past gated, withdrawn and
throttled candidates before it reports.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
The review ran three rounds through the correctness, rules and matrix
lenses. What it found, and what changed:

- The binary e2e's beforeAll budget was smaller than the blocking
  subprocess deadlines it serializes. Each plugin install now has its
  own deadline, the hook's budget is derived from the installs and the
  boot, and afterAll stops a host a failed hook left running.
- providerWithApiKey claimed to be a line-for-line restatement but
  skipped upstream's credentialRef(ref) check. It now throws the same
  TypeError for a ref outside the grammar (a host-editor-contracts
  case pins it), and its comment names the deliberate differences.
- The pinned e2e-host cell let cordis and the loader float at patch
  level. The overrides now force both, the cache key includes them,
  and the job asserts them against HOST_VERSIONS.
- The pi-ai-first characterization asserted every facet under one
  KNOWN_DEFECT title. Each apply order now generates one test per row
  and facet, titled with that order's own tags.
- CONTRIBUTING said the contract tests pin F11-F20, and the plan said
  P-T1 pins every fact. Both now give the same accounting: which facts
  are pinned by number, which P-R1 exercises first, and why F9 and F17
  are not pinned.
- CONTRIBUTING put the e2e's dummy keys in the profile's credentials
  file; they are in the temporary home's credentials document.
- F5 had no real-host pin: host-shapes now pins that a route-level api
  and baseURL collapse pi's protocols onto one. F6 is recorded as first
  exercised by P-R1.
- The credential-mapping row pinned only the api-key branch of
  toPiCredential. host-credentials now covers all three branches, and
  the env branch is asserted through pi's Models.getAuth, so the case
  fails without it.
- The plan's reason for restating hasProviderApiKey was inaccurate; it
  now says no installed copy reaches the function.
- The new credential cases cited toPiCredential at the wrong lines;
  they now cite auth.ts:74-84.
- F4's listing and dispatch halves were pinned only by KNOWN_DEFECT
  rows P-R1 flips. A plugin-less tier-A case in host-shapes now pins
  that an id pi cannot place is not listed and that a turn on it is
  INVALID_CONFIG with no request; the save and deferred-read halves
  are tagged F4 too.
- The one facet that depends on the apply order lost its tag when the
  pi-ai-first block was split. The fixture case now checks only
  structure (the facets whose value moves are the facets whose tag
  moves), and the exact pin is its own KNOWN_DEFECT case that boots no
  host. CONTRIBUTING and the plan say P-R1 drops todayRowsFor's
  override and turns that case into a BASELINE check in the same diff.

Nothing under src/ changed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
A plane entry in a pi-ai row that resolves credentials in a realm the
plugin's row does not share, or one whose headers carry an
x-opencode-plugin key, is left to pi-ai: it stays in pi-ai's config and
pi-ai serves it with its own PiAiAdapter. The OpenCode Console setup
writes exactly that shape, and the claim used to strip those entries and
serve them through the plugin's credentials, which do not hold the
Console's token, so every Console turn on a plane failed
MISSING_CREDENTIAL and key capture recorded the workspace token.

- src/utils/plane-ownership.ts (pure, 100%): the realm as the loader
  reads it, the marker, the taken and stepped-aside planes, the log line.
- route-claim: every reading of "pi-ai still holds a plane" or of the
  plane set counts only the planes the claim takes (served row, rebuild,
  mount and handover restarts, reconcile's wait, validate, teardown
  handback); one log line per stepped-aside entry; a read-only
  steppedAside() accessor.
- stream hook: no decoration of a turn on a stepped-aside plane.
- fetch patch: a request carrying the marker passes through untouched,
  with no header, key fill or key capture.
- tier B gains a realm option (realmCredentialsPlugin), and
  test/host-planes-foreign.test.ts performs it on the real host.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
The plan records the decision (D27) and the route-claim design reads the
taken planes; P3's discovery complement must exclude stepped-aside
planes. Both READMEs gain "With OpenCode Console installed" and a 1.x
bullet; AGENTS.md and CONTRIBUTING name the new module, the accessor and
tier B's realm option.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
The claim's step-aside (D27) left four gaps, each reproduced on tier B:

- A plane stepped aside by the realm alone (no x-opencode-plugin header)
  was still claimed by the fetch patch through its gateway URL: our
  session, origin and User-Agent headers went out on it and the realm's
  token was captured for the meter. The stream hook now runs such a turn
  under a `foreign` turn state, which the fetch patch passes through
  before its URL claim.
- A live save that added the marker to a plane the plugin served left the
  route with no adapter: pi-ai registered it while the plugin's handle
  still held it. A commit run now releases a plane its candidate no longer
  takes through the live handle before it returns.
- The realm was read off each row's own options, so a group's isolate was
  missed both ways. It is now compared by the key each context resolves
  credentials under (cordis's realm map), the labels kept as the fallback.
- The meter's discovery read a stepped-aside entry's key reference and
  base. It now skips a row in a realm its context does not resolve and any
  marked provider row; the 1.x Settings discovery wrapper also leaves a
  stepped-aside plane's Fetch to pi-ai.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
…he meter and live saves

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
…restart

A loader edit of llm-pi-ai's isolate alone swaps its realm map in place
with no config run and no pi-ai restart. The reconcile it schedules then
released a plane the claim no longer takes while pi-ai still held the
stripped config, leaving the route with no adapter; the reverse left
pi-ai holding a plane the claim takes, for good. The reconcile now
restarts pi-ai once it settles in both directions (after releasing, when
pi-ai lacks a plane it now keeps), asked once per need.

Tier-B cases perform the edit both ways on both apply orders and send a
turn after each, plus the reason change the log promises (marker, then
realm).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
It skipped neither the x-opencode-plugin marker nor a row whose
credentials resolve in a realm its context does not, so a click could
rewrite the Console's override row (or fail on it in a home patch).
Such entries, and references to a route only they declare, are now left
alone; with no plane entry of the user's own the purge refuses and
writes nothing. Unit cases and tier-B cases on a real realm.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
… real host

The served row is chosen by the planes the claim takes (a realm-isolated
or marked row ahead of it is passed over), the steppedAside accessor
subtracts what the served row takes, and on tier B a save to a marked
entry beside a taken plane is judged by pi-ai's check alone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
… planes

The claim reconciled on `loader/partial-dispose` only for pi-ai's row. A
live edit of the plugin's own row's `isolate`, or of a group around
either row, swaps a realm map in place with no config run, no restart and
no registry change, so the plane stayed where it was while the live
`steppedAside()` already disagreed: joining pi-ai's realm left the plane
on PiAiAdapter with this plugin's decorations on its turns, and leaving
it kept the plane on PlaneAdapter, resolving the route's key in the
process-wide realm. Every row's partial dispose now schedules the
reconcile, whose give/take restart moves the plane once.

Tier B cases, both apply orders: the plugin's row joining and leaving
pi-ai's realm, and a group around the plugin's row moving into it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
…he commit run

On the loader's volatile path the served row's raw config already holds
the candidate when its commit run arrives. Once the marker leaves that
row nothing the claim takes, `servedRow()` moves to another live pi-ai
row (a plane-less one ahead of it, or one declaring a plane of its own),
and `claim()` returned before releasing the plane the plugin still
held: pi-ai's registration of it, right after the run, met a duplicate.
The release is now decided for the row the registration was built from,
before the served-row check, and a reconcile is scheduled.

Stand-in cases for both neighbour shapes beside the single-row known
answer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
…plugin

Settings -> Models -> Fetch on a stepped-aside plane (by realm, or by
marker beside an unmarked plane) answers pi-ai's own list on a real host,
a catalog-retired id included, while the plugin's own plane drops it
(the known answer); dropping the accessor in `installDiscovery` now
fails. The meter's realm rule is performed alone: unmarked entries in a
realm the plugin does not share, with the realm's token exported, are
neither adopted nor replayed, which a disabled realm skip in
go-discovery now fails.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
…rue 1.x history

The plan's D27 row and route-claim text name the reconcile on every
row's partial dispose and the in-run release decided for the row the
registration was built from. engineering-notes says the listener strips
only the planes it takes. Both READMEs describe what the released 1.x
actually did on a Console route: its hidden-route turns failed for want
of the Console's token, and the rest carried this plugin's headers and
had their key recorded for the meter.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

viztor commented Nov 13, 2026

Copy link
Copy Markdown
Owner Author

e2e · live gateway is red on b2d5a86 because the gateway now refuses both CI keys. This PR did not cause it. I re-ran it once and the failure was identical.

  • Plain calls are refused, not just the plugin's. opencode-live.e2e.ts:125 fetches the Zen /models listing with OPENCODE_API_KEY in a plain fetch, and gets a non-OK answer. The Go /usage test sends OPENCODE_GO_API_KEY the same way and is skipped with HTTP 401 {"type":"error","error":{"type":"AuthError","message":"Unauthorized"}}. The raw protocol probes in protocol-routing.e2e.ts (lines 446, 522 and 621) also get 401 Invalid credential.
  • Every keyed turn fails the same way. All four protocols through the plane adapter fail on both planes, as do the toggle-level walks in dispatch-levels.e2e.ts, each with 401 Invalid credential. The three raw Go probes pass only because they assert "not 404", so they do not show the Go key works.
  • The diff cannot have caused it. The step-aside change (33925ef..b2d5a86) touches no e2e file, no workflow and no secret handling. This job was green on this PR at 33925ef.

Needed: new values for the repository's Actions secrets OPENCODE_API_KEY and OPENCODE_GO_API_KEY. This is likely needed if the keys were regenerated when the subscription was renewed. After that, re-running the job should be enough. The rest of CI on b2d5a86 is green, except catalog freshness, which is the vendor drift already noted above (Zen +gemini-4-hydrogen).


Generated by Claude Code

The nightly refresh (#171) regenerated src/catalog-data.ts: Zen 87 -> 88 with
gemini-4-hydrogen ("Gemini 4 Hydrogen", @ai-sdk/google, paid), Go unchanged at
32, src/go-limits-data.ts unchanged. Every document and test that pins what
moved follows it:

- README.md: the routing table's @ai-sdk/google row 9 -> 10, and the stated
  total 87 -> 88 (both mentions) with its date 2026-11-12 -> 2026-11-13; the
  catalog tests parse both the table and the sentence.
- README.zh-CN.md: the same three changes, kept in step with the English
  README for the docs-pairing gate and the bilingual prose-total test.
- docs/protocol-routing-and-merge.md: every "N of 87" becomes "of 88", the
  Google row 9 -> 10, and the counts-as-of note names #171 and its date.
- docs/redirect-planes.md: Zen "87 served" -> 88, the shapes table's Google
  Zen cell 9 -> 10, "0 of 87 on Zen" -> 88, and both counts-as-of notes name
  #171; the listed halves stay the ones #156 recorded, as #159 left them,
  since the registry was not re-read.
- test/catalog.test.ts: the bundled Zen shim's pinned length 87 -> 88. The
  free count (12), the Responses/Anthropic buckets and the both-planes overlap
  are unchanged, because the new model is paid, Google and Zen-only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
… restating them

The header still stated the split of an 85-model Zen catalog (28/30/18/8/1);
the shipped catalog is 88 (27/32/18/10/1). Restating the counts here is a
second copy the refreshes stopped keeping true, so the header now names the
SDKs and points to docs/redirect-planes.md section 2, which test/catalog.test.ts
holds to the catalog.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
The routes module header said the counts are stated once, in docs/redirect-planes.md §2. The README pair and docs/protocol-routing-and-merge.md §1 state them too, each held to the catalog by its own test in test/catalog.test.ts. The header now names §2 as the per-plane table and says the test holds that table, the README's and the protocol doc's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
…READMEs

The intro's model list and the opencode-responses example named muse-spark-1.3-contributor-free, which the shipped catalog no longer carries. Both READMEs now name muse-spark-1.3, a served Zen @ai-sdk/openai model with the example's context window, output limit and modalities, and the example's display name drops "Free".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
The refresh adds the Zen model gemini-4-hydrogen (@ai-sdk/google), so Zen
goes from 87 to 88 served models and its Google count from 9 to 10; Go and
the Go limits are unchanged. Its port on main also replaces the retired
muse-spark-1.3-contributor-free id in both READMEs and stops the routes
module header from restating per-SDK counts. Conflicts, resolved by keeping
P-R1's text and carrying the refresh's facts into it:

- README.md, README.zh-CN.md: P-R1 rewrote the protocol table as three
  columns (no "served from" column, Mistral filed as Chat Completions) and
  replaced the declare-the-route-yourself section and its YAML example with
  "Settings stays the host's". Kept P-R1's table with the Google row at 10,
  and P-R1's section, so the example's muse-spark id change has no passage
  to land in. The 88 / 2026-11-13 total sentence and the intro's
  muse-spark-1.3 merged cleanly from the port.
- docs/protocol-routing-and-merge.md: P-R1 moved the endpoints to the
  modern family ("shown on the default (modern) family", /inference/...
  paths) and refiled Mistral as Chat Completions; the port moved the counts
  to N of 88, Google to 10, the date to 2026-11-13 and added #171 to the
  refresh history. Kept P-R1's paths and rows with the port's counts, date
  and history clause.
- docs/redirect-planes.md: P-R1 rewrote the plane table's credential
  column (the entry's apiKeyEnv, then pi's OPENCODE_API_KEY) and refiled
  Mistral as openai-completions in the shape table; the port moved Zen to
  88 served and Google to 10. Kept P-R1's columns and protocol with the
  port's counts; the dated prose and the 0 of 88 sentence merged cleanly.
- src/responses-routes.ts: P-R1 deleted the module (its table lives in
  src/utils/protocols.ts); the port only rewrote its header comment. Kept
  the deletion.

src/catalog-data.ts and test/catalog.test.ts (Zen length 88) merged without
conflict.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
The refresh's gemini-4-hydrogen is a Zen @ai-sdk/google reasoning model
whose effort list is low/medium/high/xhigh. xhigh is outside Google's four
thinking levels, so usesCatalogThinkingLevels answers false for it, and the
bundled-catalog oracle in test/thinking-levels.test.ts, which expected every
Google reasoning entry to be level-only, failed on it: the flip its comment
anticipates for "a future Google entry with ... an xhigh".

The oracle now names the exception by id, with the value that excludes it,
and checks that the exception is still a Google reasoning entry the catalog
lists with that value in its effort list, so a renamed or reshaped entry
fails rather than leaving a stale exception. Every other entry is held to
the same expectation as before, and hydrogen is still asserted (false).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
main's tree equals the catalog refresh port (152d0c0) this branch already
merged, so the merge changes no file and only joins the histories.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
@viztor
viztor merged commit c7820b4 into main Nov 13, 2026
9 checks passed
viztor pushed a commit that referenced this pull request Nov 13, 2026
main is P-R1 (#113) squashed; its tree equals the P-R1 head this branch
already merged (ce889ef), so the merge changes no file.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
viztor pushed a commit that referenced this pull request Nov 13, 2026
…to claude/vigilant-mendel-bctz9n-p3

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
viztor pushed a commit that referenced this pull request Nov 13, 2026
… claude/vigilant-mendel-bctz9n-p-t2

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD
viztor added a commit that referenced this pull request Nov 13, 2026
* test: make the suite engine-independent and guard the client boundary

- Stop the run on a Node below engines.node, before collecting a file.
  Below the floor, failures came from the engine (node:sqlite,
  Intl.DurationFormat, ICU strings) and looked like code regressions.
  .node-version names the Node CI uses.
- Add a CI job that runs the unit and mock suites on Node 24, the floor
  engines.node promises but no job ran.
- The zh fallback duration unit is now 分钟, the platform's own narrow
  output, so the meter row reads the same with or without
  Intl.DurationFormat. The DurationFormat tests restore the engine as they
  found it, rather than leaving a stand-in installed or asserting the
  engine ships the constructor.
- Move the node:sqlite store fixture out of the helpers that the jsdom
  tests share, into test/support/opencode-store-fixture.ts.
- Add an architecture test that walks the client bundle's runtime import
  graph from settings-page.tsx. It forbids node built-ins, the cordis
  runtime, pi, and process.env. Checked by planting a violation.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

* fix(lifecycle): contain the internal-route mount and follow reloads in discovery

- A rejected internal-route mount ran from a `void` IIFE with no catch, so
  a Google bridge failure became an unhandled rejection, which Node's
  default mode answers by exiting the process. mountInternalRoutes now
  owns the mount and its withdrawal, and reports a rejection.
- Three early exits in registerResponsesProvider (internalRoutes off,
  every route declared, no llm-pi-ai) answered `undefined` after the
  Gemini bridges had already registered. That left them with no
  disposer, and the next reload failed DUPLICATE_ADAPTER. They now hand
  back the bridges' disposer.
- A mount that failed to start, or that captured nothing, kept its fiber
  and every route it had registered. Both paths now release the routes
  and dispose the fiber.
- Model discovery was registered on every activation, and the host's
  duplicate refusal was swallowed, so the first activation's settings
  answered for the life of the process. It is now registered once per
  root and answers from the latest activation's settings. Each namespace
  registers on its own, so one refusal no longer skips the rest.
- Move three doc blocks back above the functions they describe.

Each fix has a regression test. The two mount tests fail without the fix.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

* fix: price, list and key requests on the plane they belong to; isolate the suite

Request path and meter:
- Price a turn on its route's plane. Go-first lookups priced every Zen
  turn on deepseek-v4-pro at 0.66/1.98 instead of 1.74/3.48.
- The listing and model-info patches now act only on the routes this
  plugin claims. Before, a direct anthropic or openai route lost rows the
  OpenCode routing called "unservable", and a refused foreign id was
  answered with OpenCode's catalog.
- A relay's Go /models is answered with the Go catalog. The enrichment
  re-checked the URL against the default gateway markers and fell back to
  Zen.
- The free-model check reads a Request-carried body. That is how DSH
  sends a turn, and every keyless free turn was reported as a missing
  credential. The body is now read once for both uses.
- The meter's session fallback is the session that most recently ran a
  turn, not the most recently created one.
- A captured key's tier comes from the observed URL, then the route, and
  never the key's prefix. OpenCode issues oc_sk_ keys for Go, so a
  working Go key was filed under Zen. An OpenAI sk- key on a claimed
  relay was filed under Go.

Suite isolation:
- The unit and mock suites refuse non-loopback requests. Each unit run
  sent 54 requests to models.opencode.ai through the catalog's background
  revalidation, so on a networked runner a result could depend on the
  vendor's live catalog.
- Captured keys, session spend and the live catalog are reset after every
  test (resetCatalogState is new). Shuffled runs failed catalog cases on
  seeds 42 and 1234. test:shuffle runs the suite in random order, and the
  Node 24 CI job uses it.

Each behaviour change has a test that fails without it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

* fix(store): mount the inspection remote, and read legacy keys per integration

- The card's store-inspection row called remote.opencodeStore.inspect, but
  no client contribution carried that namespace's descriptor. The row
  reported "unavailable" on a host that had the service. storeRemote now
  carries it, with a whitelist codec, and the client mounts it.
- The host/client parity test missed this because it matched only a
  string literal in super(ctx, "name"), and the store passes a constant.
  It now resolves constants, and checks every declared Remote method
  against a client descriptor, not just every namespace.
- inspectStore parses through the whitelist instead of a cast, so a field
  the host did not mean to send (a token) is dropped. An ok:false
  envelope reports the remote's own message.
- The credential reader's legacy-row fallback was uncorrelated: one
  flagged opencode-go row hid every legacy opencode row, and the Zen key
  disappeared. The fallback is now per integration.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

* docs: describe the tree that exists, and the guarantees the suite now keeps

- AGENTS.md's repo map described nine modules this build does not have
  (the Console login, token store, org cache and sign-in row). They are
  replaced with one line saying sign-in is v2 work. The go-limits
  duplication note is gone, because attachAllowance calls allowanceFor.
  adapter-replacement.ts is marked as not wired. The client-bundle line
  names the boundary test.
- CONTRIBUTING.md documents the engine floor, test:shuffle and test:mock,
  the offline guard, the per-test state reset, where host-only fixtures
  live, and that a regression test must fail without its fix.
- The no-credential warning no longer tells the reader to "sign in
  through the settings card", which this build does not have. It points
  at readOpencodeStore instead, and a test asserts the advice.
- CLIENT_BUDGET is raised from 128 to 132 KB for the store descriptor
  and its parser (about 1.2 KB of source, not a dependency), following
  the gate's own instruction.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

* docs: add the improvement plan from the architecture review

docs/improvement-plan.md groups the review's findings by cause and orders
the fixes in nine phases after this one. Each phase lists its problems,
its steps, the test that has to fail first, its acceptance criteria and
any decision the owner needs to make. Findings are marked "measured"
(reproduced) or "traced" (read in the code). Among the measured ones:
the picker and dispatch derive reasoning levels differently for 9 of 95
catalog models.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

* docs(plan): the approved refactor plan, with the host's own contracts

Replace docs/improvement-plan.md with the plan of record. It now holds:
- the owner's decisions (D1-D11);
- what the DeepSeek Harness host supports, with references into
  packages/llm, llm-pi-ai, session-controller, gateway and
  ui-settings-models;
- the defects this audit found that are not yet fixed;
- the target architecture and the phases, one PR each, with a status
  table that each PR updates.

The decisions that change the code:
- Settings offers the whole catalog, and the composer offers only the
  declared models.
- The plugin never writes the user's profile and follows the user's
  configuration at dispatch.
- The adapter patch exists to merge provider modes and nothing else. It
  is generalized to every mode, and its catalog fill is removed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

* docs(plan): route every mode through the user's own provider

Investigation of the host's source and of the pi catalog it pins (1.1.0)
changed the routing design.

- llm-pi-ai resolves `api` and `baseUrl` per model, and pi's installed
  `opencode` catalog already spans Anthropic, Google, Completions and
  Responses with per-protocol endpoints. One route serves every mode for
  every model pi knows.
- The plugin fills only three gaps: models newer than pi's catalog
  (including all Gemini on Go), endpoint drift, and profile-level
  `api`/`baseURL` collapsing the modes.
- The new design is one unregistered catalog adapter per plane, keyed by
  the user's route id, and the adapter patch (merge-modes) delegates only
  gap models to it. That deletes the hidden routes, the listing hide,
  the stream redirect, the child mount and the Gemini bridge
  registration.
- A real-host test harness (P-T1) comes before any of it.
- One upstream change (per-entry `api`/`baseURL` in llm-pi-ai, U1)
  would remove the routing layer entirely. It would also let Settings
  save a model pi doesn't know, which pi-ai's strict write validation
  rejects today.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

* docs(plan): the plugin owns the plane routes, measured on pi 1.1.0

pi-ai registers exactly the providers in its resolved config, and the
fiber keeps whatever the internal/config waterfall returns, while the
config editor writes the raw value. A prepended listener that resolves
pi-ai's config without opencode/opencode-go therefore keeps pi-ai from
claiming them, without touching the user's file, and the plugin registers
its own PiAiAdapter under the same ids through the public API.

Measured on the real host with pi 1.1.0 and the harness's patch: both
routes held by the plugin, pi-ai's other routes unchanged, every protocol
on both planes (Gemini on Go included) sent to its own endpoint, a
catalog-unknown model id saved, listed and dispatched, and both routes
handed back to pi-ai on release.

Replaces the upstream asks (no upstream changes) and the save-gate
design, which is kept only as the fallback.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

* docs(plan): fold the reviewed design and the build order into the plan

A nine-lens review of the routing design, with two independent skeptics
per finding (78 of 82 upheld), changed five things before any code moves:

- Settings reads pi-ai's resolved config, so a read-only wrapper on
  settings.describe puts the plane entries back into the displayed value;
  writes were never affected, the Models page edits by path.
- The route claim gets an explicit lifecycle: commits are told from
  validations by identity, reconcile runs on commit, mount and teardown
  are ordered, and pi-ai restarts at most once per transition.
- The plugin's adapter is built from public pieces only, with pi-ai's
  credential rule and no captured keys on the adapter path.
- The protocol comes from the catalog map, never from a guess at the
  model's name; metadata and levels are catalog first.
- Releases stay on 1.x, phases land as stacked pull requests, and a real
  host harness plus a binary e2e job pin the behaviour first.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

* fix: repair what shipped broken in 1.4.1

- The deprecated @viztor/dsh-opencode wrapper re-exported a `default` the
  barrel does not have, an ESM link error on every host load. It now
  forwards `export *` only, written by one shared helper that a test links
  against the built barrel in a real Node process.
- isZenProvider("opencode-google") answered false: "go" inside "google"
  read as the Go plane. It now uses the plane rule the catalog already has.
- enrichModelsResponse answered a 401, 403, 429 or 5xx with a 200 catalog.
  Those statuses now pass through; a 404 is still filled from the catalog,
  because the Go plane's inference base has no /models of its own.
- The warning colour named a token the host does not define, so warnings
  rendered as caption text. They now use --dsw-alias-state-warn-label.
- The settings spinner only animated while the meter was mounted; the
  settings surface now carries its own keyframes.
- The stream hook wrote debugFile with debug off. debug is the switch and
  debugFile only where it writes, the same gate the fetch patch uses.
- The purge row reported the host's own { ok: false } failure as "not a
  purge result" and cast the answer; it now shows the host's message and
  parses through the contract's whitelisting parser.
- The endpoint copy described modern as /inference/v1, which the plugin
  never sends. It now names the per-protocol inference family, and auto as
  its alias, which is what the code already did.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

* docs(plan): P1a is in review

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

* chore: remove dead code and tighten the repository's gates

- Delete adapter-replacement.ts and its test. Nothing imported it, and
  deleting another plugin's registry entry is not a host API; the
  engineering notes record why, and what the plugin does instead.
  A gate fails the build if src/ writes the llm registry map directly.
- Delete the stream hook's `prepared` handling: the host passes the
  prepared dispatch as its own argument and never reads options.prepared.
- Untrack .freebuff/ and ignore it.
- The settings partition test walks Config.dict, which is how debugFile
  went missing from it; a manifest test checks cordis.patch.yml against
  the schema and the field registry, and the manifest's phantom
  showUsagePrice, wrong marker text and incomplete lists are fixed.
- utils/retry.ts takes its timer and logger as parameters, and the utils
  purity gate now also rejects console and global timers, walking the
  folder recursively.
- tool-fallback reads free-tier ids from the request's own plane (Go iff
  opencode-go or opencode-go-*) and loses its unreachable Anthropic
  branch. A Zen-only free model is no longer treated as free on Go.
- test/support/src.ts is the one door tests use into src/, so later
  module moves touch one file; CONTRIBUTING.md states the rule.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

* feat: a smaller public API

The barrel is the host's contract and nothing more: apply, Config,
inject, name and the config types. Everything else is internal, and
test/public-api.test.ts pins the list, so widening it is a reviewed
change. The 1.x line keeps its version scheme: nobody consumes the
removed exports (D6).

- Every test reaches src through test/support/src.ts, importing each
  name from the module that defines it; a gate rejects a static src
  import anywhere else outside its named exemptions.
- package.json no longer exports ./src/*, and check.ts fails when an
  exports target is outside the published files.
- resolveRoutedKey is deleted: it was exported and never called.
- The internalRoutes setting is deleted; the per-protocol routes always
  mount until the plugin owns the plane routes. A row that still carries
  it validates and the key is ignored.
- @viztor/dsh-opencode is no longer published. The deprecation message
  already on npm stays as its final word, and check.ts asserts that the
  release workflow verifies exactly what publish-scoped.ts publishes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

* build: pin the host the plugin runs on

The test install now runs the host users run.

- pi is pinned to 1.1.0, the version the harness's own lockfile pins,
  with the harness's patch applied verbatim (patches/, hash recorded by
  pnpm). A pnpm override holds every copy in the tree to it, not just the
  direct devDependency.
- The @deepseek-ai packages move from 0.2.1-alpha.5 to 0.2.1-rc.2, the
  version a registry install of the published dsh resolves (cordis 4.0.5,
  loader 1.0.6, schemastery 3.18.5). The boot-tier packages the real-host
  harness needs next (dsh-llm, dsh-app-boot, dsh-config-editor,
  dsh-settings, cordis-plugin-loader) are added as devDependencies.
- test/support/host-versions.ts names those versions once.
  test/host-versions.test.ts checks that the pi the plugin loads at
  runtime is 1.1.0 and the same copy the tests import, that the patch is
  applied, and that the installed cluster matches.
- mount-protocols now checks that every catalog SDK maps to a protocol
  pi's exported api modules implement, and records as a characterization
  row that pi 1.1.0's opencode-go provider has no Google API.

No src/ behaviour changes: the rc.2 builds of llm-pi-ai, cordis and
schemastery are code-identical to alpha.5.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

* test: a harness that is the real host

Add an in-process harness that runs the plugin inside the real host,
so the routing work that follows can be measured against today's
behaviour rather than against fakes. Additive only: no src/ change.

test/support/host.ts gives two tiers. Tier A is a bare cordis Context
with LlmRuntime and llm-pi-ai mounted with { providers }, for the
config waterfall and pi-ai's credential paths. Tier B is dsh-app-boot's
boot() with a temporary HOME, a test bundle, a fake credentials service
and a launch-environment snapshot, for the config editor,
settings.describe and the volatile commit. Tier B's rows carry package
names through a stub module loader, and it fixes the apply order,
because the loader starts rows in parallel.

On top of it:

- contract tests for the waterfall, the profile and Config shapes,
  credentials, and the config editor (host-waterfall, host-shapes,
  host-credentials, host-editor-contracts), plus a tier A smoke test;
- a characterization of today's behaviour on pi 1.1.0, each row tagged
  where P-R1 is expected to flip it, with S0_TODAY shared by the
  binary e2e;
- the keyless 401-vs-404 matrix: a pure classifier, its unit test and
  a gated live run;
- a binary e2e gated on DSH_HOST_E2E=1 with its probe bundle, the
  e2e-host CI job (a pinned, patched cell on PRs that touch the host
  seams or vite.config.ts, and a floating cell nightly and on
  dispatch), and the test:e2e:host script.

The binary e2e now reports why a run failed: a timeout only when its
own deadline killed the host, the signal for a crash, and the probe's
fatal message whenever there is one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

* refactor: one way to patch a host method

Add src/host/patch-method.ts (patchMethod), now the only way the
plugin wraps a method on a host object, and move the four instance
patch sites in src/models-discovery.ts onto it: the three listings
hidden by hideResponsesRoute, decorateModelDiscovery,
decorateModelInfo and decoratePlaneAdapters.

On the real host the three llm-level disposers never restored:
ctx.llm is cordis's traceable proxy, every method read returns a new
shadow proxy, so the `===` identity checks were never true and each
reload stacked a wrapper over the last. The helper reads identity
from own descriptors instead. A dispose restores only while its
wrapper is still current (otherwise it becomes a pass-through),
deletes the own property when the original was inherited, and a
same-site reinstall wraps the unpatched method rather than the
earlier install, so either disposal order ends at the exact
original.

decoratePlaneAdapters becomes one patch per (adapter, method) that
composes the adapter's plane routes in registry order, which keeps
today's nesting on pi-ai's shared adapter. rewrapOnAdaptersUpdated is
built and tested but wired to no site yet.

test/reload.test.ts runs apply A, apply B, dispose A, dispose B (and
the reverse order) for every site on own and inherited originals, on
the real host (tier A) and through apply(); test/patch-method.test.ts
covers the helper's contract. The existing per-site pins in
test/models-discovery.test.ts stay unchanged; five new cases there
record the deliberate edge changes on hosts that refuse the patch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

* chore: refresh the generated catalog and limits tables

The vendor added kimi-k3.1-pro to the Go plane, so `catalog freshness`
went red on every branch of this stack. This takes the two generated
files exactly as the nightly refresh produced them (#111), with no
hand edits, and updates the counts that hard-code the catalog's size:
the Go shim test (28 -> 29), the plane table in redirect-planes.md
(Go served 28 -> 29, the no-SDK Go cell 17 -> 18), and the Go
completions list in both READMEs. The listed half of "29 served / 33
listed" is models.dev's count, inferred from the one added entry,
because models.dev cannot be reached from here.

It no-ops once main carries #111. P-T2 turns these counts into
invariants so a refresh stops needing this.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

* feat: the plugin owns the opencode and opencode-go routes

The plugin now serves the opencode and opencode-go routes itself,
through supported host paths only, and the hidden-route workaround is
gone.

A prepended internal/config listener hands pi-ai a copy of its config
without the two plane entries, so pi-ai keeps every other route. On a
commit the plugin reconciles: it builds one plane adapter, a subclass
of the host's PiAiAdapter, over pure profiles (utils/plane-profile,
plane-model, model-metadata and protocols) and registers only the
planes that are declared. A Settings candidate has the plane entries
it changes validated strictly. A read-only settings.describe graft
puts the committed plane entries back, so the host still sees them.

Deleted: responses-provider, responses-routes, google-bridge,
pi-provider and utils/google-replay; the listing hide and the
discovery decoration; the stream redirect; the D19 settings; and the
hidden ids in DEFAULT_PROVIDERS and cordis.patch.yml.

What users see: any saved id is accepted and served, and an id no
catalog knows dispatches as completions. The protocol comes from the
catalog and model info follows the user's own row first. A model
whose reasoning is only on or off offers Off/On. The default endpoint
is the per-protocol inference family. Keys resolve by pi-ai's own
rule, and a plane lists its declared ids even when no key resolves.

When several pi-ai rows declare planes, the row the plugin serves
stays the same while it is live and still declares one, so no row is
left stripped with nobody serving it. If the host's pi-ai module
cannot be read, the plugin steps aside even before any plane is
declared, so a later Settings save goes to pi-ai's own check.

The characterization rows this changes are flipped to new baselines;
every other row stays green unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

* test: measure which endpoint families the gateway serves

A measurement phase: the default endpoint family stays `modern`, so no
plugin behaviour changes and no characterization row flips.

- Name the default in one constant, AUTO_ENDPOINT_FAMILY in
  src/utils/plane-model.ts, which is what `auto` resolves to, and pin
  it in test/endpoint-family.test.ts.
- Extend the keyless matrix to three URL shapes (legacy, root, modern)
  over four protocols and two planes, add LEGACY_DISPATCH, and add one
  keyless free-model probe with its own pure classifier.
- Add minimal keyed probes (pure test/support/keyed-probes.ts and its
  e2e): one tiny request per plane, protocol and shape, sent with the
  plane's own key and the plugin's identity from one shared constant,
  PROBE_IDENTITY. Only the cells the default sends to are asserted;
  the rest are recorded.
- Gate the live job by tier with OPENCODE_E2E_KEYED (none, probes,
  full; unset means full). CI arms `probes` on a pull request that
  touches what the probes send, `full` nightly and on dispatch, and
  passes the secrets only when the tier is not `none`.
- Write a dated endpoint-family record from both live suites to the
  run summary and an `endpoint-families` artifact, and add §13 to
  docs/redirect-planes.md with every cell pending the first CI run.

The live part runs in CI only; no live result is invented here. A
failing keyed modern cell in CI is the evidence for a later `fix:`
that flips the default.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

* chore: refresh the generated catalog and limits tables

The vendor renamed the Go plane's one Gemini model (dropping its
contributor suffix, halving its price and publishing an allowance for
it where the limits table had none) and added a free model with no
SDK, so `catalog freshness` went red on every branch of this stack.
This takes the two generated files exactly as the nightly refresh
produced them (#111), with no hand edits, and follows them:

- the Go Gemini fixtures in six test files (attachment-services,
  catalog, go-google, go-google-prepare, models-discovery,
  responses-routes) now name the renamed id, since each means "the
  Go Gemini model the catalog serves";
- the usage panel test built its unknown allowance from the table's
  one unpriced row, which the vendor has now priced, so it builds the
  `Number.NaN` allowance the generator emits itself;
- the Go shim count in catalog.test.ts (29 -> 30);
- redirect-planes.md: Go 29 -> 30 served, the no-SDK Go cell
  18 -> 19, and "0 of 29" -> "0 of 30" in the prose;
- both READMEs: the renamed Gemini id, and the new model in the Go
  completions list, since it declares no SDK.

The listed half of "30 served / 34 listed" is models.dev's count,
inferred from one rename (no change) and one addition (+1), because
models.dev cannot be reached from here. The dated S0 measurement row
in improvement-plan.md keeps the id it was measured with.

It no-ops once main carries #111.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

* test: skip free models the gateway has withdrawn in the live suite

The live routing suite picks its free-tier subjects from the catalog by
asking the gateway, but it counted "400 Upstream request failed: Model
is unavailable" as an observable answer. The upstream withdrew
nemotron-3-ultra-free while the catalog still lists it, so the picker
chose it and two cases asserted on a model the gateway no longer
serves; the reachability case also failed on a 429 throttle.

A withdrawn model (that 400, or a 410) is now skipped like a throttled
one. The free-tier routing case only accepts a candidate whose answer
is the free-tier gate it asserts, so an unmetered model cannot become
its subject, and the reachability walk moves past gated, withdrawn and
throttled candidates before it reports.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

* fix: address the second review of P-T1b

The review ran three rounds through the correctness, rules and matrix
lenses. What it found, and what changed:

- The binary e2e's beforeAll budget was smaller than the blocking
  subprocess deadlines it serializes. Each plugin install now has its
  own deadline, the hook's budget is derived from the installs and the
  boot, and afterAll stops a host a failed hook left running.
- providerWithApiKey claimed to be a line-for-line restatement but
  skipped upstream's credentialRef(ref) check. It now throws the same
  TypeError for a ref outside the grammar (a host-editor-contracts
  case pins it), and its comment names the deliberate differences.
- The pinned e2e-host cell let cordis and the loader float at patch
  level. The overrides now force both, the cache key includes them,
  and the job asserts them against HOST_VERSIONS.
- The pi-ai-first characterization asserted every facet under one
  KNOWN_DEFECT title. Each apply order now generates one test per row
  and facet, titled with that order's own tags.
- CONTRIBUTING said the contract tests pin F11-F20, and the plan said
  P-T1 pins every fact. Both now give the same accounting: which facts
  are pinned by number, which P-R1 exercises first, and why F9 and F17
  are not pinned.
- CONTRIBUTING put the e2e's dummy keys in the profile's credentials
  file; they are in the temporary home's credentials document.
- F5 had no real-host pin: host-shapes now pins that a route-level api
  and baseURL collapse pi's protocols onto one. F6 is recorded as first
  exercised by P-R1.
- The credential-mapping row pinned only the api-key branch of
  toPiCredential. host-credentials now covers all three branches, and
  the env branch is asserted through pi's Models.getAuth, so the case
  fails without it.
- The plan's reason for restating hasProviderApiKey was inaccurate; it
  now says no installed copy reaches the function.
- The new credential cases cited toPiCredential at the wrong lines;
  they now cite auth.ts:74-84.
- F4's listing and dispatch halves were pinned only by KNOWN_DEFECT
  rows P-R1 flips. A plugin-less tier-A case in host-shapes now pins
  that an id pi cannot place is not listed and that a turn on it is
  INVALID_CONFIG with no request; the save and deferred-read halves
  are tagged F4 too.
- The one facet that depends on the apply order lost its tag when the
  pi-ai-first block was split. The fixture case now checks only
  structure (the facets whose value moves are the facets whose tag
  moves), and the exact pin is its own KNOWN_DEFECT case that boots no
  host. CONTRIBUTING and the plan say P-R1 drops todayRowsFor's
  override and turns that case into a BASELINE check in the same diff.

Nothing under src/ changed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

* test: follow the vendor catalog refresh

The vendor renamed the Go plane's Gemini model from
gemini-4-argon-contributor to gemini-4-argon. The tier-B fixtures
(S0_PROFILE, S0_TODAY, CHARACTERIZED_PROFILE through S0, and
todayRowsFor's pi-ai-first override) declared the old id as the Go gap
id: one the plugin's catalog describes and pi 1.1.0 cannot place. After
the refresh the catalog no longer described it. They now name the
renamed id, which still plays that role because pi 1.1.0's opencode-go
table has no gemini-4-argon either. The host-shapes and editor-contract
cases follow the same fixture. The pi 1.3.0 note in mount-protocols
names that pi's own row, so it stays as it is.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

* fix: address the second review of P-H

What the review found, and what changed:

- rewrapOnAdaptersUpdated reinstalls its site inside the plugin's own
  teardown and on every foreign registry change. Its JSDoc in
  src/host/patch-method.ts now says so, counts the reinstalls a release
  causes, says a caller disposes it before releasing its adapters, and
  notes that a throw from a reinstall is logged by the host.
- The plan of record said all three P-H edge changes are unreachable on
  the real host for one reason; the third (the live-reload listing) is
  unreachable for a different one. docs/improvement-plan.md now gives
  each its reason.
- The tier-A reload cases for the listing and discovery sites proved the
  row only structurally. test/reload.test.ts now gives every site a
  tier-A probe whose decorated answer must differ from the host's own,
  so "survivor still decorates" and "last dispose restores" can fail.
- "patchMethod is the only way the plugin wraps a host method" was
  asserted in docs and enforced by nothing. test/architecture.test.ts
  now has a textual tripwire over everything apply() reaches, with a
  self-check of planted violations, and the apply()-twice reload case
  requires a patch record on every own function apply() leaves on the
  stand-in.
- The tripwire missed several write forms (Object.assign,
  Object.defineProperty/defineProperties, Reflect.setPrototypeOf,
  Object.setPrototypeOf, and an Object.* target reached through a
  property path such as registration?.adapter or this.llm). The
  patterns now catch them, and the self-check plants each one (17
  planted, up from 13). Nothing new in src/ matches.
- The docs said "any Reflect.* write" where the pattern matches a fixed
  set. The test docstring, AGENTS.md and the plan's P-H section now
  name the four Reflect and four Object methods matched, and why a
  prototype swap is listed.

No runtime behaviour changed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

* test: follow the vendor catalog refresh

The refreshed Go catalog serves its Gemini model as gemini-4-argon, so the
plane-dispatch mock, the plane-model cases and the binary e2e's probe
expectations name the new id; the Go Google base they assert is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

* fix: address the second review of P-R1

The review ran three rounds (found 20, 5 and 8; upheld 19, 4 and 8).
What it found and what changed:

- A completions model whose effort list names none lost its Off.
  utils/model-metadata.ts now offers Off with none as its wire value
  on completions, as pi 1.1.0's own entries do; Responses unchanged.
  Pinned in model-metadata, plane-model and a new mock-gateway case.
- A Go entry with no apiKeyEnv authenticates with OPENCODE_API_KEY
  (pi's lookup, D20), and nothing said so. README "Changed in 1.x" and
  step 3 now name it; host-planes.test.ts pins both cases.
- A Settings save of a plane was refused as "cannot be validated yet"
  while the host modules were unloaded. host/route-claim.ts now judges
  it with the last modules that loaded; route-claim.test.ts pins it.
- Fiber.update(config) with noSave false would persist the stripped
  config and drop both planes. Recorded as a constraint in
  route-claim.ts and the plan; no caller does it today.
- Stale text: cordis-context.ts comments (stream redirect, isolate,
  root inject), deep-dive.md (pi's Go lookup, who answers Fetch),
  CONTRIBUTING (field count, the host-modules floor), and historical
  banners on opencode-parity-plan.md and upstream-findings.md.
- The mock body-shape row asserted only body.model. It now checks each
  protocol's required fields and that the gateway answered 200, or
  the vendor's 404 for the unknown id.
- Route-set rows: removing the last plane now pins replace([]) on the
  live handle, and "booting with none" waits for the plugin's row to
  be active instead of sleeping 50 ms.
- The live protocol-routing suite now sends paid models through the
  plane adapter on a tier-A host, not only raw probes.
- README's credential-isolation claim was false: the Go meter can send
  the Zen key. README.md, README.zh-CN.md, deep-dive.md,
  credentials.md and redirect-planes.md now give the real order and
  the 403 remedies; readme-parity.test.ts keeps both READMEs in step.
- The keySource hint described Automatic backwards. settings-copy.ts
  (en and zh) now gives literal key, captured key, then reference.
- The live adapter blocks dropped a protocol whose model was
  withdrawn. They now walk up to three candidates per protocol
  (support/live-walk.ts), pass a "model is unavailable" turn only when
  a raw probe confirms it (support/live-probe.ts), and skip naming a
  protocol no candidate reached.
- A mock comment claimed a body passed shape checks the mock lacks.

Declined: a toggle beside an effort list still offers no Off, which
moves baseline facets and needs the owner's decision; the Models
store's join stays a documented restatement rather than importing a
client UI package; the PR description is edited outside the tree.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

* ci: gate freshness only on a pull request's own generator changes

The vendor moves its model catalog, its Go limits page and its CLI
version almost daily. Each move turned every open pull request red,
although none of them caused it: vendor drift is not a regression of
the change under test.

A pull request now fails a freshness check only when its diff changes
that check's generator code: the script, every local module it loads,
transitively, and the pinned version file for the CLI check.
scripts/freshness-scope.ts derives those inputs from the sources and
makes the call, so a new import moves them without a list to edit.
Any other pull request reports drift as a warning, as a push to main
already did. The offline check that every copy of the CLI version
agrees with the pin still gates every pull request, since only the
repository can break it. The gates fail closed: a lost scope answer
gates rather than skips.

The nightly run still regenerates the two tables and proposes the
refresh as its own pull request, and warns on the CLI version.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

* chore: refresh the generated catalog after Zen retired deepseek-v4-flash

The vendor retired `deepseek-v4-flash` from the Zen plane (it stays on
Go). This pull request changes the catalog generator's own code, so
under the freshness scoping it carries the refresh itself. This takes
`src/catalog-data.ts` exactly as the nightly refresh produced it (#111),
with no hand edits; `src/go-limits-data.ts` did not change. It then
updates the counts that still hard-code the catalog's size:

- the Zen shim count in catalog.test.ts (82 -> 81);
- redirect-planes.md: Zen served 82 -> 81, the no-SDK Zen cell 25 -> 24,
  and "0 of 82" -> "0 of 81" in the prose;
- protocol-routing-and-merge.md: the per-protocol shares now read "of 81",
  with completions at 24;
- both READMEs: the no-SDK row is 24 and the active total is 81. The
  Chinese table had read 27 before this change, and now matches the
  English one.

"127 listed" is unchanged, because the vendor retires a model by marking
it `status: "deprecated"`, which keeps it listed. This could not be
confirmed against models.dev, which cannot be reached from here.

It no-ops once main carries #111.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

* test: give the freshness walk's dynamic import a script-local target

The walk's fixture held the text `await import("../src/lazy.ts")`. The
test tree's own architecture gate, which starts on the next branch of
this stack, reads a dynamic `src` import anywhere in a test file's text,
including inside a string. It counted that fixture line as a test
bypassing `test/support/src.ts`, so every branch above this one failed
the gate once this pull request was merged into it.

The case is about the dynamic form, not about `src`, so the import now
names `./lazy.ts` beside the script. The walk resolves it from the
importing file's own directory, which is the same rule, and the expected
inputs follow it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

* fix: address the second review of P-R2

What the review found, and what changed:

- The keyed flip rule read a default cell's retired-model answer as a
  retired model without checking the same model on the other shapes.
  planeOutcome now reads each default cell beside the legacy and root
  cells of the same plane and protocol.
- A refused key counted as flip evidence even when every shape refused
  it. It is now "check the CI secret" when a sibling refused the key
  and none accepted it, and "nothing else measured the key" only when
  no sibling refused it; a path-gone, rate-limited or busy sibling no
  longer blocks the CI-secret reason.
- Breaks no flip could fix (accepted on the root shape only, or Go's
  Google, which no flip moves) were called flip evidence, and the
  "protocol" and "gone" reasons named causes nothing measured. A
  wrong-protocol or path-gone cell is now decided by the cell that
  endpoint: legacy dispatches to: "refused" (a flip would not serve
  it) when that cell refused readably, and always for Go's Google;
  "unmeasured" (re-run) when it measured nothing. Neither is flip
  evidence.
- An account answer counted as the flip target accepting a protocol or
  a model, though the account's gate answers before either is read. It
  now counts only beside a refused key or a gone path, and a retired
  model next to an account answer no longer fails the plane.
- The keyless free probe failed on a busy upstream (503/529) that the
  keyed classifier treats as routed. It now moves on past a busy
  candidate, and its dated record no longer pairs a derived
  upstream-busy with the last retired candidate's model and status.
- A busy upstream followed by no answer failed the keyed plane, against
  the verdict definitions in redirect-planes.md section 13. Section 13
  now says what the code does (no answer on the last attempt is
  transport-error unless an earlier attempt was a 429), and a test pins
  that it fails as unreadable, not as flip evidence.
- Comments and docs said P-R2 had already measured the families while
  section 13 is pending its first CI run; said models.opencode.ai
  declares the root shape, which the generated catalog contradicts;
  said legacy cells are asserted nowhere live; and said a keyed
  auth-refused means the plugin's own turns would be refused, though a
  default Go turn sends a different key. All corrected.
- CONTRIBUTING's key and mirror bullets now cover the keyed probes.
- The plan no longer claims P-R2 opens with a green pin, gives the
  conditional flip a phase, and says the keyed probes decide a plane
  from the default cells read beside their siblings, not alone, and
  that an unreadable answer is never flip evidence.
- Tests now cover the tier wiring of the four keyed live suites, no
  probe following a redirect, the push-to-none arm and the publish and
  upload steps of the CI wiring, the free probe moving on past a busy
  upstream, and the record being rewritten after every cell.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

* chore: refresh the generated catalog after Zen retired deepseek-v4-flash

Ported from the stack's base (d13cbe2 on #105) so this pull request's
own `catalog freshness` goes green now, rather than waiting for the base
to be merged up. It no-ops once the merge from P-R1 brings the same
change.

It takes `src/catalog-data.ts` exactly as the nightly refresh produced
it (#111). The counts that restate the catalog's size follow it, on this
branch's own wording of those passages:

- the Zen shim count in catalog.test.ts (82 -> 81);
- redirect-planes.md: Zen served 82 -> 81, the no-SDK Zen cell 25 -> 24,
  and "0 of 82" -> "0 of 81";
- protocol-routing-and-merge.md: the per-protocol shares now read "of 81",
  with completions at 24;
- both READMEs: the no-SDK row is 24, and the active total is 81.

"127 listed" is unchanged, because a model the vendor retires stays
listed as `status: "deprecated"`.

(cherry picked from commit d13cbe2410642254df0d0cf1392a5083386a0c6a)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

* fix: send Google's thinking level to Gemini models the catalog gives levels

The live suite caught the Go plane's Gemini model refusing a thinking
budget: the gateway answered that the model takes a thinking level and
not a budget. Pi chooses a thinking level over a thinking budget only
by matching the model id against its own pattern, so a model the
pattern does not name was sent a budget it cannot accept.

The plugin now makes that decision from the catalog, as D16 asks: a
model whose catalog entry offers discrete effort levels on the Google
protocol gets a thinking level. It builds that request through pi's
public exports, on both the enabled and the disabled path, and hands
every other model back to pi's own path unchanged.

Tests pin it: the pure rule over the catalog, a differential check
that the wrapper builds the same request as pi's own streamSimple for
an id pi already recognises, the lazy loading of the pi pieces, and a
mock-gateway case that the turn completes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

* docs: correct the Go shape count, the README date and the install range

Three documentation defects the synced base carries into P-R1. None was
caused by the merge (each is identical on the merge-base and on both
parents), and each wants the same change on the lowest branch (#105) and,
for the README date, on P-R2's cherry-pick of d13cbe2.

- PRE-1, docs/redirect-planes.md: "The Go plane serves 12 models on
  shapes other than completions" restores the count its own table gives
  (5 openai + 5 anthropic + 1 google = 11, the Go shim's 30 less its 19
  no-SDK models). The 12 was right when written (6 + 6) and a later
  refresh moved the cells but not the sentence. test/catalog.test.ts now
  holds the sentence to the table's non-completions Go sum, so the next
  refresh cannot strand it again.
- PRE-2, README.md and README.zh-CN.md: the 81-model count is dated
  2026-10-27, the refresh that produced it (d13cbe2); on 2026-10-23 the
  count was 82, so the date no longer matched the number.
- PRE-3, README.md and README.zh-CN.md: both install snippets name
  "^1.4.1" (package.json's version), where they had drifted to ^0.14.0 and
  ^0.12.0, neither of which can resolve to a 1.x release.
  test/readme-parity.test.ts pins the two snippets to one range on
  package.json's major.

Both new assertions were checked against the stale values: each fails on
them and passes on the corrected text.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

* docs: date the refreshed model count and correct three stale figures

The README's 81-model count kept the date of the 82-model count it
replaced (2026-10-23); it now carries the date of the refresh that
produced it (2026-10-27). Also corrected, all present-tense statements
the catalog had moved past:

- both README install snippets named ^0.14.0 and ^0.12.0, ranges that
  cannot resolve to any 1.x release; both now name package.json's
  ^1.4.1;
- redirect-planes.md said the Go plane serves 12 models on shapes other
  than completions; its own table gives 11 (30 less 19 with no SDK);
- engineering-notes.md tied a general rule to "the 82" models the shim
  ships; it now says "the models it ships", which a refresh cannot
  strand.

P-R1 carries the same corrections (dec00c8) with tests that hold the
README range and the Go sentence to their sources.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

* docs: close the audit's remaining findings on P-R1

Each finding the third audit round left open, re-verified on this head:

- README.zh-CN.md still says Zen keys never query the Go quota endpoint:
  already fixed (0cceb81, dec00c8). The feature bullet, step 3, the
  EntitlementError row and the credential bullet match README.md.
- The withdrawn-model fix drops a protocol instead of trying its next
  model: already fixed (0cceb81). walkPerProtocol walks up to three
  candidates per protocol and names an unreached protocol in a skip;
  test/live-walk.test.ts pins both.
- N1's claim stands in docs/deep-dive.md and the keySource hint
  describes auto backwards: already fixed (0cceb81) in deep-dive.md and
  both hint strings. The same claim survived in a source comment,
  KEY_SOURCE_ORDER's "tier safety" note in src/go-discovery.ts; this
  commit corrects it: a key is filed by the plane its request went to,
  so a Go entry without apiKeyEnv files the Zen key as a Go key.
- PR #113 lists counts, not the flipped rows and the four records:
  fixed in the PR body.
- The R2 fix passes the live rows without performing a protocol, the
  skip decided by message text: already fixed (0cceb81). A "gone"
  verdict needs a raw probe of the same id to confirm it, and a
  protocol no candidate reached skips by name.
- README.zh-CN.md lacks the N1 fix, the Go apiKeyEnv step and the
  "Changed in 1.x" bullet: already fixed (0cceb81).
- PR #113 omits the D20 Go behaviour change: fixed in the PR body.
- docs/deep-dive.md says resolveGoApiKey excludes Zen keys and a
  Zen-only setup hides the ring: already fixed (0cceb81).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

* chore: follow the vendor's retirement of nemotron-3-ultra-free

The vendor retired `nemotron-3-ultra-free` from the Zen plane, so
`catalog freshness` gated this pull request (it changes the generator's
inputs). `src/catalog-data.ts` is taken exactly as main regenerated it
(#132), which today's registry still matches apart from that one row,
with no hand edits. What restates the catalog follows it:

- Zen 81 -> 80 served, no-SDK 24 -> 23, free 9 -> 8: catalog.test.ts,
  redirect-planes.md, protocol-routing-and-merge.md and both READMEs
  ("127 listed" stays; a retired model stays listed as deprecated);
- the README model lists, in both languages, now name ids the catalog
  ships: the Zen and Go lists had kept seven ids the vendor has since
  retired. The lists are main's; this branch's own prose around them
  (how the Go Google base is served) is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

* test: walk past a model the gateway answers ModelDeprecated for

The live gateway job failed on `gemini-3-flash`: the registry still lists
it, and the gateway now answers it with an error of type
`ModelDeprecated`. The adapter walk moves past a withdrawn model only
when both its pre-filter (`isModelGoneMessage`) and the raw probe that
confirms it (`modelGone`) recognise the answer, and neither knew this
shape, so a vendor retirement read as the plugin's refusal.

Both now recognise `ModelDeprecated` (by type, at any status, as main's
`retiredAtTheGateway` does). `test/live-probe.test.ts` is new and holds
`modelGone` to each shape; `keyless-matrix.test.ts` gains the message
row. Removing either rule fails its test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

* test: install the plane-lookup world before each test

The suite's setup file resets the live catalog after every test, so main's once-per-file install was gone by the second case and three cases failed. Installing before each one keeps the file independent of run order.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

* test(scripts): move the generated-table skip into the CLI-version rules

The copy scan's file filter lived in the top-level script, where nothing could exercise it. It now sits in scripts/vendor-version.ts beside the other rules, with cases of its own, and a test keeps its list equal to freshness-scope's. Because the script imports that module, it is now an input of the cli:version check, and the strict input list says so.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

* test(e2e): read an upstream's 400 "Model is unavailable" as a retirement

The live suite's local modelGone rule gave way to main's verdict module in the merge. Its one shape main lacked, a 400 whose body says the upstream withdrew the model, now lives in retiredAtTheGateway with a unit case, so the free-tier walk and the subject pickers skip it instead of failing on it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

* docs(testing): describe the suite this branch keeps

Main's test-system page predates the offline guard, the per-test reset and the engine floor: it placed the store fixture in the shared helpers and called the plane-lookup world per file. It now says where the store fixture lives and why, that a live-catalog world is installed per test, and what the CLI-version rules cover.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

* test(e2e): keep the free-tier case's narrowing in the subject picker

Taking main's usableAsSubject rule in the merge dropped the picker's accept argument, so the free-tier case could pick an unmetered model and assert the gate on a 200. The case's own narrowing now applies after the shared rule.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

* chore(vendor): follow opencode 1.19.5

The vendor published `opencode/1.19.5`, and `cli:version` gates this pull
request because its diff touches that check's inputs. Main made the same
bump in #132 after comparing every cited vendor source between the two
tags: the one that moved (`session/llm/request.ts`) only dropped an
unused type import, so no header the plugin emulates changed. This takes
the same bump in the 17 files that carry a copy here; `cli:version
--copies` reports all 58 copies and mentions at 1.19.5.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

* docs(plan): restack the plan onto 1.8.0 and the alpha host

Main moved to 1.8.0 and onto the harness's alpha track while the stack was open, and landed several things the plan had scheduled. The plan of record now says so: D14' and D22' replace the pi 1.1.0 and rc.2 pins, D17 restates pi-ai's new defaults, D23 records the freshness scoping, D24 and D25 adopt main's debug gate and tool-fallback plane, and D9, D16 and D19 stand with notes. F10 is gone on pi 1.3. A new phase P-R3 (honest refusals) follows P-T3, each phase has its restack adjustment, the status table shows the restack, and the two community plugins' lessons are a ranked table with what not to adopt.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

* build: raise the client bundle ceiling to 136 KB

Main and this branch each raised the tripwire from 128 to 132 KB, each measured without the other's surface. Merged, the client is 135604 bytes, about 0.4 KB over; the owner's standing rule for growth like this is to raise the ceiling, not cut content.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

* docs: complete the restack of P0

Close the restack review's findings on #105. Every change is prose, a
comment or a log line; no behaviour moves, so no test is added.

- ci.yml: the pnpm/action-setup comment no longer quotes a pnpm version;
  it names the `packageManager` field, which main's bump had made stale.
  release.yml carries the same stale sentence on main and is left to a
  main-side follow-up.
- scripts/freshness-scope.ts: the GENERATED_OUTPUTS and dataInputs docs
  name scripts/vendor-version.ts (GENERATED_TABLES, PINNED_VERSION), where
  the copy and the regular expression now live.
- test/e2e/protocol-routing.e2e.ts: the free-tier walk's report names the
  causes that can reach it (gated, withdrawn, refused for the account,
  never answered); a throttled walk skips, so "throttled" is gone.
- README pair: the Chinese file now says what the English says about
  403 FreeTierError (the headers are not its cause and the patch does
  not open the free tier), carries the Auto Review route clause and the
  two paragraphs on the API-key-only build and the host restart; the
  English yaml fence drops the nonexistent hiddenModels key.
- README pair: the compatibility section restates the typert peer range
  package.json declares and what its two clauses admit.
- docs/improvement-plan.md: F2, G1 and G2 carry the pi 1.1.0 restack
  note; the sources line names both harness tags; the P1a-P2 test matrix
  says the wrapper is retired; P0's status row counts ten defect fixes.
- docs/protocol-routing-and-merge.md, docs/redirect-planes.md: the count
  tables are dated 2026-10-31 (the shipped shim), apart from the
  documents' measurement stamps; both READMEs name
  `catalog:shim -- --write` as the regeneration command.
- AGENTS.md: the What-moved table names src/utils/retry.ts; the coverage
  line points at vite.config.ts instead of quoting stale thresholds.
- CONTRIBUTING.md: the restart check names CARD_FIELDS rather than a
  field count.
- docs/testing.md: rule 1 counts an upstream's 400 "Model is unavailable"
  as a retirement, and rule 2 says the body, not the status, makes it one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

* docs: close the restack review of P0

Close the second review round's findings on the restacked P0. Docs only;
no generated file is touched.

- both-kept-2-R2-1: docs/deep-dive.md no longer names the retired ids
  the shim was said to suppress (main's regenerated shim serves one of
  them on Zen). It states the rule instead: a retired model is a row the
  registry marks deprecated, the generator drops it per plane, and ids
  are not restated; the enrichment item says "registry-deprecated rows".
- rules-docs-2-R2-1: the PR body is replaced at push time from the
  updated draft, with the verification rows re-measured on this head
  and a restack section (what merged from main, what this PR dropped,
  the main-side follow-ups). Nothing is pushed here.
- rules-docs-2-R2-2: the plan's defects table is retitled to where each
  defect now stands; the four rows main fixed are struck with the fix
  named, the purge row keeps only what is still open (a RemoteError
  object shows the generic message, P1a), and D19 records that the
  README's hiddenModels was removed in P0.
- rules-docs-2-R2-3: README.zh-CN.md's merge rule follows the English
  and the code: a row is dropped only when the registry marks it
  deprecated, not when a per-provider list names it. One sentence, so
  the pairing gate's counts are unchanged.
- rules-docs-2-R2-4: CONTRIBUTING.md no longer contradicts the
  repository: warnings are reported and gate in P12 (as its own command
  list says); a manual release creates the GitHub Release and never
  touches CHANGELOG.md; the release step points at the "Verified on
  DeepSeek Harness" line in both READMEs; the e2e section lists all
  five files.
- rules-docs-2-R2-5: both READMEs drop the test total nothing pinned;
  engineering-notes.md's "every count" paragraph records it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

* docs: hold the meter's allowance figures to the generated table

The third review round of the P0 restack. Each outcome:

- The meter mock-ups in both READMEs and both quota-meter guides paired
  one model with another model's monthly allowance. They now name the
  model whose row the figures are, and the panel mock-up's rate line
  prints that model's catalog rates in the format the panel uses. A new
  catalog test reads every "<id> Go $a · Plus $b" figure in the four
  documents back against the generated allowance table; it failed on
  the old mock-ups and holds a known minimum of matches, so a regex that
  stops matching cannot pass as clean.
- engineering-notes: the "deeper half" measurement is scoped to the Go
  plane it was taken on. Both ids are Zen rows and neither has a row in
  the Go catalog, which is what the plane-aware lookup misses; the
  sentence no longer claims they are absent from both shims.
- deep-dive: the two "up to" figures for context window and max output
  are dropped, per the same file's rule that counts which move with the
  vendor are not restated.
- docs/README.md indexes every document in the directory: the test
  system guide, upstream findings, the CLI API note, the parity plan,
  and the plugin page pair, whose two languages the opening sentence
  now names.

No generated file moves.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

* style: lint and format the merged purge row and token test

The merge resolution read the RemoteError's message by property access,
which the lint gate's prefer-destructuring rule rejects as an error, and
left the merged host-tokens test unformatted. Same behaviour, now through
the gate.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

* fix: complete the restack of P1a

Outcomes of the merge review:

- both-kept-1-F1, resolutions-1-P1A-2, rules-docs-1-P1A-DOCS-1:
  docs/redirect-planes.md now states D19 (`auto`, the default, is an alias
  of `modern`; `legacy` pins the zen family) instead of "follows the
  catalog". The models-discovery repair comment is left for P-R1, which
  deletes that block, and is named in the PR's follow-ups.
- resolutions-1-P1A-1: the catalog's refusal describe gains the Zen-plane
  404 fill, the non-JSON 200 answer, and a retry-after header that must
  survive the passthrough (mutation-checked).
- resolutions-1-P1A-3: the plan's defect row for the RemoteError purge
  message is struck as fixed by P1a.
- rules-docs-1-P1A-DOCS-2: the plan's status and restack rows name the
  warning token's dropped fallback, matching the build-order item.
- rules-docs-1-P1A-DOCS-4: the host-tokens header names usage-pill among
  the meter files the card scan leaves out.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

* docs: state the measured meter-scan reasons and P1a's refusal test cases

- fixed: rules-docs-2-P1A-1 — the host-tokens header drops the template
  clause (no meter file builds a token name from a template) and names the
  measured reasons: usage-pill.tsx would pass the scan; usage-ui.ts and
  usage-panel.tsx each spell the `--dsw-alias-state-*` wildcard in prose, and
  usage-ui.ts names `--dsw-radius-full` behind a literal 999px fallback.
  CARD_FILES is unchanged.
- fixed: rules-docs-2-P1A-2 — the plan's status row, struck defect row,
  build-order item and restack row name the three refusal-passthrough test
  cases P1a adds (the Zen-plane 404 fill, the non-JSON 200 answer, the
  retry-after passthrough); the passthrough itself is on main (#133).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

* test: one catalogue fixture, behind the one door

P1b's divergent-catalog seed and main's synthetic rows (#138) built the
same thing twice, and main's copy imported src directly. They are now
one module, test/support/catalogs.ts, which reaches src through
test/support/src.ts: the seed is built from main's payload builder, and
the plane-lookup and plane-divergence tests take their src names from
the door too (coreToolDecision is added to it).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

* docs: describe debugFile by the gate main chose (D24)

The settings registry and the manifest still said the file is written
only while debug is on. Under main's rule, which this stack adopted as
D24, naming debugFile writes both halves of the log and debug governs
the console alone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

* style: satisfy main's promise lint in the …
viztor added a commit that referenced this pull request Nov 13, 2026
* test: make the suite engine-independent and guard the client boundary

- Stop the run on a Node below engines.node, before collecting a file.
  Below the floor, failures came from the engine (node:sqlite,
  Intl.DurationFormat, ICU strings) and looked like code regressions.
  .node-version names the Node CI uses.
- Add a CI job that runs the unit and mock suites on Node 24, the floor
  engines.node promises but no job ran.
- The zh fallback duration unit is now 分钟, the platform's own narrow
  output, so the meter row reads the same with or without
  Intl.DurationFormat. The DurationFormat tests restore the engine as they
  found it, rather than leaving a stand-in installed or asserting the
  engine ships the constructor.
- Move the node:sqlite store fixture out of the helpers that the jsdom
  tests share, into test/support/opencode-store-fixture.ts.
- Add an architecture test that walks the client bundle's runtime import
  graph from settings-page.tsx. It forbids node built-ins, the cordis
  runtime, pi, and process.env. Checked by planting a violation.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

* fix(lifecycle): contain the internal-route mount and follow reloads in discovery

- A rejected internal-route mount ran from a `void` IIFE with no catch, so
  a Google bridge failure became an unhandled rejection, which Node's
  default mode answers by exiting the process. mountInternalRoutes now
  owns the mount and its withdrawal, and reports a rejection.
- Three early exits in registerResponsesProvider (internalRoutes off,
  every route declared, no llm-pi-ai) answered `undefined` after the
  Gemini bridges had already registered. That left them with no
  disposer, and the next reload failed DUPLICATE_ADAPTER. They now hand
  back the bridges' disposer.
- A mount that failed to start, or that captured nothing, kept its fiber
  and every route it had registered. Both paths now release the routes
  and dispose the fiber.
- Model discovery was registered on every activation, and the host's
  duplicate refusal was swallowed, so the first activation's settings
  answered for the life of the process. It is now registered once per
  root and answers from the latest activation's settings. Each namespace
  registers on its own, so one refusal no longer skips the rest.
- Move three doc blocks back above the functions they describe.

Each fix has a regression test. The two mount tests fail without the fix.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

* fix: price, list and key requests on the plane they belong to; isolate the suite

Request path and meter:
- Price a turn on its route's plane. Go-first lookups priced every Zen
  turn on deepseek-v4-pro at 0.66/1.98 instead of 1.74/3.48.
- The listing and model-info patches now act only on the routes this
  plugin claims. Before, a direct anthropic or openai route lost rows the
  OpenCode routing called "unservable", and a refused foreign id was
  answered with OpenCode's catalog.
- A relay's Go /models is answered with the Go catalog. The enrichment
  re-checked the URL against the default gateway markers and fell back to
  Zen.
- The free-model check reads a Request-carried body. That is how DSH
  sends a turn, and every keyless free turn was reported as a missing
  credential. The body is now read once for both uses.
- The meter's session fallback is the session that most recently ran a
  turn, not the most recently created one.
- A captured key's tier comes from the observed URL, then the route, and
  never the key's prefix. OpenCode issues oc_sk_ keys for Go, so a
  working Go key was filed under Zen. An OpenAI sk- key on a claimed
  relay was filed under Go.

Suite isolation:
- The unit and mock suites refuse non-loopback requests. Each unit run
  sent 54 requests to models.opencode.ai through the catalog's background
  revalidation, so on a networked runner a result could depend on the
  vendor's live catalog.
- Captured keys, session spend and the live catalog are reset after every
  test (resetCatalogState is new). Shuffled runs failed catalog cases on
  seeds 42 and 1234. test:shuffle runs the suite in random order, and the
  Node 24 CI job uses it.

Each behaviour change has a test that fails without it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

* fix(store): mount the inspection remote, and read legacy keys per integration

- The card's store-inspection row called remote.opencodeStore.inspect, but
  no client contribution carried that namespace's descriptor. The row
  reported "unavailable" on a host that had the service. storeRemote now
  carries it, with a whitelist codec, and the client mounts it.
- The host/client parity test missed this because it matched only a
  string literal in super(ctx, "name"), and the store passes a constant.
  It now resolves constants, and checks every declared Remote method
  against a client descriptor, not just every namespace.
- inspectStore parses through the whitelist instead of a cast, so a field
  the host did not mean to send (a token) is dropped. An ok:false
  envelope reports the remote's own message.
- The credential reader's legacy-row fallback was uncorrelated: one
  flagged opencode-go row hid every legacy opencode row, and the Zen key
  disappeared. The fallback is now per integration.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

* docs: describe the tree that exists, and the guarantees the suite now keeps

- AGENTS.md's repo map described nine modules this build does not have
  (the Console login, token store, org cache and sign-in row). They are
  replaced with one line saying sign-in is v2 work. The go-limits
  duplication note is gone, because attachAllowance calls allowanceFor.
  adapter-replacement.ts is marked as not wired. The client-bundle line
  names the boundary test.
- CONTRIBUTING.md documents the engine floor, test:shuffle and test:mock,
  the offline guard, the per-test state reset, where host-only fixtures
  live, and that a regression test must fail without its fix.
- The no-credential warning no longer tells the reader to "sign in
  through the settings card", which this build does not have. It points
  at readOpencodeStore instead, and a test asserts the advice.
- CLIENT_BUDGET is raised from 128 to 132 KB for the store descriptor
  and its parser (about 1.2 KB of source, not a dependency), following
  the gate's own instruction.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

* docs: add the improvement plan from the architecture review

docs/improvement-plan.md groups the review's findings by cause and orders
the fixes in nine phases after this one. Each phase lists its problems,
its steps, the test that has to fail first, its acceptance criteria and
any decision the owner needs to make. Findings are marked "measured"
(reproduced) or "traced" (read in the code). Among the measured ones:
the picker and dispatch derive reasoning levels differently for 9 of 95
catalog models.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

* docs(plan): the approved refactor plan, with the host's own contracts

Replace docs/improvement-plan.md with the plan of record. It now holds:
- the owner's decisions (D1-D11);
- what the DeepSeek Harness host supports, with references into
  packages/llm, llm-pi-ai, session-controller, gateway and
  ui-settings-models;
- the defects this audit found that are not yet fixed;
- the target architecture and the phases, one PR each, with a status
  table that each PR updates.

The decisions that change the code:
- Settings offers the whole catalog, and the composer offers only the
  declared models.
- The plugin never writes the user's profile and follows the user's
  configuration at dispatch.
- The adapter patch exists to merge provider modes and nothing else. It
  is generalized to every mode, and its catalog fill is removed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

* docs(plan): route every mode through the user's own provider

Investigation of the host's source and of the pi catalog it pins (1.1.0)
changed the routing design.

- llm-pi-ai resolves `api` and `baseUrl` per model, and pi's installed
  `opencode` catalog already spans Anthropic, Google, Completions and
  Responses with per-protocol endpoints. One route serves every mode for
  every model pi knows.
- The plugin fills only three gaps: models newer than pi's catalog
  (including all Gemini on Go), endpoint drift, and profile-level
  `api`/`baseURL` collapsing the modes.
- The new design is one unregistered catalog adapter per plane, keyed by
  the user's route id, and the adapter patch (merge-modes) delegates only
  gap models to it. That deletes the hidden routes, the listing hide,
  the stream redirect, the child mount and the Gemini bridge
  registration.
- A real-host test harness (P-T1) comes before any of it.
- One upstream change (per-entry `api`/`baseURL` in llm-pi-ai, U1)
  would remove the routing layer entirely. It would also let Settings
  save a model pi doesn't know, which pi-ai's strict write validation
  rejects today.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

* docs(plan): the plugin owns the plane routes, measured on pi 1.1.0

pi-ai registers exactly the providers in its resolved config, and the
fiber keeps whatever the internal/config waterfall returns, while the
config editor writes the raw value. A prepended listener that resolves
pi-ai's config without opencode/opencode-go therefore keeps pi-ai from
claiming them, without touching the user's file, and the plugin registers
its own PiAiAdapter under the same ids through the public API.

Measured on the real host with pi 1.1.0 and the harness's patch: both
routes held by the plugin, pi-ai's other routes unchanged, every protocol
on both planes (Gemini on Go included) sent to its own endpoint, a
catalog-unknown model id saved, listed and dispatched, and both routes
handed back to pi-ai on release.

Replaces the upstream asks (no upstream changes) and the save-gate
design, which is kept only as the fallback.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

* docs(plan): fold the reviewed design and the build order into the plan

A nine-lens review of the routing design, with two independent skeptics
per finding (78 of 82 upheld), changed five things before any code moves:

- Settings reads pi-ai's resolved config, so a read-only wrapper on
  settings.describe puts the plane entries back into the displayed value;
  writes were never affected, the Models page edits by path.
- The route claim gets an explicit lifecycle: commits are told from
  validations by identity, reconcile runs on commit, mount and teardown
  are ordered, and pi-ai restarts at most once per transition.
- The plugin's adapter is built from public pieces only, with pi-ai's
  credential rule and no captured keys on the adapter path.
- The protocol comes from the catalog map, never from a guess at the
  model's name; metadata and levels are catalog first.
- Releases stay on 1.x, phases land as stacked pull requests, and a real
  host harness plus a binary e2e job pin the behaviour first.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

* fix: repair what shipped broken in 1.4.1

- The deprecated @viztor/dsh-opencode wrapper re-exported a `default` the
  barrel does not have, an ESM link error on every host load. It now
  forwards `export *` only, written by one shared helper that a test links
  against the built barrel in a real Node process.
- isZenProvider("opencode-google") answered false: "go" inside "google"
  read as the Go plane. It now uses the plane rule the catalog already has.
- enrichModelsResponse answered a 401, 403, 429 or 5xx with a 200 catalog.
  Those statuses now pass through; a 404 is still filled from the catalog,
  because the Go plane's inference base has no /models of its own.
- The warning colour named a token the host does not define, so warnings
  rendered as caption text. They now use --dsw-alias-state-warn-label.
- The settings spinner only animated while the meter was mounted; the
  settings surface now carries its own keyframes.
- The stream hook wrote debugFile with debug off. debug is the switch and
  debugFile only where it writes, the same gate the fetch patch uses.
- The purge row reported the host's own { ok: false } failure as "not a
  purge result" and cast the answer; it now shows the host's message and
  parses through the contract's whitelisting parser.
- The endpoint copy described modern as /inference/v1, which the plugin
  never sends. It now names the per-protocol inference family, and auto as
  its alias, which is what the code already did.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

* docs(plan): P1a is in review

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

* chore: remove dead code and tighten the repository's gates

- Delete adapter-replacement.ts and its test. Nothing imported it, and
  deleting another plugin's registry entry is not a host API; the
  engineering notes record why, and what the plugin does instead.
  A gate fails the build if src/ writes the llm registry map directly.
- Delete the stream hook's `prepared` handling: the host passes the
  prepared dispatch as its own argument and never reads options.prepared.
- Untrack .freebuff/ and ignore it.
- The settings partition test walks Config.dict, which is how debugFile
  went missing from it; a manifest test checks cordis.patch.yml against
  the schema and the field registry, and the manifest's phantom
  showUsagePrice, wrong marker text and incomplete lists are fixed.
- utils/retry.ts takes its timer and logger as parameters, and the utils
  purity gate now also rejects console and global timers, walking the
  folder recursively.
- tool-fallback reads free-tier ids from the request's own plane (Go iff
  opencode-go or opencode-go-*) and loses its unreachable Anthropic
  branch. A Zen-only free model is no longer treated as free on Go.
- test/support/src.ts is the one door tests use into src/, so later
  module moves touch one file; CONTRIBUTING.md states the rule.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

* feat: a smaller public API

The barrel is the host's contract and nothing more: apply, Config,
inject, name and the config types. Everything else is internal, and
test/public-api.test.ts pins the list, so widening it is a reviewed
change. The 1.x line keeps its version scheme: nobody consumes the
removed exports (D6).

- Every test reaches src through test/support/src.ts, importing each
  name from the module that defines it; a gate rejects a static src
  import anywhere else outside its named exemptions.
- package.json no longer exports ./src/*, and check.ts fails when an
  exports target is outside the published files.
- resolveRoutedKey is deleted: it was exported and never called.
- The internalRoutes setting is deleted; the per-protocol routes always
  mount until the plugin owns the plane routes. A row that still carries
  it validates and the key is ignored.
- @viztor/dsh-opencode is no longer published. The deprecation message
  already on npm stays as its final word, and check.ts asserts that the
  release workflow verifies exactly what publish-scoped.ts publishes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

* build: pin the host the plugin runs on

The test install now runs the host users run.

- pi is pinned to 1.1.0, the version the harness's own lockfile pins,
  with the harness's patch applied verbatim (patches/, hash recorded by
  pnpm). A pnpm override holds every copy in the tree to it, not just the
  direct devDependency.
- The @deepseek-ai packages move from 0.2.1-alpha.5 to 0.2.1-rc.2, the
  version a registry install of the published dsh resolves (cordis 4.0.5,
  loader 1.0.6, schemastery 3.18.5). The boot-tier packages the real-host
  harness needs next (dsh-llm, dsh-app-boot, dsh-config-editor,
  dsh-settings, cordis-plugin-loader) are added as devDependencies.
- test/support/host-versions.ts names those versions once.
  test/host-versions.test.ts checks that the pi the plugin loads at
  runtime is 1.1.0 and the same copy the tests import, that the patch is
  applied, and that the installed cluster matches.
- mount-protocols now checks that every catalog SDK maps to a protocol
  pi's exported api modules implement, and records as a characterization
  row that pi 1.1.0's opencode-go provider has no Google API.

No src/ behaviour changes: the rc.2 builds of llm-pi-ai, cordis and
schemastery are code-identical to alpha.5.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

* test: a harness that is the real host

Add an in-process harness that runs the plugin inside the real host,
so the routing work that follows can be measured against today's
behaviour rather than against fakes. Additive only: no src/ change.

test/support/host.ts gives two tiers. Tier A is a bare cordis Context
with LlmRuntime and llm-pi-ai mounted with { providers }, for the
config waterfall and pi-ai's credential paths. Tier B is dsh-app-boot's
boot() with a temporary HOME, a test bundle, a fake credentials service
and a launch-environment snapshot, for the config editor,
settings.describe and the volatile commit. Tier B's rows carry package
names through a stub module loader, and it fixes the apply order,
because the loader starts rows in parallel.

On top of it:

- contract tests for the waterfall, the profile and Config shapes,
  credentials, and the config editor (host-waterfall, host-shapes,
  host-credentials, host-editor-contracts), plus a tier A smoke test;
- a characterization of today's behaviour on pi 1.1.0, each row tagged
  where P-R1 is expected to flip it, with S0_TODAY shared by the
  binary e2e;
- the keyless 401-vs-404 matrix: a pure classifier, its unit test and
  a gated live run;
- a binary e2e gated on DSH_HOST_E2E=1 with its probe bundle, the
  e2e-host CI job (a pinned, patched cell on PRs that touch the host
  seams or vite.config.ts, and a floating cell nightly and on
  dispatch), and the test:e2e:host script.

The binary e2e now reports why a run failed: a timeout only when its
own deadline killed the host, the signal for a crash, and the probe's
fatal message whenever there is one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

* refactor: one way to patch a host method

Add src/host/patch-method.ts (patchMethod), now the only way the
plugin wraps a method on a host object, and move the four instance
patch sites in src/models-discovery.ts onto it: the three listings
hidden by hideResponsesRoute, decorateModelDiscovery,
decorateModelInfo and decoratePlaneAdapters.

On the real host the three llm-level disposers never restored:
ctx.llm is cordis's traceable proxy, every method read returns a new
shadow proxy, so the `===` identity checks were never true and each
reload stacked a wrapper over the last. The helper reads identity
from own descriptors instead. A dispose restores only while its
wrapper is still current (otherwise it becomes a pass-through),
deletes the own property when the original was inherited, and a
same-site reinstall wraps the unpatched method rather than the
earlier install, so either disposal order ends at the exact
original.

decoratePlaneAdapters becomes one patch per (adapter, method) that
composes the adapter's plane routes in registry order, which keeps
today's nesting on pi-ai's shared adapter. rewrapOnAdaptersUpdated is
built and tested but wired to no site yet.

test/reload.test.ts runs apply A, apply B, dispose A, dispose B (and
the reverse order) for every site on own and inherited originals, on
the real host (tier A) and through apply(); test/patch-method.test.ts
covers the helper's contract. The existing per-site pins in
test/models-discovery.test.ts stay unchanged; five new cases there
record the deliberate edge changes on hosts that refuse the patch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

* chore: refresh the generated catalog and limits tables

The vendor added kimi-k3.1-pro to the Go plane, so `catalog freshness`
went red on every branch of this stack. This takes the two generated
files exactly as the nightly refresh produced them (#111), with no
hand edits, and updates the counts that hard-code the catalog's size:
the Go shim test (28 -> 29), the plane table in redirect-planes.md
(Go served 28 -> 29, the no-SDK Go cell 17 -> 18), and the Go
completions list in both READMEs. The listed half of "29 served / 33
listed" is models.dev's count, inferred from the one added entry,
because models.dev cannot be reached from here.

It no-ops once main carries #111. P-T2 turns these counts into
invariants so a refresh stops needing this.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

* feat: the plugin owns the opencode and opencode-go routes

The plugin now serves the opencode and opencode-go routes itself,
through supported host paths only, and the hidden-route workaround is
gone.

A prepended internal/config listener hands pi-ai a copy of its config
without the two plane entries, so pi-ai keeps every other route. On a
commit the plugin reconciles: it builds one plane adapter, a subclass
of the host's PiAiAdapter, over pure profiles (utils/plane-profile,
plane-model, model-metadata and protocols) and registers only the
planes that are declared. A Settings candidate has the plane entries
it changes validated strictly. A read-only settings.describe graft
puts the committed plane entries back, so the host still sees them.

Deleted: responses-provider, responses-routes, google-bridge,
pi-provider and utils/google-replay; the listing hide and the
discovery decoration; the stream redirect; the D19 settings; and the
hidden ids in DEFAULT_PROVIDERS and cordis.patch.yml.

What users see: any saved id is accepted and served, and an id no
catalog knows dispatches as completions. The protocol comes from the
catalog and model info follows the user's own row first. A model
whose reasoning is only on or off offers Off/On. The default endpoint
is the per-protocol inference family. Keys resolve by pi-ai's own
rule, and a plane lists its declared ids even when no key resolves.

When several pi-ai rows declare planes, the row the plugin serves
stays the same while it is live and still declares one, so no row is
left stripped with nobody serving it. If the host's pi-ai module
cannot be read, the plugin steps aside even before any plane is
declared, so a later Settings save goes to pi-ai's own check.

The characterization rows this changes are flipped to new baselines;
every other row stays green unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

* test: measure which endpoint families the gateway serves

A measurement phase: the default endpoint family stays `modern`, so no
plugin behaviour changes and no characterization row flips.

- Name the default in one constant, AUTO_ENDPOINT_FAMILY in
  src/utils/plane-model.ts, which is what `auto` resolves to, and pin
  it in test/endpoint-family.test.ts.
- Extend the keyless matrix to three URL shapes (legacy, root, modern)
  over four protocols and two planes, add LEGACY_DISPATCH, and add one
  keyless free-model probe with its own pure classifier.
- Add minimal keyed probes (pure test/support/keyed-probes.ts and its
  e2e): one tiny request per plane, protocol and shape, sent with the
  plane's own key and the plugin's identity from one shared constant,
  PROBE_IDENTITY. Only the cells the default sends to are asserted;
  the rest are recorded.
- Gate the live job by tier with OPENCODE_E2E_KEYED (none, probes,
  full; unset means full). CI arms `probes` on a pull request that
  touches what the probes send, `full` nightly and on dispatch, and
  passes the secrets only when the tier is not `none`.
- Write a dated endpoint-family record from both live suites to the
  run summary and an `endpoint-families` artifact, and add §13 to
  docs/redirect-planes.md with every cell pending the first CI run.

The live part runs in CI only; no live result is invented here. A
failing keyed modern cell in CI is the evidence for a later `fix:`
that flips the default.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

* feat: Settings offers the catalog's models pi does not list

For the opencode and opencode-go routes, the Settings model list now
shows pi-ai's own rows first, in pi's order, and then every model the
plane's catalog can serve that pi does not list and the catalog has
not retired. Ids that pi's registry has retired are dropped, a field a
row lacks is filled from the catalog, and every row is cut down to the
five fields the host's discovery contract accepts, with absent values
omitted rather than set to undefined.

While pi-ai restarts and its discovery is withdrawn (NO_DISCOVERY),
the list answers the catalog rows alone instead of the error.

This rewrites the existing discoverModels wrapper rather than adding a
second one. Other routes keep today's enrich-only merge, because pi-ai
serves them and cannot serve an id it does not list. The plugin's own
model-discovery namespaces, which Settings never asked, are deleted
with their lifecycle tests, and RouteClaim gains a claiming() seam.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

* chore: refresh the generated catalog and limits tables

The vendor renamed the Go plane's one Gemini model (dropping its
contributor suffix, halving its price and publishing an allowance for
it where the limits table had none) and added a free model with no
SDK, so `catalog freshness` went red on every branch of this stack.
This takes the two generated files exactly as the nightly refresh
produced them (#111), with no hand edits, and follows them:

- the Go Gemini fixtures in six test files (attachment-services,
  catalog, go-google, go-google-prepare, models-discovery,
  responses-routes) now name the renamed id, since each means "the
  Go Gemini model the catalog serves";
- the usage panel test built its unknown allowance from the table's
  one unpriced row, which the vendor has now priced, so it builds the
  `Number.NaN` allowance the generator emits itself;
- the Go shim count in catalog.test.ts (29 -> 30);
- redirect-planes.md: Go 29 -> 30 served, the no-SDK Go cell
  18 -> 19, and "0 of 29" -> "0 of 30" in the prose;
- both READMEs: the renamed Gemini id, and the new model in the Go
  completions list, since it declares no SDK.

The listed half of "30 served / 34 listed" is models.dev's count,
inferred from one rename (no change) and one addition (+1), because
models.dev cannot be reached from here. The dated S0 measurement row
in improvement-plan.md keeps the id it was measured with.

It no-ops once main carries #111.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

* test: skip free models the gateway has withdrawn in the live suite

The live routing suite picks its free-tier subjects from the catalog by
asking the gateway, but it counted "400 Upstream request failed: Model
is unavailable" as an observable answer. The upstream withdrew
nemotron-3-ultra-free while the catalog still lists it, so the picker
chose it and two cases asserted on a model the gateway no longer
serves; the reachability case also failed on a 429 throttle.

A withdrawn model (that 400, or a 410) is now skipped like a throttled
one. The free-tier routing case only accepts a candidate whose answer
is the free-tier gate it asserts, so an unmetered model cannot become
its subject, and the reachability walk moves past gated, withdrawn and
throttled candidates before it reports.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

* fix: address the second review of P-T1b

The review ran three rounds through the correctness, rules and matrix
lenses. What it found, and what changed:

- The binary e2e's beforeAll budget was smaller than the blocking
  subprocess deadlines it serializes. Each plugin install now has its
  own deadline, the hook's budget is derived from the installs and the
  boot, and afterAll stops a host a failed hook left running.
- providerWithApiKey claimed to be a line-for-line restatement but
  skipped upstream's credentialRef(ref) check. It now throws the same
  TypeError for a ref outside the grammar (a host-editor-contracts
  case pins it), and its comment names the deliberate differences.
- The pinned e2e-host cell let cordis and the loader float at patch
  level. The overrides now force both, the cache key includes them,
  and the job asserts them against HOST_VERSIONS.
- The pi-ai-first characterization asserted every facet under one
  KNOWN_DEFECT title. Each apply order now generates one test per row
  and facet, titled with that order's own tags.
- CONTRIBUTING said the contract tests pin F11-F20, and the plan said
  P-T1 pins every fact. Both now give the same accounting: which facts
  are pinned by number, which P-R1 exercises first, and why F9 and F17
  are not pinned.
- CONTRIBUTING put the e2e's dummy keys in the profile's credentials
  file; they are in the temporary home's credentials document.
- F5 had no real-host pin: host-shapes now pins that a route-level api
  and baseURL collapse pi's protocols onto one. F6 is recorded as first
  exercised by P-R1.
- The credential-mapping row pinned only the api-key branch of
  toPiCredential. host-credentials now covers all three branches, and
  the env branch is asserted through pi's Models.getAuth, so the case
  fails without it.
- The plan's reason for restating hasProviderApiKey was inaccurate; it
  now says no installed copy reaches the function.
- The new credential cases cited toPiCredential at the wrong lines;
  they now cite auth.ts:74-84.
- F4's listing and dispatch halves were pinned only by KNOWN_DEFECT
  rows P-R1 flips. A plugin-less tier-A case in host-shapes now pins
  that an id pi cannot place is not listed and that a turn on it is
  INVALID_CONFIG with no request; the save and deferred-read halves
  are tagged F4 too.
- The one facet that depends on the apply order lost its tag when the
  pi-ai-first block was split. The fixture case now checks only
  structure (the facets whose value moves are the facets whose tag
  moves), and the exact pin is its own KNOWN_DEFECT case that boots no
  host. CONTRIBUTING and the plan say P-R1 drops todayRowsFor's
  override and turns that case into a BASELINE check in the same diff.

Nothing under src/ changed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

* test: follow the vendor catalog refresh

The vendor renamed the Go plane's Gemini model from
gemini-4-argon-contributor to gemini-4-argon. The tier-B fixtures
(S0_PROFILE, S0_TODAY, CHARACTERIZED_PROFILE through S0, and
todayRowsFor's pi-ai-first override) declared the old id as the Go gap
id: one the plugin's catalog describes and pi 1.1.0 cannot place. After
the refresh the catalog no longer described it. They now name the
renamed id, which still plays that role because pi 1.1.0's opencode-go
table has no gemini-4-argon either. The host-shapes and editor-contract
cases follow the same fixture. The pi 1.3.0 note in mount-protocols
names that pi's own row, so it stays as it is.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

* fix: address the second review of P-H

What the review found, and what changed:

- rewrapOnAdaptersUpdated reinstalls its site inside the plugin's own
  teardown and on every foreign registry change. Its JSDoc in
  src/host/patch-method.ts now says so, counts the reinstalls a release
  causes, says a caller disposes it before releasing its adapters, and
  notes that a throw from a reinstall is logged by the host.
- The plan of record said all three P-H edge changes are unreachable on
  the real host for one reason; the third (the live-reload listing) is
  unreachable for a different one. docs/improvement-plan.md now gives
  each its reason.
- The tier-A reload cases for the listing and discovery sites proved the
  row only structurally. test/reload.test.ts now gives every site a
  tier-A probe whose decorated answer must differ from the host's own,
  so "survivor still decorates" and "last dispose restores" can fail.
- "patchMethod is the only way the plugin wraps a host method" was
  asserted in docs and enforced by nothing. test/architecture.test.ts
  now has a textual tripwire over everything apply() reaches, with a
  self-check of planted violations, and the apply()-twice reload case
  requires a patch record on every own function apply() leaves on the
  stand-in.
- The tripwire missed several write forms (Object.assign,
  Object.defineProperty/defineProperties, Reflect.setPrototypeOf,
  Object.setPrototypeOf, and an Object.* target reached through a
  property path such as registration?.adapter or this.llm). The
  patterns now catch them, and the self-check plants each one (17
  planted, up from 13). Nothing new in src/ matches.
- The docs said "any Reflect.* write" where the pattern matches a fixed
  set. The test docstring, AGENTS.md and the plan's P-H section now
  name the four Reflect and four Object methods matched, and why a
  prototype swap is listed.

No runtime behaviour changed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

* test: follow the vendor catalog refresh

The refreshed Go catalog serves its Gemini model as gemini-4-argon, so the
plane-dispatch mock, the plane-model cases and the binary e2e's probe
expectations name the new id; the Go Google base they assert is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

* fix: address the second review of P-R1

The review ran three rounds (found 20, 5 and 8; upheld 19, 4 and 8).
What it found and what changed:

- A completions model whose effort list names none lost its Off.
  utils/model-metadata.ts now offers Off with none as its wire value
  on completions, as pi 1.1.0's own entries do; Responses unchanged.
  Pinned in model-metadata, plane-model and a new mock-gateway case.
- A Go entry with no apiKeyEnv authenticates with OPENCODE_API_KEY
  (pi's lookup, D20), and nothing said so. README "Changed in 1.x" and
  step 3 now name it; host-planes.test.ts pins both cases.
- A Settings save of a plane was refused as "cannot be validated yet"
  while the host modules were unloaded. host/route-claim.ts now judges
  it with the last modules that loaded; route-claim.test.ts pins it.
- Fiber.update(config) with noSave false would persist the stripped
  config and drop both planes. Recorded as a constraint in
  route-claim.ts and the plan; no caller does it today.
- Stale text: cordis-context.ts comments (stream redirect, isolate,
  root inject), deep-dive.md (pi's Go lookup, who answers Fetch),
  CONTRIBUTING (field count, the host-modules floor), and historical
  banners on opencode-parity-plan.md and upstream-findings.md.
- The mock body-shape row asserted only body.model. It now checks each
  protocol's required fields and that the gateway answered 200, or
  the vendor's 404 for the unknown id.
- Route-set rows: removing the last plane now pins replace([]) on the
  live handle, and "booting with none" waits for the plugin's row to
  be active instead of sleeping 50 ms.
- The live protocol-routing suite now sends paid models through the
  plane adapter on a tier-A host, not only raw probes.
- README's credential-isolation claim was false: the Go meter can send
  the Zen key. README.md, README.zh-CN.md, deep-dive.md,
  credentials.md and redirect-planes.md now give the real order and
  the 403 remedies; readme-parity.test.ts keeps both READMEs in step.
- The keySource hint described Automatic backwards. settings-copy.ts
  (en and zh) now gives literal key, captured key, then reference.
- The live adapter blocks dropped a protocol whose model was
  withdrawn. They now walk up to three candidates per protocol
  (support/live-walk.ts), pass a "model is unavailable" turn only when
  a raw probe confirms it (support/live-probe.ts), and skip naming a
  protocol no candidate reached.
- A mock comment claimed a body passed shape checks the mock lacks.

Declined: a toggle beside an effort list still offers no Off, which
moves baseline facets and needs the owner's decision; the Models
store's join stays a documented restatement rather than importing a
client UI package; the PR description is edited outside the tree.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

* ci: gate freshness only on a pull request's own generator changes

The vendor moves its model catalog, its Go limits page and its CLI
version almost daily. Each move turned every open pull request red,
although none of them caused it: vendor drift is not a regression of
the change under test.

A pull request now fails a freshness check only when its diff changes
that check's generator code: the script, every local module it loads,
transitively, and the pinned version file for the CLI check.
scripts/freshness-scope.ts derives those inputs from the sources and
makes the call, so a new import moves them without a list to edit.
Any other pull request reports drift as a warning, as a push to main
already did. The offline check that every copy of the CLI version
agrees with the pin still gates every pull request, since only the
repository can break it. The gates fail closed: a lost scope answer
gates rather than skips.

The nightly run still regenerates the two tables and proposes the
refresh as its own pull request, and warns on the CLI version.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

* chore: refresh the generated catalog after Zen retired deepseek-v4-flash

The vendor retired `deepseek-v4-flash` from the Zen plane (it stays on
Go). This pull request changes the catalog generator's own code, so
under the freshness scoping it carries the refresh itself. This takes
`src/catalog-data.ts` exactly as the nightly refresh produced it (#111),
with no hand edits; `src/go-limits-data.ts` did not change. It then
updates the counts that still hard-code the catalog's size:

- the Zen shim count in catalog.test.ts (82 -> 81);
- redirect-planes.md: Zen served 82 -> 81, the no-SDK Zen cell 25 -> 24,
  and "0 of 82" -> "0 of 81" in the prose;
- protocol-routing-and-merge.md: the per-protocol shares now read "of 81",
  with completions at 24;
- both READMEs: the no-SDK row is 24 and the active total is 81. The
  Chinese table had read 27 before this change, and now matches the
  English one.

"127 listed" is unchanged, because the vendor retires a model by marking
it `status: "deprecated"`, which keeps it listed. This could not be
confirmed against models.dev, which cannot be reached from here.

It no-ops once main carries #111.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

* test: give the freshness walk's dynamic import a script-local target

The walk's fixture held the text `await import("../src/lazy.ts")`. The
test tree's own architecture gate, which starts on the next branch of
this stack, reads a dynamic `src` import anywhere in a test file's text,
including inside a string. It counted that fixture line as a test
bypassing `test/support/src.ts`, so every branch above this one failed
the gate once this pull request was merged into it.

The case is about the dynamic form, not about `src`, so the import now
names `./lazy.ts` beside the script. The walk resolves it from the
importing file's own directory, which is the same rule, and the expected
inputs follow it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

* fix: address the second review of P-R2

What the review found, and what changed:

- The keyed flip rule read a default cell's retired-model answer as a
  retired model without checking the same model on the other shapes.
  planeOutcome now reads each default cell beside the legacy and root
  cells of the same plane and protocol.
- A refused key counted as flip evidence even when every shape refused
  it. It is now "check the CI secret" when a sibling refused the key
  and none accepted it, and "nothing else measured the key" only when
  no sibling refused it; a path-gone, rate-limited or busy sibling no
  longer blocks the CI-secret reason.
- Breaks no flip could fix (accepted on the root shape only, or Go's
  Google, which no flip moves) were called flip evidence, and the
  "protocol" and "gone" reasons named causes nothing measured. A
  wrong-protocol or path-gone cell is now decided by the cell that
  endpoint: legacy dispatches to: "refused" (a flip would not serve
  it) when that cell refused readably, and always for Go's Google;
  "unmeasured" (re-run) when it measured nothing. Neither is flip
  evidence.
- An account answer counted as the flip target accepting a protocol or
  a model, though the account's gate answers before either is read. It
  now counts only beside a refused key or a gone path, and a retired
  model next to an account answer no longer fails the plane.
- The keyless free probe failed on a busy upstream (503/529) that the
  keyed classifier treats as routed. It now moves on past a busy
  candidate, and its dated record no longer pairs a derived
  upstream-busy with the last retired candidate's model and status.
- A busy upstream followed by no answer failed the keyed plane, against
  the verdict definitions in redirect-planes.md section 13. Section 13
  now says what the code does (no answer on the last attempt is
  transport-error unless an earlier attempt was a 429), and a test pins
  that it fails as unreadable, not as flip evidence.
- Comments and docs said P-R2 had already measured the families while
  section 13 is pending its first CI run; said models.opencode.ai
  declares the root shape, which the generated catalog contradicts;
  said legacy cells are asserted nowhere live; and said a keyed
  auth-refused means the plugin's own turns would be refused, though a
  default Go turn sends a different key. All corrected.
- CONTRIBUTING's key and mirror bullets now cover the keyed probes.
- The plan no longer claims P-R2 opens with a green pin, gives the
  conditional flip a phase, and says the keyed probes decide a plane
  from the default cells read beside their siblings, not alone, and
  that an unreadable answer is never flip evidence.
- Tests now cover the tier wiring of the four keyed live suites, no
  probe following a redirect, the push-to-none arm and the publish and
  upload steps of the CI wiring, the free probe moving on past a busy
  upstream, and the record being rewritten after every cell.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

* chore: refresh the generated catalog after Zen retired deepseek-v4-flash

Ported from the stack's base (d13cbe2 on #105) so this pull request's
own `catalog freshness` goes green now, rather than waiting for the base
to be merged up. It no-ops once the merge from P-R1 brings the same
change.

It takes `src/catalog-data.ts` exactly as the nightly refresh produced
it (#111). The counts that restate the catalog's size follow it, on this
branch's own wording of those passages:

- the Zen shim count in catalog.test.ts (82 -> 81);
- redirect-planes.md: Zen served 82 -> 81, the no-SDK Zen cell 25 -> 24,
  and "0 of 82" -> "0 of 81";
- protocol-routing-and-merge.md: the per-protocol shares now read "of 81",
  with completions at 24;
- both READMEs: the no-SDK row is 24, and the active total is 81.

"127 listed" is unchanged, because a model the vendor retires stays
listed as `status: "deprecated"`.

(cherry picked from commit d13cbe2410642254df0d0cf1392a5083386a0c6a)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

* test: follow the vendor catalog refresh

The merged catalog renames the Go Gemini model from
gemini-4-argon-contributor to gemini-4-argon; the Settings fetch
assertions (tier B and the binary e2e) now name the refreshed id.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

* fix: send Google's thinking level to Gemini models the catalog gives levels

The live suite caught the Go plane's Gemini model refusing a thinking
budget: the gateway answered that the model takes a thinking level and
not a budget. Pi chooses a thinking level over a thinking budget only
by matching the model id against its own pattern, so a model the
pattern does not name was sent a budget it cannot accept.

The plugin now makes that decision from the catalog, as D16 asks: a
model whose catalog entry offers discrete effort levels on the Google
protocol gets a thinking level. It builds that request through pi's
public exports, on both the enabled and the disabled path, and hands
every other model back to pi's own path unchanged.

Tests pin it: the pure rule over the catalog, a differential check
that the wrapper builds the same request as pi's own streamSimple for
an id pi already recognises, the lazy loading of the pi pieces, and a
mock-gateway case that the turn completes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

* docs: correct the Go shape count, the README date and the install range

Three documentation defects the synced base carries into P-R1. None was
caused by the merge (each is identical on the merge-base and on both
parents), and each wants the same change on the lowest branch (#105) and,
for the README date, on P-R2's cherry-pick of d13cbe2.

- PRE-1, docs/redirect-planes.md: "The Go plane serves 12 models on
  shapes other than completions" restores the count its own table gives
  (5 openai + 5 anthropic + 1 google = 11, the Go shim's 30 less its 19
  no-SDK models). The 12 was right when written (6 + 6) and a later
  refresh moved the cells but not the sentence. test/catalog.test.ts now
  holds the sentence to the table's non-completions Go sum, so the next
  refresh cannot strand it again.
- PRE-2, README.md and README.zh-CN.md: the 81-model count is dated
  2026-10-27, the refresh that produced it (d13cbe2); on 2026-10-23 the
  count was 82, so the date no longer matched the number.
- PRE-3, README.md and README.zh-CN.md: both install snippets name
  "^1.4.1" (package.json's version), where they had drifted to ^0.14.0 and
  ^0.12.0, neither of which can resolve to a 1.x release.
  test/readme-parity.test.ts pins the two snippets to one range on
  package.json's major.

Both new assertions were checked against the stale values: each fails on
them and passes on the corrected text.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

* docs: date the refreshed model count and correct three stale figures

The README's 81-model count kept the date of the 82-model count it
replaced (2026-10-23); it now carries the date of the refresh that
produced it (2026-10-27). Also corrected, all present-tense statements
the catalog had moved past:

- both README install snippets named ^0.14.0 and ^0.12.0, ranges that
  cannot resolve to any 1.x release; both now name package.json's
  ^1.4.1;
- redirect-planes.md said the Go plane serves 12 models on shapes other
  than completions; its own table gives 11 (30 less 19 with no SDK);
- engineering-notes.md tied a general rule to "the 82" models the shim
  ships; it now says "the models it ships", which a refresh cannot
  strand.

P-R1 carries the same corrections (dec00c8) with tests that hold the
README range and the Go sentence to their sources.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

* fix: address the second review of P3

The second review of P3 ran three rounds across the correctness, rules
and test-matrix lenses, and upheld 11, 4 and 4 findings. What it found,
and what changed:

- On a served plane, Settings' fetch offered pi's stale capacities, and
  adopting a row saved them over the catalog's, contrary to D17 and the
  README. Every row on a served plane is now read catalog-first, with
  pi filling only what the catalog lacks; a route the plugin does not
  serve stays pi-first (src/models-discovery.ts).
- enrichModels=false turns off the D3 complement and the NO_DISCOVERY
  fallback, but the README and the P3 spec stated both unconditionally.
  The README, the AGENTS.md map, the deep-dive and the plan's P3 landed
  note now name the switch.
- Retired pi rows are dropped only once a live catalog read has landed,
  but the docs said always. They now say so, and that offline pi's list
  passes through.
- The enrichModels copy said the picker shows declared models only,
  which contradicts D3. It now says the picker shows the declared
  models, or the plane's catalog when the entry declares none
  (src/settings-copy.ts and both READMEs).
- The plan listed the defect P3 fixes as not yet fixed, with stale line
  references. It is struck through and marked fixed by P3.
- F21 was a host fact missing from both lists of pinned facts, and no
  test named it. Both lists (the plan and CONTRIBUTING.md) now name it,
  and so does the pi-ai restart test.
- The gap-row save test never showed the id was missing from the
  composer before the save. It now asserts that first.
- The README's "Changed in 1.x" Fetch entry described the complement
  unconditionally and left out the catalog-first values. It now states
  both.
- README troubleshooting gave no cause for a retired pi id staying in
  Fetch while offline. It now names an unread catalog. Its fix says the
  first fetch once models.opencode.ai is reachable starts the refresh
  and still returns the old list, because the refresh runs behind the
  read and nothing runs it on a timer, so fetch again a moment later
  (README.md and README.zh-CN.md).

Carried to landing, because only a push can close it: the binary e2e
Settings fetch row has not run in the floating e2e-host cell on this
head, and the PR body lacks the plan's records and describes the merge
as it was before the catalog-first change. That needs a dispatch of
ci.yml on the pushed head and a rewrite of the PR body.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

* docs: close the audit's remaining findings on P-R1

Each finding the third audit round left open, re-verified on this head:

- README.zh-CN.md still says Zen keys never query the Go quota endpoint:
  already fixed (0cceb81, dec00c8). The feature bullet, step 3, the
  EntitlementError row and the credential bullet match README.md.
- The withdrawn-model fix drops a protocol instead of trying its next
  model: already fixed (0cceb81). walkPerProtocol walks up to three
  candidates per protocol and names an unreached protocol in a skip;
  test/live-walk.test.ts pins both.
- N1's claim stands in docs/deep-dive.md and the keySource hint
  describes auto backwards: already fixed (0cceb81) in deep-dive.md and
  both hint strings. The same claim survived in a source comment,
  KEY_SOURCE_ORDER's "tier safety" note in src/go-discovery.ts; this
  commit corrects it: a key is filed by the plane its request went to,
  so a Go entry without apiKeyEnv files the Zen key as a Go key.
- PR #113 lists counts, not the flipped rows and the four records:
  fixed in the PR body.
- The R2 fix passes the live rows without performing a protocol, the
  skip decided by message text: already fixed (0cceb81). A "gone"
  verdict needs a raw probe of the same id to confirm it, and a
  protocol no candidate reached skips by name.
- README.zh-CN.md lacks the N1 fix, the Go apiKeyEnv step and the
  "Changed in 1.x" bullet: already fixed (0cceb81).
- PR #113 omits the D20 Go behaviour change: fixed in the PR body.
- docs/deep-dive.md says resolveGoApiKey excludes Zen keys and a
  Zen-only setup hides the ring: already fixed (0cceb81).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

* chore: follow the vendor's retirement of nemotron-3-ultra-free

The vendor retired `nemotron-3-ultra-free` from the Zen plane, so
`catalog freshness` gated this pull request (it changes the generator's
inputs). `src/catalog-data.ts` is taken exactly as main regenerated it
(#132), which today's registry still matches apart from that one row,
with no hand edits. What restates the catalog follows it:

- Zen 81 -> 80 served, no-SDK 24 -> 23, free 9 -> 8: catalog.test.ts,
  redirect-planes.md, protocol-routing-and-merge.md and both READMEs
  ("127 listed" stays; a retired model stays listed as deprecated);
- the README model lists, in both languages, now name ids the catalog
  ships: the Zen and Go lists had kept seven ids the vendor has since
  retired. The lists are main's; this branch's own prose around them
  (how the Go Google base is served) is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

* test: walk past a model the gateway answers ModelDeprecated for

The live gateway job failed on `gemini-3-flash`: the registry still lists
it, and the gateway now answers it with an error of type
`ModelDeprecated`. The adapter walk moves past a withdrawn model only
when both its pre-filter (`isModelGoneMessage`) and the raw probe that
confirms it (`modelGone`) recognise the answer, and neither knew this
shape, so a vendor retirement read as the plugin's refusal.

Both now recognise `ModelDeprecated` (by type, at any status, as main's
`retiredAtTheGateway` does). `test/live-probe.test.ts` is new and holds
`modelGone` to each shape; `keyless-matrix.test.ts` gains the message
row. Removing either rule fails its test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

* test: install the plane-lookup world before each test

The suite's setup file resets the live catalog after every test, so main's once-per-file install was gone by the second case and three cases failed. Installing before each one keeps the file independent of run order.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

* test(scripts): move the generated-table skip into the CLI-version rules

The copy scan's file filter lived in the top-level script, where nothing could exercise it. It now sits in scripts/vendor-version.ts beside the other rules, with cases of its own, and a test keeps its list equal to freshness-scope's. Because the script imports that module, it is now an input of the cli:version check, and the strict input list says so.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

* test(e2e): read an upstream's 400 "Model is unavailable" as a retirement

The live suite's local modelGone rule gave way to main's verdict module in the merge. Its one shape main lacked, a 400 whose body says the upstream withdrew the model, now lives in retiredAtTheGateway with a unit case, so the free-tier walk and the subject pickers skip it instead of failing on it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

* docs(testing): describe the suite this branch keeps

Main's test-system page predates the offline guard, the per-test reset and the engine floor: it placed the store fixture in the shared helpers and called the plane-lookup world per file. It now says where the store fixture lives and why, that a live-catalog world is installed per test, and what the CLI-version rules cover.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

* test(e2e): keep the free-tier case's narrowing in the subject picker

Taking main's usableAsSubject rule in the merge dropped the picker's accept argument, so the free-tier case could pick an unmetered model and assert the gate on a 200. The case's own narrowing now applies after the shared rule.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

* chore(vendor): follow opencode 1.19.5

The vendor published `opencode/1.19.5`, and `cli:version` gates this pull
request because its diff touches that check's inputs. Main made the same
bump in #132 after comparing every cited vendor source between the two
tags: the one that moved (`session/llm/request.ts`) only dropped an
unused type import, so no header the plugin emulates changed. This takes
the same bump in the 17 files that carry a copy here; `cli:version
--copies` reports all 58 copies and mentions at 1.19.5.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

* docs(plan): restack the plan onto 1.8.0 and the alpha host

Main moved to 1.8.0 and onto the harness's alpha track while the stack was open, and landed several things the plan had scheduled. The plan of record now says so: D14' and D22' replace the pi 1.1.0 and rc.2 pins, D17 restates pi-ai's new defaults, D23 records the freshness scoping, D24 and D25 adopt main's debug gate and tool-fallback plane, and D9, D16 and D19 stand with notes. F10 is gone on pi 1.3. A new phase P-R3 (honest refusals) follows P-T3, each phase has its restack adjustment, the status table shows the restack, and the two community plugins' lessons are a ranked table with what not to adopt.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

* build: raise the client bundle ceiling to 136 KB

Main and this branch each raised the tripwire from 128 to 132 KB, each measured without the other's surface. Merged, the client is 135604 bytes, about 0.4 KB over; the owner's standing rule for growth like this is to raise the ceiling, not cut content.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

* docs: complete the restack of P0

Close the restack review's findings on #105. Every change is prose, a
comment or a log line; no behaviour moves, so no test is added.

- ci.yml: the pnpm/action-setup comment no longer quotes a pnpm version;
  it names the `packageManager` field, which main's bump had made stale.
  release.yml carries the same stale sentence on main and is left to a
  main-side follow-up.
- scripts/freshness-scope.ts: the GENERATED_OUTPUTS and dataInputs docs
  name scripts/vendor-version.ts (GENERATED_TABLES, PINNED_VERSION), where
  the copy and the regular expression now live.
- test/e2e/protocol-routing.e2e.ts: the free-tier walk's report names the
  causes that can reach it (gated, withdrawn, refused for the account,
  never answered); a throttled walk skips, so "throttled" is gone.
- README pair: the Chinese file now says what the English says about
  403 FreeTierError (the headers are not its cause and the patch does
  not open the free tier), carries the Auto Review route clause and the
  two paragraphs on the API-key-only build and the host restart; the
  English yaml fence drops the nonexistent hiddenModels key.
- README pair: the compatibility section restates the typert peer range
  package.json declares and what its two clauses admit.
- docs/improvement-plan.md: F2, G1 and G2 carry the pi 1.1.0 restack
  note; the sources line names both harness tags; the P1a-P2 test matrix
  says the wrapper is retired; P0's status row counts ten defect fixes.
- docs/protocol-routing-and-merge.md, docs/redirect-planes.md: the count
  tables are dated 2026-10-31 (the shipped shim), apart from the
  documents' measurement stamps; both READMEs name
  `catalog:shim -- --write` as the regeneration command.
- AGENTS.md: the What-moved table names src/utils/retry.ts; the coverage
  line points at vite.config.ts instead of quoting stale thresholds.
- CONTRIBUTING.md: the restart check names CARD_FIELDS rather than a
  field count.
- docs/testing.md: rule 1 counts an upstream's 400 "Model is unavailable"
  as a retirement, and rule 2 says the body, not the status, makes it one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

* docs: close the restack review of P0

Close the second review round's findings on the restacked P0. Docs only;
no generated file is touched.

- both-kept-2-R2-1: docs/deep-dive.md no longer names the retired ids
  the shim was said to suppress (main's regenerated shim serves one of
  them on Zen). It states the rule instead: a retired model is a row the
  registry marks deprecated, the generator drops it per plane, and ids
  are not restated; the enrichment item says "registry-deprecated rows".
- rules-docs-2-R2-1: the PR body is replaced at push time from the
  updated draft, with the verification rows re-measured on this head
  and a restack section (what merged from main, what this PR dropped,
  the main-side follow-ups). Nothing is pushed here.
- rules-docs-2-R2-2: the plan's defects table is retitled to where each
  defect now stands; the four rows main fixed are struck with the fix
  named, the purge row keeps only what is still open (a RemoteError
  object shows the generic message, P1a), and D19 records that the
  README's hiddenModels was removed in P0.
- rules-docs-2-R2-3: README.zh-CN.md's merge rule follows the English
  and the code: a row is dropped only when the registry marks it
  deprecated, not when a per-provider list names it. One sentence, so
  the pairing gate's counts are unchanged.
- rules-docs-2-R2-4: CONTRIBUTING.md no longer contradicts the
  repository: warnings are reported and gate in P12 (as its own command
  list says); a manual release creates the GitHub Release and never
  touches CHANGELOG.md; the release step points at the "Verified on
  DeepSeek Harness" line in both READMEs; the e2e section lists all
  five files.
- rules-docs-2-R2-5: both READMEs drop the test total nothing pinned;
  engineering-notes.md's "every count" paragraph records it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

* docs: hold the meter's allowance figures to the generated table

The third review round of the P0 restack. Each outcome:

- The meter mock-ups in both READMEs and both quota-meter guides paired
  one model with another model's monthly allowance. They now name the
  model whose row the figures are, and the panel mock-up's rate line
  prints that model's catalog rates in the format the panel uses. A new
  catalog test reads every "<id> Go $a · Plus $b" figure in the four
  documents back against the generated allowance table; it failed on
  the old mock-ups and holds a known minimum of matches, so a regex that
  stops matching cannot pass as clean.
- engineering-notes: the "deeper half" measurement is scoped to the Go
  plane it was taken on. Both ids are Zen rows and neither has a row in
  the Go catalog, whic…
viztor added a commit that referenced this pull request Nov 13, 2026
* test: make the suite engine-independent and guard the client boundary

- Stop the run on a Node below engines.node, before collecting a file.
  Below the floor, failures came from the engine (node:sqlite,
  Intl.DurationFormat, ICU strings) and looked like code regressions.
  .node-version names the Node CI uses.
- Add a CI job that runs the unit and mock suites on Node 24, the floor
  engines.node promises but no job ran.
- The zh fallback duration unit is now 分钟, the platform's own narrow
  output, so the meter row reads the same with or without
  Intl.DurationFormat. The DurationFormat tests restore the engine as they
  found it, rather than leaving a stand-in installed or asserting the
  engine ships the constructor.
- Move the node:sqlite store fixture out of the helpers that the jsdom
  tests share, into test/support/opencode-store-fixture.ts.
- Add an architecture test that walks the client bundle's runtime import
  graph from settings-page.tsx. It forbids node built-ins, the cordis
  runtime, pi, and process.env. Checked by planting a violation.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

* fix(lifecycle): contain the internal-route mount and follow reloads in discovery

- A rejected internal-route mount ran from a `void` IIFE with no catch, so
  a Google bridge failure became an unhandled rejection, which Node's
  default mode answers by exiting the process. mountInternalRoutes now
  owns the mount and its withdrawal, and reports a rejection.
- Three early exits in registerResponsesProvider (internalRoutes off,
  every route declared, no llm-pi-ai) answered `undefined` after the
  Gemini bridges had already registered. That left them with no
  disposer, and the next reload failed DUPLICATE_ADAPTER. They now hand
  back the bridges' disposer.
- A mount that failed to start, or that captured nothing, kept its fiber
  and every route it had registered. Both paths now release the routes
  and dispose the fiber.
- Model discovery was registered on every activation, and the host's
  duplicate refusal was swallowed, so the first activation's settings
  answered for the life of the process. It is now registered once per
  root and answers from the latest activation's settings. Each namespace
  registers on its own, so one refusal no longer skips the rest.
- Move three doc blocks back above the functions they describe.

Each fix has a regression test. The two mount tests fail without the fix.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

* fix: price, list and key requests on the plane they belong to; isolate the suite

Request path and meter:
- Price a turn on its route's plane. Go-first lookups priced every Zen
  turn on deepseek-v4-pro at 0.66/1.98 instead of 1.74/3.48.
- The listing and model-info patches now act only on the routes this
  plugin claims. Before, a direct anthropic or openai route lost rows the
  OpenCode routing called "unservable", and a refused foreign id was
  answered with OpenCode's catalog.
- A relay's Go /models is answered with the Go catalog. The enrichment
  re-checked the URL against the default gateway markers and fell back to
  Zen.
- The free-model check reads a Request-carried body. That is how DSH
  sends a turn, and every keyless free turn was reported as a missing
  credential. The body is now read once for both uses.
- The meter's session fallback is the session that most recently ran a
  turn, not the most recently created one.
- A captured key's tier comes from the observed URL, then the route, and
  never the key's prefix. OpenCode issues oc_sk_ keys for Go, so a
  working Go key was filed under Zen. An OpenAI sk- key on a claimed
  relay was filed under Go.

Suite isolation:
- The unit and mock suites refuse non-loopback requests. Each unit run
  sent 54 requests to models.opencode.ai through the catalog's background
  revalidation, so on a networked runner a result could depend on the
  vendor's live catalog.
- Captured keys, session spend and the live catalog are reset after every
  test (resetCatalogState is new). Shuffled runs failed catalog cases on
  seeds 42 and 1234. test:shuffle runs the suite in random order, and the
  Node 24 CI job uses it.

Each behaviour change has a test that fails without it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

* fix(store): mount the inspection remote, and read legacy keys per integration

- The card's store-inspection row called remote.opencodeStore.inspect, but
  no client contribution carried that namespace's descriptor. The row
  reported "unavailable" on a host that had the service. storeRemote now
  carries it, with a whitelist codec, and the client mounts it.
- The host/client parity test missed this because it matched only a
  string literal in super(ctx, "name"), and the store passes a constant.
  It now resolves constants, and checks every declared Remote method
  against a client descriptor, not just every namespace.
- inspectStore parses through the whitelist instead of a cast, so a field
  the host did not mean to send (a token) is dropped. An ok:false
  envelope reports the remote's own message.
- The credential reader's legacy-row fallback was uncorrelated: one
  flagged opencode-go row hid every legacy opencode row, and the Zen key
  disappeared. The fallback is now per integration.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

* docs: describe the tree that exists, and the guarantees the suite now keeps

- AGENTS.md's repo map described nine modules this build does not have
  (the Console login, token store, org cache and sign-in row). They are
  replaced with one line saying sign-in is v2 work. The go-limits
  duplication note is gone, because attachAllowance calls allowanceFor.
  adapter-replacement.ts is marked as not wired. The client-bundle line
  names the boundary test.
- CONTRIBUTING.md documents the engine floor, test:shuffle and test:mock,
  the offline guard, the per-test state reset, where host-only fixtures
  live, and that a regression test must fail without its fix.
- The no-credential warning no longer tells the reader to "sign in
  through the settings card", which this build does not have. It points
  at readOpencodeStore instead, and a test asserts the advice.
- CLIENT_BUDGET is raised from 128 to 132 KB for the store descriptor
  and its parser (about 1.2 KB of source, not a dependency), following
  the gate's own instruction.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

* docs: add the improvement plan from the architecture review

docs/improvement-plan.md groups the review's findings by cause and orders
the fixes in nine phases after this one. Each phase lists its problems,
its steps, the test that has to fail first, its acceptance criteria and
any decision the owner needs to make. Findings are marked "measured"
(reproduced) or "traced" (read in the code). Among the measured ones:
the picker and dispatch derive reasoning levels differently for 9 of 95
catalog models.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

* docs(plan): the approved refactor plan, with the host's own contracts

Replace docs/improvement-plan.md with the plan of record. It now holds:
- the owner's decisions (D1-D11);
- what the DeepSeek Harness host supports, with references into
  packages/llm, llm-pi-ai, session-controller, gateway and
  ui-settings-models;
- the defects this audit found that are not yet fixed;
- the target architecture and the phases, one PR each, with a status
  table that each PR updates.

The decisions that change the code:
- Settings offers the whole catalog, and the composer offers only the
  declared models.
- The plugin never writes the user's profile and follows the user's
  configuration at dispatch.
- The adapter patch exists to merge provider modes and nothing else. It
  is generalized to every mode, and its catalog fill is removed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

* docs(plan): route every mode through the user's own provider

Investigation of the host's source and of the pi catalog it pins (1.1.0)
changed the routing design.

- llm-pi-ai resolves `api` and `baseUrl` per model, and pi's installed
  `opencode` catalog already spans Anthropic, Google, Completions and
  Responses with per-protocol endpoints. One route serves every mode for
  every model pi knows.
- The plugin fills only three gaps: models newer than pi's catalog
  (including all Gemini on Go), endpoint drift, and profile-level
  `api`/`baseURL` collapsing the modes.
- The new design is one unregistered catalog adapter per plane, keyed by
  the user's route id, and the adapter patch (merge-modes) delegates only
  gap models to it. That deletes the hidden routes, the listing hide,
  the stream redirect, the child mount and the Gemini bridge
  registration.
- A real-host test harness (P-T1) comes before any of it.
- One upstream change (per-entry `api`/`baseURL` in llm-pi-ai, U1)
  would remove the routing layer entirely. It would also let Settings
  save a model pi doesn't know, which pi-ai's strict write validation
  rejects today.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

* docs(plan): the plugin owns the plane routes, measured on pi 1.1.0

pi-ai registers exactly the providers in its resolved config, and the
fiber keeps whatever the internal/config waterfall returns, while the
config editor writes the raw value. A prepended listener that resolves
pi-ai's config without opencode/opencode-go therefore keeps pi-ai from
claiming them, without touching the user's file, and the plugin registers
its own PiAiAdapter under the same ids through the public API.

Measured on the real host with pi 1.1.0 and the harness's patch: both
routes held by the plugin, pi-ai's other routes unchanged, every protocol
on both planes (Gemini on Go included) sent to its own endpoint, a
catalog-unknown model id saved, listed and dispatched, and both routes
handed back to pi-ai on release.

Replaces the upstream asks (no upstream changes) and the save-gate
design, which is kept only as the fallback.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

* docs(plan): fold the reviewed design and the build order into the plan

A nine-lens review of the routing design, with two independent skeptics
per finding (78 of 82 upheld), changed five things before any code moves:

- Settings reads pi-ai's resolved config, so a read-only wrapper on
  settings.describe puts the plane entries back into the displayed value;
  writes were never affected, the Models page edits by path.
- The route claim gets an explicit lifecycle: commits are told from
  validations by identity, reconcile runs on commit, mount and teardown
  are ordered, and pi-ai restarts at most once per transition.
- The plugin's adapter is built from public pieces only, with pi-ai's
  credential rule and no captured keys on the adapter path.
- The protocol comes from the catalog map, never from a guess at the
  model's name; metadata and levels are catalog first.
- Releases stay on 1.x, phases land as stacked pull requests, and a real
  host harness plus a binary e2e job pin the behaviour first.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

* fix: repair what shipped broken in 1.4.1

- The deprecated @viztor/dsh-opencode wrapper re-exported a `default` the
  barrel does not have, an ESM link error on every host load. It now
  forwards `export *` only, written by one shared helper that a test links
  against the built barrel in a real Node process.
- isZenProvider("opencode-google") answered false: "go" inside "google"
  read as the Go plane. It now uses the plane rule the catalog already has.
- enrichModelsResponse answered a 401, 403, 429 or 5xx with a 200 catalog.
  Those statuses now pass through; a 404 is still filled from the catalog,
  because the Go plane's inference base has no /models of its own.
- The warning colour named a token the host does not define, so warnings
  rendered as caption text. They now use --dsw-alias-state-warn-label.
- The settings spinner only animated while the meter was mounted; the
  settings surface now carries its own keyframes.
- The stream hook wrote debugFile with debug off. debug is the switch and
  debugFile only where it writes, the same gate the fetch patch uses.
- The purge row reported the host's own { ok: false } failure as "not a
  purge result" and cast the answer; it now shows the host's message and
  parses through the contract's whitelisting parser.
- The endpoint copy described modern as /inference/v1, which the plugin
  never sends. It now names the per-protocol inference family, and auto as
  its alias, which is what the code already did.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

* docs(plan): P1a is in review

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

* chore: remove dead code and tighten the repository's gates

- Delete adapter-replacement.ts and its test. Nothing imported it, and
  deleting another plugin's registry entry is not a host API; the
  engineering notes record why, and what the plugin does instead.
  A gate fails the build if src/ writes the llm registry map directly.
- Delete the stream hook's `prepared` handling: the host passes the
  prepared dispatch as its own argument and never reads options.prepared.
- Untrack .freebuff/ and ignore it.
- The settings partition test walks Config.dict, which is how debugFile
  went missing from it; a manifest test checks cordis.patch.yml against
  the schema and the field registry, and the manifest's phantom
  showUsagePrice, wrong marker text and incomplete lists are fixed.
- utils/retry.ts takes its timer and logger as parameters, and the utils
  purity gate now also rejects console and global timers, walking the
  folder recursively.
- tool-fallback reads free-tier ids from the request's own plane (Go iff
  opencode-go or opencode-go-*) and loses its unreachable Anthropic
  branch. A Zen-only free model is no longer treated as free on Go.
- test/support/src.ts is the one door tests use into src/, so later
  module moves touch one file; CONTRIBUTING.md states the rule.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

* feat: a smaller public API

The barrel is the host's contract and nothing more: apply, Config,
inject, name and the config types. Everything else is internal, and
test/public-api.test.ts pins the list, so widening it is a reviewed
change. The 1.x line keeps its version scheme: nobody consumes the
removed exports (D6).

- Every test reaches src through test/support/src.ts, importing each
  name from the module that defines it; a gate rejects a static src
  import anywhere else outside its named exemptions.
- package.json no longer exports ./src/*, and check.ts fails when an
  exports target is outside the published files.
- resolveRoutedKey is deleted: it was exported and never called.
- The internalRoutes setting is deleted; the per-protocol routes always
  mount until the plugin owns the plane routes. A row that still carries
  it validates and the key is ignored.
- @viztor/dsh-opencode is no longer published. The deprecation message
  already on npm stays as its final word, and check.ts asserts that the
  release workflow verifies exactly what publish-scoped.ts publishes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

* build: pin the host the plugin runs on

The test install now runs the host users run.

- pi is pinned to 1.1.0, the version the harness's own lockfile pins,
  with the harness's patch applied verbatim (patches/, hash recorded by
  pnpm). A pnpm override holds every copy in the tree to it, not just the
  direct devDependency.
- The @deepseek-ai packages move from 0.2.1-alpha.5 to 0.2.1-rc.2, the
  version a registry install of the published dsh resolves (cordis 4.0.5,
  loader 1.0.6, schemastery 3.18.5). The boot-tier packages the real-host
  harness needs next (dsh-llm, dsh-app-boot, dsh-config-editor,
  dsh-settings, cordis-plugin-loader) are added as devDependencies.
- test/support/host-versions.ts names those versions once.
  test/host-versions.test.ts checks that the pi the plugin loads at
  runtime is 1.1.0 and the same copy the tests import, that the patch is
  applied, and that the installed cluster matches.
- mount-protocols now checks that every catalog SDK maps to a protocol
  pi's exported api modules implement, and records as a characterization
  row that pi 1.1.0's opencode-go provider has no Google API.

No src/ behaviour changes: the rc.2 builds of llm-pi-ai, cordis and
schemastery are code-identical to alpha.5.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

* test: a harness that is the real host

Add an in-process harness that runs the plugin inside the real host,
so the routing work that follows can be measured against today's
behaviour rather than against fakes. Additive only: no src/ change.

test/support/host.ts gives two tiers. Tier A is a bare cordis Context
with LlmRuntime and llm-pi-ai mounted with { providers }, for the
config waterfall and pi-ai's credential paths. Tier B is dsh-app-boot's
boot() with a temporary HOME, a test bundle, a fake credentials service
and a launch-environment snapshot, for the config editor,
settings.describe and the volatile commit. Tier B's rows carry package
names through a stub module loader, and it fixes the apply order,
because the loader starts rows in parallel.

On top of it:

- contract tests for the waterfall, the profile and Config shapes,
  credentials, and the config editor (host-waterfall, host-shapes,
  host-credentials, host-editor-contracts), plus a tier A smoke test;
- a characterization of today's behaviour on pi 1.1.0, each row tagged
  where P-R1 is expected to flip it, with S0_TODAY shared by the
  binary e2e;
- the keyless 401-vs-404 matrix: a pure classifier, its unit test and
  a gated live run;
- a binary e2e gated on DSH_HOST_E2E=1 with its probe bundle, the
  e2e-host CI job (a pinned, patched cell on PRs that touch the host
  seams or vite.config.ts, and a floating cell nightly and on
  dispatch), and the test:e2e:host script.

The binary e2e now reports why a run failed: a timeout only when its
own deadline killed the host, the signal for a crash, and the probe's
fatal message whenever there is one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

* refactor: one way to patch a host method

Add src/host/patch-method.ts (patchMethod), now the only way the
plugin wraps a method on a host object, and move the four instance
patch sites in src/models-discovery.ts onto it: the three listings
hidden by hideResponsesRoute, decorateModelDiscovery,
decorateModelInfo and decoratePlaneAdapters.

On the real host the three llm-level disposers never restored:
ctx.llm is cordis's traceable proxy, every method read returns a new
shadow proxy, so the `===` identity checks were never true and each
reload stacked a wrapper over the last. The helper reads identity
from own descriptors instead. A dispose restores only while its
wrapper is still current (otherwise it becomes a pass-through),
deletes the own property when the original was inherited, and a
same-site reinstall wraps the unpatched method rather than the
earlier install, so either disposal order ends at the exact
original.

decoratePlaneAdapters becomes one patch per (adapter, method) that
composes the adapter's plane routes in registry order, which keeps
today's nesting on pi-ai's shared adapter. rewrapOnAdaptersUpdated is
built and tested but wired to no site yet.

test/reload.test.ts runs apply A, apply B, dispose A, dispose B (and
the reverse order) for every site on own and inherited originals, on
the real host (tier A) and through apply(); test/patch-method.test.ts
covers the helper's contract. The existing per-site pins in
test/models-discovery.test.ts stay unchanged; five new cases there
record the deliberate edge changes on hosts that refuse the patch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

* chore: refresh the generated catalog and limits tables

The vendor added kimi-k3.1-pro to the Go plane, so `catalog freshness`
went red on every branch of this stack. This takes the two generated
files exactly as the nightly refresh produced them (#111), with no
hand edits, and updates the counts that hard-code the catalog's size:
the Go shim test (28 -> 29), the plane table in redirect-planes.md
(Go served 28 -> 29, the no-SDK Go cell 17 -> 18), and the Go
completions list in both READMEs. The listed half of "29 served / 33
listed" is models.dev's count, inferred from the one added entry,
because models.dev cannot be reached from here.

It no-ops once main carries #111. P-T2 turns these counts into
invariants so a refresh stops needing this.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

* feat: the plugin owns the opencode and opencode-go routes

The plugin now serves the opencode and opencode-go routes itself,
through supported host paths only, and the hidden-route workaround is
gone.

A prepended internal/config listener hands pi-ai a copy of its config
without the two plane entries, so pi-ai keeps every other route. On a
commit the plugin reconciles: it builds one plane adapter, a subclass
of the host's PiAiAdapter, over pure profiles (utils/plane-profile,
plane-model, model-metadata and protocols) and registers only the
planes that are declared. A Settings candidate has the plane entries
it changes validated strictly. A read-only settings.describe graft
puts the committed plane entries back, so the host still sees them.

Deleted: responses-provider, responses-routes, google-bridge,
pi-provider and utils/google-replay; the listing hide and the
discovery decoration; the stream redirect; the D19 settings; and the
hidden ids in DEFAULT_PROVIDERS and cordis.patch.yml.

What users see: any saved id is accepted and served, and an id no
catalog knows dispatches as completions. The protocol comes from the
catalog and model info follows the user's own row first. A model
whose reasoning is only on or off offers Off/On. The default endpoint
is the per-protocol inference family. Keys resolve by pi-ai's own
rule, and a plane lists its declared ids even when no key resolves.

When several pi-ai rows declare planes, the row the plugin serves
stays the same while it is live and still declares one, so no row is
left stripped with nobody serving it. If the host's pi-ai module
cannot be read, the plugin steps aside even before any plane is
declared, so a later Settings save goes to pi-ai's own check.

The characterization rows this changes are flipped to new baselines;
every other row stays green unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

* test: measure which endpoint families the gateway serves

A measurement phase: the default endpoint family stays `modern`, so no
plugin behaviour changes and no characterization row flips.

- Name the default in one constant, AUTO_ENDPOINT_FAMILY in
  src/utils/plane-model.ts, which is what `auto` resolves to, and pin
  it in test/endpoint-family.test.ts.
- Extend the keyless matrix to three URL shapes (legacy, root, modern)
  over four protocols and two planes, add LEGACY_DISPATCH, and add one
  keyless free-model probe with its own pure classifier.
- Add minimal keyed probes (pure test/support/keyed-probes.ts and its
  e2e): one tiny request per plane, protocol and shape, sent with the
  plane's own key and the plugin's identity from one shared constant,
  PROBE_IDENTITY. Only the cells the default sends to are asserted;
  the rest are recorded.
- Gate the live job by tier with OPENCODE_E2E_KEYED (none, probes,
  full; unset means full). CI arms `probes` on a pull request that
  touches what the probes send, `full` nightly and on dispatch, and
  passes the secrets only when the tier is not `none`.
- Write a dated endpoint-family record from both live suites to the
  run summary and an `endpoint-families` artifact, and add §13 to
  docs/redirect-planes.md with every cell pending the first CI run.

The live part runs in CI only; no live result is invented here. A
failing keyed modern cell in CI is the evidence for a later `fix:`
that flips the default.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

* feat: Settings offers the catalog's models pi does not list

For the opencode and opencode-go routes, the Settings model list now
shows pi-ai's own rows first, in pi's order, and then every model the
plane's catalog can serve that pi does not list and the catalog has
not retired. Ids that pi's registry has retired are dropped, a field a
row lacks is filled from the catalog, and every row is cut down to the
five fields the host's discovery contract accepts, with absent values
omitted rather than set to undefined.

While pi-ai restarts and its discovery is withdrawn (NO_DISCOVERY),
the list answers the catalog rows alone instead of the error.

This rewrites the existing discoverModels wrapper rather than adding a
second one. Other routes keep today's enrich-only merge, because pi-ai
serves them and cannot serve an id it does not list. The plugin's own
model-discovery namespaces, which Settings never asked, are deleted
with their lifecycle tests, and RouteClaim gains a claiming() seam.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

* test: restructure the suite

Phase P-T2 of docs/improvement-plan.md. Test-only: no source file
changes and no behaviour changes.

- Every socket-bound case is in the mock suite. The nine unit files
  that booted the mock gateway through testing-kit.ts are merged into
  test/mock/request-shapes.mock.ts and a new protocol-endpoints.mock.ts
  table. The two offline e2e files that bind a port
  (patched-fetch-headers, refusal-classification) moved there too. The
  device-login routes left the gateway mock.
- createMockContext, HarnessSimulator and the rest of testing-kit.ts
  are gone. Services are built on a real cordis Context, and
  lifecycle.test.ts mounts the plugin the way the Loader does
  (test/support/composition.ts). degradedHost remains only for the
  degraded-composition diagnostics.
- 43 cases that tested only the harness or the mock gateway are
  deleted. 33 duplicated cases are merged and 16 restatements are
  deleted; the plan names each one.
- Hard-coded catalog counts and vendor ids are now invariants, or a
  row picked by shape, so a vendor catalog refresh no longer breaks
  the suite by construction.
- The seven largest test files are split by subject, the live suites
  read their environment through test/support/e2e-env.ts, and the
  duplicated helpers and hard-coded ses_ ids are removed.
- src/utils/session-id.ts, src/opencode-store-contract.ts and the e2e
  environment reader have their own tests, and src/lifecycle.ts,
  src/stream-hook.ts and src/fetch-patch.ts have per-file coverage
  floors.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

* chore: refresh the generated catalog and limits tables

The vendor renamed the Go plane's one Gemini model (dropping its
contributor suffix, halving its price and publishing an allowance for
it where the limits table had none) and added a free model with no
SDK, so `catalog freshness` went red on every branch of this stack.
This takes the two generated files exactly as the nightly refresh
produced them (#111), with no hand edits, and follows them:

- the Go Gemini fixtures in six test files (attachment-services,
  catalog, go-google, go-google-prepare, models-discovery,
  responses-routes) now name the renamed id, since each means "the
  Go Gemini model the catalog serves";
- the usage panel test built its unknown allowance from the table's
  one unpriced row, which the vendor has now priced, so it builds the
  `Number.NaN` allowance the generator emits itself;
- the Go shim count in catalog.test.ts (29 -> 30);
- redirect-planes.md: Go 29 -> 30 served, the no-SDK Go cell
  18 -> 19, and "0 of 29" -> "0 of 30" in the prose;
- both READMEs: the renamed Gemini id, and the new model in the Go
  completions list, since it declares no SDK.

The listed half of "30 served / 34 listed" is models.dev's count,
inferred from one rename (no change) and one addition (+1), because
models.dev cannot be reached from here. The dated S0 measurement row
in improvement-plan.md keeps the id it was measured with.

It no-ops once main carries #111.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

* test: skip free models the gateway has withdrawn in the live suite

The live routing suite picks its free-tier subjects from the catalog by
asking the gateway, but it counted "400 Upstream request failed: Model
is unavailable" as an observable answer. The upstream withdrew
nemotron-3-ultra-free while the catalog still lists it, so the picker
chose it and two cases asserted on a model the gateway no longer
serves; the reachability case also failed on a 429 throttle.

A withdrawn model (that 400, or a 410) is now skipped like a throttled
one. The free-tier routing case only accepts a candidate whose answer
is the free-tier gate it asserts, so an unmetered model cannot become
its subject, and the reachability walk moves past gated, withdrawn and
throttled candidates before it reports.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

* fix: address the second review of P-T1b

The review ran three rounds through the correctness, rules and matrix
lenses. What it found, and what changed:

- The binary e2e's beforeAll budget was smaller than the blocking
  subprocess deadlines it serializes. Each plugin install now has its
  own deadline, the hook's budget is derived from the installs and the
  boot, and afterAll stops a host a failed hook left running.
- providerWithApiKey claimed to be a line-for-line restatement but
  skipped upstream's credentialRef(ref) check. It now throws the same
  TypeError for a ref outside the grammar (a host-editor-contracts
  case pins it), and its comment names the deliberate differences.
- The pinned e2e-host cell let cordis and the loader float at patch
  level. The overrides now force both, the cache key includes them,
  and the job asserts them against HOST_VERSIONS.
- The pi-ai-first characterization asserted every facet under one
  KNOWN_DEFECT title. Each apply order now generates one test per row
  and facet, titled with that order's own tags.
- CONTRIBUTING said the contract tests pin F11-F20, and the plan said
  P-T1 pins every fact. Both now give the same accounting: which facts
  are pinned by number, which P-R1 exercises first, and why F9 and F17
  are not pinned.
- CONTRIBUTING put the e2e's dummy keys in the profile's credentials
  file; they are in the temporary home's credentials document.
- F5 had no real-host pin: host-shapes now pins that a route-level api
  and baseURL collapse pi's protocols onto one. F6 is recorded as first
  exercised by P-R1.
- The credential-mapping row pinned only the api-key branch of
  toPiCredential. host-credentials now covers all three branches, and
  the env branch is asserted through pi's Models.getAuth, so the case
  fails without it.
- The plan's reason for restating hasProviderApiKey was inaccurate; it
  now says no installed copy reaches the function.
- The new credential cases cited toPiCredential at the wrong lines;
  they now cite auth.ts:74-84.
- F4's listing and dispatch halves were pinned only by KNOWN_DEFECT
  rows P-R1 flips. A plugin-less tier-A case in host-shapes now pins
  that an id pi cannot place is not listed and that a turn on it is
  INVALID_CONFIG with no request; the save and deferred-read halves
  are tagged F4 too.
- The one facet that depends on the apply order lost its tag when the
  pi-ai-first block was split. The fixture case now checks only
  structure (the facets whose value moves are the facets whose tag
  moves), and the exact pin is its own KNOWN_DEFECT case that boots no
  host. CONTRIBUTING and the plan say P-R1 drops todayRowsFor's
  override and turns that case into a BASELINE check in the same diff.

Nothing under src/ changed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

* test: follow the vendor catalog refresh

The vendor renamed the Go plane's Gemini model from
gemini-4-argon-contributor to gemini-4-argon. The tier-B fixtures
(S0_PROFILE, S0_TODAY, CHARACTERIZED_PROFILE through S0, and
todayRowsFor's pi-ai-first override) declared the old id as the Go gap
id: one the plugin's catalog describes and pi 1.1.0 cannot place. After
the refresh the catalog no longer described it. They now name the
renamed id, which still plays that role because pi 1.1.0's opencode-go
table has no gemini-4-argon either. The host-shapes and editor-contract
cases follow the same fixture. The pi 1.3.0 note in mount-protocols
names that pi's own row, so it stays as it is.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

* fix: address the second review of P-H

What the review found, and what changed:

- rewrapOnAdaptersUpdated reinstalls its site inside the plugin's own
  teardown and on every foreign registry change. Its JSDoc in
  src/host/patch-method.ts now says so, counts the reinstalls a release
  causes, says a caller disposes it before releasing its adapters, and
  notes that a throw from a reinstall is logged by the host.
- The plan of record said all three P-H edge changes are unreachable on
  the real host for one reason; the third (the live-reload listing) is
  unreachable for a different one. docs/improvement-plan.md now gives
  each its reason.
- The tier-A reload cases for the listing and discovery sites proved the
  row only structurally. test/reload.test.ts now gives every site a
  tier-A probe whose decorated answer must differ from the host's own,
  so "survivor still decorates" and "last dispose restores" can fail.
- "patchMethod is the only way the plugin wraps a host method" was
  asserted in docs and enforced by nothing. test/architecture.test.ts
  now has a textual tripwire over everything apply() reaches, with a
  self-check of planted violations, and the apply()-twice reload case
  requires a patch record on every own function apply() leaves on the
  stand-in.
- The tripwire missed several write forms (Object.assign,
  Object.defineProperty/defineProperties, Reflect.setPrototypeOf,
  Object.setPrototypeOf, and an Object.* target reached through a
  property path such as registration?.adapter or this.llm). The
  patterns now catch them, and the self-check plants each one (17
  planted, up from 13). Nothing new in src/ matches.
- The docs said "any Reflect.* write" where the pattern matches a fixed
  set. The test docstring, AGENTS.md and the plan's P-H section now
  name the four Reflect and four Object methods matched, and why a
  prototype swap is listed.

No runtime behaviour changed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

* test: follow the vendor catalog refresh

The refreshed Go catalog serves its Gemini model as gemini-4-argon, so the
plane-dispatch mock, the plane-model cases and the binary e2e's probe
expectations name the new id; the Go Google base they assert is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

* fix: address the second review of P-R1

The review ran three rounds (found 20, 5 and 8; upheld 19, 4 and 8).
What it found and what changed:

- A completions model whose effort list names none lost its Off.
  utils/model-metadata.ts now offers Off with none as its wire value
  on completions, as pi 1.1.0's own entries do; Responses unchanged.
  Pinned in model-metadata, plane-model and a new mock-gateway case.
- A Go entry with no apiKeyEnv authenticates with OPENCODE_API_KEY
  (pi's lookup, D20), and nothing said so. README "Changed in 1.x" and
  step 3 now name it; host-planes.test.ts pins both cases.
- A Settings save of a plane was refused as "cannot be validated yet"
  while the host modules were unloaded. host/route-claim.ts now judges
  it with the last modules that loaded; route-claim.test.ts pins it.
- Fiber.update(config) with noSave false would persist the stripped
  config and drop both planes. Recorded as a constraint in
  route-claim.ts and the plan; no caller does it today.
- Stale text: cordis-context.ts comments (stream redirect, isolate,
  root inject), deep-dive.md (pi's Go lookup, who answers Fetch),
  CONTRIBUTING (field count, the host-modules floor), and historical
  banners on opencode-parity-plan.md and upstream-findings.md.
- The mock body-shape row asserted only body.model. It now checks each
  protocol's required fields and that the gateway answered 200, or
  the vendor's 404 for the unknown id.
- Route-set rows: removing the last plane now pins replace([]) on the
  live handle, and "booting with none" waits for the plugin's row to
  be active instead of sleeping 50 ms.
- The live protocol-routing suite now sends paid models through the
  plane adapter on a tier-A host, not only raw probes.
- README's credential-isolation claim was false: the Go meter can send
  the Zen key. README.md, README.zh-CN.md, deep-dive.md,
  credentials.md and redirect-planes.md now give the real order and
  the 403 remedies; readme-parity.test.ts keeps both READMEs in step.
- The keySource hint described Automatic backwards. settings-copy.ts
  (en and zh) now gives literal key, captured key, then reference.
- The live adapter blocks dropped a protocol whose model was
  withdrawn. They now walk up to three candidates per protocol
  (support/live-walk.ts), pass a "model is unavailable" turn only when
  a raw probe confirms it (support/live-probe.ts), and skip naming a
  protocol no candidate reached.
- A mock comment claimed a body passed shape checks the mock lacks.

Declined: a toggle beside an effort list still offers no Off, which
moves baseline facets and needs the owner's decision; the Models
store's join stays a documented restatement rather than importing a
client UI package; the PR description is edited outside the tree.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

* ci: gate freshness only on a pull request's own generator changes

The vendor moves its model catalog, its Go limits page and its CLI
version almost daily. Each move turned every open pull request red,
although none of them caused it: vendor drift is not a regression of
the change under test.

A pull request now fails a freshness check only when its diff changes
that check's generator code: the script, every local module it loads,
transitively, and the pinned version file for the CLI check.
scripts/freshness-scope.ts derives those inputs from the sources and
makes the call, so a new import moves them without a list to edit.
Any other pull request reports drift as a warning, as a push to main
already did. The offline check that every copy of the CLI version
agrees with the pin still gates every pull request, since only the
repository can break it. The gates fail closed: a lost scope answer
gates rather than skips.

The nightly run still regenerates the two tables and proposes the
refresh as its own pull request, and warns on the CLI version.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

* chore: refresh the generated catalog after Zen retired deepseek-v4-flash

The vendor retired `deepseek-v4-flash` from the Zen plane (it stays on
Go). This pull request changes the catalog generator's own code, so
under the freshness scoping it carries the refresh itself. This takes
`src/catalog-data.ts` exactly as the nightly refresh produced it (#111),
with no hand edits; `src/go-limits-data.ts` did not change. It then
updates the counts that still hard-code the catalog's size:

- the Zen shim count in catalog.test.ts (82 -> 81);
- redirect-planes.md: Zen served 82 -> 81, the no-SDK Zen cell 25 -> 24,
  and "0 of 82" -> "0 of 81" in the prose;
- protocol-routing-and-merge.md: the per-protocol shares now read "of 81",
  with completions at 24;
- both READMEs: the no-SDK row is 24 and the active total is 81. The
  Chinese table had read 27 before this change, and now matches the
  English one.

"127 listed" is unchanged, because the vendor retires a model by marking
it `status: "deprecated"`, which keeps it listed. This could not be
confirmed against models.dev, which cannot be reached from here.

It no-ops once main carries #111.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

* test: give the freshness walk's dynamic import a script-local target

The walk's fixture held the text `await import("../src/lazy.ts")`. The
test tree's own architecture gate, which starts on the next branch of
this stack, reads a dynamic `src` import anywhere in a test file's text,
including inside a string. It counted that fixture line as a test
bypassing `test/support/src.ts`, so every branch above this one failed
the gate once this pull request was merged into it.

The case is about the dynamic form, not about `src`, so the import now
names `./lazy.ts` beside the script. The walk resolves it from the
importing file's own directory, which is the same rule, and the expected
inputs follow it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

* fix: address the second review of P-R2

What the review found, and what changed:

- The keyed flip rule read a default cell's retired-model answer as a
  retired model without checking the same model on the other shapes.
  planeOutcome now reads each default cell beside the legacy and root
  cells of the same plane and protocol.
- A refused key counted as flip evidence even when every shape refused
  it. It is now "check the CI secret" when a sibling refused the key
  and none accepted it, and "nothing else measured the key" only when
  no sibling refused it; a path-gone, rate-limited or busy sibling no
  longer blocks the CI-secret reason.
- Breaks no flip could fix (accepted on the root shape only, or Go's
  Google, which no flip moves) were called flip evidence, and the
  "protocol" and "gone" reasons named causes nothing measured. A
  wrong-protocol or path-gone cell is now decided by the cell that
  endpoint: legacy dispatches to: "refused" (a flip would not serve
  it) when that cell refused readably, and always for Go's Google;
  "unmeasured" (re-run) when it measured nothing. Neither is flip
  evidence.
- An account answer counted as the flip target accepting a protocol or
  a model, though the account's gate answers before either is read. It
  now counts only beside a refused key or a gone path, and a retired
  model next to an account answer no longer fails the plane.
- The keyless free probe failed on a busy upstream (503/529) that the
  keyed classifier treats as routed. It now moves on past a busy
  candidate, and its dated record no longer pairs a derived
  upstream-busy with the last retired candidate's model and status.
- A busy upstream followed by no answer failed the keyed plane, against
  the verdict definitions in redirect-planes.md section 13. Section 13
  now says what the code does (no answer on the last attempt is
  transport-error unless an earlier attempt was a 429), and a test pins
  that it fails as unreadable, not as flip evidence.
- Comments and docs said P-R2 had already measured the families while
  section 13 is pending its first CI run; said models.opencode.ai
  declares the root shape, which the generated catalog contradicts;
  said legacy cells are asserted nowhere live; and said a keyed
  auth-refused means the plugin's own turns would be refused, though a
  default Go turn sends a different key. All corrected.
- CONTRIBUTING's key and mirror bullets now cover the keyed probes.
- The plan no longer claims P-R2 opens with a green pin, gives the
  conditional flip a phase, and says the keyed probes decide a plane
  from the default cells read beside their siblings, not alone, and
  that an unreadable answer is never flip evidence.
- Tests now cover the tier wiring of the four keyed live suites, no
  probe following a redirect, the push-to-none arm and the publish and
  upload steps of the CI wiring, the free probe moving on past a busy
  upstream, and the record being rewritten after every cell.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

* chore: refresh the generated catalog after Zen retired deepseek-v4-flash

Ported from the stack's base (d13cbe2 on #105) so this pull request's
own `catalog freshness` goes green now, rather than waiting for the base
to be merged up. It no-ops once the merge from P-R1 brings the same
change.

It takes `src/catalog-data.ts` exactly as the nightly refresh produced
it (#111). The counts that restate the catalog's size follow it, on this
branch's own wording of those passages:

- the Zen shim count in catalog.test.ts (82 -> 81);
- redirect-planes.md: Zen served 82 -> 81, the no-SDK Zen cell 25 -> 24,
  and "0 of 82" -> "0 of 81";
- protocol-routing-and-merge.md: the per-protocol shares now read "of 81",
  with completions at 24;
- both READMEs: the no-SDK row is 24, and the active total is 81.

"127 listed" is unchanged, because a model the vendor retires stays
listed as `status: "deprecated"`.

(cherry picked from commit d13cbe2410642254df0d0cf1392a5083386a0c6a)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

* test: follow the vendor catalog refresh

The merged catalog renames the Go Gemini model from
gemini-4-argon-contributor to gemini-4-argon; the Settings fetch
assertions (tier B and the binary e2e) now name the refreshed id.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

* fix: send Google's thinking level to Gemini models the catalog gives levels

The live suite caught the Go plane's Gemini model refusing a thinking
budget: the gateway answered that the model takes a thinking level and
not a budget. Pi chooses a thinking level over a thinking budget only
by matching the model id against its own pattern, so a model the
pattern does not name was sent a budget it cannot accept.

The plugin now makes that decision from the catalog, as D16 asks: a
model whose catalog entry offers discrete effort levels on the Google
protocol gets a thinking level. It builds that request through pi's
public exports, on both the enabled and the disabled path, and hands
every other model back to pi's own path unchanged.

Tests pin it: the pure rule over the catalog, a differential check
that the wrapper builds the same request as pi's own streamSimple for
an id pi already recognises, the lazy loading of the pi pieces, and a
mock-gateway case that the turn completes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

* docs: correct the Go shape count, the README date and the install range

Three documentation defects the synced base carries into P-R1. None was
caused by the merge (each is identical on the merge-base and on both
parents), and each wants the same change on the lowest branch (#105) and,
for the README date, on P-R2's cherry-pick of d13cbe2.

- PRE-1, docs/redirect-planes.md: "The Go plane serves 12 models on
  shapes other than completions" restores the count its own table gives
  (5 openai + 5 anthropic + 1 google = 11, the Go shim's 30 less its 19
  no-SDK models). The 12 was right when written (6 + 6) and a later
  refresh moved the cells but not the sentence. test/catalog.test.ts now
  holds the sentence to the table's non-completions Go sum, so the next
  refresh cannot strand it again.
- PRE-2, README.md and README.zh-CN.md: the 81-model count is dated
  2026-10-27, the refresh that produced it (d13cbe2); on 2026-10-23 the
  count was 82, so the date no longer matched the number.
- PRE-3, README.md and README.zh-CN.md: both install snippets name
  "^1.4.1" (package.json's version), where they had drifted to ^0.14.0 and
  ^0.12.0, neither of which can resolve to a 1.x release.
  test/readme-parity.test.ts pins the two snippets to one range on
  package.json's major.

Both new assertions were checked against the stale values: each fails on
them and passes on the corrected text.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

* docs: date the refreshed model count and correct three stale figures

The README's 81-model count kept the date of the 82-model count it
replaced (2026-10-23); it now carries the date of the refresh that
produced it (2026-10-27). Also corrected, all present-tense statements
the catalog had moved past:

- both README install snippets named ^0.14.0 and ^0.12.0, ranges that
  cannot resolve to any 1.x release; both now name package.json's
  ^1.4.1;
- redirect-planes.md said the Go plane serves 12 models on shapes other
  than completions; its own table gives 11 (30 less 19 with no SDK);
- engineering-notes.md tied a general rule to "the 82" models the shim
  ships; it now says "the models it ships", which a refresh cannot
  strand.

P-R1 carries the same corrections (dec00c8) with tests that hold the
README range and the Go sentence to their sources.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

* fix: address the second review of P3

The second review of P3 ran three rounds across the correctness, rules
and test-matrix lenses, and upheld 11, 4 and 4 findings. What it found,
and what changed:

- On a served plane, Settings' fetch offered pi's stale capacities, and
  adopting a row saved them over the catalog's, contrary to D17 and the
  README. Every row on a served plane is now read catalog-first, with
  pi filling only what the catalog lacks; a route the plugin does not
  serve stays pi-first (src/models-discovery.ts).
- enrichModels=false turns off the D3 complement and the NO_DISCOVERY
  fallback, but the README and the P3 spec stated both unconditionally.
  The README, the AGENTS.md map, the deep-dive and the plan's P3 landed
  note now name the switch.
- Retired pi rows are dropped only once a live catalog read has landed,
  but the docs said always. They now say so, and that offline pi's list
  passes through.
- The enrichModels copy said the picker shows declared models only,
  which contradicts D3. It now says the picker shows the declared
  models, or the plane's catalog when the entry declares none
  (src/settings-copy.ts and both READMEs).
- The plan listed the defect P3 fixes as not yet fixed, with stale line
  references. It is struck through and marked fixed by P3.
- F21 was a host fact missing from both lists of pinned facts, and no
  test named it. Both lists (the plan and CONTRIBUTING.md) now name it,
  and so does the pi-ai restart test.
- The gap-row save test never showed the id was missing from the
  composer before the save. It now asserts that first.
- The README's "Changed in 1.x" Fetch entry described the complement
  unconditionally and left out the catalog-first values. It now states
  both.
- README troubleshooting gave no cause for a retired pi id staying in
  Fetch while offline. It now names an unread catalog. Its fix says the
  first fetch once models.opencode.ai is reachable starts the refresh
  and still returns the old list, because the refresh runs behind the
  read and nothing runs it on a timer, so fetch again a moment later
  (README.md and README.zh-CN.md).

Carried to landing, because only a push can close it: the binary e2e
Settings fetch row has not run in the floating e2e-host cell on this
head, and the PR body lacks the plan's records and describes the merge
as it was before the catalog-first change. That needs a dispatch of
ci.yml on the pushed head and a rewrite of the PR body.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

* chore: refresh the generated catalog after Zen retired deepseek-v4-flash

Ported from P-R2 (29b43ca, itself from d13cbe2 on #105) so this pull
request's own `catalog freshness` goes green now, rather than waiting
for P3 to carry it up. It no-ops once the merge from P3 brings the same
change.

It takes `src/catalog-data.ts` exactly as the nightly refresh produced
it (#111), and the counts the docs restate follow it (Zen 81 served,
no-SDK 24, "of 81"). test/catalog.test.ts keeps this branch's
invariants, which hold the shim to its own rules rather than to a
count, so it needed no change.

(cherry picked from commit 29b43cadeba7699b421855251c267f5365dc2f5f)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

* docs: close the audit's remaining findings on P-R1

Each finding the third audit round left open, re-verified on this head:

- README.zh-CN.md still says Zen keys never query the Go quota endpoint:
  already fixed (0cceb81, dec00c8). The feature bullet, step 3, the
  EntitlementError row and the credential bullet match README.md.
- The withdrawn-model fix drops a protocol instead of trying its next
  model: already fixed (0cceb81). walkPerProtocol walks up to three
  candidates per protocol and names an unreached protocol in a skip;
  test/live-walk.test.ts pins both.
- N1's claim stands in docs/deep-dive.md and the keySource hint
  describes auto backwards: already fixed (0cceb81) in deep-dive.md and
  both hint strings. The same claim survived in a source comment,
  KEY_SOURCE_ORDER's "tier safety" note in src/go-discovery.ts; this
  commit corrects it: a key is filed by the plane its request went to,
  so a Go entry without apiKeyEnv files the Zen key as a Go key.
- PR #113 lists counts, not the flipped rows and the four records:
  fixed in the PR body.
- The R2 fix passes the live rows without performing a protocol, the
  skip decided by message text: already fixed (0cceb81). A "gone"
  verdict needs a raw probe of the same id to confirm it, and a
  protocol no candidate reached skips by name.
- README.zh-CN.md lacks the N1 fix, the Go apiKeyEnv step and the
  "Changed in 1.x" bullet: already fixed (0cceb81).
- PR #113 omits the D20 Go behaviour change: fixed in the PR body.
- docs/deep-dive.md says resolveGoApiKey excludes Zen keys and a
  Zen-only setup hides the ring: already fixed (0cceb81).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

* chore: follow the vendor's retirement of nemotron-3-ultra-free

The vendor retired `nemotron-3-ultra-free` from the Zen plane, so
`catalog freshness` gated this pull request (it changes the generator's
inputs). `src/catalog-data.ts` is taken exactly as main regenerated it
(#132), which today's registry still matches apart from that one row,
with no hand edits. What restates the catalog follows it:

- Zen 81 -> 80 served, no-SDK 24 -> 23, free 9 -> 8: catalog.test.ts,
  redirect-planes.md, protocol-routing-and-merge.md and both READMEs
  ("127 listed" stays; a retired model stays listed as deprecated);
- the README model lists, in both languages, now name ids the catalog
  ships: the Zen and Go lists had kept seven ids the vendor has since
  retired. The lists are main's; this branch's own prose around them
  (how the Go Google base is served) is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

* test: walk past a model the gateway answers ModelDeprecated for

The live gateway job failed on `gemini-3-flash`: the registry still lists
it, and the gateway now answers it with an error of type
`ModelDeprecated`. The adapter walk moves past a withdrawn model only
when both its pre-filter (`isModelGoneMessage`) and the raw probe that
confirms it (`modelGone`) recognise the answer, and neither knew this
shape, so a vendor retirement read as the plugin's refusal.

Both now recognise `ModelDeprecated` (by type, at any status, as main's
`retiredAtTheGateway` does). `test/live-probe.test.ts` is new and holds
`modelGone` to each shape; `keyless-matrix.test.ts` gains the message
row. Removing either rule fails its test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

* test: install the plane-lookup world before each test

The suite's setup file resets the live catalog after every test, so main's once-per-file install was gone by the second case and three cases failed. Installing before each one keeps the file independent of run order.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

* test(scripts): move the generated-table skip into the CLI-version rules

The copy scan's file filter lived in the top-level script, where nothing could exercise it. It now sits in scripts/vendor-version.ts beside the other rules, with cases of its own, and a test keeps its list equal to freshness-scope's. Because the script imports that module, it is now an input of the cli:version check, and the strict input list says so.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

* test(e2e): read an upstream's 400 "Model is unavailable" as a retirement

The live suite's local modelGone rule gave way to main's verdict module in the merge. Its one shape main lacked, a 400 whose body says the upstream withdrew the model, now lives in retiredAtTheGateway with a unit case, so the free-tier walk and the subject pickers skip it instead of failing on it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

* docs(testing): describe the suite this branch keeps

Main's test-system page predates the offline guard, the per-test reset and the engine floor: it placed the store fixture in the shared helpers and called the plane-lookup world per file. It now says where the store fixture lives and why, that a live-catalog world is installed per test, and what the CLI-version rules cover.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

* test(e2e): keep the free-tier case's narrowing in the subject picker

Taking main's usableAsSubject rule in the merge dropped the picker's accept argument, so the free-tier case could pick an unmetered model and assert the gate on a 200. The case's own narrowing now applies after the shared rule.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

* chore(vendor): follow opencode 1.19.5

The vendor published `opencode/1.19.5`, and `cli:version` gates this pull
request because its diff touches that check's inputs. Main made the same
bump in #132 after comparing every cited vendor source between the two
tags: the one that moved (`session/llm/request.ts`) only dropped an
unused type import, so no header the plugin emulates changed. This takes
the same bump in the 17 files that carry a copy here; `cli:version
--copies` reports all 58 copies and mentions at 1.19.5.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

* docs(plan): restack the plan onto 1.8.0 and the alpha host

Main moved to 1.8.0 and onto the harness's alpha track while the stack was open, and landed several things the plan had scheduled. The plan of record now says so: D14' and D22' replace the pi 1.1.0 and rc.2 pins, D17 restates pi-ai's new defaults, D23 records the freshness scoping, D24 and D25 adopt main's debug gate and tool-fallback plane, and D9, D16 and D19 stand with notes. F10 is gone on pi 1.3. A new phase P-R3 (honest refusals) follows P-T3, each phase has its restack adjustment, the status table shows the restack, and the two community plugins' lessons are a ranked table with what not to adopt.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

* build: raise the client bundle ceiling to 136 KB

Main and this branch each raised the tripwire from 128 to 132 KB, each measured without the other's surface. Merged, the client is 135604 bytes, about 0.4 KB over; the owner's standing rule for growth like this is to raise the ceiling, not cut content.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

* docs: complete the restack of P0

Close the restack review's findings on #105. Every change is prose, a
comment or a log line; no behaviour moves, so no test is added.

- ci.yml: the pnpm/action-setup comment no longer quotes a pnpm version;
  it names the `packageManager` field, which main's bump had made stale.
  release.yml carries the same stale sentence on main and is left to a
  main-side follow-up.
- scripts/freshness-scope.ts: the GENERATED_OUTPUTS and dataInputs docs
  name scripts/vendor-version.ts (GENERATED_TABLES, PINNED_VERSION), where
  the copy and the regular expression now live.
- test/e2e/protocol-routing.e2e.ts: the free-tier walk's report names the
  causes that can reach it (gated, withdrawn, refused for the account,
  never answered); a throttled walk skips, so "throttled" is gone.
- README pair: the Chinese file now says what the English says about
  403 FreeTierError (the headers are not its cause and the patch does
  not open the free tier), carries the Auto Review route clause and the
  two paragraphs on the API-key-only build and the host restart; the
  English yaml fence drops the nonexistent hiddenModels key.
- README pair: the compatibility section restates the typert peer range
  package.json declares and what its two clauses admit.
- docs/improvement-plan.md: F2, G1 and G2 carry the pi 1.1.0 restack
  note; the sources line names both harness tags; the P1a-P2 test matrix
  says the wrapper is retired; P0's status row counts ten defect fixes.
- docs/protocol-routing-and-merge.md, docs/redirect-planes.md: the count
  tables are dated 2026-10-31 (the shipped shim), apart from the
  documents' measurement stamps; both READMEs name
  `catalog:shim -- --write` as the regeneration command.
- AGENTS.md: the What-moved table names src/utils/retry.ts; the coverage
  line points at vite.config.ts instead of quoting stale thresholds.
- CONTRIBUTING.md: the restart check names CARD_FIELDS rather than a
  field count.
- docs/testing.md: rule 1 counts an upstream's 400 "Model is unavailable"
  as a retirement, and rule 2 says the body, not the status, makes it one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141fKp5k6hiaJcP5ovhGtSD

* docs: close the restack review of P0

Close the second review round's findings on the restacked P0. Docs only;
no generated file is touched.

- both-kept-2-R2-1: docs/deep-dive.md no longer names the retired ids
  the shim was said to suppress (main's regenerated shim serves one of
  them on Zen). It states the rule instead: a r…
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants