# Changelog

All notable changes to this project are documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.6.0] - 2026-09-20

**Upstream alignment round: P0 (correctness) + P1 (decision core)** against
`pi-shift-router` v1.6.0, from this project's v1.0.0-era port baseline;
**GUI settings card review rounds (R6–R8)** — registration fix,
model-catalog remote, layout overlap and information architecture;
**runtime visibility (R9)** — route notices; and **packaging/distribution
alignment (R10)** — harness packages become peers, engines raised to the
supported line, and all four install channels documented. The normative contract
is [`SPEC.md`](SPEC.md); the audit, including what was deliberately **not** ported
and why, is [`ALIGNMENT.md`](ALIGNMENT.md).

> A boot-aborting defect shipped in the alignment round and was fixed before
> this release ([`ALIGNMENT.md`](ALIGNMENT.md) §R3): the install had been
> verified only at the composition layer, which never instantiates a plugin.

> ⚠ **Routing behaviour changes immediately with no config edit.** The
> decision rule is replaced (vote counting → expected cost) and two legacy
> knobs change meaning. See "Changed — routing semantics" and `SPEC.md` §15.

> **Route notices (R9) are written into the session.** A tier or model switch
> is appended as one `[shift-router] …` message on the `agent/pre-step`
> waterfall; `ux.routerLogVerbose` additionally reports every judged turn.
> The text names the plugin because the Chat client never renders
> `source.plugin`. See `SPEC.md` §13.1 and `ALIGNMENT.md` §R9.

### Added

- **EV (expected-cost) routing.** The turn runs on the Smart tier iff
  `pSmart ≥ θ`, where `θ = 1 / economics.reworkPenalty` and `pSmart` is the
  Judge's confidence read as evidence for Smart (`c` for a `smart` verdict,
  `1 − c` for a `fast` verdict). θ is price-independent: the price delta cancels
  out, so the single knob is how badly a wrong downgrade hurts. Replaces the
  confidence-weighted window ratio, which counted votes instead of weighing cost.
- **`routing.economics`** config block: `reworkPenalty` (R, default 3),
  `downgradeMemory` (consecutive decisive fast turns required before
  smart → fast, default 2), and `mode`.
- **Gear presets** as top-level commands — `/router eco` (R=2, θ=0.5),
  `/router default` (R=3, θ≈0.33), `/router sport` (R=5, θ=0.2) — persisted to
  the settings namespace and tab-completable.
- **`decisionTier`** on every routing decision: the tier this turn will
  actually run at, after EV, hold and manual override. Orchestration entry and
  telemetry read this single signal instead of re-deriving a tier from the raw
  verdict.
- **`routing.cacheAware.sameFamilyPenalty`** (default 1.5): when both tiers
  share a provider, θ is divided by this factor. A smaller bar means fewer
  downgrades, so the warm prompt cache survives longer.
- **Judge `orchestrate` signal**: the Judge may now explicitly say whether the
  Smart tier should delegate to Fast workers. `false` vetoes orchestration;
  absent falls back to the tier-based default.
- **Judge prompt rules** from upstream v1.3.0–v1.4.2: an explicit tier / gear /
  orchestration request is a certainty and must be reported with
  `confidence ≥ 0.9`, evaluated before torn-task signals; document handling and
  tedious bulk batches classify as `fast` unless they set direction.
- **`/router allow-workers [on\|off]`** (C4(a)): writes this plugin's Fast-tier
  chain into the harness's own `subagent-model-selection` allowlist so the
  orchestrator can pin workers to Fast instead of letting them inherit the Smart
  model. The settings provider is namespace-agnostic, so no ownership transfer is
  needed; `off` revokes authorisation but keeps the routes; an impossible write
  (a profile without that namespace, no settings service, an empty Fast chain)
  reports the specific reason. Needs `@deepseek-ai/dsh-tool-subagent`'s
  namespace, which is pinned against the owning package by test.
- **Acceptance audit (C1)**, a fallback review that never blocks: deterministic
  checks (every dispatched worker reported, a CTO summary exists, the run did not
  end at a cap) always run, and a small Fast-tier auditor call verifies that the
  acceptance claim is grounded in the worker results, aligned with the user's
  goal, and not placeholder work. Findings surface as `Last audit:` in
  `/router status`. DSH adaptations: evidence is collected from the plugin's own
  event streams instead of an `agent_end` transcript, the auditor runs through
  `ctx.llm.stream` (the plugin holds no credentials), the prompt is inlined
  rather than shipped as a `.md`, and the LLM half is detached at the turn
  boundary so an audit can never delay a turn.
- **Convergence protocol in the orchestrator prompt (C2)**: every re-delegation
  must carry a `## Failure report` with *what failed* / *where* / *the acceptance
  test to re-run now*; re-sending the same report is forbidden (that is the
  signal to take the phase over), and the takeover threshold is the same value
  the router enforces as a hard cap.
- **Per-worker cost attribution (C3)**: `orchestration.spend` plus a bounded
  `workerSpends` ledger, shown by `/router status` as
  `Orchestration spend: $X · N/M workers reported`. Upstream read a worker's cost
  off the subagent tool result; DSH publishes it on the **child session's** own
  `assistant/message` events, and `dsh-subagent` sets `header.parentSession`, so
  the delegating task is identified exactly rather than inferred. A worker is one
  ledger row accumulated across its messages and priced with the model it
  actually ran.
- **`orchestration.maxSpendUsd`** (default `0` = off) and
  **`orchestration.workerLedgerCap`** (default `20`). The budget is part of
  `capHit`, so it denies further delegation exactly like the round and
  escalation caps; `capReason()` words the reason once for both the deny and the
  prompt's wrap-up notice. `spend` is monotonic and never derived from the
  bounded ledger, so no display truncation can leak budget.
- **`ux.promptSectionOrder`** (default `150`): sort position of the orchestrator
  system-prompt section. DSH allocates section order centrally (`SECTION_ORDERS`)
  and reserves no slot for a third-party section, so this is configuration rather
  than a platform constant — and it must not be resolved through
  `getSectionOrder()`, which returns `undefined` for non-platform names while
  `section()` throws on a non-finite order (the same boot-abort class as an
  undeclared service read).
- **`Worker delegation:` line in `/router status`**: whether subagent delegation
  can be pinned to the Fast tier, and the consequence when it cannot (§7.4).
- **`lastDecision`** state (verdict, confidence, reason, action,
  `decisionTier`, held) for the "why did it route this way" section of
  `/router status`.
- **`actualProvider` / `actualModel`** state: the model that actually produced
  the last assistant message, kept separate from the router's *intended* model
  so a stale intent can never be displayed as fact.
- **Worker-model startup self-check**: when orchestration is on and the Fast
  chain is non-empty, the plugin warns if model-selectable delegation is not
  available, naming the harness setting to enable and stating the consequence
  (workers would otherwise inherit the Smart model).

### Changed — build baseline

- **SDK baseline caught up to the runtime the harness actually ships:**
  `@deepseek-ai/*` `0.1.0-rc.6` → `0.1.5-rc.2`, `@deepseek-ai/cordis` `4.0.1` →
  `4.0.2`, `@deepseek-ai/schemastery` `^3.18.1` → `^3.18.2`. Until now the plugin
  compiled against one SDK and ran against another; the checkouts that made that
  survivable are gone, and three real breaks surfaced immediately:
  - `settingsNamespace()` no longer exists — a namespace is now a branded string
    (`SettingsNamespace`) validated by `register()`, so the literal is used
    directly. The E2E's settings probe had been importing the *old* constructor
    from its own (stale) dependency copy, which is why it never caught this.
  - `dsh-client-runtime` **does not exist at this baseline** (the profile's entry
    is a dangling symlink into the npx cache). The client half's type-only
    `ClientContext` / `SettingsScope` imports are replaced by `Context` from
    cordis plus the packages that actually declare the augmentations:
    `ctx.slots` now comes from `dsh-client-ui-renderer/client`, and the client
    `SettingsScope` from `dsh-client-ui-settings/client`.
  - `@deepseek-ai/dsh-tool-subagent` is now a (type-only) devDependency, so the
    host's `subagent-model-selection` surface has real types instead of a
    structural cast.

### Changed — routing semantics

- **A Judge outage is now a HOLD, not a `fast` verdict.** Previously every
  Judge endpoint failing produced `{tier:'fast', source:'fallback'}`, which the
  router treated as decisive evidence and pushed into the window — two
  consecutive outages silently downgraded a Smart session. Now `source ===
  'fallback'` keeps the current tier, records a hold entry, and never extends
  (or counts as) a downgrade streak.
- **A verdict below `window.minConfidence` is likewise a hold** rather than an
  ignored sample: ignoring it let the remaining entries decide alone.
- **Downgrade now requires `downgradeMemory` consecutive decisive fast
  decisions.** Any hold or smart entry breaks the streak (upstream v1.4.0).
- **Strict model authority**: a tier change is recorded even when both tiers
  resolve to the same model id, so tier identity — not the model string —
  carries the decision.
- **Failover signatures extended** (upstream v1.2.0/v1.4.1/v1.4.3):
  - HTTP **402** and `insufficient balance` / `余额不足` — an unfunded account
    used to stay pinned, so every turn re-tried it while the router kept
    choosing it.
  - usage-limit exhaustion with no HTTP status (`The usage limit has been
    reached`, `usage_limit_reached`).
  - `unsupported_model` / `model_not_found` / a model reference adjacent to
    "not supported" — a decommissioned model now fails over instead of pinning
    the tier.
- **Explicit tier / orchestration requests are honoured**: orchestration gates
  on `decisionTier`, so the raw verdict can no longer inject the CTO prompt
  while the fast model runs the turn.
- **Escalation counts consecutive worker failures**; a successful worker
  result resets the streak, so isolated failures no longer burn the cap.
- **Orchestration state leaked by an interrupted turn is swept** at the next
  turn start (previously a turn that never reached its stop boundary left
  orchestration active, caps enforcing, and the orchestrator prompt rendered
  into every following turn).

### Removed

- **The plugin's duplicate throughput (TPS) machinery** —
  `tokensPerSecond`, `recordSpeed`, the speed window, `SPEED_WINDOW_SIZE`,
  `failover.speedWindowSize`, the `assistant/chunk` listener, the related state
  fields and the `tok/s` line in `/router status`. DeepSeek Harness already
  renders `tok/s` natively in the chat message footer and the trajectory panel,
  and derives it from **decode time** (`outputTokens / (decodeMs / 1000)`), which
  is strictly better than this plugin's wall-clock estimate. Duplicating it
  meant two competing figures for one thing, one of them worse.
  *Deliberately not ported from upstream v1.4.2.*
- **Dead code and an unused dependency** found by a reference scan:
  `jsonStr` and the never-wired `FALLBACK_PROMPT` / `judgeFallbackPrompt` in
  `judge.ts`, `formatDuration` in `stats.ts` (a duplicate of
  `failover.formatRemaining` with no caller), and the `@deepseek-ai/dsh-timeout`
  direct dependency (no source file imports it; it remains a transitive one).
- **`orchestration.requireSmartModel`** — `decisionTier` now reports truthfully
  whether the Smart tier will run, so orchestration already cannot fire without
  a resolvable Smart model. The knob's only reachable effect was injecting the
  CTO prompt onto a Fast-tier run (the upstream "CTO loop on the fast model"
  bug class).

### Fixed

- Status output now reports the model that **actually** ran, not the router's
  intent (upstream v1.4.2 Bug B).
- Orchestration no longer prompts for delegation that the router cannot honour.

### Fixed — pre-install re-verification

- **The client roster named a package that no longer exists.** `dsh.client.inject`
  still listed `@deepseek-ai/dsh-client-runtime`, which the current baseline does
  not ship, and it did not list `@deepseek-ai/dsh-client-ui-renderer` — the
  package that declares the `slots` service the card injects. The stale id is
  inert (the module loader silently skips unknown inject ids), so this was a
  correctness fix, not an outage; a test now forbids writing it back.
- **Install-time contract is now a gate.** `tests/packaged-install.test.ts`
  asserts, against the BUILT artifacts, that the host half imports only declared
  `dependencies`, that the browser half `require()`s only platform seed words or
  `dsh.client`-declared packages, and that `files` ships what the artifacts and
  READMEs need; `npm run test:e2e` now also packs the tarball, installs it into a
  second scratch profile and boots it, where devDependencies are absent.
- Load safety is now tested per configuration: the empty row, routing disabled,
  `manual`, `off`, orchestration off, an empty Fast chain and the full
  costs/audit surface each load for real.

### Changed — packaging and distribution (ecosystem alignment)

- **Harness packages are now `peerDependencies`, not `dependencies`.** The
  compiled output requires `@deepseek-ai/dsh-llm` and `@deepseek-ai/schemastery`
  at runtime (and `@deepseek-ai/cordis` is the plugin framework), so those three
  are peers; the remaining `@deepseek-ai/*` imports — the type-only host
  contracts and the client roster — moved to `devDependencies`, where they
  belong. A private copy of a harness package can leave the plugin holding a
  different module instance than the host that consumes it (SPEC §1.5).
- **`engines.node` is `^22.19.0 || >=24.0.0`** (was `>=22.0.0`), the ecosystem's
  supported runtime line.
- **`prepare` no longer type-checks.** It is the git install path's build step
  and runs in a stranger's tree, where a type error is *their* install failing;
  `build` keeps the full type-checked pipeline for CI and `prepublishOnly`
  re-runs the gates before anything reaches a registry.
- **The install docs cover all four channels** — npm, tarball, git (with the
  `allowBuilds` authorization and commit pinning it needs) and a local checkout —
  plus the `--dump-config` step that confirms the layer landed. The READMEs carry
  the `dsh-plugin` badge the official README asks plugin repositories to use for
  discoverability; the repository already carries that GitHub topic.
- The audit behind these changes — the official publish doc, what published npm
  plugins actually declare, and the community checker's rules including the one
  we deliberately do not copy — is in [`ALIGNMENT.md`](ALIGNMENT.md) §R10.

### Added — runtime visibility (route notices)

- **A routing switch is now written into the session, not just into a log ring.**
  Until now an enabled plugin produced no visible trace at runtime: it never
  changes the session's selected model (it overrides the wire model per request),
  its only surfaces were `/router …` and the settings card, and
  `ux.routerLogVerbose` wrote to `ctx.logger`, which the shipped DSH profiles
  export nowhere. A tier or model switch now appends one `form: 'notice'`
  message on the `agent/pre-step` waterfall — the same channel the harness's own
  model-selection notice uses — so the transcript says which tier and model the
  turn runs on, why the Judge decided that, and how long it took (SPEC §13.1).
- **`ux.routerLogVerbose` now means what it says.** It still feeds `ctx.logger`,
  and it additionally emits a notice on *every* judged turn, including one that
  holds position, instead of promising a per-turn account nobody could read.
- **The notice names the plugin.** It starts with `[shift-router]` because the
  durable `source.plugin` field is never rendered: the Chat client draws only the
  message content, and the collapsed row only the `summary`. This is also why the
  `[model changed: …]` line users see is **not** this plugin — it is
  `@deepseek-ai/dsh-agent`'s model-selection notice, and it fires on session-model
  changes, which the router never makes (ALIGNMENT §R9).
- No status bar was invented: DSH's client `SlotMap` has no statusbar/toolbar
  seat, so the harness-proven notice channel is used instead.

### Changed — settings card layout and information architecture

- **Elements overlapped in the model rows.** The row's role badge (Primary /
  Fallback n) overflowed the fixed 20px grid track it was given and covered the
  provider select, and every unit suffix (`ms`, `tokens`, …) sat inside its input
  box, directly under the spin buttons a `type="number"` control draws there.
  The row now sizes its badge track to its content, and the unit is a sibling of
  the input instead of an absolute overlay. Measured before/after with
  `e2e/browser-check.mjs`: 21 overlapping pairs → 0.
- **The open card showed everything at once.** The 9 settings that change how
  requests are routed (master switch, both tier chains, routing mode, economics
  preset and penalty, orchestration mode, round budget, spend cap, audit switch)
  stay in the open view; the other 21 — judge limits, window and cache tuning,
  failover timing, telemetry, prompt ordering, legacy leftovers — moved into one
  *Advanced* disclosure that starts collapsed and keeps its sub-groups. The
  default-visible set is now pinned by a test.
- **All descriptions rewritten for users.** Hints used to explain the
  implementation (θ, cache divisor, worker ledger, ring buffer, prompt sort
  position); they now say what the setting does to your requests and when you
  would change it, in both locales.

### Fixed — the card asked for model ids by hand

- **Model controls never loaded the deployment's models.** The card read the
  catalog through `ctx.get('connection')?.api.llm.models()`, which does not
  exist: `ctx.connection` is the wire client (state/identity), and the catalog
  belongs to `ctx.remote.session.modelCatalog()` — the same remote the `/model`
  selector uses. `connection?.api` was therefore always `undefined`, the card
  never issued a load, and every provider/model control silently fell back to a
  text box. The card now reads that remote (reactively, via `ctx.inject`, so it
  survives being mounted late) and re-reads it on `llm/adapters-updated`,
  `settings/document-updated`, `credentials/reference-updated` and
  `connection/reset`.

### Changed — settings card UX

- Provider failures are shown against the rows they affect instead of leaving an
  empty dropdown; an empty chain, a duplicated route and an identical Fast/Smart
  primary are called out inline (they were startup logs, and a stock profile
  exports no log sink); chain rows reorder with ↑/↓ instead of delete-and-retype;
  numeric fields use `type="number"` with the config schema's own
  `min`/`max`/`step`; `legacy` fields are marked as accepted-but-ignored; and the
  collapsed header summarises the effective configuration. Manual entry remains
  only as the explicit **Custom…** option.

### Fixed — GUI settings card never registered

- **The settings card did not appear in Settings → Plugins → Plugin
  configuration.** `settings.plugin.item` is a **`keyed`** slot whose cell key is
  the settings namespace, and the card registered with `id` instead of `key`, so
  the registry threw (`keyed slot "settings.plugin.item" requires options.key`)
  and the tab's `entry.options.key` projection never matched it. The card plugin
  loads before the Settings panel declares the slot, so the throw surfaced only
  as an uncaught browser error. It now registers `key: 'shift-router'` — the
  literal the host half registers through `ctx.settings.register()`.
- **The slot contract is no longer re-spelled locally.** The client half used to
  declare `settings.plugin.item` itself, as a `list` slot, so the compiler
  enforced a contract that does not exist and approved the `id` form above. It
  now imports the `SlotMap` entry type-only from the package that declares it
  (`@deepseek-ai/dsh-client-ui-settings-plugins`), turning a drift upstream into
  a compile error. Regression: `tests/client-card-slot.test.ts` drives the card's
  real `apply()` against the harness's real `SlotCore`, in both load orders.

### Fixed — installation verification round

- **DSH failed to start with the plugin installed.** The worker-model self-check
  read `ctx.subagentModelSelection` as an undeclared service. A Cordis context is
  a proxy whose `get` trap throws `cannot get property "…" without inject` for any
  service the plugin did not declare *while that service is absent* — including
  the ordinary window in which a sibling row is still mounting. Because the read
  ran inside `apply`, the throw failed the plugin fiber and aborted the whole
  plugin tree (`dsh: plugin tree failed to load … cannot get property
  "subagentModelSelection" without inject`), on the plugin's **default**
  configuration (`orchestration.mode: auto`). The service is now read through
  `ctx.get('subagentModelSelection')` — the documented way to probe an optional
  dependency — and the self-check subscribes reactively via `ctx.inject`, with a
  race-free re-check at orchestration entry. Regression:
  `tests/plugin-load.test.ts`, which loads the plugin through `ctx.plugin()`
  against a real Cordis context.
- **The self-check's warning was unreachable.** Cordis's logger only fills an
  in-memory ring unless an exporter is registered, and the shipped DSH
  compositions register none — so the warning (and every other `ctx.logger` line)
  reaches neither the terminal nor the UI. The same fact is now rendered by
  `/router status` as a `Worker delegation:` line, and the visibility limit is
  stated in SPEC §13 and both READMEs.
- **`orchestration.mode: auto` was never exercised by the test suite.** Both E2E
  fixtures pinned `mode: off`, which is exactly how the boot regression escaped;
  `--dump-config` cannot substitute, because it composes configuration without
  instantiating a plugin. The upgrade-path fixture now mirrors a real
  pre-alignment profile (`mode: auto`), and a new
  `e2e/orchestration-overlay.yml` boots the default configuration with the
  web-only `subagent-model-selection-settings` row mounted.

### Fixed in review

An independent adversarial review (with mutation testing) audited this round
before release. Its findings, all fixed:

- **An embedded 402 in the failure text was not matched.** `failure.status`
  and the balance keywords were handled, but `"HTTP 402 Payment Required"` fell
  through to the status regex, which listed 429 and 5xx only — so exactly the
  adapters that fold the status into the message kept a dead account pinned.
- **A completed message without usage did not age the prompt cache.**
  `lastActivityAt` was set after the `if (!usage) return` early return, so the
  state still read "no message has completed yet" and the warm-cache downgrade
  gate stayed open — the cost inversion cache-aware routing exists to prevent.
- **A forced model borrowed the verdict's tier.** `/route-force <provider/model>`
  while the Judge said `smart` reported `decisionTier: 'smart'`, enough to start
  an orchestration turn on a user-pinned model. The tier now comes from the
  model itself.
- **`downgradeMemory` larger than `window.size` silently pinned the router to
  Smart.** The streak lives in the window, so the requirement now saturates at
  the window size and `/router status` reports the cap.
- **The `⚠ legacy` warning fired for the inert default.** A config carrying the
  pre-EV `sameFamilyThreshold: 0.9` was told an override was in force (and that
  the strong cache divisor applied) when neither was true.
- **The GUI hint for `minConfidence` stated the opposite of the code** about
  holds and the downgrade streak.
- **The worker-model self-check stayed silent when the harness exposes no
  `subagent-model-selection` service** — which is precisely when delegation is
  not selectable. It now warns, and the decision is a unit-tested pure function.
- Removed state and exports that nothing read: `orchestration.startedAt` and
  `remainingCooldownMs`.
- Documentation corrections: SPEC §10 no longer lists the removed knob or the
  wrong defaults for the two intentionally-unset legacy leaves; SPEC §2 and §4
  agree that a hold **breaks** the streak; telemetry scope is the routed
  top-level agent, not "every message"; the ROADMAP no longer claims rounds
  settle on result (they settle on dispatch, deliberately); the README backoff
  ladder is 1m → 4m → 16m → 1h04m → 4h16m → 6h, not "1h"; `SPEC.md`,
  `ROADMAP.md` and `ALIGNMENT.md` now ship in the npm tarball instead of being
  linked from the README but absent from the package.

The review also proved two boundaries were held by the code but not by the
suite (`pSmart >= θ` and the inclusive idle gate); both now have equality tests.
Re-running the review's mutations against the updated suite: **11/11 caught**.

### Migration

Schemastery passes unknown keys through, so removals are inert leftovers rather
than load failures. Meaning changes are the ones to watch:

| Key | Before | Now |
|---|---|---|
| `routing.window.threshold` | the downgrade bar | **legacy** raw-θ override; the old default `0.6` is inert, only a different value is honoured (and flagged `⚠ legacy` in `/router status`) |
| `routing.cacheAware.sameFamilyThreshold` | the raised downgrade bar | **legacy** sentinel: a non-default value implies `sameFamilyPenalty = 3.0`; the old default `0.9` is inert |
| `orchestration.requireSmartModel` | could force orchestration without a resolvable Smart model | removed (ignored) |
| `failover.speedWindowSize` | TPS window size | removed (ignored) |

To migrate deliberately: set `routing.economics.mode` (or `reworkPenalty`) and
leave `routing.window.threshold` unset.

### Documentation

- Added [`SPEC.md`](SPEC.md) — the normative contract (decision pipeline, EV
  rule, decision memory, cache-aware routing, Judge contract, failover
  signatures, orchestration, config reference, commands, gates).
- Added [`ALIGNMENT.md`](ALIGNMENT.md) — the upstream alignment audit and
  prioritised work list, including §R3 (the installation-verification round).
- `SPEC.md` §1.4 ("Cordis plugin invariants") states the four rules this plugin's
  code must satisfy — declare what you need, probe what is optional, never guess a
  platform constant, and never let an optional capability change loadability —
  each with a regression in the suite. §13 records that the stock compositions
  export no `ctx.logger` sink, so anything a user must be able to read has to be
  surfaced by a command.
- `ROADMAP.md`: retired the "v0.x maps one-to-one" note, recorded the port
  baseline (upstream v1.0.0) and the alignment target (v1.6.0), and added an
  upstream-version alignment table.

## [0.5.0] - 2026-08-15

### Changed

- **GUI card redesigned for the review round** — the card now matches the
  host-plane look and interaction states exactly (native `PluginCard` chrome:
  hover border, open background, focus-visible outlines, the DSW chevron SVG
  instead of a text triangle, `label-primary` save button, disabled opacity)
  and fixes the review findings:
  - **Title is the plugin name**: the card header now reads **"Shift-Router"**
    (was "模型路由 / Model router"), consistent with the plugin's own name.
  - **Card description carries the author signature**:
    `…。作者：green-dalii` / `… Author: green-dalii` (review round 3).
  - **Line-height normalization (review round 3)** — fixes a real layout bug:
    several inline styles carried native-CSS *pixel* line-heights as React
    *unitless numbers* (`lineHeight: 17`), which CSS interprets as **17 ×
    font-size** — group headings ballooned to ~176px and override/unsaved
    badges to ~187px. All text now uses tight unitless multipliers (single
    line `1.2`, multi-line hint `1.3`), fixed-height controls drop
    line-height entirely, and field hints are clamped to 2 lines with a
    hover tooltip. Measured effect: card ~3000px → ~1900px; every element's
    computed line-height is now < 2.2× its font size.
  - **Clean, grouped layout**: seven bordered section blocks (General /
    Models / Routing / Orchestration / Failover / Telemetry / Logs & UX) with
    one-line summaries, sub-groups for the Routing section (Judge / Decision
    window / Cache-aware), and the native field-separator rhythm.
  - **Compact settings-row form** (review round 2): each scalar field is one
    grid row — label + hint on the left, control right-aligned on the same
    line — instead of three stacked lines. Measured effect: per-field height
    ~108px → ~59px, card height ~4077px → ~3000px, with controls uniformly
    right-aligned and no overflow.
  - **No more duplicated hint text**: units and ranges moved out of the
    descriptions into unit suffixes inside the numeric inputs (`ms`, `tokens`,
    `0–1`, `rounds`, `calls`), and every hint was rewritten in plain language
    (the old `快速层占比达到该值即保持快速（[0,1]）。 ([0,1])` duplication is
    gone).
- **Fast / Smart model specification + fallback order are now in the card**:
  a new Models section with an ordered row editor for `tiers.fast.models` and
  `tiers.smart.models`. The row order is the in-tier fallback order — the
  first available model wins, the rest are its fallbacks — which is exactly
  how `findBestModelForTier` / failover consume the chain. **Provider and
  model dropdowns are auto-loaded from DSH's runtime model catalog**
  (`llm.models`, the same catalog the DSH settings surface reads): only
  providers with a currently advertised model list appear — the declarative
  `llm.providers` directory (dormant routes) is intentionally not read, so
  the card shows exactly the models DSH is configured with, in any
  deployment, with nothing hardcoded. A "Custom…" escape covers values
  outside the catalog and a graceful free-text fallback covers catalog
  loading/failure. Only `pricing` remains CLI/patch-only.
- **Controls polished**: booleans render as toggle switches (`role="switch"`)
  whose ON state uses the business accent (`--dsw-alias-state-business-primary`,
  a mid-tone blue in both themes) with a static white shadowed knob — clearly
  readable in light and dark themes (the previous white-on-near-white dark
  ON state is fixed); enums as a styled select with the DSW chevron, model
  rows in the native grid layout with dashed empty state and icon remove
  buttons.
- Client form model extended: `models` field type, `ModelRow`, row-draft
  parsing (blank rows dropped, half-filled rows block save), and save-plan
  support for the tier chains (batched into one `tiers` section write,
  deep-pruned so cleared chains don't leave `{fast:{}}` shells).

### Added

- Tests: 109 unit tests (added model-chain draft parsing, tier-chain save
  plans, cleared-chain pruning, the model-catalog loader incl. the wire
  `result.ok`/`value` envelope, and updated the GUI/CLI registry parity:
  model lists are now GUI-exposed, `pricing` stays CLI-only).
- **`CONTRIBUTING.md` rewritten for the DSH environment** (review round 3):
  the DSH dev loop (live config changes / HMR for patch edits vs build +
  restart for host and client code, the in-process client-bundle cache, the
  package-name mount requirement, the settings-whitelist patch), the manual
  browser E2E recipe, registry-parity and line-height guidelines, and the
  local-review commit workflow.
- **`ROADMAP.md` added** (review round 3), modeled on the upstream
  pi-shift-router ROADMAP: released-version table (v0.1.0–v0.5.0), a
  DSH-adapted planned table (cost deep view, log-to-file, tool-result
  classification, cross-turn orchestration, multi-worker fanout, GUI pricing
  editor, catalog live refresh, CI coverage), explicit non-goals, and
  cross-links.

## [0.4.0] - 2026-08-15

### Added

- **GUI configuration card** — the package now ships a browser-side (client)
  module that registers a "Model router" card in the GUI's Settings → Plugins
  → Plugin configuration section (the `settings.plugin.item` slot of the
  official `dsh-client-ui-settings-plugins` section). The card:
  - renders every **scalar** leaf of the `shift-router` settings namespace as
    a form (booleans, numbers, enums), grouped by section, with staged saving,
    per-field reset-to-default, override markers, and a read-only notice when
    the deployment stores settings read-only;
  - writes the same namespace `/router config` edits (per-section
    `settings.mutate`-equivalent scope writes, revision-fenced), so the two
    surfaces stay consistent in real time;
  - is built by the extended `npm run build` pipeline (`tsc` host → `tsc`
    client → `tsdown` client bundle, `dist/client.js`, CJS closure-factory per
    the `packages/client/tsdown.client.ts` protocol) and discovered through
    the `dsh.client` manifest — the plugin must be mounted by package name
    (`dsh-shift-router`) for the card to be served.
  - **Upstream whitelist caveat (0.1.0-rc.6)**: the Web API proxy
    (`@deepseek-ai/dsh-host-apiproxy`) only serves settings namespaces on its
    hardcoded `WEB_SETTINGS_NAMESPACES` list to the browser; a third-party
    namespace is filtered out of `settings.describe` even when registered.
    `scripts/expose-gui-settings.mjs` adds `shift-router` to that list in the
    profile's installed copy (idempotent) — run it once per profile and
    restart; the README documents the upstream "deferred work" comment.
- Tests: 95 unit tests (added the client form model: path helpers, draft
  parsing, section-patch save plan, GUI/CLI field-registry parity against
  `CONFIG_FIELDS`, and the whitelist-patch logic).

### Changed

- `package.json`: new `dsh.client` manifest, `exports["./client"]`, and a
  two-program `build`/`typecheck` (`tsconfig.client.json` + `tsdown.config.ts`).

## [0.3.0] - 2026-08-15

### Added

- **Interactive `/router config` editor** — the command now renders a numbered
  field list with current values (one row per editable leaf, type-annotated),
  plus four editing subcommands:
  - `get <N|path>` — show one field's current value.
  - `set <N|path> <value>` — set one field by index or dotted path (JSON
    values auto-parsed); indexes are stable (registry order).
  - `unset <N|path>` — clear a single user override via the official
    `settings.mutate` path-op write (`{op:'unset'}`), so the field reverts to
    its composition default without touching the rest of the user section.
  - `diff` — list the raw user-section overrides the settings layer currently
    holds, each with its effective value.
- Tests: 72 unit tests (added `/router config` editor helpers: field registry
  integrity, index/path resolution, path reading, value formatting, leaf
  flattening).

## [0.2.0] - 2026-08-15

### Changed

- **Orchestration hard caps are now enforced, not just prompted**:
  - Every `subagent` delegation while an orchestration turn is active increments `orchestration.rounds` (`tools/pre-execute`); every failed (`isError`) subagent result increments `orchestration.escalations` (`tools/result`).
  - At the cap the `subagent` tool is denied outright and the orchestrator system-prompt section switches to a "wrap up now" notice (`buildCapNotice`).
- **`routing.mode` is now functional** (was display-only): `auto` = judge + routing + failover + orchestration; `manual` = only explicit `/route-force` overrides (no judge); `off` = fully passive for model selection.
- **Removed `ux.quietMode` and the `/router quiet` command** — the plugin sends no notifications, so the toggle was dead config.
- Hardcoded runtime parameters moved into `Config` (all with safe defaults, now range-validated):
  - `routing.judgeMaxTokens` (was `JUDGE_MAX_TOKENS`), `routing.judgePromptCap` (was `JUDGE_PROMPT_CAP`).
  - `failover.baseMs` / `failover.maxMs` / `failover.startAttempts4xx` / `failover.speedWindowSize` (were module constants).
  - `telemetry.callLogCap` (bounds the per-message attribution log).
- Config schema now range-constrains numeric fields (`min`/`max`/`natural`/`percent`) so invalid configuration fails loudly at load and on `/router config set` — never silently misbehaves. Dropped the `as never` nested-default hacks (leaf defaults cover missing objects).
- `agent/request-error` attributes the failure to the exact model last put on the wire (`lastRequestProvider`/`lastRequestModel`, recorded in `agent/request`) instead of scanning session events.
- Telemetry attributes each message to the tier that owns its model (`findTierForModel`) rather than the router's current tier, so manual overrides / same-provider switches are billed to the right tier.
- Model-availability memo is cleared on every config refresh, so adapter/config changes are re-probed instead of serving stale results.
- `/router config` surfaces the schema's rejection message on failed `set`/`reset` instead of a generic error.
- `scope.watch()` disposal is registered as an effect (explicit teardown on HMR reload).
- `processRoute` stamps window entries with the injected `now` (deterministic, pure).

### Added

- Packaging: `prepare` script (self-contained `tsc` build for git installs), `exports` map, `README.zh-CN.md` in `files`.
- Tests: 62 unit tests (added failover-policy, cap-enforcement, config-schema, and deterministic-timestamp cases).

### Removed

- `@deepseek-ai/dsh-scope` direct dependency (transitive only).

## [0.1.0] - 2026-08-14

### Added

- Initial release — a DeepSeek Harness adaptation of pi-shift-router.
- Two-tier (Fast ↔ Smart) LLM-Judge routing:
  - Judge runs on the Fast-tier model chain via `ctx.llm.stream()` (harness adapters/credentials; no hand-built fetch).
  - `agent/pre-step` turn-start classification; `agent/request` per-step model override.
  - Instant upgrades; confidence-weighted sliding-window downgrade gate.
  - Cache-aware routing (same-provider threshold raise + warm-cache hold).
- Runtime failover:
  - `agent/request-error` cooldown marking + `{kind:'retry'}` same-tier fallback.
  - Exponential backoff 1m → 4m → 16m → 1h → 6h (4xx starts at 16m).
  - Cooldown recovery on a successful assistant message.
- Task-level orchestration:
  - Smart verdicts escalate to a CTO run with an injected orchestrator system-prompt section.
  - Hard caps: `maxRounds` and `escalationThreshold`.
  - Adapted to the DSH `subagent` tool contract (fresh-session workers, deployment-pinned worker model).
- Configuration:
  - Schemastery `Config` schema; `shift-router` settings namespace.
  - Editable live via the GUI settings panel and `/router config set|set-fast|set-smart|reset` (persisted).
- Commands: `/router` (status/stats/on/off/quiet/verbose/orchestrate/config) and `/route-force`.
- Cost telemetry: per-tier tokens/throughput and optional pricing table; savings vs. all-turns-on-Smart baseline.
- Tests: 52 unit tests (routing engine, failover, judge parsing, orchestration) + credential-free headless e2e (fake adapter, settings persistence probe).
