diff --git a/.buildkite/pipeline.yml b/.buildkite/pipeline.yml deleted file mode 100644 index 98203829ac..0000000000 --- a/.buildkite/pipeline.yml +++ /dev/null @@ -1,52 +0,0 @@ -# Buildkite pipeline for Codewhale. -# -# Why this file exists: the `codewhale-ci` pipeline's steps lived in the -# Buildkite UI and ran a 67-second smoke test (`cargo fmt` plus two focused -# crates). That is not this repository's CI, so a green Buildkite build proved -# almost nothing. Steps belong in the repo, reviewable in the same diff as the -# code they gate. -# -# Scope, stated honestly: this covers the Linux and macOS legs only. Buildkite -# hosted agents are Linux and macOS; there is no hosted Windows. `Test -# (windows-latest)` in .github/workflows/ci.yml runs the installer PATH helper -# and an NSIS installer regression on a product that ships a Windows installer, -# so that leg must stay on GitHub Actions until a Windows agent exists. Do not -# read a green build here as full platform coverage. -# -# This pipeline is advisory until its checks are proven green on real pull -# requests. The required checks in the `protect-main` ruleset are still the -# GitHub Actions contexts; moving them is a separate, deliberate change. - -env: - CARGO_TERM_COLOR: always - CARGO_INCREMENTAL: "0" - # Matches .github/workflows/ci.yml: the product's thread stack, not a default. - RUST_MIN_STACK: "16777216" - -steps: - - group: ":rust: Linux" - key: linux - steps: - - label: ":rust: fmt + clippy" - key: lint - agents: - queue: linux-large - timeout_in_minutes: 45 - command: .buildkite/steps/lint.sh - - - label: ":rust: workspace tests" - key: test-linux - agents: - queue: linux-large - timeout_in_minutes: 90 - command: .buildkite/steps/test.sh - - - group: ":apple: macOS" - key: macos - steps: - - label: ":apple: workspace tests" - key: test-macos - agents: - queue: macos-large - timeout_in_minutes: 90 - command: .buildkite/steps/test.sh diff --git a/.buildkite/steps/common.sh b/.buildkite/steps/common.sh deleted file mode 100644 index 7f60f2f8df..0000000000 --- a/.buildkite/steps/common.sh +++ /dev/null @@ -1,51 +0,0 @@ -#!/usr/bin/env bash -# Shared setup for Buildkite steps. Sourced, not executed. -set -euo pipefail - -# Hosted Linux agents are bare containers; macOS agents ship Xcode and brew. -# Only install what the workspace actually links against (dbus, via the -# keyring/secret-store path) so a step failure is about the code, not apt. -if [ "$(uname -s)" = "Linux" ]; then - SUDO="" - command -v sudo >/dev/null 2>&1 && SUDO="sudo" - for i in 1 2 3 4 5; do - $SUDO apt-get update && break - echo "apt-get update failed (attempt $i); retrying in 15s" >&2 - sleep 15 - done - $SUDO apt-get install -y --no-install-recommends \ - ca-certificates curl pkg-config libdbus-1-dev build-essential -fi - -# The Linux job runs as root, and test.sh re-execs the suite as an -# unprivileged user (root ignores permission bits, which silently defeats every -# read-only assertion). A toolchain under /root is unreadable after that swap -- -# build 1445 got `cargo: command not found` at uid 1000 -- so install it -# somewhere both users can reach. -if [ "$(id -u)" = "0" ] && [ "$(uname -s)" = "Linux" ]; then - export CARGO_HOME=/opt/cargo - export RUSTUP_HOME=/opt/rustup -else - export CARGO_HOME="${CARGO_HOME:-$HOME/.cargo}" - export RUSTUP_HOME="${RUSTUP_HOME:-$HOME/.rustup}" -fi -export PATH="$CARGO_HOME/bin:$PATH" - -# rust-toolchain.toml pins `stable`; rustup honours it on first cargo call. -if ! command -v cargo >/dev/null 2>&1; then - curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \ - | sh -s -- -y --profile minimal --default-toolchain stable --no-modify-path -fi -# shellcheck disable=SC1091 -[ -f "$CARGO_HOME/env" ] && . "$CARGO_HOME/env" -export PATH="$CARGO_HOME/bin:$PATH" - -# Readable+traversable by the unprivileged user; cargo still needs to write -# into CARGO_HOME for `cargo install`, so that stays root-owned until test.sh -# hands it over. -if [ "$(id -u)" = "0" ] && [ "$(uname -s)" = "Linux" ]; then - chmod -R a+rX "$CARGO_HOME" "$RUSTUP_HOME" 2>/dev/null || true -fi - -cargo --version -rustc --version diff --git a/.buildkite/steps/lint.sh b/.buildkite/steps/lint.sh deleted file mode 100755 index 1017ce8b0d..0000000000 --- a/.buildkite/steps/lint.sh +++ /dev/null @@ -1,20 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail -cd "$(dirname "$0")/../.." -# shellcheck source=/dev/null -. .buildkite/steps/common.sh - -rustup component add rustfmt clippy - -echo "--- cargo fmt" -cargo fmt --all -- --check - -# The allow list is copied verbatim from .github/workflows/ci.yml. Keep the two -# in step: a lint that is denied there and allowed here makes this pipeline a -# weaker gate that still reports green. -echo "--- cargo clippy" -cargo clippy --workspace --all-targets --all-features --locked -- \ - -D warnings \ - -A clippy::uninlined_format_args \ - -A clippy::too_many_arguments \ - -A clippy::unnecessary_map_or diff --git a/.buildkite/steps/test.sh b/.buildkite/steps/test.sh deleted file mode 100755 index bfb87c1a9c..0000000000 --- a/.buildkite/steps/test.sh +++ /dev/null @@ -1,58 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail -cd "$(dirname "$0")/../.." -# shellcheck source=/dev/null -. .buildkite/steps/common.sh - -echo "--- hermetic test-home boundary" -sh scripts/with-hermetic-test-home.test.sh - -# nextest profile `ci` lives in .config/nextest.toml alongside the test-group -# bounds that serialize the binary-spawning integration suites. -if ! command -v cargo-nextest >/dev/null 2>&1; then - cargo install cargo-nextest --locked --version 0.9.* || cargo install cargo-nextest --locked -fi - -# Hosted Linux agents run the job as root. That is not equivalent to GitHub's -# `runner` user: root ignores permission bits, so every test that makes a path -# read-only and asserts the write is refused instead *succeeds* at writing and -# fails the assertion. Build 1443 failed exactly four tests this way -- -# an_unwritable_home_reports_the_failure_and_still_answers, -# contract_edit_rejects_read_only_target_before_atomic_replace, -# failed_apply_rolls_back_to_the_prior_document, and one fleet executor case -- -# none of which are product defects. -# -# Drop to an unprivileged user rather than skipping them: those tests guard -# data-loss and permission behaviour, and a CI lane that silently cannot -# exercise them is a weaker gate reporting green. -run_suite() { - echo "--- workspace tests" - scripts/with-hermetic-test-home.sh cargo nextest run --workspace --all-features --locked --profile ci - echo "--- doctests" - scripts/with-hermetic-test-home.sh cargo test --workspace --all-features --locked --doc -} - -if [ "$(id -u)" = "0" ] && [ "$(uname -s)" = "Linux" ]; then - id -u builder >/dev/null 2>&1 || useradd -m -s /bin/bash builder - # cargo writes into CARGO_HOME (registry, git checkouts) and ./target, so - # both must belong to the user that will actually run the suite. - chown -R builder:builder . "$CARGO_HOME" "$RUSTUP_HOME" 2>/dev/null || true - echo "--- re-exec as unprivileged user (root ignores permission bits)" - # The suite is expanded by the unprivileged child shell. - # shellcheck disable=SC2016 - exec runuser -u builder -- env \ - HOME=/home/builder \ - PATH="$PATH" CARGO_HOME="$CARGO_HOME" RUSTUP_HOME="$RUSTUP_HOME" \ - CARGO_TERM_COLOR="${CARGO_TERM_COLOR:-always}" \ - CARGO_INCREMENTAL="${CARGO_INCREMENTAL:-0}" \ - RUST_MIN_STACK="${RUST_MIN_STACK:-16777216}" \ - bash -eo pipefail -c ' - cd "$1" - echo "--- workspace tests (uid $(id -u))" - scripts/with-hermetic-test-home.sh cargo nextest run --workspace --all-features --locked --profile ci - echo "--- doctests" - scripts/with-hermetic-test-home.sh cargo test --workspace --all-features --locked --doc - ' _ "$PWD" -fi - -run_suite diff --git a/.cargo/audit.toml b/.cargo/audit.toml index 22c2fbd088..1ac44e2ee2 100644 --- a/.cargo/audit.toml +++ b/.cargo/audit.toml @@ -1,21 +1,5 @@ -# cargo-audit configuration (read by `cargo audit`). -# -# The advisories below are all "unmaintained" warnings — NOT security -# vulnerabilities. Each crate is pulled in transitively only by the `starlark` -# 0.13.0 family (starlark / starlark_syntax / starlark_map), which crates/tui -# depends on directly for Starlark execpolicy files. -# `cargo tree -i ` confirms starlark is the sole path for each. -# -# There is no fix available without an upstream `starlark` release that drops -# these deps, and none is exploitable here. They are accepted for now and -# tracked in this file so `cargo audit` stays clean for genuinely new advisories. -# Remove an entry once a starlark upgrade/removal drops the transitive dep -# (re-check with `cargo tree -i derivative` and `cargo audit`). -# -# Audit #11, scratchpad/bug-audit-2026-06-24.md. +# No suppressions: the old Starlark dependencies are absent from Cargo.lock. +# Keep remaining maintenance warnings visible in cargo-audit. The separately +# scoped cargo-deny exception is explained in docs/dependency-maintenance.md. [advisories] -ignore = [ - "RUSTSEC-2024-0388", # derivative 2.2.0 unmaintained — transitive via starlark 0.13.0 - "RUSTSEC-2025-0057", # fxhash 0.2.1 unmaintained — transitive via starlark_map 0.13.0 - "RUSTSEC-2024-0436", # paste 1.0.15 unmaintained — transitive via starlark 0.13.0 -] +ignore = [] diff --git a/.cnb.yml b/.cnb.yml index fe7aa4bb1d..42cc91d71a 100644 --- a/.cnb.yml +++ b/.cnb.yml @@ -34,20 +34,12 @@ cargo fmt --all -- --check cargo check --workspace --all-targets --locked cargo clippy --workspace --all-targets --all-features --locked -- -D warnings - # Hermetic HOME so libtest's shared process cannot see a populated - # ~/.codewhale/config.toml (#5355 config-fixture family). Tests stay in - # the suite; isolation is scheduling, not deletion. - hermetic_home="${TMPDIR:-/tmp}/cw-cnb-hermetic-home-$$" - mkdir -p "${hermetic_home}/.codewhale" - export HOME="${hermetic_home}" - export USERPROFILE="${hermetic_home}" - export CODEWHALE_HOME="${hermetic_home}/.codewhale" - unset CODEWHALE_CONFIG_PATH DEEPSEEK_CONFIG_PATH DEEPSEEK_HOME || true - RUST_MIN_STACK=16777216 cargo test --workspace --all-features --locked + # Use the shared test HOME boundary while retaining the real toolchain. + RUST_MIN_STACK=16777216 sh scripts/with-hermetic-test-home.sh cargo test --workspace --all-features --locked # Parity gates as first-class steps so drift surfaces as a named failure, # not a buried workspace-test entry. Mirrors release.yml's parity job. - cargo test -p codewhale-protocol --test parity_protocol --locked - cargo test -p codewhale-state --test parity_state --locked + sh scripts/with-hermetic-test-home.sh cargo test -p codewhale-protocol --test parity_protocol --locked + sh scripts/with-hermetic-test-home.sh cargo test -p codewhale-state --test parity_state --locked .linux_rust_gates: &linux_rust_gates name: linux rust gates @@ -183,7 +175,7 @@ $: apt-get install -y git musl-tools nodejs pkg-config rustup target add x86_64-unknown-linux-musl - ./scripts/release/check-versions.sh + ./scripts/release/check-versions.sh --require-dated-release ./scripts/release/check-ohos-deps.sh checkout_sha="$(git rev-parse 'HEAD^{commit}')" commit_sha="${CNB_COMMIT:-${checkout_sha}}" diff --git a/.gitattributes b/.gitattributes index 9e68378570..a7804b888f 100644 --- a/.gitattributes +++ b/.gitattributes @@ -39,3 +39,6 @@ scripts/release/*.sh text eol=lf # Everything else auto-detects (default). * text=auto + +# Preserve the qualified upstream font license, including its trailing space. +web/public/brand/fonts/OFL.txt text eol=lf whitespace=-blank-at-eol diff --git a/.gitguardian.yml b/.gitguardian.yml new file mode 100644 index 0000000000..9896da692f --- /dev/null +++ b/.gitguardian.yml @@ -0,0 +1,5 @@ +# This exact fixture is a deliberately public TEST-ONLY signing key. +# It is never a production trust anchor and signs only local test payloads. +# No other key files are excluded. +paths-ignore: + - docs/cloud-facts/fixtures/test-only-signing-key.pem diff --git a/.github/APPROVED_CONTRIBUTORS b/.github/APPROVED_CONTRIBUTORS index d95ffe62ce..7d21a401cf 100644 --- a/.github/APPROVED_CONTRIBUTORS +++ b/.github/APPROVED_CONTRIBUTORS @@ -72,3 +72,4 @@ all:M-Maciej all:serephus all:Pinvou all:SparkofSpike +all:goransh-walia diff --git a/.github/scripts/release-workflows.test.js b/.github/scripts/release-workflows.test.js index 8d93b4be0b..b6515bdd40 100755 --- a/.github/scripts/release-workflows.test.js +++ b/.github/scripts/release-workflows.test.js @@ -484,15 +484,36 @@ const cnbRustGates = cnb.match( assert.ok(cnbRustGates, "CNB must retain the shared Rust workspace gate"); assert.match( cnbRustGates[1], - /timeout: 45m[\s\S]*export CARGO_BUILD_JOBS=1[\s\S]*export CARGO_PROFILE_TEST_DEBUG=0[\s\S]*cargo check --workspace --all-targets --locked[\s\S]*cargo clippy --workspace --all-targets --all-features --locked -- -D warnings[\s\S]*RUST_MIN_STACK=16777216 cargo test --workspace --all-features --locked/, + /timeout: 45m[\s\S]*export CARGO_BUILD_JOBS=1[\s\S]*export CARGO_PROFILE_TEST_DEBUG=0[\s\S]*cargo check --workspace --all-targets --locked[\s\S]*cargo clippy --workspace --all-targets --all-features --locked -- -D warnings[\s\S]*RUST_MIN_STACK=16777216 sh scripts\/with-hermetic-test-home.sh cargo test --workspace --all-features --locked/, "CNB must serialize the memory-heavy Rust gate and preserve the workspace test stack contract", ); -assert.match( +assert.doesNotMatch( cnbRustGates[1], - /export HOME="\$\{hermetic_home\}"[\s\S]*export CODEWHALE_HOME="\$\{hermetic_home\}\/\.codewhale"[\s\S]*unset CODEWHALE_CONFIG_PATH DEEPSEEK_CONFIG_PATH DEEPSEEK_HOME/, - "CNB workspace tests must not read a populated runner ~/.codewhale (#5355)", + /export (?:HOME|USERPROFILE|CODEWHALE_HOME)=/, + "CNB must reuse the shared test-home boundary without overriding legacy migration fixtures", ); +// Cover every test invocation, including named parity and narrow crate gates. +// These launchers protect production dependencies as well as cfg(test) code. +let hermeticInvocations = 0; +for (const [label, workflow, expected] of [["CI", ci, 5], ["release", release, 3], ["CNB", cnb, 3]]) { + const commands = workflow.split("\n").filter((line) => + !line.trimStart().startsWith("#") && /\bcargo (?:test|nextest run)\b/.test(line), + ); + assert.equal(commands.length, expected, `${label} must retain every Rust test invocation`); + for (const command of commands) { + assert.match(command, /sh scripts\/with-hermetic-test-home.sh cargo (?:test|nextest run)\b/, + `${label} Rust tests must use the shared test-home boundary`); + } + hermeticInvocations += commands.length; +} +for (const name of ["Run tests", "Run doctests"]) { + const step = namedStep(ciTestJob, name); + assert.match(step, /shell: bash/, `${name} must invoke the POSIX helper on Windows too`); + assert.match(step, /RUST_MIN_STACK: '16777216'/); +} +console.log(`Hermetic Rust workflow invocations OK: ${hermeticInvocations} checks passed.`); + const nextest = read(".config/nextest.toml"); const integrationGroup = nextest.search(/^filter = 'binary\(integration\)'$/m); const telemetryGroup = nextest.indexOf( @@ -540,6 +561,11 @@ const cnbTagStamp = cnbTagRelease[1].indexOf( const cnbTagBuild = cnbTagRelease[1].indexOf( "cargo build --jobs 2 --release --locked \\", ); +const cnbTagVersionCheck = cnbTagRelease[1].indexOf( + "./scripts/release/check-versions.sh --require-dated-release", +); +assert.ok(cnbTagVersionCheck >= 0, "CNB publication must reject undated source candidates"); +assert.ok(cnbTagVersionCheck < cnbTagBuild, "CNB must validate release notes before building public assets"); assert.match(cnbTagRelease[1], /checkout_sha="\$\(git rev-parse 'HEAD\^\{commit\}'\)"/); assert.match(cnbTagRelease[1], /commit_sha="\$\{CNB_COMMIT:-\$\{checkout_sha\}\}"/); assert.match(cnbTagRelease[1], /CNB_COMMIT[\s\S]*does not match checkout[\s\S]*exit 1/); diff --git a/.github/scripts/update-homebrew-tap.sh b/.github/scripts/update-homebrew-tap.sh index f93e1e9c4c..4119b5354b 100755 --- a/.github/scripts/update-homebrew-tap.sh +++ b/.github/scripts/update-homebrew-tap.sh @@ -72,6 +72,7 @@ class ${class_name} < Formula homepage "https://github.com/Hmbown/CodeWhale" version "${VERSION}" license "MIT" + depends_on "node" ${extra_header} on_macos do if Hardware::CPU.arm? diff --git a/.github/scripts/update-homebrew-tap.test.sh b/.github/scripts/update-homebrew-tap.test.sh index c06ada3ef4..e18a848e1d 100755 --- a/.github/scripts/update-homebrew-tap.test.sh +++ b/.github/scripts/update-homebrew-tap.test.sh @@ -37,6 +37,7 @@ grep -Fq 'class Codewhale < Formula' "${formula}" grep -Fq 'class DeepseekTui < Formula' "${legacy}" grep -Fq 'deprecate! date: "2026-08-14", because: "renamed to codewhale"' "${legacy}" grep -Fq 'desc "Agentic terminal for open-source and open-weight coding models"' "${formula}" +grep -Fq 'depends_on "node"' "${formula}" test "$(grep -Fc 'resource "codew" do' "${formula}")" -eq 4 grep -Fq 'bin.install Dir["*"].first => "codew"' "${formula}" grep -Fq 'system "#{bin}/codew", "--version"' "${formula}" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e9104aeb17..2f6107b10b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -217,6 +217,7 @@ jobs: bash scripts/release/verify-remote-tag.test.sh bash packaging/aur/render.test.sh sh scripts/dev-cache.test.sh + sh scripts/with-hermetic-test-home.test.sh bash .github/scripts/update-homebrew-tap.test.sh node .github/scripts/release-workflows.test.js node --test scripts/release/assemble-release-assets.test.js @@ -276,6 +277,8 @@ jobs: # `tsc` ever running in CI. `npm test` compiles first (tsc -p ./), so # this is the type-check gate for the extension too. run: npm test + - name: Package VS Code extension + run: npm run package safety-gate: name: Safety gate @@ -312,15 +315,10 @@ jobs: save-if: ${{ github.ref == 'refs/heads/main' }} - name: Hermetic safety and authorization tests env: - HOME: ${{ runner.temp }}/cw-hermetic-home - USERPROFILE: ${{ runner.temp }}/cw-hermetic-home - CODEWHALE_HOME: ${{ runner.temp }}/cw-hermetic-home/.codewhale RUST_MIN_STACK: "8388608" run: | - mkdir -p "${HOME}" "${CODEWHALE_HOME}" - unset CODEWHALE_CONFIG_PATH DEEPSEEK_CONFIG_PATH DEEPSEEK_HOME || true - cargo test -p codewhale-tui --lib --locked -- command_safety auto_review authority sandbox - cargo test -p codewhale-execpolicy --locked + sh scripts/with-hermetic-test-home.sh cargo test -p codewhale-tui --lib --locked -- command_safety auto_review authority sandbox + sh scripts/with-hermetic-test-home.sh cargo test -p codewhale-execpolicy --locked lint: name: Lint @@ -513,7 +511,7 @@ jobs: cache-bin: false save-if: ${{ github.ref == 'refs/heads/main' }} - name: Run workflow crate tests - run: cargo test -p codewhale-workflow --locked + run: sh scripts/with-hermetic-test-home.sh cargo test -p codewhale-workflow --locked test: name: Test @@ -620,8 +618,13 @@ jobs: # executable; retries are off, so a flake is a red run, not a hidden # one. nextest does not run doctests — the next step keeps them. if: needs.changes.outputs.heavy == 'true' && (matrix.os != 'ubuntu-latest' || github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request') - run: cargo nextest run --workspace --all-features --locked --profile ci + shell: bash + run: sh scripts/with-hermetic-test-home.sh cargo nextest run --workspace --all-features --locked --profile ci env: + # sccache 0.17 panics resolving its config directory under the + # isolated Windows home before Cargo can compile or run any test. + # Bypass only that optional cache; keep the full suite and isolation. + RUSTC_WRAPPER: ${{ matrix.os != 'windows-latest' && env.RUSTC_WRAPPER || '' }} # Give test threads the stack the product gives itself. main.rs runs # the owner thread and every tokio worker at # CODEWHALE_MAIN_STACK_BYTES (16 MiB) because the engine and @@ -637,8 +640,10 @@ jobs: RUST_MIN_STACK: '16777216' - name: Run doctests if: needs.changes.outputs.heavy == 'true' && (matrix.os != 'ubuntu-latest' || github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request') - run: cargo test --workspace --all-features --locked --doc + shell: bash + run: sh scripts/with-hermetic-test-home.sh cargo test --workspace --all-features --locked --doc env: + RUSTC_WRAPPER: ${{ matrix.os != 'windows-latest' && env.RUSTC_WRAPPER || '' }} RUST_MIN_STACK: '16777216' # The Ubuntu lint lane validates non-RSS backlog fields. Run the same # source-bound measurement on macOS so loss or growth of RSS evidence diff --git a/.github/workflows/codewhale-review.yml b/.github/workflows/codewhale-review.yml index 4749b8b2e2..a387431c7f 100644 --- a/.github/workflows/codewhale-review.yml +++ b/.github/workflows/codewhale-review.yml @@ -5,49 +5,21 @@ name: Codewhale PR Review # line comments, anchored to the PR head SHA. CODEOWNERS (@Hmbown) stays the # human owner — this review posts alongside it and never approves. # -# Setup — full step-by-step guide in docs/GITHUB_APP.md. Summary: -# 1. Key: Settings -> Secrets and variables -> Actions -> New repository -# secret `CODEWHALE_API_KEY`. This is the canonical name: it is your -# Codewhale account's review key, not any one vendor's. The workflow maps -# it into whatever env var the configured provider expects. -# BYOK alternative: set the provider's own key instead -# (`ZAI_API_KEY`, `DEEPSEEK_API_KEY`, `OPENROUTER_API_KEY`, -# `ANTHROPIC_API_KEY`); any one of them is enough. -# 2. Which agent runs the review: repository variables -# `CODEWHALE_REVIEW_PROVIDER` (e.g. `zai`) and `CODEWHALE_REVIEW_MODEL` -# (e.g. `GLM-5.3`). Both optional — see "Route selection" below. -# 3. Optional identity: to post as the Codewhale Agent GitHub App instead -# of the workflow's github-token identity, set repository variable -# `CODEWHALE_APP_ID` and secret `CODEWHALE_APP_PRIVATE_KEY`; the job -# mints an installation token via actions/create-github-app-token. -# 4. Optional output budget: repository variable -# `CODEWHALE_REVIEW_MAX_OUTPUT_TOKENS` — see "Output budget" below. -# Until at least one accepted key exists the job no-ops with a notice (stays -# green), so this workflow is safe to merge before it is configured. +# Setup: docs/GITHUB_APP.md. CODEWHALE_API_KEY is an account machine key; +# it stays on the Codewhale relay and is never copied into a vendor variable. +# Account mode requires an explicit account-catalog provider/model id in +# CODEWHALE_REVIEW_MODEL. BYOK uses the provider's own key unchanged. # -# Actions-syntax note (why the `env:` hoist below exists): -# the `secrets` context is NOT available in a job-level `if:`. It IS -# available in a job-level `env:`, and step-level `if:` can read the `env` -# context. So the key-presence test is evaluated once into -# `env.HAS_ANY_KEY` at job scope and every step gates on that string. -# Only non-secret booleans live at job scope; the key values themselves are -# injected into the single step that needs them. +# Only same-repository PRs receive model/App secrets or execute the candidate +# build. Fork PRs fetch objects against a trusted base checkout, but do not run +# a model review. Keep this pull_request event: a PR must not gain secrets by +# being fetched for its diff. Same-repository authors already have write access. # -# Route selection: -# `CODEWHALE_REVIEW_PROVIDER` is passed straight through as -# `codewhale review --provider `, which pins the route. Without it a -# model offered by more than one configured route hard-errors -# ("available from configured provider route(s): openrouter, zai"). When -# the variable is unset the provider is inferred from which key is present. -# -# Output budget: -# GLM-5.3 is a reasoning model: it emits `reasoning_content` before -# `content`, and both are charged against `max_tokens`. A small cap -# therefore yields an EMPTY review rather than an error. The CLI's -# automatic cap (64K) is already generous, so this workflow sets no cap by -# default; `CODEWHALE_REVIEW_MAX_OUTPUT_TOKENS` can override it but is -# rejected below a floor that leaves no room for the answer. The run step -# also fails loudly on a zero-length review instead of reporting success. +# CODEWHALE_REVIEW_MAX_CHARS bounds each complete ordered review pass +# (default 200000). CODEWHALE_REVIEW_MAX_PASSES defaults to 1; increasing it is +# an explicit spend/duration choice. Exceeding either bound never posts a prefix. +# Missing keys and provider outages keep the existing advisory policy, and +# their explicit non-run receipts must never be counted as completed reviews. on: pull_request: @@ -66,7 +38,7 @@ jobs: env: # `secrets` is unavailable in a job-level `if:` but allowed here; these # are booleans about presence, never key material. - HAS_ANY_KEY: ${{ secrets.CODEWHALE_API_KEY != '' || secrets.ZAI_API_KEY != '' || secrets.MODELSTUDIO_API_KEY != '' || secrets.DEEPSEEK_API_KEY != '' || secrets.OPENROUTER_API_KEY != '' || secrets.ANTHROPIC_API_KEY != '' }} + HAS_ANY_KEY: ${{ github.event.pull_request.head.repo.full_name == github.repository && (secrets.CODEWHALE_API_KEY != '' || secrets.ZAI_API_KEY != '' || secrets.MODELSTUDIO_API_KEY != '' || secrets.DEEPSEEK_API_KEY != '' || secrets.OPENROUTER_API_KEY != '' || secrets.ANTHROPIC_API_KEY != '') }} HAS_APP_KEY: ${{ secrets.CODEWHALE_APP_PRIVATE_KEY != '' }} permissions: contents: read @@ -80,13 +52,48 @@ jobs: - name: Skip when no review key is configured if: env.HAS_ANY_KEY != 'true' run: | - echo "::notice::No Codewhale review key is set — skipping. Add repository secret CODEWHALE_API_KEY (or a provider key: ZAI_API_KEY / MODELSTUDIO_API_KEY / DEEPSEEK_API_KEY / OPENROUTER_API_KEY / ANTHROPIC_API_KEY) to enable it." + echo "::notice::No Codewhale review ran: model secrets are unavailable or this is a fork PR. Configure a review key for same-repository PRs; review fork PRs separately with a trusted build." + echo "Codewhale review: not run (no eligible review credentials; this is not a clean-review result)." >> "$GITHUB_STEP_SUMMARY" - - name: Checkout repository - if: env.HAS_ANY_KEY == 'true' + - name: Checkout pinned review source uses: actions/checkout@v7 with: - fetch-depth: 1 + ref: ${{ github.event.pull_request.head.repo.full_name == github.repository && github.event.pull_request.head.sha || github.event.pull_request.base.sha }} + fetch-depth: 0 + persist-credentials: false + + - name: Make exact PR diff objects available + env: + GH_TOKEN: ${{ github.token }} + PR_NUMBER: ${{ github.event.pull_request.number }} + PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }} + PR_BASE_SHA: ${{ github.event.pull_request.base.sha }} + REVIEW_SOURCE_SHA: ${{ github.event.pull_request.head.repo.full_name == github.repository && github.event.pull_request.head.sha || github.event.pull_request.base.sha }} + run: | + set -euo pipefail + [[ "$PR_NUMBER" =~ ^[1-9][0-9]*$ ]] || { echo "::error::Invalid PR number"; exit 1; } + for SHA in "$PR_HEAD_SHA" "$PR_BASE_SHA" "$REVIEW_SOURCE_SHA"; do + [[ "$SHA" =~ ^([0-9a-fA-F]{40}|[0-9a-fA-F]{64})$ ]] || { echo "::error::Invalid pinned commit"; exit 1; } + done + [ "$(git rev-parse HEAD)" = "$REVIEW_SOURCE_SHA" ] + [ "$(git rev-parse --is-shallow-repository)" = false ] + # Fetch objects via the base repository's PR ref. Do not check out + # the fetched head, initialize submodules, or run PR hooks/filters. + git -c core.hooksPath=/dev/null -c credential.helper= -c 'credential.helper=!gh auth git-credential' \ + fetch --no-tags --no-recurse-submodules origin \ + "+refs/pull/${PR_NUMBER}/head:refs/codewhale-review/head" + [ "$(git rev-parse 'refs/codewhale-review/head^{commit}')" = "$PR_HEAD_SHA" ] || { + echo "::error::PR head changed during checkout; rerun for the current revision." + exit 1 + } + git cat-file -e "${PR_BASE_SHA}^{commit}" + git cat-file -e "${PR_HEAD_SHA}^{commit}" + MERGE_BASE=$(git merge-base --all "$PR_BASE_SHA" "$PR_HEAD_SHA") + [[ "$MERGE_BASE" =~ ^([0-9a-fA-F]{40}|[0-9a-fA-F]{64})$ ]] || { + echo "::error::The pinned PR commits do not have one available merge base." + exit 1 + } + [ "$(git rev-parse HEAD)" = "$REVIEW_SOURCE_SHA" ] - name: Mint Codewhale Agent app token if: env.HAS_ANY_KEY == 'true' && env.HAS_APP_KEY == 'true' && vars.CODEWHALE_APP_ID != '' @@ -116,16 +123,15 @@ jobs: - name: Build codewhale if: env.HAS_ANY_KEY == 'true' - run: cargo build --release -p codewhale-cli + run: cargo build --release --locked -p codewhale-cli - name: Run Codewhale PR review if: env.HAS_ANY_KEY == 'true' env: GH_TOKEN: ${{ steps.app-token.outputs.token || github.token }} - # Canonical Codewhale account key. Mapped below into whichever - # provider env var the configured route expects. + # Account machine key, consumed only by the existing account/relay path. CODEWHALE_API_KEY: ${{ secrets.CODEWHALE_API_KEY }} - # BYOK fallbacks: a provider key used directly, no mapping needed. + # Provider credentials remain separate and are never overwritten. ZAI_API_KEY: ${{ secrets.ZAI_API_KEY }} # Alibaba Model Studio Token Plan (DeepSeek V4 Pro / Qwen 3.8 on the # founder's credit); all Model Studio kinds read MODELSTUDIO_API_KEY. @@ -136,66 +142,60 @@ jobs: CODEWHALE_REVIEW_PROVIDER: ${{ vars.CODEWHALE_REVIEW_PROVIDER }} CODEWHALE_REVIEW_MODEL: ${{ vars.CODEWHALE_REVIEW_MODEL }} CODEWHALE_REVIEW_MAX_OUTPUT_TOKENS: ${{ vars.CODEWHALE_REVIEW_MAX_OUTPUT_TOKENS }} + CODEWHALE_REVIEW_MAX_CHARS: ${{ vars.CODEWHALE_REVIEW_MAX_CHARS }} + CODEWHALE_REVIEW_MAX_PASSES: ${{ vars.CODEWHALE_REVIEW_MAX_PASSES }} PR_NUMBER: ${{ github.event.pull_request.number }} run: | - set -uo pipefail + set -euo pipefail - # --- Which agent reviews this PR ------------------------------- - # Explicit repository variable wins. Otherwise: the canonical - # Codewhale key defaults to the z.ai Coding Plan route (the route - # verified end-to-end), and a BYOK-only repo gets the provider whose - # key it actually set. + # Account mode uses the existing machine precondition and Codewhale + # model relay. A configured account provider is not a vendor key. PROVIDER="${CODEWHALE_REVIEW_PROVIDER:-}" - if [ -z "$PROVIDER" ]; then - if [ -n "${CODEWHALE_API_KEY:-}" ]; then PROVIDER=zai - elif [ -n "${ZAI_API_KEY:-}" ]; then PROVIDER=zai - elif [ -n "${MODELSTUDIO_API_KEY:-}" ]; then PROVIDER=modelstudio-token-plan - elif [ -n "${DEEPSEEK_API_KEY:-}" ]; then PROVIDER=deepseek - elif [ -n "${OPENROUTER_API_KEY:-}" ]; then PROVIDER=openrouter - elif [ -n "${ANTHROPIC_API_KEY:-}" ]; then PROVIDER=anthropic - fi - fi - if [ -z "$PROVIDER" ]; then - echo "::error::No review key resolved to a provider. This should be unreachable (HAS_ANY_KEY was true)." - exit 1 - fi - - # --- Map the canonical key onto that provider's env var --------- - # Names only are ever printed; values never are. - KEY_VAR="" - case "$PROVIDER" in - zai|z-ai|zhipu|glm) KEY_VAR=ZAI_API_KEY ;; - deepseek|deepseek-cn) KEY_VAR=DEEPSEEK_API_KEY ;; - openrouter) KEY_VAR=OPENROUTER_API_KEY ;; - anthropic|claude) KEY_VAR=ANTHROPIC_API_KEY ;; - *) - # No mapping for a custom provider. With an account key set that - # is a real misconfiguration (hard error below). BYOK-only repos - # are fine — the provider's own secret is used directly — so say - # so without a red ::error:: annotation on a correct config. - if [ -z "${CODEWHALE_API_KEY:-}" ]; then - echo "::warning::CODEWHALE_REVIEW_PROVIDER='${PROVIDER}' has no CODEWHALE_API_KEY mapping in this workflow, and none is needed: no account key is set, so the provider's own BYOK secret is used directly." - else - echo "::error::CODEWHALE_REVIEW_PROVIDER='${PROVIDER}' has no CODEWHALE_API_KEY mapping in this workflow. Set that provider's own key as a repository secret, or add the mapping here." - fi - KEY_VAR="" - ;; - esac + MODEL="${CODEWHALE_REVIEW_MODEL:-}" if [ -n "${CODEWHALE_API_KEY:-}" ]; then - if [ -z "$KEY_VAR" ]; then + if [ -n "$PROVIDER" ] && [ "$PROVIDER" != codewhale ]; then + echo "::error::With CODEWHALE_API_KEY, set CODEWHALE_REVIEW_PROVIDER=codewhale or leave it unset. Provider keys are never overwritten." + exit 1 + fi + PROVIDER=codewhale + if [[ ! "$MODEL" =~ ^[A-Za-z0-9][A-Za-z0-9._-]*/[^[:space:]]+$ ]]; then + echo "::error::Account review requires CODEWHALE_REVIEW_MODEL as an exact provider/model id from the account catalog." exit 1 fi - # The canonical account key is authoritative for the chosen route. - export "$KEY_VAR=$CODEWHALE_API_KEY" - echo "Review key: CODEWHALE_API_KEY -> ${KEY_VAR} (provider: ${PROVIDER})" + ./target/release/codewhale --no-project-config account agent > /dev/null + echo "Review key: Codewhale account relay (provider: codewhale)" else + if [ -z "$PROVIDER" ]; then + if [ -n "${ZAI_API_KEY:-}" ]; then PROVIDER=zai + elif [ -n "${MODELSTUDIO_API_KEY:-}" ]; then PROVIDER=modelstudio-token-plan + elif [ -n "${DEEPSEEK_API_KEY:-}" ]; then PROVIDER=deepseek + elif [ -n "${OPENROUTER_API_KEY:-}" ]; then PROVIDER=openrouter + elif [ -n "${ANTHROPIC_API_KEY:-}" ]; then PROVIDER=anthropic + fi + fi + if [ -z "$PROVIDER" ]; then + echo "::error::No BYOK review provider is configured." + exit 1 + fi echo "Review key: BYOK provider secret (provider: ${PROVIDER})" fi + MAX_CHARS="${CODEWHALE_REVIEW_MAX_CHARS:-200000}" + if [[ ! "$MAX_CHARS" =~ ^[1-9][0-9]{0,6}$ ]] || [ "$MAX_CHARS" -gt 8388608 ]; then + echo "::error::CODEWHALE_REVIEW_MAX_CHARS must be an integer from 1 to 8388608." + exit 1 + fi + + MAX_PASSES="${CODEWHALE_REVIEW_MAX_PASSES:-1}" + if [[ ! "$MAX_PASSES" =~ ^[1-9][0-9]?$ ]] || [ "$MAX_PASSES" -gt 64 ]; then + echo "::error::CODEWHALE_REVIEW_MAX_PASSES must be an integer from 1 to 64." + exit 1 + fi + echo "Review limits: ${MAX_CHARS} characters per pass, at most ${MAX_PASSES} passes" + # --- Output budget --------------------------------------------- - # GLM-5.3 spends max_tokens on reasoning_content before it emits any - # content, so an undersized cap returns an empty review, not an - # error. Unset means "use the CLI's automatic 64K cap". + # Some models share an output budget between reasoning and content. + # Leave room for the review; unset keeps the CLI's automatic cap. BUDGET="${CODEWHALE_REVIEW_MAX_OUTPUT_TOKENS:-}" if [ -n "$BUDGET" ]; then case "$BUDGET" in @@ -204,7 +204,7 @@ jobs: exit 1 ;; esac if [ "$BUDGET" -lt 8192 ]; then - echo "::error::CODEWHALE_REVIEW_MAX_OUTPUT_TOKENS=${BUDGET} is below the 8192 floor. A reasoning model (GLM-5.3) would spend the whole budget on reasoning_content and return an empty review." + echo "::error::CODEWHALE_REVIEW_MAX_OUTPUT_TOKENS=${BUDGET} is below the 8192 floor. Leave room for reasoning and the final review." exit 1 fi export CODEWHALE_MAX_OUTPUT_TOKENS="$BUDGET" @@ -214,12 +214,15 @@ jobs: fi # --- Run -------------------------------------------------------- - REVIEW_ARGS=(--pr "$PR_NUMBER" --post --provider "$PROVIDER") - if [ -n "${CODEWHALE_REVIEW_MODEL:-}" ]; then - REVIEW_ARGS+=(--model "$CODEWHALE_REVIEW_MODEL") + # Global route/model flags keep the existing CLI resolver on the + # selected credential boundary before the review subcommand starts. + CLI_ARGS=(--no-project-config --provider "$PROVIDER") + if [ -n "$MODEL" ]; then + CLI_ARGS+=(--model "$MODEL") fi + REVIEW_ARGS=(--pr "$PR_NUMBER" --repo "$GITHUB_REPOSITORY" --max-chars "$MAX_CHARS" --max-passes "$MAX_PASSES" --post) set +e - OUTPUT=$(./target/release/codewhale review "${REVIEW_ARGS[@]}" 2>&1) + OUTPUT=$(./target/release/codewhale "${CLI_ARGS[@]}" review "${REVIEW_ARGS[@]}" 2>&1) STATUS=$? set -e echo "$OUTPUT" @@ -244,6 +247,7 @@ jobs: if echo "$OUTPUT" | grep -qE 'LLM error: HTTP (401|402|403|408|429|5[0-9][0-9])'; then REASON=$(echo "$OUTPUT" | grep -oE 'LLM error: HTTP (401|402|403|408|429|5[0-9][0-9])[^"]{0,80}' | head -1) echo "::warning::Codewhale review could not run (${REASON}). The PR is not blocked — provider funding/config is founder-gated." + echo "Codewhale review: not run (provider unavailable; this is not a clean-review result)." >> "$GITHUB_STEP_SUMMARY" # Silence is not success: leave one visible, idempotent note on the # PR so a non-run never passes for a clean review. Only the HTTP # status line is quoted, never the model output. diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8bf880ce79..e68619314e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -170,7 +170,7 @@ jobs: -A clippy::collapsible_if \ -A clippy::assertions_on_constants - name: Workspace tests - run: cargo test --workspace --all-features --locked + run: sh scripts/with-hermetic-test-home.sh cargo test --workspace --all-features --locked env: # Match the CI test lane: test threads get the same stack the product # gives itself (main.rs CODEWHALE_MAIN_STACK_BYTES). See the note in @@ -178,9 +178,9 @@ jobs: # engine/runtime futures on a stack that never ships. RUST_MIN_STACK: '16777216' - name: Protocol schema parity - run: cargo test -p codewhale-protocol --test parity_protocol --locked + run: sh scripts/with-hermetic-test-home.sh cargo test -p codewhale-protocol --test parity_protocol --locked - name: State persistence parity - run: cargo test -p codewhale-state --test parity_state --locked + run: sh scripts/with-hermetic-test-home.sh cargo test -p codewhale-state --test parity_state --locked - name: Lockfile drift guard run: git diff --exit-code -- Cargo.lock diff --git a/.gitignore b/.gitignore index 99d8260948..d5e3cdd06a 100644 --- a/.gitignore +++ b/.gitignore @@ -48,6 +48,8 @@ dist/ *.log # Generated +# Local design critiques and operator receipts do not ship with the product. +/.impeccable/ outputs/ tmp/ backup/ @@ -63,7 +65,6 @@ docs/*.pdf *.cmd !scripts/** !.github/scripts/** -!.buildkite/** !web/public/install.sh !packaging/winget/** test.txt diff --git a/.impeccable/critique/2026-09-05T00-42-39Z__web-components-getting-started-steps-tsx.md b/.impeccable/critique/2026-09-05T00-42-39Z__web-components-getting-started-steps-tsx.md deleted file mode 100644 index 17240a73a7..0000000000 --- a/.impeccable/critique/2026-09-05T00-42-39Z__web-components-getting-started-steps-tsx.md +++ /dev/null @@ -1,67 +0,0 @@ ---- -target: Website install step -total_score: 21 -max_score: 28 -na_heuristics: 1,3,9 -p0_count: 0 -p1_count: 0 -target_identity: "file:/Volumes/VIXinSSD/CW/worktrees/cw-site-github-install-guide-20260904/web/components/getting-started-steps.tsx" -target_fingerprint: "sha256:f5180f0101eebebe501c90cb467dccca62da8c3ae726a8f1ca59bc2ab5cbe50f" -target_path: /Volumes/VIXinSSD/CW/worktrees/cw-site-github-install-guide-20260904/web/components/getting-started-steps.tsx -timestamp: 2026-09-05T00-42-39Z -slug: web-components-getting-started-steps-tsx -closed: true ---- -Method: dual-agent (A: /root/critique_design · B: /root/critique_detector) - -# Website install step — independent Assessment A - -Scope: only the changed install step in `web/lib/content/getting-started.ts`, rendered by `web/components/getting-started-steps.tsx`. Read the workspace, repository, and `web/AGENTS.md` guidance. No context script, detector, installation, or external request was run. No detector findings were seen. No source files were edited. - -## Verdict - -The copy achieves the intended hierarchy: published GitHub release on macOS/Linux first; `codewhale update` for later releases; npm/Cargo available through the full guide; local development builds explicitly separate. English and Chinese convey the same four facts. No critical copy defect found. This is a successful narrow correction, not an assessment of the complete guide or website. - -The wording is concrete and restrained. It belongs to a technical product and does not need a broader visual redesign. The shared content source preserves consistency between its consumers and avoids a page-specific localization fork. - -## Actual observations - -- Opened the built `/en/docs/guide` and `/zh/docs/guide` in a fresh native CUA Chrome Guest tab. Both displayed the changed paragraph, installer command, `codewhale doctor`, and descriptive localized Full install guide link. -- The complete installer command is present in the accessibility text in both locales. -- In the inspected desktop layout, the new long command extends beyond the narrow first-column code box and requires horizontal scrolling. The visible first line stops partway through the URL; the full release-installer destination and pipe are not visible together. This is contained code-block overflow, not established page-level overflow or lost text. -- Source inspection confirms `
` semantics, `overflow-x: auto` and `white-space: pre`. The renderer offers no copy control. Full keyboard scrolling, text selection, screen-reader announcements, and contrast were not tested.
-- Local installer source selects GitHub release assets and handles Darwin/Linux; local docs show this same installer and the later updater. These are source receipts, not proof of current public downloads or successful installation.
-
-## Priority issue
-
-**P2 — The longer primary install command is awkward to inspect and copy at the rendered desktop width.** The new default command makes the existing horizontal-scroll presentation more consequential: a first-time reader cannot see the complete command in one glance. Make a narrowly scoped improvement for this install command: permit soft wrapping without changing its text, or reuse an existing accessible copy control. If keeping horizontal scrolling, verify keyboard reachability and full-command selection. Do not expand this into a redesign of the four-step grid or the untouched steps.
-
-No P0/P1 issue found within this copy slice. The release/update/alternative/local-build wording needs no additional prerequisite text or warning banner.
-
-## Applicable heuristic scores
-
-| Heuristic | Score | Scope-specific basis |
-|---|---:|---|
-| System status | n/a | Static instruction; no installation was performed. |
-| Match to real world | 4 | Clear platform, release provenance, later-update instruction. |
-| User control/freedom | n/a | No stateful operation in this slice. |
-| Consistency | 4 | Shared EN/ZH content, matching local installer/docs. |
-| Error prevention | 3 | Published release and local development build are distinguished. |
-| Recognition over recall | 2 | Complete command requires horizontal inspection. |
-| Flexibility/efficiency | 2 | Long command has no direct copy action; keyboard scrolling unverified. |
-| Aesthetic/minimalism | 3 | Short, focused paragraph; one primary route with alternatives deferred. |
-| Error recovery | n/a | Installer outcome/recovery is outside the reviewed copy. |
-| Help/documentation | 3 | Clear localized link to deeper installation guidance. |
-| **Total** | **21/28** | **Good; narrow command usability improvement remains.** |
-
-Cognitive load is low for the changed decision: one recommended installation path, one validation command, and one deeper-reading link. Alternatives are named without presenting competing command menus. The long command adds interaction effort rather than conceptual ambiguity.
-
-## Evidence limits
-
-The CUA browser provider was unavailable; native CUA Chrome supplied a fresh Guest tab. EN/ZH desktop screenshots were observed without changing viewport/window size; output image size was 768×930, exact CSS viewport unmeasured. The Guest tab was closed, then browser ownership was returned to Assessment B. No mobile inspection, homepage rendering, external GitHub/install URL validation, install/update execution, broad website audit, or test suite was performed by Assessment A. Untouched steps and full-guide content were not assessed.
-
-Questions skipped: the user already authorized implementation.
-
-## Independent responsive evidence
-
-B ran the narrow markup detector once: exit 0, zero findings. Native Chrome at 390px confirmed EN/ZH command-container overflow beyond the grid, and keyboard Tab reached the localized Full install guide link with a visible outline. Root independently measured the same intrinsic-width problem in CUA: a 343px grid allocated a 382.6px track, extending the command box beyond the viewport. This strengthens A’s scoped P2 without changing its provisional 21/28 score. No P0/P1 established. Both assessments were isolated, and A completed before B findings reached synthesis. No mutable overlay was available; native screenshots and read-only DOM geometry supplied the evidence. No installer or updater was executed.
diff --git a/.impeccable/critique/2026-09-05T01-31-05Z__web-app-locale-page-tsx.md b/.impeccable/critique/2026-09-05T01-31-05Z__web-app-locale-page-tsx.md
deleted file mode 100644
index 1ab5d141e3..0000000000
--- a/.impeccable/critique/2026-09-05T01-31-05Z__web-app-locale-page-tsx.md
+++ /dev/null
@@ -1,49 +0,0 @@
----
-score: 23
-score_max: 32
-p0: 0
-p1: 1
-p2: 2
-p3: 2
-target_identity: "file:/Volumes/VIXinSSD/CW/worktrees/cw-website-app-alignment-20260904/web/app/[locale]/page.tsx"
-target_fingerprint: "sha256:16203671072c07e4e9ea8e679a5fe3482c6d78e6518cb7b53a3f0c18768866b1"
-target_path: /Volumes/VIXinSSD/CW/worktrees/cw-website-app-alignment-20260904/web/app/[locale]/page.tsx
-timestamp: 2026-09-05T01-31-05Z
-slug: web-app-locale-page-tsx
-closed: true
----
-# Integrated website critique and polish
-
-Mode: Persuade (home), Read (guide). Two isolated assessments; A completed
-before B findings were read. A scored 23/32 applicable points. B's one
-homepage detector scan returned zero findings. Native browser evidence
-covered English and Chinese home/product/guide at the available window size;
-reviewers did not independently verify a controlled 390px viewport.
-
-Preserve the illustrated folio, real supplied TUI capture, strong serif
-headline, and explicit release/development/unavailable distinctions.
-
-Priority backlog processed:
-- P1: first-arrival consent focus and forced choice. Removed initial focus,
-  added Close/Escape dismissal without granting, and retained full disclosure
-  behind native details with a short visible summary.
-- P2: dark illustration under hero text. Reserved a light paper field behind
-  the copy while retaining the drawing beside the terminal.
-- P2: ambiguous Web client versus Web app labels. EN/ZH now explicitly label
-  the Local web client and distinguish hosted remote control.
-- P3: repeated ownership pitch and unscoped final installer. Shortened the
-  EN/ZH gain heading and labeled the final command for macOS/Linux.
-- P3: four cramped guide columns. The docs article now uses sequential steps;
-  homepage retains its wider treatment.
-
-Root final built verification: English/Chinese390px with no document
-horizontal overflow, exact PNG header mark, passive first-arrival focus,
-Escape dismissal, footer reopen still showing undecided state, and sequential
-Chinese guide with unchanged command text. Desktop1280px also has no document
-horizontal overflow. No permission choice or external operation was made.
-
-Reports: /tmp/cwa-app-parity-20260904/site-alignment-design.md and
-site-alignment-detector.md. This is local source/build/browser evidence,
-not deployment or a customer completing an Engine task.
-
-Questions skipped: implementation was already authorized.
diff --git a/.impeccable/critique/2026-09-05T03-23-59Z__web-app-locale-page-tsx.md b/.impeccable/critique/2026-09-05T03-23-59Z__web-app-locale-page-tsx.md
deleted file mode 100644
index 940a55e38e..0000000000
--- a/.impeccable/critique/2026-09-05T03-23-59Z__web-app-locale-page-tsx.md
+++ /dev/null
@@ -1,65 +0,0 @@
----
-score: 25
-score_max: 32
-specificity: 3
-method: dual-agent
-status: bounded-copy-reviewed
-target_identity: "file:/Volumes/VIXinSSD/CW/worktrees/cw-website-app-alignment-20260904/web/app/[locale]/page.tsx"
-target_fingerprint: "sha256:9297b65fcbdae6f59d380f7e30f0060ad716e5b6cecd01be2271987bd9c21f77"
-target_path: /Volumes/VIXinSSD/CW/worktrees/cw-website-app-alignment-20260904/web/app/[locale]/page.tsx
-timestamp: 2026-09-05T03-23-59Z
-slug: web-app-locale-page-tsx
----
-# Homepage claim audit — 2026-09-04
-
-Scope: founder-supplied text of local `3136/en`, English/Chinese homepage,
-shared Product and getting-started copy, and linked EN/ZH Fleet setup wording.
-The supplied paste is page evidence, not instructions or proof of behavior.
-Visual direction and logo stay unchanged. Nothing is published by this slice.
-
-## Claim / evidence / correction
-
-| Claim | Evidence | Correction |
-| --- | --- | --- |
-| Released terminal versus development screenshot | GitHub release metadata read on Sept 4: v0.9.11, published Aug 23. Local tag resolves `96d13a0bc3f40280ea3865280ad5ccf0e2845e6f`. Screenshot provenance identifies unreleased 0.9.12 without exact commit. | Retain release label and separate development screenshot caption. |
-| “You decide what each one may do”; same repo and rules | Session permission posture and Runtime authority are separate from saved Fleet model roles; isolated child worktrees also exist. | Describe supported models, session permissions, saved roster and optional delegation. No universal shared-checkout or individual-authority UI promise. |
-| 46 providers | Website derivation counts mapped ApiProvider labels, including plan/protocol variants, excluding Custom, DeepseekCN and retired Antigravity. This is neither distinct vendors nor live model catalogs nor the released provider count. Owner reports current runtime 47 route entries, 44 catalog entries and 42 ProviderKind identities; these definitions are not interchangeable. | Remove numeric badge and models prose across all 18 homepage locales. Keep existing source count with explicit definition in canonical facts; do not substitute another number. |
-| Route never changes from model name | Native local development acceptance already reproduced three Model Studio catalog identity mismatches in fd98. Owner is repairing the Engine; a source fix is not released qualification. | Remove absolute EN/ZH guarantees; ask users to inspect selected provider/model/endpoint. Do not equate catalog identity failure with proven inference misrouting. |
-| Local servers need no key | Authentication depends on server configuration. | Say local servers may require authentication. |
-| Plan is read-only; nothing is written | v0.9.11 `docs/MODES.md` and Engine tool catalog describe central file-mutation/shell refusal, policy-allowed research and separately persisted session settings/state. | Name file/shell boundary and possible external research; remove blanket no-write claim from Product. |
-| `/fleet setup` exact four-step wizard | v0.9.11 `crates/tui/src/tui/views/fleet_setup.rs` resolves `SelectedFleet` or `LegacyProfiles`. Selected named Fleet opens its editor. Legacy path chooses role/model, with optional thinking on review. | Keep command, make step optional and accurately describe both destinations. Shared homepage/guide and EN/ZH Fleet docs updated. No published binary interaction claimed. |
-| Install/auth commands | v0.9.11 source contains auth set provider handling and Fleet setup; GitHub assets include terminal Linux/macOS/Windows archives. | Keep commands and released terminal target statement. No fresh install, key write or provider call performed. |
-| Public web sign-in and same-session remote control available | Source routes and local fixtures exist. This audit has no deployed authenticated same-session task acceptance. | EN/ZH homepage/Product say development preview and explicitly unverified public remote control. |
-| Desktop alpha builds exist on three platforms | Local macOS Dogfood startup has a receipt; Linux/Windows downloadable Tauri artifacts are not established. Windows terminal installer is not Tauri desktop proof. | Describe tested local macOS development build and no released desktop app download. |
-| Nothing on this site can charge you | An absolute statement exceeds evidence and obscures provider billing. | State terminal needs no Codewhale account; provider usage belongs to the user's provider account; account creation does not purchase model access. |
-| Ask/Auto-Review/Full Access absolutes | Approval rules, saved posture and hard policy boundaries affect actual execution. | Product describes rule-based prompts/review and persistent hard boundaries; removes universal every-write and never-default claims. |
-
-## Evidence boundaries
-
-Read-only release metadata and asset inventory:
-`/tmp/cwa-app-parity-20260904/site-release-claims-v0911.json`.
-No release artifact was downloaded or executed during this slice. Git tag source
-is source evidence, not a fresh installed task acceptance. No authenticated
-public web flow, provider request, deployment, billing action or customer task
-was performed. Source/provider integration remains independently qualified by
-its owner.
-
-Numeric models prose is corrected in all homepage languages. Broader hero,
-availability and linked documentation claims outside EN/ZH remain unaudited in
-this slice and must not be treated as reviewed translations. The English and
-Chinese scope is deliberate; a passing locale shape check does not establish
-translation accuracy or truth across the rest of the site.
-
-## Verification
-
-Initial tests: 405 passed, 2 failed (old keyless-launch phrasing assertion and
-models-body placeholder parity). The old assertion now checks the narrower
-behavior; every homepage models paragraph no longer uses the ambiguous count.
-Final tests: 407 passed across 47 files, 0 failed.
-Further build/browser/critique receipts are recorded below when complete.
-
-Final validation: facts, 23-topic docs, locale/catalog checks passed; lint 0 errors and 2 pre-existing logo-image warnings; webpack production build generated785 pages. Parent inspected EN desktop and saved `/tmp/cwa-app-parity-20260904/screenshots/site-claims-en-desktop.png`.
-
-Isolated Impeccable A:25/32 applicable, specificity3/4. B ran the actual detector:0 findings; inspected EN desktop and390 screenshot. A had mobile screenshot timeouts and collected EN/ZH DOM evidence only; B did not complete Chinese browser verification. No complete bilingual visual QA claim. Reports: `/tmp/cwa-app-parity-20260904/site-claims-design.md` and `site-claims-detector.md`.
-
-Two bounded P2 refinements remain for the founder's newly requested hero-scale slice: mark the Fleet heading visibly optional and verify/fix the narrow install Copy control's out-of-bounds geometry. The copy audit is committed separately to release canonical facts ownership; this is not a declaration that the full homepage is polished. The newly requested replacement terminal capture and default-on/opt-out analytics policy are separate in-progress changes, not included in this commit.
diff --git a/.impeccable/critique/2026-09-05T03-51-38Z__web-app-locale-page-tsx.md b/.impeccable/critique/2026-09-05T03-51-38Z__web-app-locale-page-tsx.md
deleted file mode 100644
index 978e801d19..0000000000
--- a/.impeccable/critique/2026-09-05T03-51-38Z__web-app-locale-page-tsx.md
+++ /dev/null
@@ -1,44 +0,0 @@
----
-target_identity: "file:/Volumes/VIXinSSD/CW/worktrees/cw-website-app-alignment-20260904/web/app/[locale]/page.tsx"
-target_fingerprint: "sha256:f6b255122ad152248255f391aaa3034bb30f620c279bec26d3d88ef465a51c2f"
-target_path: /Volumes/VIXinSSD/CW/worktrees/cw-website-app-alignment-20260904/web/app/[locale]/page.tsx
-timestamp: 2026-09-05T03-51-38Z
-slug: web-app-locale-page-tsx
-closed: true
----
-Method: dual-agent (A: critique_design; B: critique_detector).
-
-# Terminal hero scale — 2026-09-04
-
-The founder-provided terminal capture now spans the hero width beneath the
-headline and actions. Mobile stacks the same content. Fleet setup is explicitly
-optional in EN/ZH. Install wrappers allow shrinking so the command scrolls
-inside its own box and the Copy button stays within the page.
-
-Source asset: codewhale-ops/design/website-20260904/founder-tui-15fe-20260904.png,
-2760x1494, SHA256 5a762fcee58428b9745710a459b3f0ad2ccadf37406d271473701b5f065de762.
-It is a development capture, not evidence of a new published release.
-
-Verification:407 tests passed across47 files; production webpack build785
-pages. Final lint0 errors,2 existing logo img warnings; locale/catalog checks
-passed. Logs: /tmp/cwa-app-parity-20260904/site-scale-{tests,build,final-lint,final-locales}.log.
-
-Direct in-app browser inspection: EN/ZH at390x844 and1280x900. At390, both
-pages report innerWidth390, clientWidth375 and scrollWidth375 (scrollbar uses
-15px). Copy bounds EN290.53–347, ZH296.31–347: both contained. Keyboard
-focus on EN Copy is visible; command scrolling is independent of page width.
-Clipboard submission itself was not exercised. Screenshots under
-/tmp/cwa-app-parity-20260904/screenshots/:
-site-scale-en-390.png, site-scale-zh-390.png,
-site-scale-install-en-390.png, site-scale-en-desktop.png,
-site-scale-zh-desktop.png. Temporary viewport override reset and QA tab closed.
-
-Independent review: A27/32, specificity3/4; B zero findings across its scoped
-TSX scans. Reports /tmp/cwa-app-parity-20260904/scale-voice-design.md and
-scale-voice-detector.md. Reviewers could not complete narrow browser checks;
-parent verification above supplies that missing evidence. Optional Fleet
-framing and narrow install containment findings are resolved for this slice.
-
-No hosted CI, deployment, provider call, or customer task proof. Default-on
-usage migration and moving website usage controls to Privacy remain pending
-and are deliberately not claimed by this visual slice.
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6a5f6342ad..53534852c9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,16 +7,39 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 ## [Unreleased]
 
-## [0.9.13] - 2026-09-07
+## [0.9.13] - Unreleased candidate
 
-Codewhale v0.9.13 is the integrity release for 0.9.12: multiline paste is
-one paste again, truncated tool arguments can no longer execute, strict
+Codewhale v0.9.13 source candidate addresses integrity issues in 0.9.12:
+multiline paste is one paste again, truncated tool arguments can no longer execute, strict
 ACP clients connect again, concurrent instances stop destroying each
-other's queued text, and the Computer Use bundle ships at plugin 0.2.0
+other's queued text, and the Computer Use bundle includes plugin 0.2.1
 with an accessibility-first pointer.
 
 ### Fixed
 
+- Cancelling a foreground shell wait stops its owned process group even when
+  the tool future is dropped. Explicitly backgrounded jobs retain their
+  ownership. Interrupted tool receipts distinguish work that started from
+  calls skipped before execution, and returned tool failures remain errors in
+  the next model request.
+- Saved Fleet model identifiers retain exact spelling through selection,
+  role pins, and roster changes, so changing one saved model does not modify
+  another identifier that differs only in letter case.
+- Chat wrapping reserves its scrollbar gutter consistently, keeping long
+  identifiers readable when the viewport changes.
+- The Engine keeps large send-message futures off the event loop's stack,
+  preventing stack exhaustion when a restored session starts a provider turn.
+- New, imported, and live session titles skip runtime handoffs and use the
+  first real user prompt. Explicitly renamed titles retain priority
+  (#6012, thanks @SparkofSpike).
+- UI dispatch acceptance now precedes Engine execution, so a delayed acceptance
+  callback cannot overwrite a turn that has already started or completed.
+  Cancelling before acceptance preserves the prompt and leaves the next
+  dispatch usable.
+- Bottom-chrome effort is omitted when the route cannot prove an effective
+  tier; `/status` retains the full explanation. Cost remains visible when
+  known, and `cost: unknown` remains on metered routes lacking a reading (#5950).
+
 - Pasting multiline text is one paste again. 0.9.12 gated the
   paste-burst heuristic off whenever bracketed paste was *requested*, but
   a terminal can accept `EnableBracketedPaste` and still deliver a paste
@@ -80,22 +103,50 @@ with an accessibility-first pointer.
 
 ### Changed
 
+- The website uses Shannon Sans with versioned local font assets and retained
+  serif, monospace, and language fallbacks. Terminal fonts are unchanged.
+- `codewhale metrics` reports recorded model requests and stream recovery
+  separately from provider-reported token usage, with coverage for missing
+  and duplicate receipts. Status messages and cumulative snapshots do not
+  add requests or count tokens again.
+- Runtime turn receipts retain the Engine's terminal model-request, stream-retry,
+  and resume counters separately from displayed status and provider-reported
+  usage. These counters do not count HTTP retries inside a provider client or
+  establish provider billing.
+- Initial tool definitions no longer repeat shell interpreter guidance and
+  agent lifecycle/scope instructions in multiple description fields. Parameter
+  schemas, approval rules and dispatch behavior are preserved. This reduces
+  prompt schema size; it does not establish a provider billing regression.
+
 - The built-in Computer Use plugin bundle is refreshed to the standalone
-  plugin's 0.2.0 runtime (vendored from `Hmbown/codewhale-cu-plugin`
-  PR #12 @ `906b433`): the native macOS accessibility backend with an
+  plugin's 0.2.1 runtime (vendored from `Hmbown/codewhale-cu-plugin`
+  at `724ad258`): the native macOS accessibility backend with an
   a11y-first pointer strategy (covered points are refused, previews are
   drawn), the permission-owning desktop-app socket transport, remote
   computers over ssh and HarmonyOS HDC with contained temp handling,
   truthful win32 PowerShell failure reporting, and the shared
-  allow-listed request handler for the app socket and ssh agent. The
+  allow-listed request handler for the app socket and ssh agent.
+  The bundle retains the hardening port from
+  [standalone plugin PR #12](https://github.com/Hmbown/codewhale-cu-plugin/pull/12).
+  Single left clicks on macOS element targets now revalidate and press
+  the observed element directly, without substituting a point hit-test
+  or raw-pointer fallback. Explicit event clicks retain the app-ownership
+  guard; a sent press still requires visual verification. Background mode
+  permits application-bound keys and accessibility actions while refusing
+  shared-pointer gestures; it does not provide an isolated desktop. The
   embed list gained the five new runtime files, and a consistency test
   now pins the embed list to the vendored tree so the bundle cannot rot
   silently again. Because the bundle's content hash changes, Computer
   Use deactivates and asks for a fresh review after upgrading — that is
   the designed fail-closed path for a desktop-driving plugin. The
-  desktop app itself stays an opt-in install from the plugin
-  distribution; the bundled server runs direct mode and says how to get
-  the app.
+  current macOS source candidate also embeds the compiled native helper,
+  so its bundled server can run directly without a separate Computer Use
+  app or a compiler on the user's machine. Accessibility and Screen
+  Recording permissions belong to the hosting app or terminal and remain
+  user-controlled. The CLI's Computer Use server requires Node.js 20 or newer. These are source
+  candidate changes; they do not establish published-package or platform
+  qualification. See the [included plugin guide](crates/tui/plugins/computer-use/README.md)
+  for platform requirements and limitations.
 - `/statusline` drives the bottom chrome again. Since the 0.9.12 shell
   redesign the posture bar and the metrics line were built independently of
   `tui.status_items`, so every toggle in the picker except the balance fetch
@@ -105,7 +156,7 @@ with an accessibility-first pointer.
   bar's plan/act/operate chip. The `status`, `agents`, `reasoning_replay`,
   `prefix_stability`, `git_branch`, `last_tool_elapsed` and `rate_limit`
   items drove nothing and are retired; an existing `config.toml` still loads
-  and those keys are ignored (#5950).
+  and those keys are ignored (#5950, #5962).
 - The context reading is back on screen at every fullness. 0.9.12 painted
   `ctx NN%` only from 50% up, which left most of a session with no context
   signal at all; it now paints from 0% and keeps its warning colour from 80%
@@ -173,6 +224,29 @@ with an accessibility-first pointer.
 
 ### Added
 
+- Native plugin authoring guides now cover English and Chinese. The explicit
+  offline converter supports selected portable Skills and static Streamable
+  HTTP MCP declarations from OpenCode and DSH. Unsupported executable hooks,
+  automatic OAuth and policy-bearing configurations are refused; generated
+  bundles still require native installation, review and trust. Legacy SSE
+  fallback is not reproduced (#5827, requested by @giancarlocp).
+
+- Signed cloud model facts can refresh provider capabilities and prices while
+  preserving verified cached data when a refresh fails. A dispatched request
+  keeps its selected price snapshot so later catalog updates cannot change its
+  recorded cost (#5752).
+- Saved sessions preserve exact provider routes. Auxiliary model calls settle
+  their usage once against the route and price snapshot that executed them,
+  including recovery, rather than resolving a new price at completion
+  (#5726, #5848).
+- `[tui].posture_bar` and `[tui].metrics_line` accept `full`, `compact`, or
+  `hidden`, also available through `/config`. Compact preserves the existing
+  rows' essential fields; hidden returns their space to the transcript (#5973).
+- Optional model-bound tool-output redaction opt-out, with two explicit startup
+  confirmations and a receipt bound to the readable config contents and
+  modification time. Unconfirmed requests keep masking enabled; routing and
+  stored goal summaries remain redacted (#5982, thanks @SparkofSpike).
+
 - The `rusty-alloc` cargo feature on `codewhale-tui` and `codewhale-cli`
   opts the binaries into the `rusty_alloc` global allocator (the mimalloc
   v2.4.5 architecture remade in pure Rust — no C compiler or build script
@@ -225,29 +299,6 @@ with an accessibility-first pointer.
   three explicitly. `--use` saves the new secret as this machine's local
   `codewhale` provider credential in the same secret store `codewhale auth`
   uses; nothing is uploaded.
-- `sandbox_backend = "shannon"`: shell commands run as signed ShannonNet
-  capability invocations (`cap://sandbox/exec`) on a worker that may live on
-  another tailnet node. Codewhale opens a Task World per session for its
-  durable `codewhale` Agent and every command leaves a receipt in
-  `shannon trace`. New keys `sandbox_shannon_home` and
-  `sandbox_shannon_capability`; tool metadata now reports the actual
-  external backend kind instead of always `opensandbox`.
-- `/shannon [world|trace|children]` inspects the session's ShannonNet
-  World: agent, projected capabilities, children, and receipts.
-- ShannonNet sub-agents get compiled context: the session's native-memory
-  hits are imported with provenance and the child's projected World decides
-  what it sees (confidential notes never cross); the session World is
-  checkpointed and closed when the backend drops.
-- Sub-agents under delegated authority: with the ShannonNet backend the
-  `agent` tool spawns a child identity with a World projected from the
-  session World, the child's shell commands are signed as that child, and a
-  join receipt is recorded when it finishes. `SandboxBackend::for_child` /
-  `child_joined` default to sharing the parent backend for other backends.
-- Workspace sync for the ShannonNet backend (`sandbox_shannon_sync`, default
-  on): the session's non-ignored files are shipped into the worker's
-  per-World session container before each command — full tree first, then
-  only changes and deletions — so remote builds and tests run on the files
-  just edited locally and their outputs persist across commands.
 - `Git` grows a `commit_plan` action: a propose-only planner that splits the
   working tree into ordered atomic commits (#3999). It groups whole files —
   lock files ride with their manifest, tests ride with the source they name —
@@ -262,29 +313,29 @@ with an accessibility-first pointer.
 
 ### Contributors
 
-- **[@nsfoxer](https://github.com/nsfoxer)** — reported the 0.9.12
-  multiline-paste regression with a root-cause analysis that made the
-  fix a one-day turnaround (#5981).
-- **@Nefelibata1024** — confirmed the paste regression's impact.
-- **[@Gabriel-Degret](https://github.com/Gabriel-Degret)** — reported
-  `allow_insecure_http` being silently dropped in 0.9.12, with the
-  valid-key list that pinned it (#5991).
-- **[@Lujc0523](https://github.com/Lujc0523)** — reported the ACP
-  `initialize` schema violation that made Codewhale unusable from
-  JetBrains IDEs (#5969).
-- **[@gaord](https://github.com/gaord)** — the fleet role-precedence
-  recovery (#5945) and the README link to the community VS Code
-  frontend (#5992).
-- **[@goransh-walia](https://github.com/goransh-walia)** — the
-  propose-only `commit_plan` rework (#5870).
+- **[@gaord](https://github.com/gaord)** — contributed Fleet schema inspection, role precedence and worker deliverable receipts, and linked the community VS Code frontend ([#5944](https://github.com/Hmbown/Codewhale/pull/5944), [#5945](https://github.com/Hmbown/Codewhale/pull/5945), [#5946](https://github.com/Hmbown/Codewhale/pull/5946), [#5992](https://github.com/Hmbown/Codewhale/pull/5992)).
+- **[@goransh-walia](https://github.com/goransh-walia)** — contributed the propose-only commit-planning rework ([#5870](https://github.com/Hmbown/Codewhale/pull/5870)).
+- **[@7jrxt42BxFZo4iAnN4CX](https://github.com/7jrxt42BxFZo4iAnN4CX)** — documented turn budgets and goal configuration, and reported gaps in command discovery, Fleet navigation, human waits, state hooks, history and provider routing ([#5996](https://github.com/Hmbown/Codewhale/pull/5996), [#5952](https://github.com/Hmbown/Codewhale/issues/5952), [#5954](https://github.com/Hmbown/Codewhale/issues/5954), [#6003](https://github.com/Hmbown/Codewhale/issues/6003), [#6004](https://github.com/Hmbown/Codewhale/issues/6004), [#6006](https://github.com/Hmbown/Codewhale/issues/6006), [#6007](https://github.com/Hmbown/Codewhale/issues/6007)).
+- **[@SparkofSpike](https://github.com/SparkofSpike)** — contributed two-stage consent for opting out of model-bound credential redaction ([#5982](https://github.com/Hmbown/Codewhale/pull/5982)).
+- **[@aboimpinto](https://github.com/aboimpinto)** — moved session lifecycle and session-control commands onto shared command contracts ([#5902](https://github.com/Hmbown/Codewhale/pull/5902), [#5951](https://github.com/Hmbown/Codewhale/pull/5951)).
+- **[@EvanProgramming](https://github.com/EvanProgramming)** — reported Windows input and CRLF-write defects, and contributed CRLF preservation and an injectable Windows input runner ([#5908](https://github.com/Hmbown/Codewhale/issues/5908), [#5909](https://github.com/Hmbown/Codewhale/issues/5909), [#5910](https://github.com/Hmbown/Codewhale/pull/5910), [#5911](https://github.com/Hmbown/Codewhale/pull/5911), [#5912](https://github.com/Hmbown/Codewhale/pull/5912)).
+- **[@wuisabel-gif](https://github.com/wuisabel-gif)** — added custom-theme discovery, preview and selection in the theme picker ([#5907](https://github.com/Hmbown/Codewhale/pull/5907)).
+- **[@zhuowp](https://github.com/zhuowp)** — matched model-visible shell guidance to the interpreter selected for execution ([#5900](https://github.com/Hmbown/Codewhale/pull/5900)).
+- **[@nsfoxer](https://github.com/nsfoxer)** — reported the multiline-paste regression and incomplete provider model lists ([#5981](https://github.com/Hmbown/Codewhale/issues/5981), [#6009](https://github.com/Hmbown/Codewhale/issues/6009)).
+- **[@Nefelibata1024](https://github.com/Nefelibata1024)** — confirmed the multiline-paste regression's impact ([#5981](https://github.com/Hmbown/Codewhale/issues/5981)).
+- **[@Gabriel-Degret](https://github.com/Gabriel-Degret)** — reported the loss of the allow_insecure_http provider setting ([#5991](https://github.com/Hmbown/Codewhale/issues/5991)).
+- **[@Lujc0523](https://github.com/Lujc0523)** — reported the ACP initialize schema violation affecting strict IDE clients ([#5969](https://github.com/Hmbown/Codewhale/issues/5969)).
+- **[@mo-vic](https://github.com/mo-vic)** — proposed storing evicted context on disk so it can be retrieved later ([#6008](https://github.com/Hmbown/Codewhale/issues/6008)).
+- **[@giancarlocp](https://github.com/giancarlocp)** — requested a plugin authoring guide and OpenCode plugin conversion ([#5827](https://github.com/Hmbown/Codewhale/discussions/5827)).
+- **[@hxfhd](https://github.com/hxfhd)** — supplied a Windows reproduction of a turn stopping before its stated next tool action ([#6010](https://github.com/Hmbown/Codewhale/discussions/6010)).
 
 ### Notes
 
 - Upgrading from 0.9.12 with Computer Use trusted and enabled: the
-  bundle's content hash changes with the 0.2.0 refresh, so the plugin
+  bundle's content hash changes with the 0.2.1 refresh, so the plugin
   deactivates and asks for a fresh review — that is the designed
   fail-closed path for a desktop-driving plugin. Re-trust it from the
-  Extensions page.
+  Plugins page.
 - The multiline-paste fix restores v9.11 behavior on terminals that
   accept `EnableBracketedPaste` but deliver pastes as keystrokes
   (reported on Windows 11 / PowerShell). Verified at the input-contract
@@ -8210,7 +8261,7 @@ overflow report and `/theme` picker edge-wrapping patch in #1814.
 Older releases (v0.8.39 and earlier) are archived in [docs/CHANGELOG_ARCHIVE.md](docs/CHANGELOG_ARCHIVE.md).
 
 [Unreleased]: https://github.com/Hmbown/CodeWhale/compare/v0.9.12...HEAD
-[0.9.13]: https://github.com/Hmbown/CodeWhale/compare/v0.9.12...v0.9.13
+[0.9.13]: https://github.com/Hmbown/CodeWhale/compare/v0.9.12...HEAD
 [0.9.12]: https://github.com/Hmbown/CodeWhale/compare/v0.9.11...v0.9.12
 [0.9.11]: https://github.com/Hmbown/CodeWhale/compare/v0.9.10...v0.9.11
 [0.9.10]: https://github.com/Hmbown/CodeWhale/compare/v0.9.9...v0.9.10
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 4496a9e1f1..1ba581c3b3 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -420,28 +420,40 @@ branding, or global prompts without prior maintainer sign-off.
 
 ## Project Structure
 
-codewhale is a Cargo workspace. The live runtime and the majority of TUI,
-engine, and tool code currently live in `crates/tui/src/`. Smaller workspace
-crates provide shared abstractions that are being extracted incrementally.
-
-```
-crates/
-├── tui/           codewhale-tui binary (interactive TUI + runtime API)
-├── cli/           codewhale binary (dispatcher facade)
-├── app-server/    HTTP/SSE + JSON-RPC transport
-├── core/          Agent loop / session / turn management
-├── protocol/      Request/response framing
-├── config/        Config loading, profiles, env precedence
-├── state/         SQLite thread/session persistence
-├── tools/         Typed tool specs and lifecycle
-├── mcp/           MCP client + stdio server
-├── hooks/         Lifecycle hooks (stdout/jsonl/webhook)
-├── execpolicy/    Approval/sandbox policy engine
-├── agent/         Model/provider registry
-```
-
-See [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) for the live data flow across
-these crates, including the bottom-up build order.
+Codewhale is a Cargo workspace with one Engine implementation in
+`crates/tui/src/core/engine/`. The public `codewhale` executable links the
+TUI/runtime library; interactive sessions, noninteractive runs and the Runtime
+API share that Engine.
+
+| Path | Purpose |
+| --- | --- |
+| `crates/cli/` | Public command entrypoint, configuration commands and runtime dispatch |
+| `crates/tui/` | Interactive terminal, Engine, tools, Runtime API and embedded local web client |
+| `crates/core/`, `crates/protocol/`, `crates/state/` | Request construction, session/turn types, protocol framing and persistence |
+| Other `crates/` | Shared configuration, credentials, telemetry, hooks, workflow and packaging support; see each Cargo manifest |
+| `web/` | Public Next.js website and documentation; separate from the embedded Runtime web client |
+| `telemetry-ingest/` | Telemetry service, schemas and service tests |
+| `extensions/`, `integrations/` | Editor integration and external-service bridges |
+| `npm/`, `packaging/`, `nix/` | npm wrappers/SDK and platform installation definitions |
+| `computer/snapshots/` | Cloud Computer image definitions, pinned independently of the source checkout |
+| `deploy/` | Deployment templates consumed by setup scripts, including Tencent Lighthouse services |
+| `fleets/`, `workflows/` | Distributed Fleet definitions and workflow examples |
+| `brand/` | Source artwork and generated brand variants used by the README, website and terminal |
+| `docs/` | User/developer documentation, schemas, fixtures and referenced release material |
+| `scripts/`, `.github/`, `.cnb.yml` | Development, validation, CI and release tooling |
+| `patches/` | Vendored dependency fixes, including their licensing files |
+
+Generated files that the product embeds or validates, such as model catalogs,
+website facts and schemas, remain tracked with their generators. Platform
+mirrors such as `.winget/` are retained when their packaging tools require them.
+Keep local critique output, temporary verification reports and personal
+operator instructions outside the tracked product tree; describe the change
+and its validation in the pull request. Do not copy workspace-level operator
+`AGENTS.md` or `CLAUDE.md` files into this repository.
+
+See [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) for the runtime data flow and
+[the build guide](docs/BUILD_PERFORMANCE.md) for crate dependencies and local
+verification.
 
 ## Submitting Changes
 
diff --git a/Cargo.lock b/Cargo.lock
index 9bcba1b616..270dd5836d 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -653,9 +653,9 @@ checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527"
 
 [[package]]
 name = "chacha20"
-version = "0.10.1"
+version = "0.10.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81"
+checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06"
 dependencies = [
  "cfg-if",
  "cpufeatures 0.3.0",
@@ -846,6 +846,23 @@ dependencies = [
  "zeroize",
 ]
 
+[[package]]
+name = "codewhale-cloud-facts"
+version = "0.9.13"
+dependencies = [
+ "codewhale-config",
+ "codewhale-release",
+ "libc",
+ "reqwest 0.13.4",
+ "semver",
+ "serde",
+ "serde_json",
+ "tempfile",
+ "tokio",
+ "tracing",
+ "windows-sys 0.61.2",
+]
+
 [[package]]
 name = "codewhale-command-contract"
 version = "0.9.13"
@@ -858,11 +875,15 @@ name = "codewhale-config"
 version = "0.9.13"
 dependencies = [
  "anyhow",
+ "base64 0.22.1",
+ "chrono",
  "codewhale-execpolicy",
  "codewhale-paths",
  "codewhale-secrets",
  "fd-lock",
  "libc",
+ "ring",
+ "semver",
  "serde",
  "serde_json",
  "sha2 0.11.0",
@@ -1061,6 +1082,7 @@ dependencies = [
  "clap",
  "clap_complete",
  "codewhale-build-support",
+ "codewhale-cloud-facts",
  "codewhale-command-contract",
  "codewhale-config",
  "codewhale-core",
@@ -1086,6 +1108,7 @@ dependencies = [
  "futures-util",
  "globset",
  "htmd",
+ "hyper-util",
  "ignore",
  "image",
  "jsonschema",
@@ -1106,6 +1129,7 @@ dependencies = [
  "rmcp",
  "rusqlite",
  "rust-i18n",
+ "rust-i18n-support",
  "rustls",
  "rusty_alloc-api",
  "schemars",
@@ -3805,19 +3829,6 @@ version = "0.3.33"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e"
 
-[[package]]
-name = "plist"
-version = "1.10.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7da1d65da6dd5d1e44199ac0f58712d241c0f439f80adea8924d832384087f85"
-dependencies = [
- "base64 0.22.1",
- "indexmap",
- "quick-xml",
- "serde",
- "time",
-]
-
 [[package]]
 name = "png"
 version = "0.18.1"
@@ -3958,15 +3969,6 @@ version = "2.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3"
 
-[[package]]
-name = "quick-xml"
-version = "0.41.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e660451e55124f798a69a5af3f49ccfbefbd41910eefd25caf2393e1f3473ec1"
-dependencies = [
- "memchr",
-]
-
 [[package]]
 name = "quinn"
 version = "0.11.11"
@@ -5334,14 +5336,11 @@ dependencies = [
  "flate2",
  "fnv",
  "once_cell",
- "plist",
  "regex-syntax",
  "serde",
  "serde_derive",
- "serde_json",
  "thiserror 2.0.20",
  "walkdir",
- "yaml-rust",
 ]
 
 [[package]]
@@ -5599,7 +5598,6 @@ dependencies = [
  "powerfmt",
  "serde_core",
  "time-core",
- "time-macros",
 ]
 
 [[package]]
@@ -5608,16 +5606,6 @@ version = "0.1.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109"
 
-[[package]]
-name = "time-macros"
-version = "0.2.32"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85"
-dependencies = [
- "num-conv",
- "time-core",
-]
-
 [[package]]
 name = "tiny_http"
 version = "0.12.0"
@@ -6866,15 +6854,6 @@ dependencies = [
  "markup5ever",
 ]
 
-[[package]]
-name = "yaml-rust"
-version = "0.4.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85"
-dependencies = [
- "linked-hash-map",
-]
-
 [[package]]
 name = "yansi"
 version = "1.0.1"
diff --git a/Cargo.toml b/Cargo.toml
index bf909dfcdc..b7077a7211 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -4,6 +4,7 @@ members = [
     "crates/app-server",
     "crates/build-support",
     "crates/cli",
+    "crates/cloud-facts",
     "crates/command-contract",
     "crates/config",
     "crates/core",
@@ -55,6 +56,7 @@ encoding_rs = "0.8.35"
 # Sole user is crates/workflow-js (schemaui is gone); keep the graph on one
 # jsonschema/jsonschema-regex/referencing/fancy-regex stack.
 jsonschema = { version = "0.52", default-features = false }
+libc = "0.2"
 reqwest = { version = "0.13.1", default-features = false, features = ["json", "rustls-no-provider", "socks"] }
 # NOT "parallel": the Workflow VM stays single-threaded and bridges to the
 # multi-thread engine over channels (see crates/workflow-js).
diff --git a/DESIGN.md b/DESIGN.md
index 01519dee71..95978ac569 100644
--- a/DESIGN.md
+++ b/DESIGN.md
@@ -52,12 +52,12 @@ typography:
     lineHeight: 1.1
     letterSpacing: "-0.022em"
   subheading:
-    fontFamily: "IBM Plex Sans Condensed, ui-sans-serif, system-ui, sans-serif"
+    fontFamily: "Shannon Sans, ui-sans-serif, system-ui, sans-serif"
     fontSize: "1.12rem"
     fontWeight: 600
     lineHeight: 1.25
   body:
-    fontFamily: "IBM Plex Sans, ui-sans-serif, system-ui, sans-serif"
+    fontFamily: "Shannon Sans, ui-sans-serif, system-ui, sans-serif"
     fontSize: "1rem"
     fontWeight: 400
     lineHeight: 1.6
@@ -180,16 +180,16 @@ semantic names and never repeats a hex:
 
 ## Typography
 
-Four faces, one job each:
+Three faces with distinct roles:
 
 - **Newsreader 400/500 (+ italic)** — the display voice: `h1`, `h2`, the
   gain columns' titles, the chapter title on the water. Book weight, tracking
   −0.022em, `text-wrap: balance`. Loaded through `next/font/google` as
   `--font-serif`. Never used below 1.3rem.
-- **IBM Plex Sans Condensed 500/600** — `h3`/`h4` and the small headings:
-  the product's own label face, `--font-display`.
-- **IBM Plex Sans 400/500/600** — body, buttons, links (`--font-body`).
-  Measure ≤ 70ch.
+- **Shannon Sans variable 100–900** — body, buttons, links and small headings.
+  `--font-body` and the historic `--font-display`/condensed role share one local
+  upright face; their existing weights and scale distinguish the roles. Measure
+  ≤ 70ch. The font and its OFL notice live in `web/public/brand/fonts/`.
 - **JetBrains Mono 400/500** — code, the `cw` dot chain, the plate's rubric
   (`AGENTIC COMPUTING, ON YOUR TERMS`), the running heads (`02 / YOUR MODELS`).
   These rubrics are the only tracked uppercase on the site.
diff --git a/LICENSE b/LICENSE
index d20b90d5f5..8a3702b657 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,6 +1,6 @@
 MIT License
 
-Copyright (c) 2024-2025 DeepSeek CLI Contributors
+Copyright (c) 2024-2025 DeepSeek-TUI Contributors
 
 Permission is hereby granted, free of charge, to any person obtaining a copy
 of this software and associated documentation files (the "Software"), to deal
diff --git a/README.ar.md b/README.ar.md
index 8c4420dac6..654234cf1e 100644
--- a/README.ar.md
+++ b/README.ar.md
@@ -1,9 +1,11 @@
-
+
 # Codewhale
 
-Codewhale وكيل مفتوح المصدر للبرمجة عبر الطرفية، مبني بلغة Rust ويتطور علنًا بالتعاون مع الأشخاص الذين يستخدمونه.
+Codewhale وكيل مفتوح المصدر يقرأ مشروعك ويعدّل الملفات ويشغّل الأوامر ويتحقق من عمله باستخدام نموذج مستضاف أو محلي تختاره. ابدأ بمهمة واحدة في الطرفية. وللأعمال الأكبر، وزّع أجزاء العمل على وكلاء بنماذج وأدوار مختلفة.
 
-![Codewhale يعمل في طرفية](assets/screenshot.webp)
+![Codewhale يعمل في طرفية](web/public/codewhale-tui-171acee.png)
+
+*معاينة للطرفية من بنية تطوير للإصدار v0.9.12.*
 
 [English](README.md) · [简体中文](README.zh-CN.md) · [日本語](README.ja-JP.md) · [Tiếng Việt](README.vi.md) · [Bahasa Indonesia](README.id.md) · [한국어](README.ko-KR.md) · [Español](README.es-419.md) · [Português](README.pt-BR.md) · [Русский](README.ru.md) · [Українська](README.uk.md) · [Français](README.fr.md) · [Deutsch](README.de.md) · [繁體中文](README.zh-TW.md) · [हिन्दी](README.hi.md) · [Türkçe](README.tr.md) · [Italiano](README.it.md) · [Polski](README.pl.md) · [Català](README.ca.md)
 
@@ -21,15 +23,17 @@ curl -fsSL https://codewhale.net/install.sh | sh
 "$HOME/.local/bin/codewhale"
 ```
 
+يختار المثبّت أحدث إصدار منشور. ويصف [سجل التغييرات](CHANGELOG.md) أيضًا النسخة المرشحة غير المنشورة للإصدار التالي؛ ولا تُضمّن هذه التغييرات في التنزيلات المنشورة حتى يصبح الإصدار متاحًا.
+
 على Windows، نزّل المثبّت أو الأرشيف المناسب من [GitHub Releases](https://github.com/Hmbown/CodeWhale/releases/latest). لتحديث تثبيت مباشر موجود، شغّل `codewhale update`، أو `codewhale update --check` للفحص فقط. يعرض المحدّث مسار الملف التنفيذي ويحتفظ بالبنيات الأحدث. npm وCargo خياران ثانويان؛ راجع [دليل التثبيت](docs/INSTALL.md) للانتقال من تثبيت يديره مدير حزم وإعداد PATH.
 
-يساعدك Codewhale عند التشغيل الأول على الاتصال بموفّر أو البقاء دون اتصال. ويدعم أيضًا Cargo وDocker وNix وScoop والأرشيفات المبنية مسبقًا وAndroid/Termux ومرآة CNB. راجع [دليل التثبيت](docs/INSTALL.md).
+يساعدك Codewhale عند التشغيل الأول على الاتصال بموفّر أو إعداد Codewhale دون اتصال. تتطلب ردود النموذج الاتصال بنموذج مستضاف أو محلي. ويدعم Codewhale أيضًا npm وCargo كخياري تحزيم ثانويين، إلى جانب Docker وNix وScoop وAndroid/Termux ومرآة CNB اختيارية. تتوفر تعليمات انتقال للتثبيتات الحالية التي يديرها مدير حزم. راجع [المساعدة بشأن التثبيت وPATH](docs/INSTALL.md).
 
 يمكن تفعيل الإكمال بمفتاح Tab بأمر واحد لكل واجهة أوامر — `codewhale completion bash|zsh|fish|powershell|elvish`. راجع [إكمال واجهة الأوامر](docs/INSTALL.md#8-shell-completions).
 
 ## الاستخدام
 
-تحدث إلى Codewhale كما تتحدث إلى زميل في فريقك:
+افتح طرفية في مجلد مشروعك وشغّل `codewhale`. اختر موفّرك باستخدام `/provider` ونموذجك باستخدام `/model`. ثم صِف مهمة محددة:
 
 ```text
 Fix the failing tests and explain what changed.
@@ -41,16 +45,24 @@ Fix the failing tests and explain what changed.
 codewhale exec "fix the failing tests and explain what changed"
 ```
 
-يستطيع Codewhale قراءة مستودعك وتعديل الملفات وتشغيل الأوامر وفحص النتائج ومواصلة العمل نحو هدف. وأنت من يقرر مقدار الوصول الذي تمنحه له.
+يستطيع Codewhale قراءة مستودعك وتعديل الملفات وتشغيل الأوامر وفحص النتائج ومواصلة العمل نحو هدف. استخدم `/mode plan` للاستكشاف دون تغيير الملفات أو تنفيذ أوامر واجهة الأوامر، و`/mode work` عندما تريد إجراء تغييرات. اضغط `Shift+Tab` لاختيار Ask أو Auto-Review أو Full Access؛ يوضح [دليل الأوضاع والصلاحيات](docs/MODES.md) ما يسمح به كل خيار.
+
+## الطرفية والتطبيقات وComputer Use
+
+تتصل الطرفية والعملاء الرسوميون ببيئة Codewhale Runtime، التي تشغّل الوكيل وأدواته:
+
+- **الطرفية:** يفتح `codewhale` الواجهة التفاعلية؛ ويشغّل `codewhale exec` مهمة من برنامج نصي أو مهمة CI.
+- **المتصفح المحلي:** يفتح `codewhale web` [عميل الويب المحلي](docs/WEB.md) المرفق، والمتصل ببيئة التشغيل نفسها.
+- **تطبيقات Codewhale للويب وسطح المكتب:** بيئات عمل رسومية قيد التطوير. تُدرج معلومات توفرها في [صفحة المنتج](https://codewhale.net/en/product).
 
-## الواجهة الرسومية
+**يضيف Computer Use أدوات لمراقبة التطبيقات الأخرى والتفاعل معها.** الإضافة مضمنة في الشيفرة المصدرية الحالية. راجع صلاحيات الوصول التي تطلبها وفعّلها قبل الاستخدام؛ وتظل أذونات نظام التشغيل ومتطلبات المنصة سارية. راجع [دليل Computer Use](crates/tui/plugins/computer-use/README.md) المرفق و[إعداد الإضافات](docs/PLUGINS.md).
 
-هل تفضّل واجهة رسومية؟ إضافة CodeWhale for VS Code التي يحافظ عليها المجتمع تضع العميل نفسه في الشريط الجانبي لبرنامج VS Code — الدردشة والمحادثات المتسلسلة والفرق المباشرة وإدارة المهام، كل ذلك عبر نفس Runtime API، وتبقى الجلسات متزامنة مع الطرفية. ثبّتها من [سوق VS Code](https://marketplace.visualstudio.com/items?itemName=HengQuWorld.brotherwhale-vscode)؛ المصدر على [GitHub](https://github.com/HengQuWorld/CodeWhale-VSCode).
+يتصل امتداد CodeWhale لبرنامج VS Code، الذي يصونه المجتمع، ببيئة Runtime المحلية من الشريط الجانبي. ثبّته من [سوق VS Code](https://marketplace.visualstudio.com/items?itemName=HengQuWorld.brotherwhale-vscode)؛ والشيفرة المصدرية على [GitHub](https://github.com/HengQuWorld/CodeWhale-VSCode).
 
 ## لماذا Codewhale
 
-- **استخدم النموذج الذي تريده.** اتصل بموفّرين مستضافين أو بنماذج محلية عبر Ollama أو vLLM أو SGLang. بدّل الموفّر والنموذج باستخدام `/model`.
-- **ابقَ مسيطرًا.** وضع Plan للقراءة فقط. تجعل أوضاع Ask وAuto-Review وFull Access سلوك الموافقة واضحًا. يتراجع `/undo` عن الجولة الأخيرة، ويعيد `/restore` مساحة العمل إلى لقطة سابقة.
+- **اختر نماذجك.** اتصل بموفّرين مستضافين أو بنماذج محلية عبر Ollama أو vLLM أو SGLang. استخدم `/provider` لتغيير الموفّر و`/model` لاختيار نموذج.
+- **ابقَ مسيطرًا.** افحص الإجراءات المقترحة والتغييرات الناتجة في الملفات. تحدد إعدادات الموافقة متى تلزم المراجعة؛ ويظل Full Access ملتزمًا بالحدود الصارمة للسياسات. يساعدك `/undo` و`/restore` على استعادة مساحة العمل بعد التغييرات.
 - **حافظ على تنظيم الأعمال الطويلة.** احفظ الجلسات، وحدد `/goal` دائمًا، وراجع مسارات العمل قبل تشغيلها، ونسّق بين الوكلاء من دون تحويل تعليماتهم الداخلية إلى جزء من محادثتك.
 - **وسّع الوكيل الذي لديك بالفعل.** صِل خوادم MCP والمهارات، واضبط الخطافات، واحتفظ بأدوار الوكلاء كملفات مقروءة في مشروعك أو إعداداتك الشخصية.
 
@@ -69,10 +81,11 @@ codewhale exec "fix the failing tests and explain what changed"
 - [MCP](docs/MCP.md) و[الخطافات](docs/HOOKS.md) و[الإعدادات](docs/CONFIGURATION.md)
 - [عميل الويب المحلي](docs/WEB.md)
 - [جميع الوثائق](docs)
+- [بنية المستودع ودليل المساهمة](CONTRIBUTING.md#project-structure)
 
 ## انضم إلى المجتمع
 
-يتحسن Codewhale عندما يستخدمه الناس ويبلغون عما لا يبدو صحيحًا ويساعدون في إصلاحه. إذا كان أحد الموفّرين مفقودًا، أو كان مسار العمل مربكًا، أو كانت واجهة الطرفية تعيقك، [فافتح issue](https://github.com/Hmbown/CodeWhale/issues). وإذا كنت تعرف كيفية تحسينه، [فافتح pull request](CONTRIBUTING.md). نرحب بالمساهمات الأولى، ويظل كل مساهم منسوبًا إلى العمل الذي يُدمج في المشروع.
+**نرحب بتقارير الأخطاء وأفكار الميزات وطلبات السحب**، سواء كنت تستخدم Codewhale منذ أشهر أو تجربه للمرة الأولى. إذا كان أحد الموفّرين غير متاح، أو كان مسار العمل غير مريح، أو كانت واجهة الطرفية تعيقك، [فافتح issue](https://github.com/Hmbown/CodeWhale/issues/new/choose) أو [أرسل pull request](CONTRIBUTING.md) لنحسّنه معًا. نرحب بالمساهمات الأولى، ويظل كل مساهم منسوبًا إلى العمل الذي يُدمج في المشروع.
 
 انضم إلى [Discord](https://discord.gg/37gfS3ksug)، أو أضف Hunter على WeChat (`hunterbown`) واطلب الانضمام إلى مجموعة Whale Brothers.
 
diff --git a/README.ca.md b/README.ca.md
index 2522438a00..db9d8bdfae 100644
--- a/README.ca.md
+++ b/README.ca.md
@@ -1,9 +1,11 @@
-
+
 # Codewhale
 
-Codewhale és un agent de programació de codi obert per al terminal, desenvolupat amb Rust i millorat públicament amb les persones que l’utilitzen.
+Codewhale és un agent de codi obert que llegeix el teu projecte, edita fitxers, executa ordres i comprova la seva feina amb un model allotjat o local que tu tries. Comença amb una tasca al terminal. Per a una feina més gran, assigna parts de la feina a agents amb models i rols diferents.
 
-![Codewhale executant-se en un terminal](assets/screenshot.webp)
+![Codewhale executant-se en un terminal](web/public/codewhale-tui-171acee.png)
+
+*Previsualització del terminal d’una compilació de desenvolupament de la v0.9.12.*
 
 [English](README.md) · [简体中文](README.zh-CN.md) · [日本語](README.ja-JP.md) · [Tiếng Việt](README.vi.md) · [Bahasa Indonesia](README.id.md) · [한국어](README.ko-KR.md) · [Español](README.es-419.md) · [Português](README.pt-BR.md) · [Русский](README.ru.md) · [Українська](README.uk.md) · [Français](README.fr.md) · [Deutsch](README.de.md) · [繁體中文](README.zh-TW.md) · [हिन्दी](README.hi.md) · [Türkçe](README.tr.md) · [Italiano](README.it.md) · [Polski](README.pl.md) · [العربية](README.ar.md)
 
@@ -21,15 +23,17 @@ curl -fsSL https://codewhale.net/install.sh | sh
 "$HOME/.local/bin/codewhale"
 ```
 
+L’instal·lador selecciona l’última versió publicada. El [registre de canvis](CHANGELOG.md) també descriu la versió candidata, encara no publicada, de la pròxima versió; aquests canvis no s’inclouen en les descàrregues publicades fins que la versió està disponible.
+
 A Windows, descarrega l’instal·lador o l’arxiu corresponent de [GitHub Releases](https://github.com/Hmbown/CodeWhale/releases/latest). Per actualitzar una instal·lació directa existent, executa `codewhale update`, o `codewhale update --check` només per comprovar-la. L’actualitzador mostra el camí de l’executable i conserva les compilacions més noves. npm i Cargo són opcions secundàries; consulta la [guia d’instal·lació](docs/INSTALL.md) per migrar una instal·lació gestionada per paquets i configurar PATH.
 
-En la primera execució, Codewhale t’ajuda a connectar un proveïdor o a continuar sense connexió. També admet Cargo, Docker, Nix, Scoop, arxius precompilats, Android/Termux i un mirall CNB. Consulta la [guia d’instal·lació](docs/INSTALL.md).
+En la primera execució, Codewhale t’ajuda a connectar un proveïdor o a configurar Codewhale sense connexió. Les respostes requereixen un model allotjat o local connectat. Codewhale també admet npm i Cargo com a opcions secundàries de distribució, a més de Docker, Nix, Scoop, Android/Termux i un mirall CNB opcional. Les instal·lacions existents gestionades per paquets reben instruccions de migració. Consulta l’[ajuda d’instal·lació i PATH](docs/INSTALL.md).
 
 L’autocompleció amb Tab s’activa amb una sola ordre per shell — `codewhale completion bash|zsh|fish|powershell|elvish`. Consulta [l’autocompleció del shell](docs/INSTALL.md#8-shell-completions).
 
 ## Ús
 
-Parla amb Codewhale tal com parlaries amb una persona del teu equip:
+Obre un terminal a la carpeta del teu projecte i executa `codewhale`. Tria el proveïdor amb `/provider` i el model amb `/model`. Després, descriu una tasca concreta:
 
 ```text
 Fix the failing tests and explain what changed.
@@ -41,16 +45,24 @@ També pots executar una tasca sense obrir la TUI:
 codewhale exec "fix the failing tests and explain what changed"
 ```
 
-Codewhale pot llegir el teu repositori, editar fitxers, executar ordres, inspeccionar els resultats i continuar treballant cap a un objectiu. Tu decideixes quant accés li concedeixes.
+Codewhale pot llegir el teu repositori, editar fitxers, executar ordres, inspeccionar els resultats i continuar treballant cap a un objectiu. Fes servir `/mode plan` per explorar sense modificar fitxers ni executar ordres del shell, i `/mode work` quan vulguis que faci canvis. Prem `Shift+Tab` per triar Ask, Auto-Review o Full Access; la [guia de modes i permisos](docs/MODES.md) explica què permet cada opció.
+
+## Terminal, aplicacions i Computer Use
+
+El terminal i els clients gràfics es connecten al Runtime de Codewhale, que executa l’agent i les seves eines:
+
+- **Terminal:** `codewhale` obre la interfície interactiva; `codewhale exec` executa una tasca des d’un script o d’una feina de CI.
+- **Navegador local:** `codewhale web` obre el [client web local](docs/WEB.md) inclòs, que fa servir el mateix runtime.
+- **Aplicacions web i d’escriptori de Codewhale:** entorns de treball gràfics en desenvolupament. La seva disponibilitat s’indica a la [pàgina del producte](https://codewhale.net/en/product).
 
-## Interfície gràfica
+**Computer Use afegeix eines per observar altres aplicacions i interactuar-hi.** El connector està inclòs en el codi font actual. Revisa l’accés que demana i activa’l abans de fer-lo servir; els permisos del sistema operatiu i els requisits de la plataforma continuen sent necessaris. Consulta la [guia de Computer Use](crates/tui/plugins/computer-use/README.md) inclosa i la [configuració de connectors](docs/PLUGINS.md).
 
-Prefereixes una interfície gràfica? L'extensió CodeWhale for VS Code, mantinguda per la comunitat, posa el mateix agent a la barra lateral del VS Code — xat, converses per fils, diffs en directe i gestió de tasques sobre la mateixa Runtime API, i les sessions es mantenen sincronitzades amb el terminal. Instal·la-la des del [VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=HengQuWorld.brotherwhale-vscode); el codi font és a [GitHub](https://github.com/HengQuWorld/CodeWhale-VSCode).
+Per al VS Code, l’extensió CodeWhale mantinguda per la comunitat es connecta al Runtime local des d’una barra lateral. Instal·la-la des del [VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=HengQuWorld.brotherwhale-vscode); el codi font és a [GitHub](https://github.com/HengQuWorld/CodeWhale-VSCode).
 
 ## Per què Codewhale
 
-- **Fes servir el model que vulguis.** Connecta proveïdors allotjats o models locals mitjançant Ollama, vLLM o SGLang. Canvia de proveïdor i de model amb `/model`.
-- **Mantén el control.** Plan és només de lectura. Ask, Auto-Review i Full Access fan visible el comportament de les aprovacions. `/undo` desfà l’últim torn i `/restore` retorna l’espai de treball a una instantània anterior.
+- **Tria els teus models.** Connecta proveïdors allotjats o models locals mitjançant Ollama, vLLM o SGLang. Fes servir `/provider` per canviar de proveïdor i `/model` per triar un model.
+- **Mantén el control.** Revisa les accions proposades i els canvis que produeixen als fitxers. La configuració d’aprovacions determina quan cal una revisió; Full Access continua respectant els límits obligatoris de les polítiques. `/undo` i `/restore` ajuden a recuperar canvis de l’espai de treball.
 - **Mantén organitzades les feines llargues.** Desa sessions, defineix un `/goal` durador, revisa els fluxos de treball abans que s’executin i coordina agents sense convertir les seves instruccions internes en part de la teva conversa.
 - **Amplia l’agent que ja tens.** Connecta servidors MCP i habilitats, configura hooks i conserva els rols d’agent com a fitxers llegibles al projecte o a la configuració personal.
 
@@ -69,10 +81,11 @@ Llegeix l’[ordre d’autorització](docs/AUTHORIZATION_ORDER.md) per conèixer
 - [MCP](docs/MCP.md), [hooks](docs/HOOKS.md) i [configuració](docs/CONFIGURATION.md)
 - [Client web local](docs/WEB.md)
 - [Tota la documentació](docs)
+- [Estructura del repositori i guia de contribució](CONTRIBUTING.md#project-structure)
 
 ## Uneix-te a la comunitat
 
-Codewhale millora quan les persones l’utilitzen, expliquen què no funciona bé i ajuden a corregir-ho. Si falta un proveïdor, un flux de treball és incòmode o la interfície del terminal et dificulta la feina, [obre una incidència](https://github.com/Hmbown/CodeWhale/issues). Si saps com millorar-lo, [obre una pull request](CONTRIBUTING.md). Les primeres contribucions són benvingudes i qui hi contribueix conserva el reconeixement per la feina incorporada.
+**Els informes d’errors, les idees de funcionalitats i les pull requests són benvinguts**, tant si fa mesos que fas servir Codewhale com si el proves per primera vegada. Si falta un proveïdor, un flux de treball és incòmode o la interfície del terminal et dificulta la feina, [obre una incidència](https://github.com/Hmbown/CodeWhale/issues/new/choose) o [envia una pull request](CONTRIBUTING.md) perquè el puguem millorar plegats. Les primeres contribucions són benvingudes i qui hi contribueix conserva el reconeixement per la feina incorporada.
 
 Uneix-te al [Discord](https://discord.gg/37gfS3ksug), o afegeix Hunter a WeChat (`hunterbown`) i demana entrar al grup Whale Brothers.
 
diff --git a/README.de.md b/README.de.md
index c3871fbe20..9e7be52fdd 100644
--- a/README.de.md
+++ b/README.de.md
@@ -1,9 +1,11 @@
-
+
 # Codewhale
 
-Codewhale ist ein in Rust entwickelter Open-Source-Coding-Agent für dein Terminal, der gemeinsam mit seinen Nutzerinnen und Nutzern öffentlich weiterentwickelt wird.
+Codewhale ist ein Open-Source-Agent, der dein Projekt liest, Dateien bearbeitet, Befehle ausführt und seine Arbeit mit einem gehosteten oder lokalen Modell deiner Wahl prüft. Starte mit einer Aufgabe im Terminal. Teile eine größere Aufgabe auf Agenten mit verschiedenen Modellen und Rollen auf.
 
-![Codewhale in einem Terminal](assets/screenshot.webp)
+![Codewhale in einem Terminal](web/public/codewhale-tui-171acee.png)
+
+*Terminalvorschau aus einem Entwicklungsbuild von v0.9.12.*
 
 [English](README.md) · [简体中文](README.zh-CN.md) · [日本語](README.ja-JP.md) · [Tiếng Việt](README.vi.md) · [Bahasa Indonesia](README.id.md) · [한국어](README.ko-KR.md) · [Español](README.es-419.md) · [Português](README.pt-BR.md) · [Русский](README.ru.md) · [Українська](README.uk.md) · [Français](README.fr.md) · [繁體中文](README.zh-TW.md) · [हिन्दी](README.hi.md) · [Türkçe](README.tr.md) · [Italiano](README.it.md) · [Polski](README.pl.md) · [العربية](README.ar.md) · [Català](README.ca.md)
 
@@ -21,15 +23,17 @@ curl -fsSL https://codewhale.net/install.sh | sh
 "$HOME/.local/bin/codewhale"
 ```
 
+Das Installationsprogramm wählt die neueste veröffentlichte Version aus. Das [Änderungsprotokoll](CHANGELOG.md) beschreibt auch den noch unveröffentlichten Kandidaten für die nächste Version; diese Änderungen sind erst in den veröffentlichten Downloads enthalten, wenn die Version verfügbar ist.
+
 Unter Windows lade das passende Installationsprogramm oder Archiv von [GitHub Releases](https://github.com/Hmbown/CodeWhale/releases/latest) herunter. Bestehende direkte Installationen aktualisierst du mit `codewhale update`; `codewhale update --check` prüft nur. Der Updater zeigt den Pfad der ausführbaren Datei und behält neuere Builds bei. npm und Cargo sind nachrangige Paketoptionen. Hinweise zur Migration aus einer Paketverwaltung und zu PATH stehen in der [Installationsanleitung](docs/INSTALL.md).
 
-Beim ersten Start hilft dir Codewhale, einen Anbieter zu verbinden oder offline zu bleiben. Außerdem werden Cargo, Docker, Nix, Scoop, vorgefertigte Archive, Android/Termux und ein CNB-Spiegel unterstützt. Siehe [Installationsanleitung](docs/INSTALL.md).
+Beim ersten Start hilft dir Codewhale, einen Anbieter zu verbinden oder Codewhale offline einzurichten. Antworten erfordern ein verbundenes gehostetes oder lokales Modell. Codewhale unterstützt außerdem npm und Cargo als nachrangige Paketoptionen sowie Docker, Nix, Scoop, Android/Termux und einen optionalen CNB-Spiegel. Bestehende Installationen über Paketverwaltungen erhalten Migrationshinweise. Siehe die [Hilfe zu Installation und PATH](docs/INSTALL.md).
 
 Die Tab-Vervollständigung lässt sich für jede Shell mit einem einzigen Befehl aktivieren — `codewhale completion bash|zsh|fish|powershell|elvish`. Siehe [Shell-Vervollständigung](docs/INSTALL.md#8-shell-completions).
 
 ## Verwendung
 
-Sprich mit Codewhale so, wie du mit einem Teammitglied sprechen würdest:
+Öffne ein Terminal im Ordner deines Projekts und starte `codewhale`. Wähle deinen Anbieter mit `/provider` und dein Modell mit `/model`. Beschreibe dann eine konkrete Aufgabe:
 
 ```text
 Fix the failing tests and explain what changed.
@@ -41,16 +45,24 @@ Du kannst eine Aufgabe auch ausführen, ohne die TUI zu öffnen:
 codewhale exec "fix the failing tests and explain what changed"
 ```
 
-Codewhale kann dein Repository lesen, Dateien bearbeiten, Befehle ausführen, Ergebnisse prüfen und auf ein Ziel hinarbeiten. Du entscheidest, wie viel Zugriff der Agent erhält.
+Codewhale kann dein Repository lesen, Dateien bearbeiten, Befehle ausführen, Ergebnisse prüfen und auf ein Ziel hinarbeiten. Nutze `/mode plan`, um ohne Dateiänderungen oder Shell-Ausführung zu erkunden, und `/mode work`, wenn der Agent Änderungen vornehmen soll. Drücke `Shift+Tab`, um Ask, Auto-Review oder Full Access auszuwählen; die [Anleitung zu Modi und Berechtigungen](docs/MODES.md) erklärt, was jeweils erlaubt ist.
+
+## Terminal, Apps und Computer Use
+
+Das Terminal und die grafischen Clients verbinden sich mit der Codewhale Runtime, die den Agenten und seine Werkzeuge ausführt:
+
+- **Terminal:** `codewhale` öffnet die interaktive Oberfläche; `codewhale exec` führt eine Aufgabe aus einem Skript oder CI-Job aus.
+- **Lokaler Browser:** `codewhale web` öffnet den mitgelieferten [lokalen Webclient](docs/WEB.md) für dieselbe Runtime.
+- **Web- und Desktop-Apps von Codewhale:** grafische Arbeitsumgebungen in Entwicklung. Ihre Verfügbarkeit ist auf der [Produktseite](https://codewhale.net/en/product) angegeben.
 
-## Grafische Oberfläche
+**Computer Use ergänzt Werkzeuge zum Beobachten anderer Anwendungen und zur Interaktion mit ihnen.** Das Plugin ist im aktuellen Quellcode enthalten. Prüfe die angeforderten Zugriffsrechte und aktiviere es vor der Verwendung; Betriebssystemberechtigungen und Plattformanforderungen gelten weiterhin. Siehe die mitgelieferte [Anleitung zu Computer Use](crates/tui/plugins/computer-use/README.md) und die [Plugin-Einrichtung](docs/PLUGINS.md).
 
-Lieber eine grafische Oberfläche? Die von der Community gepflegte Erweiterung CodeWhale for VS Code bringt denselben Agenten in die VS-Code-Seitenleiste — Chat, Thread-Gespräche, Live-Diffs und Aufgabenverwaltung über dieselbe Runtime-API, sodass Sitzungen mit dem Terminal synchron bleiben. Installieren Sie sie aus dem [VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=HengQuWorld.brotherwhale-vscode); der Quellcode liegt auf [GitHub](https://github.com/HengQuWorld/CodeWhale-VSCode).
+Für VS Code verbindet sich die von der Community gepflegte CodeWhale-Erweiterung über eine Seitenleiste mit der lokalen Runtime. Installiere sie aus dem [VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=HengQuWorld.brotherwhale-vscode); der Quellcode liegt auf [GitHub](https://github.com/HengQuWorld/CodeWhale-VSCode).
 
 ## Warum Codewhale
 
-- **Nutze das gewünschte Modell.** Verbinde gehostete Anbieter oder lokale Modelle über Ollama, vLLM oder SGLang. Mit `/model` wechselst du Anbieter und Modell.
-- **Behalte die Kontrolle.** Plan ist schreibgeschützt. Ask, Auto-Review und Full Access machen das Genehmigungsverhalten sichtbar. `/undo` macht die letzte Interaktion rückgängig und `/restore` setzt den Arbeitsbereich auf einen früheren Snapshot zurück.
+- **Wähle deine Modelle.** Verbinde gehostete Anbieter oder lokale Modelle über Ollama, vLLM oder SGLang. Mit `/provider` wechselst du den Anbieter, mit `/model` wählst du ein Modell.
+- **Behalte die Kontrolle.** Prüfe vorgeschlagene Aktionen und die daraus entstehenden Dateiänderungen. Die Genehmigungseinstellungen bestimmen, wann eine Prüfung nötig ist; Full Access beachtet weiterhin die verbindlichen Grenzen der Richtlinien. `/undo` und `/restore` helfen bei der Wiederherstellung von Änderungen im Arbeitsbereich.
 - **Halte lange Arbeiten übersichtlich.** Speichere Sitzungen, setze ein dauerhaftes `/goal`, prüfe Workflows vor der Ausführung und koordiniere Agenten, ohne dass ihre internen Anweisungen in deinem Gesprächsverlauf erscheinen.
 - **Erweitere deinen vorhandenen Agenten.** Verbinde MCP-Server und Skills, konfiguriere Hooks und verwalte Agentenrollen als lesbare Dateien in deinem Projekt oder in deinen persönlichen Einstellungen.
 
@@ -69,10 +81,11 @@ Lies die [Autorisierungsreihenfolge](docs/AUTHORIZATION_ORDER.md) für die genau
 - [MCP](docs/MCP.md), [Hooks](docs/HOOKS.md) und [Konfiguration](docs/CONFIGURATION.md)
 - [Lokaler Webclient](docs/WEB.md)
 - [Gesamte Dokumentation](docs)
+- [Aufbau des Repositorys und Anleitung zum Mitwirken](CONTRIBUTING.md#project-structure)
 
 ## Der Community beitreten
 
-Codewhale wird besser, wenn Menschen es nutzen, Probleme melden und bei der Behebung helfen. Wenn ein Anbieter fehlt, ein Workflow umständlich ist oder dir die Terminaloberfläche im Weg steht, [eröffne ein Issue](https://github.com/Hmbown/CodeWhale/issues). Wenn du weißt, wie es besser geht, [eröffne einen Pull Request](CONTRIBUTING.md). Erste Beiträge sind willkommen, und Mitwirkende behalten die Anerkennung für ihre übernommenen Arbeiten.
+**Fehlerberichte, Funktionsideen und Pull Requests sind willkommen**, egal ob du Codewhale seit Monaten nutzt oder zum ersten Mal ausprobierst. Wenn ein Anbieter fehlt, ein Workflow umständlich ist oder dir die Terminaloberfläche im Weg steht, [eröffne ein Issue](https://github.com/Hmbown/CodeWhale/issues/new/choose) oder [sende einen Pull Request](CONTRIBUTING.md), damit wir es gemeinsam verbessern können. Erste Beiträge sind willkommen, und Mitwirkende behalten die Anerkennung für ihre übernommenen Arbeiten.
 
 Tritt unserem [Discord](https://discord.gg/37gfS3ksug) bei oder füge Hunter auf WeChat (`hunterbown`) hinzu und bitte um Aufnahme in die Whale-Brothers-Gruppe.
 
diff --git a/README.es-419.md b/README.es-419.md
index b5152e3eb7..f4a5131e55 100644
--- a/README.es-419.md
+++ b/README.es-419.md
@@ -1,9 +1,11 @@
-
+
 # Codewhale
 
-Codewhale es un agente de programación de código abierto para tu terminal, desarrollado en Rust y mejorado públicamente junto con las personas que lo usan.
+Codewhale es un agente de código abierto que lee tu proyecto, edita archivos, ejecuta comandos y comprueba su trabajo con un modelo alojado o local que tú eliges. Empieza con una tarea en la terminal. Para un trabajo más grande, asigna partes del trabajo a agentes con distintos modelos y roles.
 
-![Codewhale ejecutándose en una terminal](assets/screenshot.webp)
+![Codewhale ejecutándose en una terminal](web/public/codewhale-tui-171acee.png)
+
+*Vista previa de la terminal de una compilación de desarrollo de v0.9.12.*
 
 [English](README.md) · [简体中文](README.zh-CN.md) · [日本語](README.ja-JP.md) · [Tiếng Việt](README.vi.md) · [Bahasa Indonesia](README.id.md) · [한국어](README.ko-KR.md) · [Português](README.pt-BR.md) · [Русский](README.ru.md) · [Українська](README.uk.md) · [Français](README.fr.md) · [Deutsch](README.de.md) · [繁體中文](README.zh-TW.md) · [हिन्दी](README.hi.md) · [Türkçe](README.tr.md) · [Italiano](README.it.md) · [Polski](README.pl.md) · [العربية](README.ar.md) · [Català](README.ca.md)
 
@@ -21,15 +23,17 @@ curl -fsSL https://codewhale.net/install.sh | sh
 "$HOME/.local/bin/codewhale"
 ```
 
+El instalador selecciona la última versión publicada. El [registro de cambios](CHANGELOG.md) también describe la versión candidata aún no publicada de la próxima versión; esos cambios no se incluyen en las descargas publicadas hasta que la versión esté disponible.
+
 En Windows, descarga el instalador o archivo correspondiente de [GitHub Releases](https://github.com/Hmbown/CodeWhale/releases/latest). Para actualizar una instalación directa existente, ejecuta `codewhale update`, o `codewhale update --check` para consultar sin instalar. El actualizador muestra la ruta del ejecutable y conserva las compilaciones más recientes. npm y Cargo son opciones secundarias; consulta la [guía de instalación](docs/INSTALL.md) para migrar desde un gestor de paquetes y configurar PATH.
 
-La primera vez que se ejecuta, Codewhale te ayuda a conectar un proveedor o a seguir sin conexión. También admite Cargo, Docker, Nix, Scoop, archivos precompilados, Android/Termux y un espejo de CNB. Consulta la [guía de instalación](docs/INSTALL.md).
+La primera vez que se ejecuta, Codewhale te ayuda a conectar un proveedor o a configurar Codewhale sin conexión. Las respuestas requieren un modelo alojado o local conectado. Codewhale también admite npm y Cargo como opciones secundarias de distribución, además de Docker, Nix, Scoop, Android/Termux y un espejo opcional de CNB. Las instalaciones existentes gestionadas por paquetes reciben instrucciones de migración. Consulta la [ayuda de instalación y PATH](docs/INSTALL.md).
 
 El completado con Tab se configura con un comando por shell — `codewhale completion bash|zsh|fish|powershell|elvish`. Consulta el [completado de shell](docs/INSTALL.md#8-shell-completions).
 
 ## Uso
 
-Habla con Codewhale como hablarías con alguien de tu equipo:
+Abre una terminal en la carpeta de tu proyecto y ejecuta `codewhale`. Elige tu proveedor con `/provider` y tu modelo con `/model`. Después, describe una tarea concreta:
 
 ```text
 Fix the failing tests and explain what changed.
@@ -41,16 +45,24 @@ También puedes ejecutar una tarea sin abrir la TUI:
 codewhale exec "fix the failing tests and explain what changed"
 ```
 
-Codewhale puede leer tu repositorio, editar archivos, ejecutar comandos, revisar los resultados y seguir trabajando para alcanzar un objetivo. Tú decides cuánto acceso darle.
+Codewhale puede leer tu repositorio, editar archivos, ejecutar comandos, revisar los resultados y seguir trabajando para alcanzar un objetivo. Usa `/mode plan` para explorar sin modificar archivos ni ejecutar comandos de shell, y `/mode work` cuando quieras que haga cambios. Presiona `Shift+Tab` para elegir Ask, Auto-Review o Full Access; la [guía de modos y permisos](docs/MODES.md) explica qué permite cada opción.
+
+## Terminal, aplicaciones y Computer Use
+
+La terminal y los clientes gráficos se conectan al Runtime de Codewhale, que ejecuta el agente y sus herramientas:
+
+- **Terminal:** `codewhale` abre la interfaz interactiva; `codewhale exec` ejecuta una tarea desde un script o un trabajo de CI.
+- **Navegador local:** `codewhale web` abre el [cliente web local](docs/WEB.md) incluido, que usa el mismo runtime.
+- **Aplicaciones web y de escritorio de Codewhale:** entornos de trabajo gráficos en desarrollo. Su disponibilidad se indica en la [página del producto](https://codewhale.net/en/product).
 
-## Interfaz gráfica
+**Computer Use agrega herramientas para observar otras aplicaciones e interactuar con ellas.** El plugin está incluido en el código fuente actual. Revisa el acceso que solicita y habilítalo antes de usarlo; los permisos del sistema operativo y los requisitos de la plataforma siguen siendo necesarios. Consulta la [guía de Computer Use](crates/tui/plugins/computer-use/README.md) incluida y la [configuración de plugins](docs/PLUGINS.md).
 
-¿Prefieres una interfaz gráfica? La extensión CodeWhale for VS Code, mantenida por la comunidad, envuelve al mismo agente en la barra lateral de VS Code — chat, conversaciones por hilos, diffs en vivo y gestión de tareas sobre la misma Runtime API, para que las sesiones se mantengan sincronizadas con la terminal. Instálala desde el [VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=HengQuWorld.brotherwhale-vscode); el código fuente está en [GitHub](https://github.com/HengQuWorld/CodeWhale-VSCode).
+Para VS Code, la extensión CodeWhale mantenida por la comunidad se conecta al Runtime local desde una barra lateral. Instálala desde el [VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=HengQuWorld.brotherwhale-vscode); el código fuente está en [GitHub](https://github.com/HengQuWorld/CodeWhale-VSCode).
 
 ## Por qué Codewhale
 
-- **Usa el modelo que prefieras.** Conecta proveedores alojados o modelos locales mediante Ollama, vLLM o SGLang. Cambia de proveedor y modelo con `/model`.
-- **Mantén el control.** Plan es de solo lectura. Ask, Auto-Review y Full Access hacen visible el comportamiento de las aprobaciones. `/undo` revierte el último turno y `/restore` devuelve el espacio de trabajo a una instantánea anterior.
+- **Elige tus modelos.** Conecta proveedores alojados o modelos locales mediante Ollama, vLLM o SGLang. Usa `/provider` para cambiar de proveedor y `/model` para elegir un modelo.
+- **Mantén el control.** Revisa las acciones propuestas y los cambios que producen en los archivos. La configuración de aprobaciones determina cuándo se necesita una revisión; Full Access sigue respetando los límites obligatorios de las políticas. `/undo` y `/restore` ayudan a recuperar cambios del espacio de trabajo.
 - **Mantén organizado el trabajo de larga duración.** Guarda sesiones, establece un `/goal` duradero, revisa los flujos de trabajo antes de ejecutarlos y coordina agentes sin convertir sus instrucciones internas en parte de tu conversación.
 - **Amplía el agente que ya tienes.** Conecta servidores MCP y habilidades, configura hooks y conserva los roles de los agentes como archivos legibles en tu proyecto o configuración personal.
 
@@ -69,10 +81,11 @@ Lee el [orden de autorización](docs/AUTHORIZATION_ORDER.md) para conocer la jer
 - [MCP](docs/MCP.md), [hooks](docs/HOOKS.md) y [configuración](docs/CONFIGURATION.md)
 - [Cliente web local](docs/WEB.md)
 - [Toda la documentación](docs)
+- [Estructura del repositorio y guía de contribución](CONTRIBUTING.md#project-structure)
 
 ## Únete a la comunidad
 
-Codewhale mejora cuando las personas lo usan, informan lo que no funciona bien y ayudan a corregirlo. Si falta un proveedor, un flujo de trabajo resulta incómodo o la interfaz de terminal se interpone en tu camino, [abre un issue](https://github.com/Hmbown/CodeWhale/issues). Si sabes cómo mejorarlo, [abre un pull request](CONTRIBUTING.md). Las primeras contribuciones son bienvenidas y quienes contribuyen conservan el crédito por el trabajo que se incorpora.
+**Recibimos con gusto reportes de errores, ideas de funciones y pull requests**, tanto si llevas meses usando Codewhale como si lo pruebas por primera vez. Si falta un proveedor, un flujo de trabajo resulta incómodo o la interfaz de terminal te estorba, [abre un issue](https://github.com/Hmbown/CodeWhale/issues/new/choose) o [envía un pull request](CONTRIBUTING.md) para que podamos mejorarlo juntos. Las primeras contribuciones son bienvenidas y quienes contribuyen conservan el crédito por el trabajo que se incorpora.
 
 Únete a [Discord](https://discord.gg/37gfS3ksug), o agrega a Hunter en WeChat (`hunterbown`) y pide entrar al grupo Whale Brothers.
 
diff --git a/README.fr.md b/README.fr.md
index 925f5bb495..610a5d6fdc 100644
--- a/README.fr.md
+++ b/README.fr.md
@@ -1,9 +1,11 @@
-
+
 # Codewhale
 
-Codewhale est un agent de programmation open source pour votre terminal, développé en Rust et amélioré publiquement avec les personnes qui l’utilisent.
+Codewhale est un agent open source qui lit votre projet, modifie des fichiers, exécute des commandes et vérifie son travail avec un modèle hébergé ou local de votre choix. Commencez par une tâche dans votre terminal. Pour un travail plus important, confiez-en des parties à des agents utilisant différents modèles et rôles.
 
-![Codewhale en cours d’exécution dans un terminal](assets/screenshot.webp)
+![Codewhale en cours d’exécution dans un terminal](web/public/codewhale-tui-171acee.png)
+
+*Aperçu du terminal dans une version de développement de la v0.9.12.*
 
 [English](README.md) · [简体中文](README.zh-CN.md) · [日本語](README.ja-JP.md) · [Tiếng Việt](README.vi.md) · [Bahasa Indonesia](README.id.md) · [한국어](README.ko-KR.md) · [Español](README.es-419.md) · [Português](README.pt-BR.md) · [Русский](README.ru.md) · [Українська](README.uk.md) · [Deutsch](README.de.md) · [繁體中文](README.zh-TW.md) · [हिन्दी](README.hi.md) · [Türkçe](README.tr.md) · [Italiano](README.it.md) · [Polski](README.pl.md) · [العربية](README.ar.md) · [Català](README.ca.md)
 
@@ -21,15 +23,17 @@ curl -fsSL https://codewhale.net/install.sh | sh
 "$HOME/.local/bin/codewhale"
 ```
 
+L’installeur sélectionne la dernière version publiée. Le [journal des modifications](CHANGELOG.md) décrit aussi la version candidate, encore non publiée, de la prochaine version ; ces modifications ne sont incluses dans les téléchargements publiés qu’une fois la version disponible.
+
 Sur Windows, téléchargez l’installeur ou l’archive adaptés depuis [GitHub Releases](https://github.com/Hmbown/CodeWhale/releases/latest). Pour une installation directe existante, lancez `codewhale update`, ou `codewhale update --check` pour vérifier sans installer. L’outil affiche le chemin de l’exécutable et conserve les versions de développement plus récentes. npm et Cargo sont des options secondaires ; consultez le [guide d’installation](docs/INSTALL.md) pour migrer depuis un gestionnaire de paquets et configurer PATH.
 
-Au premier lancement, Codewhale vous aide à connecter un fournisseur ou à rester hors ligne. Il prend également en charge Cargo, Docker, Nix, Scoop, les archives précompilées, Android/Termux et un miroir CNB. Consultez le [guide d’installation](docs/INSTALL.md).
+Au premier lancement, Codewhale vous aide à connecter un fournisseur ou à configurer Codewhale hors ligne. Les réponses nécessitent un modèle hébergé ou local connecté. Codewhale prend aussi en charge npm et Cargo comme options de distribution secondaires, ainsi que Docker, Nix, Scoop, Android/Termux et un miroir CNB facultatif. Les installations existantes gérées par un gestionnaire de paquets reçoivent des instructions de migration. Consultez l’[aide à l’installation et à la configuration du PATH](docs/INSTALL.md).
 
 L’autocomplétion avec Tab s’active avec une commande par shell — `codewhale completion bash|zsh|fish|powershell|elvish`. Consultez [l’autocomplétion du shell](docs/INSTALL.md#8-shell-completions).
 
 ## Utilisation
 
-Parlez à Codewhale comme vous parleriez à un membre de votre équipe :
+Ouvrez un terminal dans le dossier de votre projet et lancez `codewhale`. Choisissez votre fournisseur avec `/provider` et votre modèle avec `/model`. Décrivez ensuite une tâche concrète :
 
 ```text
 Fix the failing tests and explain what changed.
@@ -41,16 +45,24 @@ Vous pouvez aussi exécuter une tâche sans ouvrir la TUI :
 codewhale exec "fix the failing tests and explain what changed"
 ```
 
-Codewhale peut lire votre dépôt, modifier des fichiers, exécuter des commandes, inspecter les résultats et continuer à travailler vers un objectif. Vous choisissez le niveau d’accès que vous lui accordez.
+Codewhale peut lire votre dépôt, modifier des fichiers, exécuter des commandes, inspecter les résultats et continuer à travailler vers un objectif. Utilisez `/mode plan` pour explorer sans modifier de fichiers ni exécuter de commandes shell, et `/mode work` lorsque vous souhaitez qu’il effectue des modifications. Appuyez sur `Shift+Tab` pour choisir Ask, Auto-Review ou Full Access ; le [guide des modes et des permissions](docs/MODES.md) explique ce que chaque option autorise.
+
+## Terminal, applications et Computer Use
+
+Le terminal et les clients graphiques se connectent au Runtime Codewhale, qui exécute l’agent et ses outils :
+
+- **Terminal :** `codewhale` ouvre l’interface interactive ; `codewhale exec` exécute une tâche depuis un script ou une tâche de CI.
+- **Navigateur local :** `codewhale web` ouvre le [client web local](docs/WEB.md) fourni, qui utilise le même runtime.
+- **Applications web et de bureau Codewhale :** des espaces de travail graphiques en développement. Leur disponibilité est indiquée sur la [page du produit](https://codewhale.net/en/product).
 
-## Interface graphique
+**Computer Use ajoute des outils pour observer d’autres applications et interagir avec elles.** Le plugin est inclus dans le code source actuel. Examinez les accès demandés et activez-le avant de l’utiliser ; les permissions du système d’exploitation et les exigences de la plateforme s’appliquent toujours. Consultez le [guide Computer Use](crates/tui/plugins/computer-use/README.md) inclus et la [configuration des plugins](docs/PLUGINS.md).
 
-Vous préférez une interface graphique ? L'extension CodeWhale for VS Code, maintenue par la communauté, intègre le même agent dans la barre latérale de VS Code — chat, conversations par fils, diff en direct et gestion des tâches via la même Runtime API, pour que les sessions restent synchronisées avec le terminal. Installez-la depuis le [marketplace VS Code](https://marketplace.visualstudio.com/items?itemName=HengQuWorld.brotherwhale-vscode) ; le code source est sur [GitHub](https://github.com/HengQuWorld/CodeWhale-VSCode).
+Pour VS Code, l’extension CodeWhale maintenue par la communauté se connecte au Runtime local depuis une barre latérale. Installez-la depuis le [marketplace VS Code](https://marketplace.visualstudio.com/items?itemName=HengQuWorld.brotherwhale-vscode) ; le code source est sur [GitHub](https://github.com/HengQuWorld/CodeWhale-VSCode).
 
 ## Pourquoi Codewhale
 
-- **Utilisez le modèle de votre choix.** Connectez des fournisseurs hébergés ou des modèles locaux via Ollama, vLLM ou SGLang. Changez de fournisseur et de modèle avec `/model`.
-- **Gardez le contrôle.** Le mode Plan est en lecture seule. Ask, Auto-Review et Full Access rendent le comportement des approbations explicite. `/undo` annule le dernier tour et `/restore` ramène l’espace de travail à un instantané antérieur.
+- **Choisissez vos modèles.** Connectez des fournisseurs hébergés ou des modèles locaux via Ollama, vLLM ou SGLang. Utilisez `/provider` pour changer de fournisseur et `/model` pour choisir un modèle.
+- **Gardez le contrôle.** Examinez les actions proposées et les modifications de fichiers qui en résultent. Les paramètres d’approbation déterminent quand un examen est nécessaire ; Full Access respecte toujours les limites impératives des politiques. `/undo` et `/restore` aident à récupérer les modifications de l’espace de travail.
 - **Organisez les travaux de longue durée.** Enregistrez les sessions, définissez un `/goal` durable, examinez les workflows avant leur exécution et coordonnez des agents sans faire apparaître leurs instructions internes dans votre conversation.
 - **Étendez l’agent que vous possédez déjà.** Connectez des serveurs MCP et des compétences, configurez des hooks et conservez les rôles d’agent sous forme de fichiers lisibles dans votre projet ou vos paramètres personnels.
 
@@ -69,10 +81,11 @@ Consultez l’[ordre d’autorisation](docs/AUTHORIZATION_ORDER.md) pour connaî
 - [MCP](docs/MCP.md), [hooks](docs/HOOKS.md) et [configuration](docs/CONFIGURATION.md)
 - [Client web local](docs/WEB.md)
 - [Toute la documentation](docs)
+- [Organisation du dépôt et guide de contribution](CONTRIBUTING.md#project-structure)
 
 ## Rejoindre la communauté
 
-Codewhale progresse lorsque les gens l’utilisent, signalent ce qui ne va pas et contribuent aux correctifs. S’il manque un fournisseur, si un workflow est peu pratique ou si l’interface du terminal vous gêne, [ouvrez une issue](https://github.com/Hmbown/CodeWhale/issues). Si vous savez comment l’améliorer, [ouvrez une pull request](CONTRIBUTING.md). Les premières contributions sont les bienvenues, et les personnes qui contribuent restent créditées pour le travail intégré.
+**Les signalements de bugs, les idées de fonctionnalités et les pull requests sont les bienvenus**, que vous utilisiez Codewhale depuis des mois ou que vous l’essayiez pour la première fois. S’il manque un fournisseur, si un workflow est peu pratique ou si l’interface du terminal vous gêne, [ouvrez une issue](https://github.com/Hmbown/CodeWhale/issues/new/choose) ou [envoyez une pull request](CONTRIBUTING.md) pour que nous puissions l’améliorer ensemble. Les premières contributions sont les bienvenues, et les personnes qui contribuent restent créditées pour le travail intégré.
 
 Rejoignez le [Discord](https://discord.gg/37gfS3ksug), ou ajoutez Hunter sur WeChat (`hunterbown`) et demandez à rejoindre le groupe Whale Brothers.
 
diff --git a/README.hi.md b/README.hi.md
index fe22e2590c..e919cb2712 100644
--- a/README.hi.md
+++ b/README.hi.md
@@ -1,9 +1,11 @@
-
+
 # Codewhale
 
-Codewhale आपके टर्मिनल के लिए Rust में बना एक ओपन सोर्स कोडिंग एजेंट है, जिसे इसके उपयोगकर्ताओं के साथ सार्वजनिक रूप से बेहतर बनाया जाता है।
+Codewhale एक ओपन सोर्स एजेंट है जो आपकी पसंद के होस्ट किए गए या लोकल मॉडल से आपका प्रोजेक्ट पढ़ता है, फ़ाइलें संपादित करता है, कमांड चलाता है और अपने काम की जाँच करता है। टर्मिनल में एक काम से शुरुआत करें। बड़े काम के हिस्से अलग-अलग मॉडल और भूमिकाओं वाले एजेंटों को सौंपें।
 
-![टर्मिनल में चलता Codewhale](assets/screenshot.webp)
+![टर्मिनल में चलता Codewhale](web/public/codewhale-tui-171acee.png)
+
+*v0.9.12 के विकासाधीन बिल्ड से टर्मिनल का पूर्वावलोकन।*
 
 [English](README.md) · [简体中文](README.zh-CN.md) · [日本語](README.ja-JP.md) · [Tiếng Việt](README.vi.md) · [Bahasa Indonesia](README.id.md) · [한국어](README.ko-KR.md) · [Español](README.es-419.md) · [Português](README.pt-BR.md) · [Русский](README.ru.md) · [Українська](README.uk.md) · [Français](README.fr.md) · [Deutsch](README.de.md) · [繁體中文](README.zh-TW.md) · [Türkçe](README.tr.md) · [Italiano](README.it.md) · [Polski](README.pl.md) · [العربية](README.ar.md) · [Català](README.ca.md)
 
@@ -21,15 +23,17 @@ curl -fsSL https://codewhale.net/install.sh | sh
 "$HOME/.local/bin/codewhale"
 ```
 
+इंस्टॉलर सबसे नई प्रकाशित रिलीज़ चुनता है। [बदलावों की सूची](CHANGELOG.md) में अगली रिलीज़ के अभी तक अप्रकाशित कैंडिडेट का भी विवरण है; रिलीज़ उपलब्ध होने तक ये बदलाव प्रकाशित डाउनलोड में शामिल नहीं होते।
+
 Windows पर [GitHub Releases](https://github.com/Hmbown/CodeWhale/releases/latest) से उपयुक्त इंस्टॉलर या आर्काइव डाउनलोड करें। मौजूदा सीधे इंस्टॉलेशन को अपडेट करने के लिए `codewhale update` चलाएँ; केवल जाँच के लिए `codewhale update --check` इस्तेमाल करें। अपडेटर executable का पथ दिखाता है और नए बिल्ड सुरक्षित रखता है। npm और Cargo वैकल्पिक पैकेजिंग तरीके हैं। पैकेज मैनेजर वाले इंस्टॉलेशन से माइग्रेशन और PATH के लिए [इंस्टॉलेशन गाइड](docs/INSTALL.md) देखें।
 
-पहली बार चलाने पर Codewhale आपको किसी प्रोवाइडर से जुड़ने या ऑफ़लाइन बने रहने में मदद करता है। यह Cargo, Docker, Nix, Scoop, पहले से बने आर्काइव, Android/Termux और CNB मिरर का भी समर्थन करता है। [इंस्टॉलेशन गाइड](docs/INSTALL.md) देखें।
+पहली बार चलाने पर Codewhale आपको किसी प्रोवाइडर से जुड़ने या Codewhale को ऑफ़लाइन कॉन्फ़िगर करने में मदद करता है। मॉडल से जवाब पाने के लिए किसी होस्ट किए गए या लोकल मॉडल से कनेक्शन ज़रूरी है। Codewhale अतिरिक्त पैकेजिंग विकल्पों के रूप में npm और Cargo के साथ-साथ Docker, Nix, Scoop, Android/Termux और वैकल्पिक CNB मिरर का भी समर्थन करता है। पैकेज मैनेजर से प्रबंधित मौजूदा इंस्टॉलेशन के लिए माइग्रेशन के निर्देश मिलते हैं। [इंस्टॉलेशन और PATH से जुड़ी मदद](docs/INSTALL.md) देखें।
 
 हर शेल में Tab completion के लिए केवल एक कमांड चाहिए — `codewhale completion bash|zsh|fish|powershell|elvish`। [शेल कंप्लीशन](docs/INSTALL.md#8-shell-completions) देखें।
 
 ## उपयोग
 
-Codewhale से वैसे ही बात करें जैसे आप अपनी टीम के किसी सदस्य से करेंगे:
+अपने प्रोजेक्ट फ़ोल्डर में टर्मिनल खोलें और `codewhale` चलाएँ। `/provider` से अपना प्रोवाइडर और `/model` से अपना मॉडल चुनें। फिर कोई ठोस काम बताएँ:
 
 ```text
 Fix the failing tests and explain what changed.
@@ -41,16 +45,24 @@ Fix the failing tests and explain what changed.
 codewhale exec "fix the failing tests and explain what changed"
 ```
 
-Codewhale आपकी रिपॉज़िटरी पढ़ सकता है, फ़ाइलें संपादित कर सकता है, कमांड चला सकता है, परिणामों की जाँच कर सकता है और लक्ष्य की ओर काम जारी रख सकता है। उसे कितना एक्सेस देना है, यह आप तय करते हैं।
+Codewhale आपकी रिपॉज़िटरी पढ़ सकता है, फ़ाइलें संपादित कर सकता है, कमांड चला सकता है, परिणामों की जाँच कर सकता है और लक्ष्य की ओर काम जारी रख सकता है। फ़ाइलें बदले या शेल कमांड चलाए बिना पड़ताल करने के लिए `/mode plan` इस्तेमाल करें, और बदलाव करवाने के लिए `/mode work` चुनें। Ask, Auto-Review या Full Access चुनने के लिए `Shift+Tab` दबाएँ; [मोड और अनुमतियों की गाइड](docs/MODES.md) बताती है कि हर विकल्प में क्या करने की अनुमति है।
+
+## टर्मिनल, ऐप और Computer Use
+
+टर्मिनल और ग्राफ़िकल क्लाइंट Codewhale Runtime से जुड़ते हैं, जो एजेंट और उसके टूल चलाता है:
+
+- **टर्मिनल:** `codewhale` इंटरैक्टिव इंटरफ़ेस खोलता है; `codewhale exec` किसी स्क्रिप्ट या CI जॉब से काम चलाता है।
+- **लोकल ब्राउज़र:** `codewhale web` उसी रनटाइम के लिए पैकेज में शामिल [लोकल वेब क्लाइंट](docs/WEB.md) खोलता है।
+- **Codewhale वेब और डेस्कटॉप ऐप:** विकासाधीन ग्राफ़िकल कार्यस्थल हैं। उनकी उपलब्धता [प्रोडक्ट पेज](https://codewhale.net/en/product) पर दी गई है।
 
-## GUI फ्रंटएंड
+**Computer Use दूसरे ऐप देखने और उनके साथ इंटरैक्ट करने के लिए टूल जोड़ता है।** प्लगइन मौजूदा सोर्स कोड में शामिल है। इस्तेमाल से पहले उसके माँगे गए एक्सेस की समीक्षा करें और उसे सक्षम करें; OS की अनुमतियाँ और प्लेटफ़ॉर्म की आवश्यकताएँ तब भी लागू होती हैं। शामिल [Computer Use गाइड](crates/tui/plugins/computer-use/README.md) और [प्लगइन सेटअप](docs/PLUGINS.md) देखें।
 
-क्या आप ग्राफ़िकल इंटरफ़ेस पसंद करते हैं? समुदाय द्वारा अनुरक्षित CodeWhale for VS Code एक्सटेंशन उसी एजेंट को VS Code साइडबार में लाता है — चैट, थ्रेडेड वार्तालाप, लाइव डिफ़ और कार्य प्रबंधन, सभी उसी Runtime API पर आधारित, ताकि सत्र टर्मिनल के साथ सिंक में रहें। इसे [VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=HengQuWorld.brotherwhale-vscode) से इंस्टॉल करें; सोर्स कोड [GitHub](https://github.com/HengQuWorld/CodeWhale-VSCode) पर है।
+समुदाय द्वारा अनुरक्षित VS Code का CodeWhale एक्सटेंशन साइडबार से लोकल Runtime से जुड़ता है। इसे [VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=HengQuWorld.brotherwhale-vscode) से इंस्टॉल करें; सोर्स कोड [GitHub](https://github.com/HengQuWorld/CodeWhale-VSCode) पर है।
 
 ## Codewhale क्यों
 
-- **अपनी पसंद का मॉडल इस्तेमाल करें।** होस्ट किए गए प्रोवाइडर या Ollama, vLLM अथवा SGLang के माध्यम से लोकल मॉडल जोड़ें। `/model` से प्रोवाइडर और मॉडल बदलें।
-- **नियंत्रण अपने पास रखें।** Plan केवल पढ़ने के लिए है। Ask, Auto-Review और Full Access अनुमोदन के व्यवहार को स्पष्ट बनाते हैं। `/undo` पिछला टर्न वापस करता है और `/restore` वर्कस्पेस को पहले के स्नैपशॉट पर लौटाता है।
+- **अपने मॉडल चुनें।** होस्ट किए गए प्रोवाइडर या Ollama, vLLM अथवा SGLang के माध्यम से लोकल मॉडल जोड़ें। प्रोवाइडर बदलने के लिए `/provider` और मॉडल चुनने के लिए `/model` इस्तेमाल करें।
+- **नियंत्रण अपने पास रखें।** प्रस्तावित कार्रवाइयों और उनसे फ़ाइलों में हुए बदलावों की जाँच करें। अनुमोदन की सेटिंग तय करती हैं कि समीक्षा कब ज़रूरी है; Full Access भी नीति की बाध्यकारी सीमाओं का पालन करता है। `/undo` और `/restore` बदलावों के बाद वर्कस्पेस बहाल करने में मदद करते हैं।
 - **लंबे काम को व्यवस्थित रखें।** सेशन सहेजें, स्थायी `/goal` तय करें, वर्कफ़्लो चलने से पहले उनकी समीक्षा करें और एजेंटों के आंतरिक निर्देशों को अपनी बातचीत में जोड़े बिना उनका समन्वय करें।
 - **अपने मौजूदा एजेंट को विस्तृत करें।** MCP सर्वर और स्किल जोड़ें, हुक कॉन्फ़िगर करें और एजेंट की भूमिकाओं को अपने प्रोजेक्ट या निजी सेटिंग में पढ़ने योग्य फ़ाइलों के रूप में रखें।
 
@@ -69,10 +81,11 @@ Codewhale आपकी मशीन पर उतने ही एक्से
 - [MCP](docs/MCP.md), [हुक](docs/HOOKS.md) और [कॉन्फ़िगरेशन](docs/CONFIGURATION.md)
 - [लोकल वेब क्लाइंट](docs/WEB.md)
 - [सभी दस्तावेज़](docs)
+- [रिपॉज़िटरी की संरचना और योगदान गाइड](CONTRIBUTING.md#project-structure)
 
 ## समुदाय से जुड़ें
 
-जब लोग Codewhale का उपयोग करते हैं, असुविधाओं की जानकारी देते हैं और उन्हें ठीक करने में मदद करते हैं, तब यह बेहतर बनता है। यदि कोई प्रोवाइडर उपलब्ध नहीं है, कोई वर्कफ़्लो असहज है या टर्मिनल UI आपके काम में बाधा डालता है, तो [issue खोलें](https://github.com/Hmbown/CodeWhale/issues)। यदि आप इसे बेहतर बनाने का तरीका जानते हैं, तो [pull request खोलें](CONTRIBUTING.md)। पहले योगदान का स्वागत है और स्वीकार किए गए काम का श्रेय योगदानकर्ताओं के पास रहता है।
+**बग रिपोर्ट, नए फ़ीचर के सुझाव और pull request का स्वागत है**, चाहे आप Codewhale का कई महीनों से इस्तेमाल कर रहे हों या पहली बार आज़मा रहे हों। यदि कोई प्रोवाइडर उपलब्ध नहीं है, कोई वर्कफ़्लो असहज है या टर्मिनल UI आपके काम में बाधा डालता है, तो [issue खोलें](https://github.com/Hmbown/CodeWhale/issues/new/choose) या [pull request भेजें](CONTRIBUTING.md), ताकि हम मिलकर इसे बेहतर बना सकें। पहले योगदान का स्वागत है और स्वीकार किए गए काम का श्रेय योगदानकर्ताओं के पास रहता है।
 
 [Discord](https://discord.gg/37gfS3ksug) से जुड़ें, या WeChat पर Hunter (`hunterbown`) को जोड़कर Whale Brothers समूह में शामिल होने के लिए कहें।
 
diff --git a/README.id.md b/README.id.md
index f1af1b28a2..2fba18dbd1 100644
--- a/README.id.md
+++ b/README.id.md
@@ -1,9 +1,11 @@
-
+
 # Codewhale
 
-Codewhale adalah agen pemrograman sumber terbuka untuk terminal Anda, dibuat dengan Rust dan dikembangkan secara terbuka bersama orang-orang yang menggunakannya.
+Codewhale adalah agen sumber terbuka yang membaca proyek, mengedit berkas, menjalankan perintah, dan memeriksa hasil kerjanya dengan model yang dihosting atau model lokal pilihan Anda. Mulailah dengan satu tugas di terminal. Untuk pekerjaan yang lebih besar, bagikan sebagian pekerjaan kepada agen dengan model dan peran yang berbeda.
 
-![Codewhale berjalan di terminal](assets/screenshot.webp)
+![Codewhale berjalan di terminal](web/public/codewhale-tui-171acee.png)
+
+*Pratinjau terminal dari build pengembangan v0.9.12.*
 
 [English](README.md) · [简体中文](README.zh-CN.md) · [日本語](README.ja-JP.md) · [Tiếng Việt](README.vi.md) · [한국어](README.ko-KR.md) · [Español](README.es-419.md) · [Português](README.pt-BR.md) · [Русский](README.ru.md) · [Українська](README.uk.md) · [Français](README.fr.md) · [Deutsch](README.de.md) · [繁體中文](README.zh-TW.md) · [हिन्दी](README.hi.md) · [Türkçe](README.tr.md) · [Italiano](README.it.md) · [Polski](README.pl.md) · [العربية](README.ar.md) · [Català](README.ca.md)
 
@@ -21,15 +23,17 @@ curl -fsSL https://codewhale.net/install.sh | sh
 "$HOME/.local/bin/codewhale"
 ```
 
+Installer memilih rilis terbaru yang sudah dipublikasikan. [Catatan perubahan](CHANGELOG.md) juga menjelaskan kandidat yang belum dipublikasikan untuk rilis berikutnya; perubahan tersebut baru disertakan dalam unduhan publik setelah rilisnya tersedia.
+
 Di Windows, unduh installer atau arsip yang sesuai dari [GitHub Releases](https://github.com/Hmbown/CodeWhale/releases/latest). Untuk instalasi biner langsung yang sudah ada, jalankan `codewhale update`, atau `codewhale update --check` untuk memeriksa tanpa memasang. Updater menampilkan jalur executable dan mempertahankan build yang lebih baru. npm dan Cargo adalah pilihan sekunder; lihat [panduan instalasi](docs/INSTALL.md) untuk migrasi dari pengelola paket dan pengaturan PATH.
 
-Saat pertama dijalankan, Codewhale membantu Anda menghubungkan penyedia atau tetap bekerja secara luring. Codewhale juga mendukung Cargo, Docker, Nix, Scoop, arsip siap pakai, Android/Termux, dan mirror CNB. Lihat [panduan instalasi](docs/INSTALL.md).
+Saat pertama dijalankan, Codewhale membantu Anda menghubungkan penyedia atau mengonfigurasi Codewhale secara luring. Respons model memerlukan koneksi ke model yang dihosting atau model lokal. Codewhale juga mendukung npm dan Cargo sebagai jalur pengemasan sekunder, serta Docker, Nix, Scoop, Android/Termux, dan mirror CNB opsional. Instalasi yang sudah ada melalui pengelola paket akan menerima petunjuk migrasi. Lihat [bantuan instalasi dan PATH](docs/INSTALL.md).
 
 Penyelesaian Tab cukup diaktifkan dengan satu perintah per shell — `codewhale completion bash|zsh|fish|powershell|elvish`. Lihat [penyelesaian shell](docs/INSTALL.md#8-shell-completions).
 
 ## Penggunaan
 
-Bicaralah dengan Codewhale seperti Anda berbicara dengan rekan satu tim:
+Buka terminal di folder proyek Anda dan jalankan `codewhale`. Pilih penyedia dengan `/provider` dan model dengan `/model`. Lalu jelaskan tugas yang konkret:
 
 ```text
 Fix the failing tests and explain what changed.
@@ -41,16 +45,24 @@ Atau jalankan tugas tanpa membuka TUI:
 codewhale exec "fix the failing tests and explain what changed"
 ```
 
-Codewhale dapat membaca repositori Anda, mengedit berkas, menjalankan perintah, memeriksa hasil, dan terus bekerja menuju tujuan. Anda menentukan seberapa besar akses yang dimilikinya.
+Codewhale dapat membaca repositori Anda, mengedit berkas, menjalankan perintah, memeriksa hasil, dan terus bekerja menuju tujuan. Gunakan `/mode plan` untuk menelusuri tanpa mengubah berkas atau menjalankan perintah shell, dan `/mode work` saat Anda ingin agen melakukan perubahan. Tekan `Shift+Tab` untuk memilih Ask, Auto-Review, atau Full Access; [panduan mode dan izin](docs/MODES.md) menjelaskan tindakan yang diizinkan oleh masing-masing pilihan.
+
+## Terminal, aplikasi, dan Computer Use
+
+Terminal dan klien grafis terhubung ke Codewhale Runtime, yang menjalankan agen beserta alatnya:
+
+- **Terminal:** `codewhale` membuka antarmuka interaktif; `codewhale exec` menjalankan tugas dari skrip atau job CI.
+- **Browser lokal:** `codewhale web` membuka [klien web lokal](docs/WEB.md) bawaan untuk Runtime yang sama.
+- **Aplikasi web dan desktop Codewhale:** lingkungan kerja grafis yang sedang dikembangkan. Ketersediaannya tercantum di [halaman produk](https://codewhale.net/en/product).
 
-## GUI frontend
+**Computer Use menambahkan alat untuk mengamati dan berinteraksi dengan aplikasi lain.** Plugin ini disertakan dalam kode sumber saat ini. Tinjau akses yang diminta dan aktifkan plugin sebelum digunakan; izin OS dan persyaratan platform tetap berlaku. Lihat [panduan Computer Use](crates/tui/plugins/computer-use/README.md) yang disertakan dan [pengaturan plugin](docs/PLUGINS.md).
 
-Lebih suka antarmuka grafis? Ekstensi CodeWhale for VS Code yang dikelola komunitas membungkus agen yang sama ke dalam sidebar VS Code — obrolan, percakapan berutas, diff langsung, dan pengelolaan tugas melalui Runtime API yang sama, sehingga sesi tetap sinkron dengan terminal. Pasang dari [VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=HengQuWorld.brotherwhale-vscode); kode sumber ada di [GitHub](https://github.com/HengQuWorld/CodeWhale-VSCode).
+Di VS Code, ekstensi CodeWhale yang dikelola komunitas terhubung ke Runtime lokal melalui sidebar. Pasang dari [VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=HengQuWorld.brotherwhale-vscode); kode sumber ada di [GitHub](https://github.com/HengQuWorld/CodeWhale-VSCode).
 
 ## Mengapa Codewhale
 
-- **Gunakan model yang Anda inginkan.** Hubungkan penyedia terkelola atau model lokal melalui Ollama, vLLM, atau SGLang. Ganti penyedia dan model dengan `/model`.
-- **Tetap memegang kendali.** Plan hanya dapat membaca. Ask, Auto-Review, dan Full Access menampilkan perilaku persetujuan dengan jelas. `/undo` membatalkan giliran terakhir dan `/restore` mengembalikan ruang kerja ke snapshot sebelumnya.
+- **Pilih model Anda.** Hubungkan penyedia terkelola atau model lokal melalui Ollama, vLLM, atau SGLang. Gunakan `/provider` untuk mengganti penyedia dan `/model` untuk memilih model.
+- **Tetap memegang kendali.** Periksa tindakan yang diusulkan dan perubahan berkas yang dihasilkannya. Pengaturan persetujuan menentukan kapan peninjauan diperlukan; Full Access tetap mematuhi batas kebijakan yang wajib dipenuhi. `/undo` dan `/restore` membantu memulihkan perubahan ruang kerja.
 - **Jaga agar pekerjaan panjang tetap teratur.** Simpan sesi, tetapkan `/goal` yang bertahan lama, tinjau alur kerja sebelum dijalankan, dan koordinasikan agen tanpa memasukkan instruksi internal mereka ke transkrip Anda.
 - **Perluas agen yang sudah Anda miliki.** Hubungkan server MCP dan keterampilan, konfigurasikan hook, dan simpan peran agen sebagai berkas yang mudah dibaca di proyek atau pengaturan pribadi Anda.
 
@@ -69,10 +81,11 @@ Baca [urutan otorisasi](docs/AUTHORIZATION_ORDER.md) untuk susunan kebijakan yan
 - [MCP](docs/MCP.md), [hook](docs/HOOKS.md), dan [konfigurasi](docs/CONFIGURATION.md)
 - [Klien web lokal](docs/WEB.md)
 - [Semua dokumentasi](docs)
+- [Struktur repositori dan panduan kontribusi](CONTRIBUTING.md#project-structure)
 
 ## Bergabung dengan komunitas
 
-Codewhale menjadi lebih baik ketika orang menggunakannya, melaporkan hal yang terasa kurang tepat, dan membantu memperbaikinya. Jika penyedia belum tersedia, alur kerja terasa janggal, atau UI terminal menghambat Anda, [buat issue](https://github.com/Hmbown/CodeWhale/issues). Jika Anda tahu cara memperbaikinya, [buat pull request](CONTRIBUTING.md). Kontribusi pertama sangat disambut, dan kontributor tetap menerima kredit untuk pekerjaan yang digabungkan.
+**Laporan bug, ide fitur, dan pull request selalu diterima**, baik Anda telah memakai Codewhale selama berbulan-bulan maupun baru mencobanya. Jika penyedia belum tersedia, alur kerja terasa janggal, atau UI terminal menghambat Anda, [buat issue](https://github.com/Hmbown/CodeWhale/issues/new/choose) atau [kirim pull request](CONTRIBUTING.md) agar kita dapat memperbaikinya bersama. Kontribusi pertama sangat disambut, dan kontributor tetap menerima kredit untuk pekerjaan yang digabungkan.
 
 Bergabunglah di [Discord](https://discord.gg/37gfS3ksug), atau tambahkan Hunter di WeChat (`hunterbown`) dan mintalah untuk bergabung dengan grup Whale Brothers.
 
diff --git a/README.it.md b/README.it.md
index 4ec99ae832..12a946dcc1 100644
--- a/README.it.md
+++ b/README.it.md
@@ -1,9 +1,11 @@
-
+
 # Codewhale
 
-Codewhale è un agente di programmazione open source per il terminale, sviluppato in Rust e migliorato pubblicamente insieme alle persone che lo utilizzano.
+Codewhale è un agente open source che legge il tuo progetto, modifica file, esegue comandi e verifica il proprio lavoro usando un modello ospitato o locale a tua scelta. Parti da un’attività nel terminale. Per un lavoro più grande, assegna parti del lavoro ad agenti con modelli e ruoli diversi.
 
-![Codewhale in esecuzione in un terminale](assets/screenshot.webp)
+![Codewhale in esecuzione in un terminale](web/public/codewhale-tui-171acee.png)
+
+*Anteprima del terminale da una build di sviluppo della v0.9.12.*
 
 [English](README.md) · [简体中文](README.zh-CN.md) · [日本語](README.ja-JP.md) · [Tiếng Việt](README.vi.md) · [Bahasa Indonesia](README.id.md) · [한국어](README.ko-KR.md) · [Español](README.es-419.md) · [Português](README.pt-BR.md) · [Русский](README.ru.md) · [Українська](README.uk.md) · [Français](README.fr.md) · [Deutsch](README.de.md) · [繁體中文](README.zh-TW.md) · [हिन्दी](README.hi.md) · [Türkçe](README.tr.md) · [Polski](README.pl.md) · [العربية](README.ar.md) · [Català](README.ca.md)
 
@@ -21,15 +23,17 @@ curl -fsSL https://codewhale.net/install.sh | sh
 "$HOME/.local/bin/codewhale"
 ```
 
+L’installer seleziona l’ultima versione pubblicata. Il [registro delle modifiche](CHANGELOG.md) descrive anche la versione candidata, ancora non pubblicata, della prossima versione; queste modifiche saranno incluse nei download pubblicati solo quando la versione sarà disponibile.
+
 Su Windows, scarica l’installer o l’archivio adatto da [GitHub Releases](https://github.com/Hmbown/CodeWhale/releases/latest). Per aggiornare un’installazione diretta esistente, esegui `codewhale update`, oppure `codewhale update --check` per la sola verifica. L’aggiornamento mostra il percorso dell’eseguibile e conserva le build più recenti. npm e Cargo sono opzioni secondarie; consulta la [guida all’installazione](docs/INSTALL.md) per migrare da un gestore di pacchetti e configurare PATH.
 
-Al primo avvio, Codewhale ti aiuta a collegare un provider oppure a rimanere offline. Supporta inoltre Cargo, Docker, Nix, Scoop, archivi precompilati, Android/Termux e un mirror CNB. Consulta la [guida all’installazione](docs/INSTALL.md).
+Al primo avvio, Codewhale ti aiuta a collegare un provider oppure a configurare Codewhale offline. Le risposte richiedono un modello ospitato o locale collegato. Codewhale supporta anche npm e Cargo come opzioni secondarie di distribuzione, oltre a Docker, Nix, Scoop, Android/Termux e un mirror CNB facoltativo. Le installazioni esistenti gestite da un gestore di pacchetti ricevono istruzioni per la migrazione. Consulta la [guida all’installazione e a PATH](docs/INSTALL.md).
 
 Il completamento con Tab si attiva con un solo comando per ogni shell — `codewhale completion bash|zsh|fish|powershell|elvish`. Consulta il [completamento della shell](docs/INSTALL.md#8-shell-completions).
 
 ## Utilizzo
 
-Parla con Codewhale come parleresti con un membro del tuo team:
+Apri un terminale nella cartella del tuo progetto ed esegui `codewhale`. Scegli il provider con `/provider` e il modello con `/model`. Poi descrivi un’attività concreta:
 
 ```text
 Fix the failing tests and explain what changed.
@@ -41,16 +45,24 @@ Oppure esegui un’attività senza aprire la TUI:
 codewhale exec "fix the failing tests and explain what changed"
 ```
 
-Codewhale può leggere il tuo repository, modificare file, eseguire comandi, controllare i risultati e continuare a lavorare verso un obiettivo. Sei tu a decidere quanto accesso concedergli.
+Codewhale può leggere il tuo repository, modificare file, eseguire comandi, controllare i risultati e continuare a lavorare verso un obiettivo. Usa `/mode plan` per esplorare senza modificare file né eseguire comandi shell, e `/mode work` quando vuoi che apporti modifiche. Premi `Shift+Tab` per scegliere Ask, Auto-Review o Full Access; la [guida a modalità e permessi](docs/MODES.md) spiega cosa consente ogni opzione.
+
+## Terminale, app e Computer Use
+
+Il terminale e i client grafici si collegano al Runtime di Codewhale, che esegue l’agente e i suoi strumenti:
+
+- **Terminale:** `codewhale` apre l’interfaccia interattiva; `codewhale exec` esegue un’attività da uno script o da un job di CI.
+- **Browser locale:** `codewhale web` apre il [client web locale](docs/WEB.md) incluso, che usa lo stesso runtime.
+- **App web e desktop di Codewhale:** ambienti di lavoro grafici in fase di sviluppo. La loro disponibilità è indicata nella [pagina del prodotto](https://codewhale.net/en/product).
 
-## Interfaccia grafica
+**Computer Use aggiunge strumenti per osservare altre applicazioni e interagire con esse.** Il plugin è incluso nel codice sorgente attuale. Controlla l’accesso richiesto e abilitalo prima dell’uso; i permessi del sistema operativo e i requisiti della piattaforma continuano ad applicarsi. Consulta la [guida a Computer Use](crates/tui/plugins/computer-use/README.md) inclusa e la [configurazione dei plugin](docs/PLUGINS.md).
 
-Preferisci un'interfaccia grafica? L'estensione CodeWhale for VS Code, mantenuta dalla comunità, porta lo stesso agente nella barra laterale di VS Code — chat, conversazioni a thread, diff in tempo reale e gestione delle attività sulla stessa Runtime API, così le sessioni restano sincronizzate con il terminale. Installala dal [VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=HengQuWorld.brotherwhale-vscode); il codice sorgente è su [GitHub](https://github.com/HengQuWorld/CodeWhale-VSCode).
+Per VS Code, l’estensione CodeWhale mantenuta dalla comunità si collega al Runtime locale da una barra laterale. Installala dal [VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=HengQuWorld.brotherwhale-vscode); il codice sorgente è su [GitHub](https://github.com/HengQuWorld/CodeWhale-VSCode).
 
 ## Perché Codewhale
 
-- **Usa il modello che preferisci.** Collega provider gestiti oppure modelli locali tramite Ollama, vLLM o SGLang. Cambia provider e modello con `/model`.
-- **Mantieni il controllo.** Plan è in sola lettura. Ask, Auto-Review e Full Access rendono visibile il comportamento delle approvazioni. `/undo` annulla l’ultimo turno e `/restore` riporta l’area di lavoro a uno snapshot precedente.
+- **Scegli i tuoi modelli.** Collega provider gestiti oppure modelli locali tramite Ollama, vLLM o SGLang. Usa `/provider` per cambiare provider e `/model` per scegliere un modello.
+- **Mantieni il controllo.** Controlla le azioni proposte e le modifiche ai file che ne derivano. Le impostazioni di approvazione determinano quando è necessaria una revisione; Full Access continua a rispettare i limiti vincolanti delle regole. `/undo` e `/restore` aiutano a recuperare le modifiche dell’area di lavoro.
 - **Mantieni organizzati i lavori lunghi.** Salva le sessioni, imposta un `/goal` duraturo, rivedi i workflow prima dell’esecuzione e coordina gli agenti senza trasformare le loro istruzioni interne in parte della tua conversazione.
 - **Estendi l’agente che hai già.** Collega server MCP e skill, configura gli hook e conserva i ruoli degli agenti come file leggibili nel progetto o nelle impostazioni personali.
 
@@ -69,10 +81,11 @@ Leggi l’[ordine di autorizzazione](docs/AUTHORIZATION_ORDER.md) per conoscere
 - [MCP](docs/MCP.md), [hook](docs/HOOKS.md) e [configurazione](docs/CONFIGURATION.md)
 - [Client web locale](docs/WEB.md)
 - [Tutta la documentazione](docs)
+- [Struttura del repository e guida ai contributi](CONTRIBUTING.md#project-structure)
 
 ## Unisciti alla comunità
 
-Codewhale migliora quando le persone lo usano, segnalano ciò che non funziona e aiutano a correggerlo. Se manca un provider, un workflow risulta scomodo o l’interfaccia del terminale ti ostacola, [apri una issue](https://github.com/Hmbown/CodeWhale/issues). Se sai come migliorarlo, [apri una pull request](CONTRIBUTING.md). I primi contributi sono benvenuti e chi contribuisce mantiene il riconoscimento per il lavoro integrato.
+**Segnalazioni di bug, idee per nuove funzionalità e pull request sono benvenute**, sia che usi Codewhale da mesi sia che lo provi per la prima volta. Se manca un provider, un workflow risulta scomodo o l’interfaccia del terminale ti ostacola, [apri una issue](https://github.com/Hmbown/CodeWhale/issues/new/choose) oppure [invia una pull request](CONTRIBUTING.md) per migliorarlo insieme. I primi contributi sono benvenuti e chi contribuisce mantiene il riconoscimento per il lavoro integrato.
 
 Unisciti a [Discord](https://discord.gg/37gfS3ksug), oppure aggiungi Hunter su WeChat (`hunterbown`) e chiedi di entrare nel gruppo Whale Brothers.
 
diff --git a/README.ja-JP.md b/README.ja-JP.md
index 294570f020..07aca26176 100644
--- a/README.ja-JP.md
+++ b/README.ja-JP.md
@@ -1,9 +1,11 @@
-
+
 # Codewhale
 
-Codewhale は Rust で構築された、ターミナル向けのオープンソース・コーディングエージェントです。利用者とともに、公開の場で改善を続けています。
+Codewhale は、選んだホスト型またはローカルのモデルを使ってプロジェクトを読み、ファイルを編集し、コマンドを実行して、自分の作業結果を確認するオープンソースのエージェントです。まずはターミナルで一つのタスクから始めましょう。大きな仕事では、異なるモデルや役割を持つエージェントに作業の一部を分担させられます。
 
-![ターミナルで動作する Codewhale](assets/screenshot.webp)
+![ターミナルで動作する Codewhale](web/public/codewhale-tui-171acee.png)
+
+*v0.9.12 の開発ビルドによるターミナルのプレビュー。*
 
 [English](README.md) · [简体中文](README.zh-CN.md) · [Tiếng Việt](README.vi.md) · [Bahasa Indonesia](README.id.md) · [한국어](README.ko-KR.md) · [Español](README.es-419.md) · [Português](README.pt-BR.md) · [Русский](README.ru.md) · [Українська](README.uk.md) · [Français](README.fr.md) · [Deutsch](README.de.md) · [繁體中文](README.zh-TW.md) · [हिन्दी](README.hi.md) · [Türkçe](README.tr.md) · [Italiano](README.it.md) · [Polski](README.pl.md) · [العربية](README.ar.md) · [Català](README.ca.md)
 
@@ -21,15 +23,17 @@ curl -fsSL https://codewhale.net/install.sh | sh
 "$HOME/.local/bin/codewhale"
 ```
 
+インストーラーは、公開済みの最新リリースを選択します。[変更履歴](CHANGELOG.md)には次のリリースの未公開候補版についても記載されていますが、その変更が公開ダウンロードに含まれるのは、リリースが公開されてからです。
+
 Windows では [GitHub Releases](https://github.com/Hmbown/CodeWhale/releases/latest) から対応するインストーラーまたはアーカイブを入手してください。既存の直接インストールは `codewhale update` で更新できます。確認だけなら `codewhale update --check` を使います。更新対象の実行ファイルのパスが表示され、より新しいビルドは保持されます。npm と Cargo は補助的なパッケージ導入方法です。パッケージ管理からの移行や PATH の設定は[インストールガイド](docs/INSTALL.md)を参照してください。
 
-初回起動時にプロバイダーへの接続を案内しますが、オフラインのまま使うこともできます。Codewhale は Cargo、Docker、Nix、Scoop、ビルド済みアーカイブ、Android/Termux、CNB ミラーにも対応しています。詳しくは[インストールガイド](docs/INSTALL.md)をご覧ください。
+初回起動時にプロバイダーへの接続を案内します。Codewhale の設定はオフラインでも行えます。モデルからの応答には、ホスト型またはローカルのモデルへの接続が必要です。Codewhale は補助的なパッケージ配布方法として npm と Cargo に対応し、Docker、Nix、Scoop、Android/Termux、必要に応じて利用できる CNB ミラーにも対応しています。パッケージマネージャーでインストール済みの場合は、移行手順が案内されます。[インストールと PATH のヘルプ](docs/INSTALL.md)を参照してください。
 
 各シェルの Tab 補完はコマンド一つで設定できます — `codewhale completion bash|zsh|fish|powershell|elvish`。詳しくは[シェル補完](docs/INSTALL.md#8-shell-completions)をご覧ください。
 
 ## 使い方
 
-チームメイトに話しかけるのと同じように、Codewhale に依頼します:
+プロジェクトのフォルダーでターミナルを開き、`codewhale` を実行します。`/provider` でプロバイダーを、`/model` でモデルを選び、具体的なタスクを伝えます:
 
 ```text
 Fix the failing tests and explain what changed.
@@ -41,16 +45,24 @@ TUI を開かずにタスクを実行することもできます:
 codewhale exec "fix the failing tests and explain what changed"
 ```
 
-Codewhale はリポジトリを読み、ファイルを編集し、コマンドを実行して結果を確認しながら、目標に向かって作業を続けます。どこまでアクセスを許可するかは、あなたが決められます。
+Codewhale はリポジトリを読み、ファイルを編集し、コマンドを実行して結果を確認しながら、目標に向かって作業を続けます。ファイルの変更やシェルコマンドの実行をせずに調べるには `/mode plan` を使い、変更を加えてほしいときは `/mode work` を使います。`Shift+Tab` を押すと Ask、Auto-Review、Full Access を選択できます。それぞれで許可される操作は[モードと権限のガイド](docs/MODES.md)を参照してください。
+
+## ターミナル、アプリ、Computer Use
+
+ターミナルとグラフィカルなクライアントは Codewhale Runtime に接続します。Runtime がエージェントとそのツールを実行します:
+
+- **ターミナル:** `codewhale` は対話型インターフェースを開き、`codewhale exec` はスクリプトや CI ジョブからタスクを実行します。
+- **ローカルブラウザー:** `codewhale web` は、同じ Runtime を使う同梱の[ローカル Web クライアント](docs/WEB.md)を開きます。
+- **Codewhale の Web アプリとデスクトップアプリ:** 開発中のグラフィカルな作業環境です。提供状況は[製品ページ](https://codewhale.net/en/product)をご覧ください。
 
-## GUI フロントエンド
+**Computer Use は、ほかのアプリケーションの状態を確認し、操作するためのツールを追加します。** このプラグインは現在のソースコードに含まれています。使用前に要求されるアクセス権を確認し、有効にしてください。OS の権限やプラットフォームの要件も満たす必要があります。同梱の [Computer Use ガイド](crates/tui/plugins/computer-use/README.md)と[プラグインの設定](docs/PLUGINS.md)を参照してください。
 
-グラフィカルな操作画面を好みますか?コミュニティが保守する CodeWhale for VS Code 拡張機能は、同じエージェントを VS Code サイドバーに統合します。チャット、スレッド会話、ライブ差分、タスク管理をすべて同じ Runtime API 上で行い、セッションはターミナルと同期したままです。[VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=HengQuWorld.brotherwhale-vscode) からインストールしてください。ソースコードは [GitHub](https://github.com/HengQuWorld/CodeWhale-VSCode) にあります。
+VS Code では、コミュニティが保守する CodeWhale 拡張機能がサイドバーからローカルの Runtime に接続します。[VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=HengQuWorld.brotherwhale-vscode) からインストールしてください。ソースコードは [GitHub](https://github.com/HengQuWorld/CodeWhale-VSCode) にあります。
 
 ## Codewhale を選ぶ理由
 
-- **使いたいモデルを選べます。** ホスト型プロバイダーに接続するほか、Ollama、vLLM、SGLang 経由でローカルモデルも利用できます。`/model` でプロバイダーとモデルを切り替えられます。
-- **主導権を保てます。** Plan は読み取り専用です。Ask、Auto-Review、Full Access により、承認の挙動が明確になります。`/undo` は直前のターンを取り消し、`/restore` はワークスペースを以前のスナップショットへ戻します。
+- **モデルを選べます。** ホスト型プロバイダーに接続するほか、Ollama、vLLM、SGLang 経由でローカルモデルも利用できます。`/provider` でプロバイダーを切り替え、`/model` でモデルを選択します。
+- **主導権を保てます。** 提案された操作と、その結果生じたファイルの変更を確認できます。承認設定によってレビューが必要なタイミングが決まり、Full Access でもポリシーの厳格な制約は守られます。`/undo` と `/restore` はワークスペースの変更を復元する際に役立ちます。
 - **長い作業も整理できます。** セッションを保存し、永続的な `/goal` を設定し、ワークフローを実行前に確認できます。さらに、エージェントの内部指示を会話履歴に混ぜることなく、複数のエージェントを連携させられます。
 - **今あるエージェントを拡張できます。** MCP サーバーやスキルを接続し、フックを設定し、エージェントの役割をプロジェクトまたは個人設定内の読みやすいファイルとして管理できます。
 
@@ -69,10 +81,11 @@ Codewhale は、あなたが許可した範囲のアクセス権で、あなた
 - [MCP](docs/MCP.md)、[フック](docs/HOOKS.md)、[設定](docs/CONFIGURATION.md)
 - [ローカル Web クライアント](docs/WEB.md)
 - [すべてのドキュメント](docs)
+- [リポジトリ構成とコントリビューションガイド](CONTRIBUTING.md#project-structure)
 
 ## コミュニティに参加
 
-Codewhale は、実際に使い、違和感を報告し、修正を手伝ってくださる皆さんとともに成長します。必要なプロバイダーがない、ワークフローが使いづらい、ターミナル UI が作業を妨げるといった場合は、[issue を作成](https://github.com/Hmbown/CodeWhale/issues)してください。改善方法をご存じなら、[pull request を作成](CONTRIBUTING.md)してください。初めてのコントリビューションも歓迎し、採用された成果にはコントリビューターのクレジットを残します。
+**不具合の報告、機能の提案、pull request を歓迎します。** Codewhale を何か月も使っている方も、初めて試す方もお気軽にご参加ください。必要なプロバイダーがない、ワークフローが使いづらい、ターミナル UI が作業を妨げるといった場合は、[issue を作成](https://github.com/Hmbown/CodeWhale/issues/new/choose)するか、[pull request を送信](CONTRIBUTING.md)して、一緒に改善しましょう。初めてのコントリビューションも歓迎し、採用された成果にはコントリビューターのクレジットを残します。
 
 [Discord](https://discord.gg/37gfS3ksug) に参加するか、WeChat で Hunter(`hunterbown`)を追加して Whale Brothers グループへの参加を依頼してください。
 
diff --git a/README.ko-KR.md b/README.ko-KR.md
index a0df059cbd..c9b21080ab 100644
--- a/README.ko-KR.md
+++ b/README.ko-KR.md
@@ -1,9 +1,11 @@
-
+
 # Codewhale
 
-Codewhale은 Rust로 만든 터미널용 오픈 소스 코딩 에이전트로, 사용자들과 함께 공개적으로 개선해 나갑니다.
+Codewhale은 사용자가 선택한 호스팅 모델이나 로컬 모델로 프로젝트를 읽고, 파일을 편집하고, 명령을 실행하며, 작업 결과를 확인하는 오픈 소스 에이전트입니다. 터미널에서 하나의 작업으로 시작하세요. 더 큰 작업은 서로 다른 모델과 역할을 가진 에이전트에게 나누어 맡길 수 있습니다.
 
-![터미널에서 실행 중인 Codewhale](assets/screenshot.webp)
+![터미널에서 실행 중인 Codewhale](web/public/codewhale-tui-171acee.png)
+
+*v0.9.12 개발 빌드의 터미널 미리보기입니다.*
 
 [English](README.md) · [简体中文](README.zh-CN.md) · [日本語](README.ja-JP.md) · [Tiếng Việt](README.vi.md) · [Bahasa Indonesia](README.id.md) · [Español](README.es-419.md) · [Português](README.pt-BR.md) · [Русский](README.ru.md) · [Українська](README.uk.md) · [Français](README.fr.md) · [Deutsch](README.de.md) · [繁體中文](README.zh-TW.md) · [हिन्दी](README.hi.md) · [Türkçe](README.tr.md) · [Italiano](README.it.md) · [Polski](README.pl.md) · [العربية](README.ar.md) · [Català](README.ca.md)
 
@@ -21,15 +23,17 @@ curl -fsSL https://codewhale.net/install.sh | sh
 "$HOME/.local/bin/codewhale"
 ```
 
+설치 도구는 공개된 최신 릴리스를 선택합니다. [변경 이력](CHANGELOG.md)에는 다음 릴리스의 미공개 후보 버전도 설명되어 있지만, 해당 릴리스가 공개되기 전에는 그 변경 사항이 공개 다운로드에 포함되지 않습니다.
+
 Windows에서는 [GitHub Releases](https://github.com/Hmbown/CodeWhale/releases/latest)에서 맞는 설치 프로그램이나 아카이브를 받으세요. 기존 직접 설치는 `codewhale update`로 업데이트하고, 확인만 하려면 `codewhale update --check`를 사용하세요. 업데이트 도구는 실행 파일 경로를 표시하며 더 최신인 빌드는 유지합니다. npm과 Cargo는 보조 패키지 설치 방법입니다. 패키지 관리자 설치에서 이전하거나 PATH를 설정하려면 [설치 안내서](docs/INSTALL.md)를 참조하세요.
 
-처음 실행하면 공급자 연결 과정을 안내하며, 오프라인 상태로 계속 사용할 수도 있습니다. Codewhale은 Cargo, Docker, Nix, Scoop, 사전 빌드 아카이브, Android/Termux, CNB 미러도 지원합니다. [설치 안내서](docs/INSTALL.md)를 참조하세요.
+처음 실행하면 공급자 연결 과정을 안내하며, 오프라인으로 Codewhale을 설정할 수도 있습니다. 모델의 응답을 받으려면 호스팅 모델이나 로컬 모델에 연결해야 합니다. Codewhale은 보조 패키지 설치 경로로 npm과 Cargo를 지원하며, Docker, Nix, Scoop, Android/Termux와 선택적으로 사용할 수 있는 CNB 미러도 지원합니다. 패키지 관리자로 설치한 기존 버전에는 이전 안내가 제공됩니다. [설치 및 PATH 도움말](docs/INSTALL.md)을 참조하세요.
 
 각 셸에서 Tab 자동 완성은 명령 한 줄로 설정할 수 있습니다 — `codewhale completion bash|zsh|fish|powershell|elvish`. [셸 자동 완성](docs/INSTALL.md#8-shell-completions)을 참조하세요.
 
 ## 사용법
 
-팀원에게 말하듯 Codewhale에 요청하세요:
+프로젝트 폴더에서 터미널을 열고 `codewhale`을 실행하세요. `/provider`로 공급자를, `/model`로 모델을 선택한 다음 구체적인 작업을 설명하세요:
 
 ```text
 Fix the failing tests and explain what changed.
@@ -41,16 +45,24 @@ TUI를 열지 않고 작업을 실행할 수도 있습니다:
 codewhale exec "fix the failing tests and explain what changed"
 ```
 
-Codewhale은 저장소를 읽고, 파일을 편집하고, 명령을 실행하고, 결과를 확인하며 목표를 향해 계속 작업할 수 있습니다. 어느 정도의 접근 권한을 줄지는 사용자가 결정합니다.
+Codewhale은 저장소를 읽고, 파일을 편집하고, 명령을 실행하고, 결과를 확인하며 목표를 향해 계속 작업할 수 있습니다. 파일을 변경하거나 셸 명령을 실행하지 않고 살펴보려면 `/mode plan`을 사용하고, 변경을 수행하려면 `/mode work`를 사용하세요. `Shift+Tab`을 누르면 Ask, Auto-Review, Full Access를 선택할 수 있습니다. 각 설정이 허용하는 작업은 [모드 및 권한 안내서](docs/MODES.md)에서 확인하세요.
+
+## 터미널, 앱, Computer Use
+
+터미널과 그래픽 클라이언트는 Codewhale Runtime에 연결하며, Runtime이 에이전트와 도구를 실행합니다:
+
+- **터미널:** `codewhale`은 대화형 인터페이스를 열고, `codewhale exec`는 스크립트나 CI 작업에서 태스크를 실행합니다.
+- **로컬 브라우저:** `codewhale web`은 같은 Runtime을 사용하는 내장 [로컬 웹 클라이언트](docs/WEB.md)를 엽니다.
+- **Codewhale 웹 및 데스크톱 앱:** 개발 중인 그래픽 작업 환경입니다. 이용 가능 여부는 [제품 페이지](https://codewhale.net/en/product)에서 확인할 수 있습니다.
 
-## GUI 프런트엔드
+**Computer Use는 다른 애플리케이션을 관찰하고 조작하는 도구를 추가합니다.** 이 플러그인은 현재 소스에 포함되어 있습니다. 사용 전에 요청하는 접근 권한을 검토하고 활성화하세요. OS 권한과 플랫폼 요구 사항도 충족해야 합니다. 포함된 [Computer Use 안내서](crates/tui/plugins/computer-use/README.md)와 [플러그인 설정](docs/PLUGINS.md)을 참조하세요.
 
-그래픽 인터페이스를 선호하시나요? 커뮤니티가 관리하는 CodeWhale for VS Code 확장은 동일한 에이전트를 VS Code 사이드바에 담아 채팅·스레드 대화·실시간 diff·작업 관리를 같은 Runtime API로 제공하며, 세션은 터미널과 동기화됩니다. [VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=HengQuWorld.brotherwhale-vscode)에서 설치하세요. 소스 코드는 [GitHub](https://github.com/HengQuWorld/CodeWhale-VSCode)에 있습니다.
+VS Code에서는 커뮤니티가 관리하는 CodeWhale 확장이 사이드바에서 로컬 Runtime에 연결합니다. [VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=HengQuWorld.brotherwhale-vscode)에서 설치하세요. 소스 코드는 [GitHub](https://github.com/HengQuWorld/CodeWhale-VSCode)에 있습니다.
 
 ## Codewhale을 선택하는 이유
 
-- **원하는 모델을 사용하세요.** 호스팅 공급자에 연결하거나 Ollama, vLLM, SGLang을 통해 로컬 모델을 사용할 수 있습니다. `/model`로 공급자와 모델을 전환하세요.
-- **계속 주도권을 가지세요.** Plan은 읽기 전용입니다. Ask, Auto-Review, Full Access는 승인 동작을 명확하게 보여 줍니다. `/undo`는 마지막 턴을 되돌리고 `/restore`는 작업 공간을 이전 스냅샷으로 복원합니다.
+- **모델을 선택하세요.** 호스팅 공급자에 연결하거나 Ollama, vLLM, SGLang을 통해 로컬 모델을 사용할 수 있습니다. `/provider`로 공급자를 바꾸고 `/model`로 모델을 선택하세요.
+- **계속 주도권을 가지세요.** 제안된 작업과 그 결과로 생긴 파일 변경을 확인하세요. 승인 설정은 언제 검토가 필요한지 결정하며, Full Access에서도 반드시 지켜야 하는 정책 경계는 유지됩니다. `/undo`와 `/restore`는 작업 공간의 변경을 복구하는 데 도움이 됩니다.
 - **긴 작업도 체계적으로 관리하세요.** 세션을 저장하고, 지속되는 `/goal`을 설정하고, 워크플로 실행 전에 검토하며, 에이전트의 내부 지시가 대화 기록에 섞이지 않도록 여러 에이전트를 조율할 수 있습니다.
 - **이미 사용 중인 에이전트를 확장하세요.** MCP 서버와 스킬을 연결하고, 훅을 구성하고, 에이전트 역할을 프로젝트나 개인 설정에 읽기 쉬운 파일로 보관할 수 있습니다.
 
@@ -69,10 +81,11 @@ Codewhale은 사용자가 허용한 접근 권한으로 사용자의 컴퓨터
 - [MCP](docs/MCP.md), [훅](docs/HOOKS.md), [구성](docs/CONFIGURATION.md)
 - [로컬 웹 클라이언트](docs/WEB.md)
 - [전체 문서](docs)
+- [저장소 구조 및 기여 가이드](CONTRIBUTING.md#project-structure)
 
 ## 커뮤니티 참여
 
-사람들이 Codewhale을 사용하고, 불편한 점을 알리고, 수정에 힘을 보탤 때 Codewhale은 더 좋아집니다. 필요한 공급자가 없거나 워크플로가 불편하거나 터미널 UI가 작업을 방해한다면 [issue를 등록](https://github.com/Hmbown/CodeWhale/issues)해 주세요. 개선 방법을 알고 있다면 [pull request를 등록](CONTRIBUTING.md)해 주세요. 첫 기여도 환영하며, 반영된 작업에는 기여자의 이름을 남깁니다.
+**버그 보고, 기능 제안, pull request를 환영합니다.** Codewhale을 몇 달간 사용했든 처음 사용해 보든 누구나 참여할 수 있습니다. 필요한 공급자가 없거나 워크플로가 불편하거나 터미널 UI가 작업을 방해한다면 [issue를 등록](https://github.com/Hmbown/CodeWhale/issues/new/choose)하거나 [pull request를 보내](CONTRIBUTING.md) 함께 개선해 주세요. 첫 기여도 환영하며, 반영된 작업에는 기여자의 이름을 남깁니다.
 
 [Discord](https://discord.gg/37gfS3ksug)에 참여하거나 WeChat에서 Hunter(`hunterbown`)를 추가한 뒤 Whale Brothers 그룹 참여를 요청하세요.
 
diff --git a/README.md b/README.md
index a6361433c3..be7636a266 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,9 @@
 # Codewhale
 
-Codewhale is an open source coding agent for your terminal, built in Rust and
-improved in public with the people who use it.
+Codewhale is an open-source agent that reads your project, edits files, runs
+commands, and checks its work using a hosted or local model you choose. Start
+with one task in your terminal. For a larger job, give parts of the work to
+agents with different models and roles.
 
 
   
@@ -16,10 +18,12 @@ improved in public with the people who use it.
 [![Discord](https://img.shields.io/badge/Discord-join-5865F2?logo=discord&logoColor=white)](https://discord.gg/37gfS3ksug)
 
 
-  
-  A Codewhale terminal session
+  
+  A Codewhale terminal session
 
 
+*Terminal preview from a v0.9.12 development build.*
+
 ## Install
 
 macOS / Linux — install the official GitHub release:
@@ -29,13 +33,18 @@ curl -fsSL https://codewhale.net/install.sh | sh
 "$HOME/.local/bin/codewhale"
 ```
 
+The installer selects the latest published release. The [changelog](CHANGELOG.md)
+also describes the next release's unreleased candidate; those changes are not
+included in published downloads until the release is available.
+
 Windows: download the matching installer or archive from
 [GitHub Releases](https://github.com/Hmbown/CodeWhale/releases/latest).
 For an existing direct install, run `codewhale update` (or `codewhale update --check`
 to inspect it). The updater prints the executable path and keeps newer builds.
 
 
-The first run helps you connect a provider or stay offline. Codewhale also
+The first run helps you connect a provider or configure Codewhale offline.
+Model replies require a connected hosted or local model. Codewhale also
 supports npm and Cargo as secondary packaging routes, plus Docker, Nix, Scoop,
 Android/Termux, and an optional CNB mirror. Existing package-managed installs
 receive migration instructions. See [installation and PATH help](docs/INSTALL.md).
@@ -45,7 +54,8 @@ See [shell completions](docs/INSTALL.md#8-shell-completions).
 
 ## Use
 
-Talk to Codewhale the same way you would talk to a teammate:
+Open a terminal in your project folder and run `codewhale`. Choose your provider
+with `/provider` and your model with `/model`. Then describe a concrete task:
 
 ```text
 Fix the failing tests and explain what changed.
@@ -58,24 +68,43 @@ codewhale exec "fix the failing tests and explain what changed"
 ```
 
 Codewhale can read your repository, edit files, run commands, inspect results,
-and keep working toward a goal. You decide how much access it has.
-
-## GUI frontend
-
-Prefer a graphical interface? The community-maintained CodeWhale for VS Code
-extension wraps the same agent in a VS Code sidebar — chat, threaded
-conversations, live diffs, and task management over the same Runtime API, so
-sessions stay in sync with the terminal. Install it from the
+and keep working toward a goal. Use `/mode plan` to explore without file changes
+or shell execution, and `/mode work` when you want it to make changes. Press
+`Shift+Tab` to choose Ask, Auto-Review, or Full Access; the
+[modes and permissions guide](docs/MODES.md) explains what each allows.
+
+## Terminal, apps, and Computer Use
+
+The terminal and graphical clients connect to the Codewhale Runtime, which runs
+the agent and its tools:
+
+- **Terminal:** `codewhale` opens the interactive interface; `codewhale exec`
+  runs a task from a script or CI job.
+- **Local browser:** `codewhale web` opens the bundled
+  [local web client](docs/WEB.md) for the same runtime.
+- **Codewhale web and desktop apps:** graphical workbenches in development.
+  Their availability is listed on the [product page](https://codewhale.net/en/product).
+
+**Computer Use adds tools for observing and interacting with other applications.**
+The plugin is included in the current source.
+Review its requested access and enable it before use; OS permissions and
+platform requirements still apply. See the included
+[Computer Use guide](crates/tui/plugins/computer-use/README.md) and
+[plugin setup](docs/PLUGINS.md).
+
+For VS Code, the community-maintained CodeWhale extension connects to the local
+Runtime from a sidebar. Install it from the
 [VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=HengQuWorld.brotherwhale-vscode);
 source code is on [GitHub](https://github.com/HengQuWorld/CodeWhale-VSCode).
 
 ## Why Codewhale
 
-- **Use the model you want.** Connect hosted providers or local models through
-  Ollama, vLLM, or SGLang. Switch provider and model with `/model`.
-- **Stay in control.** Plan is read-only. Ask, Auto-Review, and Full Access make
-  approval behavior visible. `/undo` reverts the last turn and `/restore`
-  returns the workspace to an earlier snapshot.
+- **Choose your models.** Connect hosted providers or local models through
+  Ollama, vLLM, or SGLang. Use `/provider` to change providers and `/model` to
+  choose a model.
+- **Stay in control.** Inspect proposed actions and resulting file changes.
+  Approval settings govern when review is needed; Full Access still respects
+  hard policy boundaries. `/undo` and `/restore` help recover workspace changes.
 - **Keep long work organized.** Save sessions, set a durable `/goal`, review
   workflows before they run, and coordinate agents without turning their
   internal instructions into your transcript.
@@ -102,15 +131,16 @@ stack and [configuration](docs/CONFIGURATION.md) for local settings.
 - [MCP](docs/MCP.md), [hooks](docs/HOOKS.md), and [configuration](docs/CONFIGURATION.md)
 - [Local web client](docs/WEB.md)
 - [All documentation](docs)
+- [Repository layout and contribution guide](CONTRIBUTING.md#project-structure)
 
 ## Join the community
 
-Codewhale gets better when people use it, report what feels wrong, and help fix
-it. If a provider is missing, a workflow is awkward, or the terminal UI gets in
-your way, [open an issue](https://github.com/Hmbown/CodeWhale/issues). If you
-know how to improve it, [open a pull request](CONTRIBUTING.md). First
-contributions are welcome, and contributors keep credit for the work that
-lands.
+**Bug reports, feature ideas, and pull requests are welcome**, whether you have
+used Codewhale for months or are trying it for the first time. If a provider is
+missing, a workflow is awkward, or the terminal UI gets in your way,
+[open an issue](https://github.com/Hmbown/CodeWhale/issues/new/choose) or
+[send a pull request](CONTRIBUTING.md) so we can improve it together. We welcome
+first contributions, and contributors keep credit for the work that lands.
 
 Join the [Discord](https://discord.gg/37gfS3ksug), or add Hunter on WeChat
 (`hunterbown`) and ask to join the Whale Brothers group.
diff --git a/README.pl.md b/README.pl.md
index 3abb93d831..7237c4e1d4 100644
--- a/README.pl.md
+++ b/README.pl.md
@@ -1,9 +1,11 @@
-
+
 # Codewhale
 
-Codewhale to agent programistyczny o otwartym kodzie źródłowym do terminala, napisany w Rust i rozwijany publicznie wspólnie z osobami, które go używają.
+Codewhale to agent o otwartym kodzie źródłowym, który czyta Twój projekt, edytuje pliki, wykonuje polecenia i sprawdza swoją pracę przy użyciu wybranego przez Ciebie modelu hostowanego lub lokalnego. Zacznij od jednego zadania w terminalu. Przy większej pracy powierz jej części agentom korzystającym z różnych modeli i pełniącym różne role.
 
-![Codewhale działający w terminalu](assets/screenshot.webp)
+![Codewhale działający w terminalu](web/public/codewhale-tui-171acee.png)
+
+*Podgląd terminala z rozwojowej kompilacji v0.9.12.*
 
 [English](README.md) · [简体中文](README.zh-CN.md) · [日本語](README.ja-JP.md) · [Tiếng Việt](README.vi.md) · [Bahasa Indonesia](README.id.md) · [한국어](README.ko-KR.md) · [Español](README.es-419.md) · [Português](README.pt-BR.md) · [Русский](README.ru.md) · [Українська](README.uk.md) · [Français](README.fr.md) · [Deutsch](README.de.md) · [繁體中文](README.zh-TW.md) · [हिन्दी](README.hi.md) · [Türkçe](README.tr.md) · [Italiano](README.it.md) · [العربية](README.ar.md) · [Català](README.ca.md)
 
@@ -21,15 +23,17 @@ curl -fsSL https://codewhale.net/install.sh | sh
 "$HOME/.local/bin/codewhale"
 ```
 
+Instalator wybiera najnowsze opublikowane wydanie. [Dziennik zmian](CHANGELOG.md) opisuje również nieopublikowanego jeszcze kandydata do kolejnego wydania; te zmiany trafią do opublikowanych plików do pobrania dopiero po udostępnieniu wydania.
+
 Na Windows pobierz odpowiedni instalator lub archiwum z [GitHub Releases](https://github.com/Hmbown/CodeWhale/releases/latest). Istniejącą instalację bezpośrednią zaktualizujesz poleceniem `codewhale update`; `codewhale update --check` służy tylko do sprawdzenia. Aktualizator pokazuje ścieżkę pliku wykonywalnego i zachowuje nowsze kompilacje. npm i Cargo to opcje dodatkowe. Migrację z menedżera pakietów i konfigurację PATH opisuje [instrukcja instalacji](docs/INSTALL.md).
 
-Przy pierwszym uruchomieniu Codewhale pomaga połączyć się z dostawcą lub pozostać w trybie offline. Obsługuje też Cargo, Docker, Nix, Scoop, gotowe archiwa, Android/Termux oraz serwer lustrzany CNB. Zobacz [instrukcję instalacji](docs/INSTALL.md).
+Przy pierwszym uruchomieniu Codewhale pomaga połączyć się z dostawcą lub skonfigurować Codewhale w trybie offline. Odpowiedzi modelu wymagają połączenia z modelem hostowanym lub lokalnym. Codewhale obsługuje również npm i Cargo jako dodatkowe sposoby instalacji, a także Docker, Nix, Scoop, Android/Termux oraz opcjonalny serwer lustrzany CNB. Dla istniejących instalacji zarządzanych przez menedżera pakietów dostępne są instrukcje migracji. Zobacz [pomoc dotyczącą instalacji i PATH](docs/INSTALL.md).
 
 Uzupełnianie klawiszem Tab można włączyć jednym poleceniem dla każdej powłoki — `codewhale completion bash|zsh|fish|powershell|elvish`. Zobacz [uzupełnianie powłoki](docs/INSTALL.md#8-shell-completions).
 
 ## Użycie
 
-Rozmawiaj z Codewhale tak, jak z osobą ze swojego zespołu:
+Otwórz terminal w folderze projektu i uruchom `codewhale`. Wybierz dostawcę poleceniem `/provider`, a model poleceniem `/model`. Następnie opisz konkretne zadanie:
 
 ```text
 Fix the failing tests and explain what changed.
@@ -41,16 +45,24 @@ Możesz też uruchomić zadanie bez otwierania TUI:
 codewhale exec "fix the failing tests and explain what changed"
 ```
 
-Codewhale może czytać Twoje repozytorium, edytować pliki, wykonywać polecenia, sprawdzać wyniki i kontynuować pracę nad celem. Ty decydujesz, jaki poziom dostępu mu przyznasz.
+Codewhale może czytać Twoje repozytorium, edytować pliki, wykonywać polecenia, sprawdzać wyniki i kontynuować pracę nad celem. Użyj `/mode plan`, aby analizować projekt bez zmian w plikach i wykonywania poleceń powłoki, a `/mode work`, gdy chcesz wprowadzać zmiany. Naciśnij `Shift+Tab`, aby wybrać Ask, Auto-Review lub Full Access; [przewodnik po trybach i uprawnieniach](docs/MODES.md) wyjaśnia, na co pozwala każdy z nich.
+
+## Terminal, aplikacje i Computer Use
+
+Terminal i klienci graficzni łączą się z Codewhale Runtime, który uruchamia agenta i jego narzędzia:
+
+- **Terminal:** `codewhale` otwiera interaktywny interfejs; `codewhale exec` uruchamia zadanie ze skryptu lub zadania CI.
+- **Lokalna przeglądarka:** `codewhale web` otwiera dołączonego [lokalnego klienta webowego](docs/WEB.md) dla tego samego środowiska wykonawczego.
+- **Aplikacje webowe i desktopowe Codewhale:** graficzne środowiska pracy w trakcie rozwoju. Informacje o ich dostępności znajdują się na [stronie produktu](https://codewhale.net/en/product).
 
-## Interfejs graficzny
+**Computer Use dodaje narzędzia do obserwowania innych aplikacji i interakcji z nimi.** Wtyczka jest dołączona do obecnego kodu źródłowego. Przed użyciem sprawdź, o jaki dostęp prosi, i włącz ją; nadal obowiązują uprawnienia systemu operacyjnego i wymagania platformy. Zobacz dołączony [przewodnik po Computer Use](crates/tui/plugins/computer-use/README.md) oraz [konfigurację wtyczek](docs/PLUGINS.md).
 
-Wolisz interfejs graficzny? Rozszerzenie CodeWhale for VS Code, utrzymywane przez społeczność, umieszcza tego samego agenta w bocznym panelu VS Code — czat, rozmowy w wątkach, diffy na żywo i zarządzanie zadaniami oparte na tej samej Runtime API, dzięki czemu sesje pozostają zsynchronizowane z terminalem. Zainstaluj je z [VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=HengQuWorld.brotherwhale-vscode); kod źródłowy znajdziesz na [GitHub](https://github.com/HengQuWorld/CodeWhale-VSCode).
+Utrzymywane przez społeczność rozszerzenie CodeWhale dla VS Code łączy się z lokalnym Runtime z panelu bocznego. Zainstaluj je z [VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=HengQuWorld.brotherwhale-vscode); kod źródłowy znajdziesz na [GitHub](https://github.com/HengQuWorld/CodeWhale-VSCode).
 
 ## Dlaczego Codewhale
 
-- **Używaj wybranego modelu.** Połącz się z hostowanymi dostawcami lub lokalnymi modelami przez Ollama, vLLM albo SGLang. Dostawcę i model zmienisz za pomocą `/model`.
-- **Zachowaj kontrolę.** Tryb Plan jest tylko do odczytu. Ask, Auto-Review i Full Access jasno pokazują sposób zatwierdzania działań. `/undo` cofa ostatnią turę, a `/restore` przywraca przestrzeń roboczą do wcześniejszej migawki.
+- **Wybieraj modele.** Połącz się z hostowanymi dostawcami lub lokalnymi modelami przez Ollama, vLLM albo SGLang. Polecenie `/provider` służy do zmiany dostawcy, a `/model` do wyboru modelu.
+- **Zachowaj kontrolę.** Sprawdzaj proponowane działania i wynikające z nich zmiany w plikach. Ustawienia zatwierdzania określają, kiedy potrzebna jest weryfikacja; Full Access nadal przestrzega nieprzekraczalnych ograniczeń zasad. `/undo` i `/restore` pomagają przywrócić przestrzeń roboczą po zmianach.
 - **Utrzymuj porządek w długich zadaniach.** Zapisuj sesje, ustawiaj trwały `/goal`, sprawdzaj przepływy pracy przed uruchomieniem i koordynuj agentów bez umieszczania ich wewnętrznych instrukcji w zapisie Twojej rozmowy.
 - **Rozszerzaj agenta, którego już masz.** Podłączaj serwery MCP i umiejętności, konfiguruj hooki oraz przechowuj role agentów jako czytelne pliki w projekcie lub ustawieniach osobistych.
 
@@ -69,10 +81,11 @@ Przeczytaj o [kolejności autoryzacji](docs/AUTHORIZATION_ORDER.md), aby poznać
 - [MCP](docs/MCP.md), [hooki](docs/HOOKS.md) i [konfiguracja](docs/CONFIGURATION.md)
 - [Lokalny klient webowy](docs/WEB.md)
 - [Cała dokumentacja](docs)
+- [Struktura repozytorium i przewodnik dla współtwórców](CONTRIBUTING.md#project-structure)
 
 ## Dołącz do społeczności
 
-Codewhale staje się lepszy, gdy ludzie go używają, zgłaszają niedogodności i pomagają je naprawiać. Jeśli brakuje dostawcy, przepływ pracy jest niewygodny albo interfejs terminala przeszkadza Ci w pracy, [otwórz issue](https://github.com/Hmbown/CodeWhale/issues). Jeśli wiesz, jak coś ulepszyć, [otwórz pull request](CONTRIBUTING.md). Pierwsze wkłady są mile widziane, a autorzy zachowują uznanie za pracę przyjętą do projektu.
+**Zgłoszenia błędów, pomysły na funkcje i pull requesty są mile widziane**, niezależnie od tego, czy używasz Codewhale od miesięcy, czy próbujesz go po raz pierwszy. Jeśli brakuje dostawcy, przepływ pracy jest niewygodny albo interfejs terminala przeszkadza Ci w pracy, [otwórz issue](https://github.com/Hmbown/CodeWhale/issues/new/choose) lub [wyślij pull request](CONTRIBUTING.md), abyśmy mogli wspólnie go ulepszyć. Pierwsze wkłady są mile widziane, a autorzy zachowują uznanie za pracę przyjętą do projektu.
 
 Dołącz do [Discorda](https://discord.gg/37gfS3ksug) albo dodaj Huntera na WeChat (`hunterbown`) i poproś o dołączenie do grupy Whale Brothers.
 
diff --git a/README.pt-BR.md b/README.pt-BR.md
index bd1d07f1f1..4b82913b94 100644
--- a/README.pt-BR.md
+++ b/README.pt-BR.md
@@ -1,9 +1,11 @@
-
+
 # Codewhale
 
-Codewhale é um agente de programação de código aberto para o seu terminal, desenvolvido em Rust e aprimorado publicamente com as pessoas que o utilizam.
+Codewhale é um agente de código aberto que lê seu projeto, edita arquivos, executa comandos e verifica o próprio trabalho usando um modelo hospedado ou local à sua escolha. Comece com uma tarefa no terminal. Para um trabalho maior, distribua partes do trabalho entre agentes com diferentes modelos e funções.
 
-![Codewhale em execução em um terminal](assets/screenshot.webp)
+![Codewhale em execução em um terminal](web/public/codewhale-tui-171acee.png)
+
+*Prévia do terminal em uma build de desenvolvimento da v0.9.12.*
 
 [English](README.md) · [简体中文](README.zh-CN.md) · [日本語](README.ja-JP.md) · [Tiếng Việt](README.vi.md) · [Bahasa Indonesia](README.id.md) · [한국어](README.ko-KR.md) · [Español](README.es-419.md) · [Русский](README.ru.md) · [Українська](README.uk.md) · [Français](README.fr.md) · [Deutsch](README.de.md) · [繁體中文](README.zh-TW.md) · [हिन्दी](README.hi.md) · [Türkçe](README.tr.md) · [Italiano](README.it.md) · [Polski](README.pl.md) · [العربية](README.ar.md) · [Català](README.ca.md)
 
@@ -21,15 +23,17 @@ curl -fsSL https://codewhale.net/install.sh | sh
 "$HOME/.local/bin/codewhale"
 ```
 
+O instalador seleciona a versão publicada mais recente. O [histórico de alterações](CHANGELOG.md) também descreve a versão candidata ainda não publicada da próxima versão; essas alterações só são incluídas nos downloads publicados quando a versão estiver disponível.
+
 No Windows, baixe o instalador ou arquivo correspondente em [GitHub Releases](https://github.com/Hmbown/CodeWhale/releases/latest). Para atualizar uma instalação direta existente, execute `codewhale update`, ou `codewhale update --check` apenas para verificar. O atualizador mostra o caminho do executável e preserva builds mais recentes. npm e Cargo são opções secundárias; consulte o [guia de instalação](docs/INSTALL.md) para migrar de um gerenciador de pacotes e configurar PATH.
 
-Na primeira execução, o Codewhale ajuda você a conectar um provedor ou a continuar offline. Ele também oferece suporte a Cargo, Docker, Nix, Scoop, arquivos pré-compilados, Android/Termux e um espelho CNB. Consulte o [guia de instalação](docs/INSTALL.md).
+Na primeira execução, o Codewhale ajuda você a conectar um provedor ou a configurar o Codewhale offline. As respostas exigem um modelo hospedado ou local conectado. O Codewhale também oferece suporte a npm e Cargo como opções secundárias de distribuição, além de Docker, Nix, Scoop, Android/Termux e um espelho CNB opcional. Instalações existentes feitas por gerenciadores de pacotes recebem instruções de migração. Consulte a [ajuda de instalação e PATH](docs/INSTALL.md).
 
 O preenchimento automático com Tab é ativado com um comando por shell — `codewhale completion bash|zsh|fish|powershell|elvish`. Consulte o [preenchimento automático do shell](docs/INSTALL.md#8-shell-completions).
 
 ## Uso
 
-Converse com o Codewhale como você conversaria com alguém da sua equipe:
+Abra um terminal na pasta do seu projeto e execute `codewhale`. Escolha seu provedor com `/provider` e seu modelo com `/model`. Depois, descreva uma tarefa concreta:
 
 ```text
 Fix the failing tests and explain what changed.
@@ -41,16 +45,24 @@ Ou execute uma tarefa sem abrir a TUI:
 codewhale exec "fix the failing tests and explain what changed"
 ```
 
-O Codewhale pode ler seu repositório, editar arquivos, executar comandos, verificar resultados e continuar trabalhando em direção a um objetivo. Você decide quanto acesso ele terá.
+O Codewhale pode ler seu repositório, editar arquivos, executar comandos, verificar resultados e continuar trabalhando em direção a um objetivo. Use `/mode plan` para explorar sem alterar arquivos nem executar comandos de shell, e `/mode work` quando quiser que ele faça alterações. Pressione `Shift+Tab` para escolher Ask, Auto-Review ou Full Access; o [guia de modos e permissões](docs/MODES.md) explica o que cada opção permite.
+
+## Terminal, aplicativos e Computer Use
+
+O terminal e os clientes gráficos se conectam ao Runtime do Codewhale, que executa o agente e suas ferramentas:
+
+- **Terminal:** `codewhale` abre a interface interativa; `codewhale exec` executa uma tarefa a partir de um script ou de um job de CI.
+- **Navegador local:** `codewhale web` abre o [cliente web local](docs/WEB.md) incluído, que usa o mesmo runtime.
+- **Aplicativos web e desktop do Codewhale:** ambientes de trabalho gráficos em desenvolvimento. A disponibilidade é informada na [página do produto](https://codewhale.net/en/product).
 
-## Interface gráfica
+**Computer Use adiciona ferramentas para observar outros aplicativos e interagir com eles.** O plugin está incluído no código-fonte atual. Revise o acesso solicitado e habilite-o antes de usar; as permissões do sistema operacional e os requisitos da plataforma continuam sendo necessários. Consulte o [guia de Computer Use](crates/tui/plugins/computer-use/README.md) incluído e a [configuração de plugins](docs/PLUGINS.md).
 
-Prefere uma interface gráfica? A extensão CodeWhale for VS Code, mantida pela comunidade, coloca o mesmo agente na barra lateral do VS Code — chat, conversas em tópicos, diffs ao vivo e gerenciamento de tarefas sobre a mesma Runtime API, mantendo as sessões sincronizadas com o terminal. Instale pelo [VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=HengQuWorld.brotherwhale-vscode); o código-fonte está no [GitHub](https://github.com/HengQuWorld/CodeWhale-VSCode).
+Para VS Code, a extensão CodeWhale mantida pela comunidade se conecta ao Runtime local por uma barra lateral. Instale pelo [VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=HengQuWorld.brotherwhale-vscode); o código-fonte está no [GitHub](https://github.com/HengQuWorld/CodeWhale-VSCode).
 
 ## Por que usar o Codewhale
 
-- **Use o modelo que quiser.** Conecte provedores hospedados ou modelos locais por meio do Ollama, vLLM ou SGLang. Alterne o provedor e o modelo com `/model`.
-- **Mantenha o controle.** O modo Plan é somente leitura. Ask, Auto-Review e Full Access tornam visível o comportamento das aprovações. `/undo` desfaz o último turno e `/restore` retorna o espaço de trabalho a um snapshot anterior.
+- **Escolha seus modelos.** Conecte provedores hospedados ou modelos locais por meio do Ollama, vLLM ou SGLang. Use `/provider` para trocar de provedor e `/model` para escolher um modelo.
+- **Mantenha o controle.** Revise as ações propostas e as alterações resultantes nos arquivos. As configurações de aprovação determinam quando uma revisão é necessária; Full Access continua respeitando os limites obrigatórios das políticas. `/undo` e `/restore` ajudam a recuperar alterações no espaço de trabalho.
 - **Mantenha trabalhos longos organizados.** Salve sessões, defina um `/goal` duradouro, revise os fluxos de trabalho antes da execução e coordene agentes sem transformar as instruções internas deles em parte da sua conversa.
 - **Amplie o agente que você já tem.** Conecte servidores MCP e habilidades, configure hooks e mantenha as funções dos agentes como arquivos legíveis no projeto ou nas suas configurações pessoais.
 
@@ -69,10 +81,11 @@ Leia a [ordem de autorização](docs/AUTHORIZATION_ORDER.md) para conhecer a hie
 - [MCP](docs/MCP.md), [hooks](docs/HOOKS.md) e [configuração](docs/CONFIGURATION.md)
 - [Cliente web local](docs/WEB.md)
 - [Toda a documentação](docs)
+- [Estrutura do repositório e guia de contribuição](CONTRIBUTING.md#project-structure)
 
 ## Participe da comunidade
 
-O Codewhale melhora quando as pessoas o utilizam, relatam o que parece errado e ajudam a corrigir. Se estiver faltando um provedor, se um fluxo de trabalho for inconveniente ou se a interface do terminal atrapalhar, [abra uma issue](https://github.com/Hmbown/CodeWhale/issues). Se souber como melhorar, [abra um pull request](CONTRIBUTING.md). Primeiras contribuições são bem-vindas, e os contribuidores mantêm o crédito pelo trabalho incorporado ao projeto.
+**Relatos de bugs, ideias de funcionalidades e pull requests são bem-vindos**, tanto de quem usa o Codewhale há meses quanto de quem está experimentando pela primeira vez. Se estiver faltando um provedor, se um fluxo de trabalho for inconveniente ou se a interface do terminal atrapalhar, [abra uma issue](https://github.com/Hmbown/CodeWhale/issues/new/choose) ou [envie um pull request](CONTRIBUTING.md) para melhorarmos juntos. Primeiras contribuições são bem-vindas, e os contribuidores mantêm o crédito pelo trabalho incorporado ao projeto.
 
 Participe do [Discord](https://discord.gg/37gfS3ksug), ou adicione Hunter no WeChat (`hunterbown`) e peça para entrar no grupo Whale Brothers.
 
diff --git a/README.ru.md b/README.ru.md
index 8d83c8344d..46c9ed0393 100644
--- a/README.ru.md
+++ b/README.ru.md
@@ -1,9 +1,11 @@
-
+
 # Codewhale
 
-Codewhale — это агент для программирования с открытым исходным кодом, работающий в терминале. Он написан на Rust и открыто развивается вместе со своими пользователями.
+Codewhale — агент с открытым исходным кодом, который читает ваш проект, редактирует файлы, выполняет команды и проверяет свою работу с помощью выбранной вами облачной или локальной модели. Начните с одной задачи в терминале. Для большой работы поручайте её части агентам с разными моделями и ролями.
 
-![Codewhale работает в терминале](assets/screenshot.webp)
+![Codewhale работает в терминале](web/public/codewhale-tui-171acee.png)
+
+*Предварительный вид терминала из сборки v0.9.12, находившейся в разработке.*
 
 [English](README.md) · [简体中文](README.zh-CN.md) · [日本語](README.ja-JP.md) · [Tiếng Việt](README.vi.md) · [Bahasa Indonesia](README.id.md) · [한국어](README.ko-KR.md) · [Español](README.es-419.md) · [Português](README.pt-BR.md) · [Українська](README.uk.md) · [Français](README.fr.md) · [Deutsch](README.de.md) · [繁體中文](README.zh-TW.md) · [हिन्दी](README.hi.md) · [Türkçe](README.tr.md) · [Italiano](README.it.md) · [Polski](README.pl.md) · [العربية](README.ar.md) · [Català](README.ca.md)
 
@@ -21,15 +23,17 @@ curl -fsSL https://codewhale.net/install.sh | sh
 "$HOME/.local/bin/codewhale"
 ```
 
+Установщик выбирает последний опубликованный выпуск. В [журнале изменений](CHANGELOG.md) также описан ещё не опубликованный кандидат следующего выпуска; эти изменения появятся в доступных для скачивания выпусках только после публикации.
+
 В Windows скачайте подходящий установщик или архив из [GitHub Releases](https://github.com/Hmbown/CodeWhale/releases/latest). Для обновления существующей прямой установки запустите `codewhale update`; для проверки без установки — `codewhale update --check`. Обновление показывает путь к исполняемому файлу и сохраняет более новые сборки. npm и Cargo — дополнительные способы установки. Переход с менеджера пакетов и настройка PATH описаны в [руководстве по установке](docs/INSTALL.md).
 
-При первом запуске Codewhale поможет подключить провайдера или остаться в автономном режиме. Он также поддерживает Cargo, Docker, Nix, Scoop, готовые архивы, Android/Termux и зеркало CNB. См. [руководство по установке](docs/INSTALL.md).
+При первом запуске Codewhale поможет подключить провайдера или настроить Codewhale автономно. Для ответов модели требуется подключённая облачная или локальная модель. Codewhale также поддерживает npm и Cargo как дополнительные способы установки, а также Docker, Nix, Scoop, Android/Termux и необязательное зеркало CNB. Для существующих установок через менеджер пакетов предусмотрены инструкции по переходу. См. [помощь по установке и PATH](docs/INSTALL.md).
 
 Для автодополнения по Tab достаточно одной команды для каждой оболочки — `codewhale completion bash|zsh|fish|powershell|elvish`. См. [автодополнение оболочки](docs/INSTALL.md#8-shell-completions).
 
 ## Использование
 
-Обращайтесь к Codewhale так же, как к коллеге по команде:
+Откройте терминал в папке проекта и запустите `codewhale`. Выберите провайдера командой `/provider`, а модель — командой `/model`. Затем опишите конкретную задачу:
 
 ```text
 Fix the failing tests and explain what changed.
@@ -41,16 +45,24 @@ Fix the failing tests and explain what changed.
 codewhale exec "fix the failing tests and explain what changed"
 ```
 
-Codewhale умеет читать ваш репозиторий, редактировать файлы, выполнять команды, проверять результаты и продолжать работу над целью. Вы сами решаете, какой доступ ему предоставить.
+Codewhale умеет читать ваш репозиторий, редактировать файлы, выполнять команды, проверять результаты и продолжать работу над целью. Используйте `/mode plan` для изучения без изменения файлов и выполнения команд оболочки, а `/mode work` — когда нужны изменения. Нажмите `Shift+Tab`, чтобы выбрать Ask, Auto-Review или Full Access; в [руководстве по режимам и разрешениям](docs/MODES.md) объясняется, что разрешает каждый вариант.
+
+## Терминал, приложения и Computer Use
+
+Терминал и графические клиенты подключаются к Codewhale Runtime, который запускает агента и его инструменты:
+
+- **Терминал:** `codewhale` открывает интерактивный интерфейс; `codewhale exec` запускает задачу из скрипта или задания CI.
+- **Локальный браузер:** `codewhale web` открывает встроенный [локальный веб-клиент](docs/WEB.md) для той же среды выполнения.
+- **Веб-приложение и настольные приложения Codewhale:** графические рабочие среды в разработке. Сведения об их доступности приведены на [странице продукта](https://codewhale.net/en/product).
 
-## Графический интерфейс
+**Computer Use добавляет инструменты для наблюдения за другими приложениями и взаимодействия с ними.** Плагин включён в текущий исходный код. Перед использованием проверьте запрашиваемый доступ и включите плагин; разрешения ОС и требования платформы по-прежнему действуют. См. включённое в репозиторий [руководство по Computer Use](crates/tui/plugins/computer-use/README.md) и [настройку плагинов](docs/PLUGINS.md).
 
-Предпочитаете графический интерфейс? Поддерживаемое сообществом расширение CodeWhale for VS Code помещает того же агента в боковую панель VS Code — чат, беседы по темам, живые diff и управление задачами поверх той же Runtime API, так что сеансы остаются синхронизированными с терминалом. Установите его из [VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=HengQuWorld.brotherwhale-vscode); исходный код — на [GitHub](https://github.com/HengQuWorld/CodeWhale-VSCode).
+Поддерживаемое сообществом расширение CodeWhale для VS Code подключается к локальному Runtime из боковой панели. Установите его из [VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=HengQuWorld.brotherwhale-vscode); исходный код — на [GitHub](https://github.com/HengQuWorld/CodeWhale-VSCode).
 
 ## Почему Codewhale
 
-- **Используйте нужную вам модель.** Подключайте облачных провайдеров или локальные модели через Ollama, vLLM или SGLang. Переключайте провайдера и модель командой `/model`.
-- **Сохраняйте контроль.** Режим Plan доступен только для чтения. Ask, Auto-Review и Full Access наглядно показывают порядок подтверждений. `/undo` отменяет последний ход, а `/restore` возвращает рабочую область к более раннему снимку.
+- **Выбирайте модели.** Подключайте облачных провайдеров или локальные модели через Ollama, vLLM или SGLang. Используйте `/provider` для смены провайдера, а `/model` — для выбора модели.
+- **Сохраняйте контроль.** Проверяйте предлагаемые действия и получившиеся изменения файлов. Настройки подтверждения определяют, когда нужна проверка; Full Access по-прежнему соблюдает жёсткие ограничения политик. `/undo` и `/restore` помогают восстановить рабочую область после изменений.
 - **Организуйте длительную работу.** Сохраняйте сеансы, задавайте постоянную `/goal`, проверяйте рабочие процессы перед запуском и координируйте агентов так, чтобы их внутренние инструкции не попадали в вашу переписку.
 - **Расширяйте уже настроенного агента.** Подключайте серверы MCP и навыки, настраивайте хуки и храните роли агентов в виде понятных файлов в проекте или личных настройках.
 
@@ -69,10 +81,11 @@ Codewhale работает на вашем компьютере с предос
 - [MCP](docs/MCP.md), [хуки](docs/HOOKS.md) и [конфигурация](docs/CONFIGURATION.md)
 - [Локальный веб-клиент](docs/WEB.md)
 - [Вся документация](docs)
+- [Структура репозитория и руководство для участников](CONTRIBUTING.md#project-structure)
 
 ## Присоединяйтесь к сообществу
 
-Codewhale становится лучше, когда люди пользуются им, сообщают о неудобствах и помогают их исправить. Если нужного провайдера нет, рабочий процесс неудобен или интерфейс терминала мешает работе, [создайте issue](https://github.com/Hmbown/CodeWhale/issues). Если вы знаете, как это улучшить, [откройте pull request](CONTRIBUTING.md). Мы рады первым вкладам, а авторство принятой работы сохраняется за участниками.
+**Мы рады сообщениям об ошибках, идеям новых функций и pull request**, независимо от того, пользуетесь ли вы Codewhale несколько месяцев или пробуете его впервые. Если нужного провайдера нет, рабочий процесс неудобен или интерфейс терминала мешает работе, [создайте issue](https://github.com/Hmbown/CodeWhale/issues/new/choose) или [отправьте pull request](CONTRIBUTING.md), чтобы мы могли улучшить проект вместе. Мы рады первым вкладам, а авторство принятой работы сохраняется за участниками.
 
 Присоединяйтесь к [Discord](https://discord.gg/37gfS3ksug) или добавьте Hunter в WeChat (`hunterbown`) и попросите принять вас в группу Whale Brothers.
 
diff --git a/README.tr.md b/README.tr.md
index 13c10b3972..cff2884806 100644
--- a/README.tr.md
+++ b/README.tr.md
@@ -1,9 +1,11 @@
-
+
 # Codewhale
 
-Codewhale, terminaliniz için Rust ile geliştirilmiş ve kullanıcılarıyla birlikte açık biçimde iyileştirilen açık kaynaklı bir kodlama ajanıdır.
+Codewhale, seçtiğiniz barındırılan veya yerel bir modeli kullanarak projenizi okuyan, dosyaları düzenleyen, komutları çalıştıran ve yaptığı işi kontrol eden açık kaynaklı bir ajandır. Terminalde tek bir görevle başlayın. Daha büyük bir işte, işin bölümlerini farklı model ve rollere sahip ajanlara verin.
 
-![Terminalde çalışan Codewhale](assets/screenshot.webp)
+![Terminalde çalışan Codewhale](web/public/codewhale-tui-171acee.png)
+
+*v0.9.12 geliştirme derlemesinden terminal önizlemesi.*
 
 [English](README.md) · [简体中文](README.zh-CN.md) · [日本語](README.ja-JP.md) · [Tiếng Việt](README.vi.md) · [Bahasa Indonesia](README.id.md) · [한국어](README.ko-KR.md) · [Español](README.es-419.md) · [Português](README.pt-BR.md) · [Русский](README.ru.md) · [Українська](README.uk.md) · [Français](README.fr.md) · [Deutsch](README.de.md) · [繁體中文](README.zh-TW.md) · [हिन्दी](README.hi.md) · [Italiano](README.it.md) · [Polski](README.pl.md) · [العربية](README.ar.md) · [Català](README.ca.md)
 
@@ -21,15 +23,17 @@ curl -fsSL https://codewhale.net/install.sh | sh
 "$HOME/.local/bin/codewhale"
 ```
 
+Yükleyici, yayımlanmış en son sürümü seçer. [Değişiklik günlüğü](CHANGELOG.md), bir sonraki sürümün henüz yayımlanmamış adayını da açıklar; bu değişiklikler, sürüm kullanıma sunulana kadar yayımlanmış indirmelere dahil edilmez.
+
 Windows’ta [GitHub Releases](https://github.com/Hmbown/CodeWhale/releases/latest) üzerinden uygun yükleyiciyi veya arşivi indirin. Mevcut doğrudan kurulumu güncellemek için `codewhale update`, yalnızca kontrol etmek için `codewhale update --check` çalıştırın. Güncelleyici çalıştırılabilir dosyanın yolunu gösterir ve daha yeni derlemeleri korur. npm ve Cargo ikincil paketleme seçenekleridir. Paket yöneticisinden geçiş ve PATH ayarları için [kurulum kılavuzuna](docs/INSTALL.md) bakın.
 
-Codewhale ilk çalıştırmada bir sağlayıcıya bağlanmanıza veya çevrimdışı kalmanıza yardımcı olur. Cargo, Docker, Nix, Scoop, önceden derlenmiş arşivler, Android/Termux ve CNB aynasını da destekler. [Kurulum kılavuzuna](docs/INSTALL.md) bakın.
+Codewhale ilk çalıştırmada bir sağlayıcıya bağlanmanıza veya Codewhale’i çevrimdışı yapılandırmanıza yardımcı olur. Model yanıtları için barındırılan ya da yerel bir modele bağlantı gerekir. Codewhale, ikincil paketleme seçenekleri olarak npm ve Cargo’nun yanı sıra Docker, Nix, Scoop, Android/Termux ve isteğe bağlı CNB aynasını da destekler. Paket yöneticisiyle yönetilen mevcut kurulumlar için geçiş talimatları sağlanır. [Kurulum ve PATH yardımına](docs/INSTALL.md) bakın.
 
 Her kabukta Tab tamamlama tek bir komutla etkinleştirilir — `codewhale completion bash|zsh|fish|powershell|elvish`. [Kabuk tamamlamalarına](docs/INSTALL.md#8-shell-completions) bakın.
 
 ## Kullanım
 
-Codewhale ile ekip arkadaşınızla konuşur gibi konuşun:
+Proje klasörünüzde bir terminal açın ve `codewhale` komutunu çalıştırın. `/provider` ile sağlayıcınızı, `/model` ile modelinizi seçin. Ardından somut bir görev tarif edin:
 
 ```text
 Fix the failing tests and explain what changed.
@@ -41,16 +45,24 @@ TUI’yi açmadan da bir görev çalıştırabilirsiniz:
 codewhale exec "fix the failing tests and explain what changed"
 ```
 
-Codewhale deponuzu okuyabilir, dosyaları düzenleyebilir, komutları çalıştırabilir, sonuçları inceleyebilir ve bir hedefe doğru çalışmayı sürdürebilir. Ne kadar erişime sahip olacağına siz karar verirsiniz.
+Codewhale deponuzu okuyabilir, dosyaları düzenleyebilir, komutları çalıştırabilir, sonuçları inceleyebilir ve bir hedefe doğru çalışmayı sürdürebilir. Dosyaları değiştirmeden veya kabuk komutlarını çalıştırmadan inceleme yapmak için `/mode plan`, değişiklik yapmak istediğinizde ise `/mode work` kullanın. Ask, Auto-Review veya Full Access seçeneklerinden birini seçmek için `Shift+Tab` tuşlarına basın; [modlar ve izinler kılavuzu](docs/MODES.md) her birinin nelere izin verdiğini açıklar.
+
+## Terminal, uygulamalar ve Computer Use
+
+Terminal ve grafik istemciler, ajanı ve araçlarını çalıştıran Codewhale Runtime’a bağlanır:
+
+- **Terminal:** `codewhale` etkileşimli arayüzü açar; `codewhale exec` bir betikten veya CI işinden görev çalıştırır.
+- **Yerel tarayıcı:** `codewhale web`, aynı çalışma zamanı için paketle birlikte gelen [yerel web istemcisini](docs/WEB.md) açar.
+- **Codewhale web ve masaüstü uygulamaları:** geliştirme aşamasındaki grafik çalışma ortamlarıdır. Kullanılabilirlikleri [ürün sayfasında](https://codewhale.net/en/product) belirtilir.
 
-## Grafik arayüz
+**Computer Use, diğer uygulamaları gözlemlemek ve onlarla etkileşime girmek için araçlar ekler.** Eklenti mevcut kaynak koduna dahildir. Kullanmadan önce istediği erişimi gözden geçirin ve eklentiyi etkinleştirin; işletim sistemi izinleri ve platform gereksinimleri geçerliliğini korur. Birlikte gelen [Computer Use kılavuzuna](crates/tui/plugins/computer-use/README.md) ve [eklenti kurulumuna](docs/PLUGINS.md) bakın.
 
-Grafik bir arayüzü mü tercih edersiniz? Topluluk tarafından bakımı yapılan CodeWhale for VS Code eklentisi aynı aracıyı VS Code kenar çubuğuna taşır — sohbet, konu tabanlı görüşmeler, canlı diff ve görev yönetimi, hepsi aynı Runtime API üzerinde; oturumlar terminalle eşitlenmiş kalır. [VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=HengQuWorld.brotherwhale-vscode) üzerinden kurun; kaynak kodu [GitHub](https://github.com/HengQuWorld/CodeWhale-VSCode) adresinde.
+Topluluk tarafından bakımı yapılan VS Code için CodeWhale eklentisi, kenar çubuğundan yerel Runtime’a bağlanır. [VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=HengQuWorld.brotherwhale-vscode) üzerinden kurun; kaynak kodu [GitHub](https://github.com/HengQuWorld/CodeWhale-VSCode) adresindedir.
 
 ## Neden Codewhale
 
-- **İstediğiniz modeli kullanın.** Barındırılan sağlayıcılara veya Ollama, vLLM ya da SGLang üzerinden yerel modellere bağlanın. Sağlayıcı ve modeli `/model` ile değiştirin.
-- **Kontrolü elinizde tutun.** Plan salt okunurdur. Ask, Auto-Review ve Full Access, onay davranışını görünür kılar. `/undo` son turu geri alır, `/restore` ise çalışma alanını önceki bir anlık görüntüye döndürür.
+- **Modellerinizi seçin.** Barındırılan sağlayıcılara veya Ollama, vLLM ya da SGLang üzerinden yerel modellere bağlanın. Sağlayıcı değiştirmek için `/provider`, model seçmek için `/model` kullanın.
+- **Kontrolü elinizde tutun.** Önerilen eylemleri ve bunların sonucunda dosyalarda oluşan değişiklikleri inceleyin. Onay ayarları ne zaman inceleme gerektiğini belirler; Full Access de politikanın kesin sınırlarına uyar. `/undo` ve `/restore`, değişikliklerden sonra çalışma alanını geri yüklemenize yardımcı olur.
 - **Uzun süren işleri düzenli tutun.** Oturumları kaydedin, kalıcı bir `/goal` belirleyin, iş akışlarını çalışmadan önce gözden geçirin ve ajanların iç talimatlarını konuşmanıza taşımadan onları koordine edin.
 - **Elinizdeki ajanı genişletin.** MCP sunucularını ve becerileri bağlayın, hook’ları yapılandırın ve ajan rollerini projenizde veya kişisel ayarlarınızda okunabilir dosyalar olarak saklayın.
 
@@ -69,10 +81,11 @@ Politikaların kesin sıralaması için [yetkilendirme sırasını](docs/AUTHORI
 - [MCP](docs/MCP.md), [hook’lar](docs/HOOKS.md) ve [yapılandırma](docs/CONFIGURATION.md)
 - [Yerel web istemcisi](docs/WEB.md)
 - [Tüm belgeler](docs)
+- [Depo yapısı ve katkıda bulunma rehberi](CONTRIBUTING.md#project-structure)
 
 ## Topluluğa katılın
 
-İnsanlar Codewhale’i kullandıkça, yanlış gelen noktaları bildirdikçe ve düzeltmeye yardımcı oldukça Codewhale daha iyi olur. Bir sağlayıcı eksikse, bir iş akışı kullanışsızsa veya terminal arayüzü işinizi zorlaştırıyorsa [bir issue açın](https://github.com/Hmbown/CodeWhale/issues). Nasıl iyileştirileceğini biliyorsanız [bir pull request açın](CONTRIBUTING.md). İlk katkılar memnuniyetle karşılanır ve katkıda bulunanların projeye alınan çalışmaları üzerindeki emeği kayda geçer.
+**Hata bildirimleri, özellik fikirleri ve pull request’ler memnuniyetle karşılanır**; Codewhale’i aylardır kullanıyor olmanız ya da ilk kez denemeniz fark etmez. Bir sağlayıcı eksikse, bir iş akışı kullanışsızsa veya terminal arayüzü işinizi zorlaştırıyorsa birlikte iyileştirebilmemiz için [bir issue açın](https://github.com/Hmbown/CodeWhale/issues/new/choose) veya [bir pull request gönderin](CONTRIBUTING.md). İlk katkılar memnuniyetle karşılanır ve katkıda bulunanların projeye alınan çalışmaları üzerindeki emeği kayda geçer.
 
 [Discord’a](https://discord.gg/37gfS3ksug) katılın veya WeChat’te Hunter’ı (`hunterbown`) ekleyip Whale Brothers grubuna katılmak istediğinizi belirtin.
 
diff --git a/README.uk.md b/README.uk.md
index 39dd458876..982821e040 100644
--- a/README.uk.md
+++ b/README.uk.md
@@ -1,9 +1,11 @@
-
+
 # Codewhale
 
-Codewhale — це агент програмування з відкритим кодом для вашого термінала, створений на Rust і вдосконалюваний публічно разом із людьми, які ним користуються.
+Codewhale — агент із відкритим кодом, який читає ваш проєкт, редагує файли, виконує команди й перевіряє свою роботу за допомогою обраної вами хмарної або локальної моделі. Почніть з одного завдання в терміналі. Для великої роботи доручайте її частини агентам із різними моделями й ролями.
 
-![Codewhale працює в терміналі](assets/screenshot.webp)
+![Codewhale працює в терміналі](web/public/codewhale-tui-171acee.png)
+
+*Попередній вигляд термінала зі збірки v0.9.12, що перебувала в розробці.*
 
 [English](README.md) · [简体中文](README.zh-CN.md) · [日本語](README.ja-JP.md) · [Tiếng Việt](README.vi.md) · [Bahasa Indonesia](README.id.md) · [한국어](README.ko-KR.md) · [Español](README.es-419.md) · [Português](README.pt-BR.md) · [Русский](README.ru.md) · [Français](README.fr.md) · [Deutsch](README.de.md) · [繁體中文](README.zh-TW.md) · [हिन्दी](README.hi.md) · [Türkçe](README.tr.md) · [Italiano](README.it.md) · [Polski](README.pl.md) · [العربية](README.ar.md) · [Català](README.ca.md)
 
@@ -21,15 +23,17 @@ curl -fsSL https://codewhale.net/install.sh | sh
 "$HOME/.local/bin/codewhale"
 ```
 
+Інсталятор обирає останній опублікований випуск. У [журналі змін](CHANGELOG.md) також описано ще не опублікований кандидат наступного випуску; ці зміни з’являться в доступних для завантаження випусках лише після публікації.
+
 У Windows завантажте відповідний інсталятор або архів із [GitHub Releases](https://github.com/Hmbown/CodeWhale/releases/latest). Для оновлення наявного прямого встановлення запустіть `codewhale update`; для перевірки без встановлення — `codewhale update --check`. Оновлювач показує шлях до виконуваного файлу та зберігає новіші збірки. npm і Cargo — додаткові способи встановлення. Перехід із менеджера пакетів і налаштування PATH описано в [посібнику зі встановлення](docs/INSTALL.md).
 
-Під час першого запуску Codewhale допоможе під’єднати провайдера або залишитися в автономному режимі. Він також підтримує Cargo, Docker, Nix, Scoop, готові архіви, Android/Termux і дзеркало CNB. Див. [посібник зі встановлення](docs/INSTALL.md).
+Під час першого запуску Codewhale допоможе під’єднати провайдера або налаштувати Codewhale автономно. Для відповідей моделі потрібна під’єднана хмарна або локальна модель. Codewhale також підтримує npm і Cargo як додаткові способи встановлення, а також Docker, Nix, Scoop, Android/Termux і необов’язкове дзеркало CNB. Для наявних установлень через менеджер пакетів передбачено інструкції з переходу. Див. [допомогу зі встановлення та PATH](docs/INSTALL.md).
 
 Для автодоповнення за Tab достатньо однієї команди для кожної оболонки — `codewhale completion bash|zsh|fish|powershell|elvish`. Див. [автодоповнення оболонки](docs/INSTALL.md#8-shell-completions).
 
 ## Використання
 
-Спілкуйтеся з Codewhale так само, як із колегою по команді:
+Відкрийте термінал у папці проєкту й запустіть `codewhale`. Оберіть провайдера командою `/provider`, а модель — командою `/model`. Потім опишіть конкретне завдання:
 
 ```text
 Fix the failing tests and explain what changed.
@@ -41,16 +45,24 @@ Fix the failing tests and explain what changed.
 codewhale exec "fix the failing tests and explain what changed"
 ```
 
-Codewhale може читати ваш репозиторій, редагувати файли, виконувати команди, перевіряти результати й продовжувати роботу над метою. Ви самі вирішуєте, який доступ йому надати.
+Codewhale може читати ваш репозиторій, редагувати файли, виконувати команди, перевіряти результати й продовжувати роботу над метою. Використовуйте `/mode plan` для дослідження без змін файлів і виконання команд оболонки, а `/mode work` — коли потрібні зміни. Натисніть `Shift+Tab`, щоб обрати Ask, Auto-Review або Full Access; у [посібнику з режимів і дозволів](docs/MODES.md) пояснено, що дозволяє кожен варіант.
+
+## Термінал, застосунки та Computer Use
+
+Термінал і графічні клієнти під’єднуються до Codewhale Runtime, який запускає агента та його інструменти:
+
+- **Термінал:** `codewhale` відкриває інтерактивний інтерфейс; `codewhale exec` запускає завдання зі скрипту або завдання CI.
+- **Локальний браузер:** `codewhale web` відкриває вбудований [локальний вебклієнт](docs/WEB.md) для того самого середовища виконання.
+- **Вебзастосунок і настільні застосунки Codewhale:** графічні робочі середовища в розробці. Відомості про їхню доступність наведено на [сторінці продукту](https://codewhale.net/en/product).
 
-## Графічний інтерфейс
+**Computer Use додає інструменти для спостереження за іншими застосунками та взаємодії з ними.** Плагін включено до поточного вихідного коду. Перед використанням перегляньте запитуваний доступ і ввімкніть плагін; дозволи ОС і вимоги платформи залишаються чинними. Див. включений до репозиторію [посібник із Computer Use](crates/tui/plugins/computer-use/README.md) та [налаштування плагінів](docs/PLUGINS.md).
 
-Віддаєте перевагу графічному інтерфейсу? Розширення CodeWhale for VS Code, яке підтримує спільнота, вміщує того самого агента в бічну панель VS Code — чат, тематичні розмови, живі diff та керування задачами на тій самій Runtime API, тож сеанси залишаються синхронізованими з терміналом. Установіть його з [VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=HengQuWorld.brotherwhale-vscode); вихідний код — на [GitHub](https://github.com/HengQuWorld/CodeWhale-VSCode).
+Розширення CodeWhale для VS Code, яке підтримує спільнота, під’єднується до локального Runtime з бічної панелі. Установіть його з [VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=HengQuWorld.brotherwhale-vscode); вихідний код — на [GitHub](https://github.com/HengQuWorld/CodeWhale-VSCode).
 
 ## Чому Codewhale
 
-- **Використовуйте потрібну вам модель.** Під’єднуйте хостингових провайдерів або локальні моделі через Ollama, vLLM чи SGLang. Змінюйте провайдера й модель за допомогою `/model`.
-- **Зберігайте контроль.** Режим Plan доступний лише для читання. Ask, Auto-Review і Full Access наочно показують поведінку погоджень. `/undo` скасовує останній хід, а `/restore` повертає робочий простір до попереднього знімка.
+- **Обирайте моделі.** Під’єднуйте хмарних провайдерів або локальні моделі через Ollama, vLLM чи SGLang. Використовуйте `/provider`, щоб змінити провайдера, і `/model`, щоб обрати модель.
+- **Зберігайте контроль.** Перевіряйте запропоновані дії та отримані зміни файлів. Налаштування погодження визначають, коли потрібна перевірка; Full Access і надалі дотримується жорстких обмежень політик. `/undo` та `/restore` допомагають відновити робочий простір після змін.
 - **Упорядковуйте тривалу роботу.** Зберігайте сеанси, установлюйте постійну `/goal`, перевіряйте робочі процеси перед запуском і координуйте агентів так, щоб їхні внутрішні інструкції не потрапляли до вашої розмови.
 - **Розширюйте вже наявного агента.** Під’єднуйте сервери MCP і навички, налаштовуйте хуки та зберігайте ролі агентів як зрозумілі файли у своєму проєкті або особистих налаштуваннях.
 
@@ -69,10 +81,11 @@ Codewhale працює на вашому комп’ютері з доступо
 - [MCP](docs/MCP.md), [хуки](docs/HOOKS.md) і [конфігурація](docs/CONFIGURATION.md)
 - [Локальний вебклієнт](docs/WEB.md)
 - [Уся документація](docs)
+- [Структура репозиторію та посібник для учасників](CONTRIBUTING.md#project-structure)
 
 ## Долучайтеся до спільноти
 
-Codewhale стає кращим, коли люди користуються ним, повідомляють про незручності й допомагають їх виправляти. Якщо потрібного провайдера немає, робочий процес незручний або інтерфейс термінала заважає роботі, [створіть issue](https://github.com/Hmbown/CodeWhale/issues). Якщо ви знаєте, як це поліпшити, [відкрийте pull request](CONTRIBUTING.md). Ми раді першим внескам, а авторство прийнятої роботи зберігається за учасниками.
+**Ми раді повідомленням про помилки, ідеям нових функцій і pull request**, незалежно від того, користуєтеся ви Codewhale кілька місяців чи пробуєте вперше. Якщо потрібного провайдера немає, робочий процес незручний або інтерфейс термінала заважає роботі, [створіть issue](https://github.com/Hmbown/CodeWhale/issues/new/choose) або [надішліть pull request](CONTRIBUTING.md), щоб ми могли поліпшити проєкт разом. Ми раді першим внескам, а авторство прийнятої роботи зберігається за учасниками.
 
 Долучайтеся до [Discord](https://discord.gg/37gfS3ksug) або додайте Hunter у WeChat (`hunterbown`) і попросіть приєднати вас до групи Whale Brothers.
 
diff --git a/README.vi.md b/README.vi.md
index 46e6dbe1f6..43285c82ee 100644
--- a/README.vi.md
+++ b/README.vi.md
@@ -1,9 +1,11 @@
-
+
 # Codewhale
 
-Codewhale là tác nhân lập trình mã nguồn mở dành cho terminal, được xây dựng bằng Rust và được cải thiện công khai cùng những người sử dụng nó.
+Codewhale là tác nhân mã nguồn mở có thể đọc dự án, chỉnh sửa tệp, chạy lệnh và kiểm tra công việc của mình bằng mô hình do nhà cung cấp lưu trữ hoặc mô hình cục bộ mà bạn chọn. Hãy bắt đầu với một tác vụ trong terminal. Với công việc lớn hơn, bạn có thể giao từng phần cho các tác nhân dùng mô hình và đảm nhiệm vai trò khác nhau.
 
-![Codewhale đang chạy trong terminal](assets/screenshot.webp)
+![Codewhale đang chạy trong terminal](web/public/codewhale-tui-171acee.png)
+
+*Hình xem trước terminal từ bản dựng phát triển v0.9.12.*
 
 [English](README.md) · [简体中文](README.zh-CN.md) · [日本語](README.ja-JP.md) · [Bahasa Indonesia](README.id.md) · [한국어](README.ko-KR.md) · [Español](README.es-419.md) · [Português](README.pt-BR.md) · [Русский](README.ru.md) · [Українська](README.uk.md) · [Français](README.fr.md) · [Deutsch](README.de.md) · [繁體中文](README.zh-TW.md) · [हिन्दी](README.hi.md) · [Türkçe](README.tr.md) · [Italiano](README.it.md) · [Polski](README.pl.md) · [العربية](README.ar.md) · [Català](README.ca.md)
 
@@ -21,15 +23,17 @@ curl -fsSL https://codewhale.net/install.sh | sh
 "$HOME/.local/bin/codewhale"
 ```
 
+Trình cài đặt chọn bản phát hành mới nhất đã được công bố. [Nhật ký thay đổi](CHANGELOG.md) cũng mô tả bản ứng viên chưa công bố của lần phát hành tiếp theo; những thay đổi đó chỉ có trong các bản tải xuống công khai khi bản phát hành tương ứng được công bố.
+
 Trên Windows, tải bộ cài hoặc gói lưu trữ phù hợp từ [GitHub Releases](https://github.com/Hmbown/CodeWhale/releases/latest). Với bản cài trực tiếp đã có, chạy `codewhale update`; dùng `codewhale update --check` nếu chỉ muốn kiểm tra. Trình cập nhật hiển thị đường dẫn tệp thực thi và giữ lại các bản dựng mới hơn. npm và Cargo là lựa chọn phụ; xem [hướng dẫn cài đặt](docs/INSTALL.md) để chuyển từ trình quản lý gói và thiết lập PATH.
 
-Trong lần chạy đầu tiên, Codewhale sẽ giúp bạn kết nối với nhà cung cấp hoặc tiếp tục làm việc ngoại tuyến. Codewhale cũng hỗ trợ Cargo, Docker, Nix, Scoop, các gói dựng sẵn, Android/Termux và bản sao CNB. Xem [hướng dẫn cài đặt](docs/INSTALL.md).
+Trong lần chạy đầu tiên, Codewhale sẽ giúp bạn kết nối với nhà cung cấp hoặc cấu hình Codewhale ngoại tuyến. Để nhận phản hồi từ mô hình, bạn cần kết nối với mô hình do nhà cung cấp lưu trữ hoặc mô hình cục bộ. Codewhale cũng hỗ trợ npm và Cargo như các hình thức đóng gói thứ cấp, cùng với Docker, Nix, Scoop, Android/Termux và bản sao CNB tùy chọn. Các bản cài đặt hiện có qua trình quản lý gói sẽ được hướng dẫn chuyển đổi. Xem [trợ giúp cài đặt và PATH](docs/INSTALL.md).
 
 Mỗi shell chỉ cần một lệnh để bật tính năng hoàn thành bằng phím Tab — `codewhale completion bash|zsh|fish|powershell|elvish`. Xem [tính năng hoàn thành của shell](docs/INSTALL.md#8-shell-completions).
 
 ## Sử dụng
 
-Hãy trò chuyện với Codewhale như khi bạn trao đổi với một đồng đội:
+Mở terminal trong thư mục dự án và chạy `codewhale`. Chọn nhà cung cấp bằng `/provider` và mô hình bằng `/model`. Sau đó mô tả một tác vụ cụ thể:
 
 ```text
 Fix the failing tests and explain what changed.
@@ -41,16 +45,24 @@ Hoặc chạy tác vụ mà không cần mở TUI:
 codewhale exec "fix the failing tests and explain what changed"
 ```
 
-Codewhale có thể đọc kho mã nguồn, chỉnh sửa tệp, chạy lệnh, kiểm tra kết quả và tiếp tục làm việc hướng đến mục tiêu. Bạn quyết định mức quyền truy cập dành cho nó.
+Codewhale có thể đọc kho mã nguồn, chỉnh sửa tệp, chạy lệnh, kiểm tra kết quả và tiếp tục làm việc hướng đến mục tiêu. Dùng `/mode plan` để tìm hiểu mà không thay đổi tệp hay thực thi lệnh shell, và `/mode work` khi bạn muốn tác nhân thực hiện thay đổi. Nhấn `Shift+Tab` để chọn Ask, Auto-Review hoặc Full Access; [hướng dẫn về chế độ và quyền](docs/MODES.md) giải thích những thao tác được phép ở mỗi lựa chọn.
+
+## Terminal, ứng dụng và Computer Use
+
+Terminal và các ứng dụng khách đồ họa kết nối với Codewhale Runtime, nơi chạy tác nhân và các công cụ của nó:
+
+- **Terminal:** `codewhale` mở giao diện tương tác; `codewhale exec` chạy tác vụ từ tập lệnh hoặc công việc CI.
+- **Trình duyệt cục bộ:** `codewhale web` mở [ứng dụng web cục bộ](docs/WEB.md) đi kèm, dùng cùng Runtime.
+- **Ứng dụng web và máy tính để bàn Codewhale:** các môi trường làm việc đồ họa đang được phát triển. Thông tin về khả năng sử dụng được liệt kê trên [trang sản phẩm](https://codewhale.net/en/product).
 
-## Giao diện GUI
+**Computer Use bổ sung công cụ để quan sát và tương tác với các ứng dụng khác.** Plugin này có trong mã nguồn hiện tại. Hãy xem xét quyền truy cập được yêu cầu và bật plugin trước khi sử dụng; các yêu cầu về quyền của hệ điều hành và nền tảng vẫn được áp dụng. Xem [hướng dẫn Computer Use](crates/tui/plugins/computer-use/README.md) đi kèm và [thiết lập plugin](docs/PLUGINS.md).
 
-Thích giao diện đồ họa hơn? Tiện ích CodeWhale for VS Code do cộng đồng duy trì đưa cùng một agent vào thanh bên VS Code — trò chuyện, hội thoại theo luồng, diff trực tiếp và quản lý tác vụ, đều dùng chung Runtime API, giúp phiên làm việc đồng bộ với thiết bị đầu cuối. Cài đặt từ [VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=HengQuWorld.brotherwhale-vscode); mã nguồn có trên [GitHub](https://github.com/HengQuWorld/CodeWhale-VSCode).
+Trong VS Code, tiện ích CodeWhale do cộng đồng duy trì kết nối với Runtime cục bộ từ thanh bên. Cài đặt từ [VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=HengQuWorld.brotherwhale-vscode); mã nguồn có trên [GitHub](https://github.com/HengQuWorld/CodeWhale-VSCode).
 
 ## Vì sao chọn Codewhale
 
-- **Dùng mô hình bạn muốn.** Kết nối với nhà cung cấp được lưu trữ hoặc với mô hình cục bộ thông qua Ollama, vLLM hay SGLang. Chuyển nhà cung cấp và mô hình bằng `/model`.
-- **Luôn nắm quyền kiểm soát.** Plan chỉ cho phép đọc. Ask, Auto-Review và Full Access hiển thị rõ cách hoạt động của việc phê duyệt. `/undo` hoàn tác lượt gần nhất, còn `/restore` đưa không gian làm việc về một ảnh chụp trước đó.
+- **Chọn mô hình của bạn.** Kết nối với nhà cung cấp dịch vụ hoặc với mô hình cục bộ thông qua Ollama, vLLM hay SGLang. Dùng `/provider` để đổi nhà cung cấp và `/model` để chọn mô hình.
+- **Luôn nắm quyền kiểm soát.** Kiểm tra các thao tác được đề xuất và những thay đổi tệp do chúng tạo ra. Cài đặt phê duyệt quyết định khi nào cần xem xét; Full Access vẫn tuân thủ các giới hạn chính sách bắt buộc. `/undo` và `/restore` giúp khôi phục các thay đổi trong không gian làm việc.
 - **Sắp xếp công việc dài hạn.** Lưu phiên, đặt `/goal` lâu dài, xem lại quy trình trước khi chạy và phối hợp các tác nhân mà không đưa chỉ dẫn nội bộ của chúng vào bản ghi hội thoại của bạn.
 - **Mở rộng tác nhân bạn đang có.** Kết nối máy chủ MCP và kỹ năng, cấu hình hook, đồng thời lưu vai trò tác nhân dưới dạng các tệp dễ đọc trong dự án hoặc phần cài đặt cá nhân.
 
@@ -69,10 +81,11 @@ Codewhale chạy trên máy của bạn với quyền truy cập do bạn cấp.
 - [MCP](docs/MCP.md), [hook](docs/HOOKS.md) và [cấu hình](docs/CONFIGURATION.md)
 - [Ứng dụng web cục bộ](docs/WEB.md)
 - [Toàn bộ tài liệu](docs)
+- [Cấu trúc kho mã và hướng dẫn đóng góp](CONTRIBUTING.md#project-structure)
 
 ## Tham gia cộng đồng
 
-Codewhale trở nên tốt hơn khi mọi người sử dụng, phản hồi những điểm chưa ổn và cùng khắc phục. Nếu thiếu một nhà cung cấp, quy trình còn bất tiện hoặc giao diện terminal cản trở công việc, hãy [mở issue](https://github.com/Hmbown/CodeWhale/issues). Nếu bạn biết cách cải thiện, hãy [mở pull request](CONTRIBUTING.md). Chúng tôi chào đón những đóng góp đầu tiên và người đóng góp luôn được ghi nhận cho phần việc đã được hợp nhất.
+**Chúng tôi chào đón báo cáo lỗi, ý tưởng tính năng và pull request**, dù bạn đã dùng Codewhale nhiều tháng hay mới thử lần đầu. Nếu thiếu một nhà cung cấp, quy trình còn bất tiện hoặc giao diện terminal cản trở công việc, hãy [mở issue](https://github.com/Hmbown/CodeWhale/issues/new/choose) hoặc [gửi pull request](CONTRIBUTING.md) để cùng cải thiện. Chúng tôi chào đón những đóng góp đầu tiên và luôn ghi nhận người đóng góp cho phần việc đã được hợp nhất.
 
 Tham gia [Discord](https://discord.gg/37gfS3ksug), hoặc thêm Hunter trên WeChat (`hunterbown`) và đề nghị tham gia nhóm Whale Brothers.
 
diff --git a/README.zh-CN.md b/README.zh-CN.md
index 8d65c24c77..7807d2c070 100644
--- a/README.zh-CN.md
+++ b/README.zh-CN.md
@@ -1,9 +1,11 @@
-
+
 # Codewhale
 
-Codewhale 是一款面向终端的开源编程智能体,使用 Rust 构建,并与用户一起在公开协作中不断改进。
+Codewhale 是一款开源智能体,可使用你选择的托管模型或本地模型读取项目、编辑文件、运行命令并检查自己的工作。从终端中的一项任务开始。对于较大的工作,可以将其中的部分任务交给使用不同模型、承担不同角色的智能体。
 
-![Codewhale 在终端中运行](assets/screenshot.webp)
+![Codewhale 在终端中运行](web/public/codewhale-tui-171acee.png)
+
+*终端预览截图来自 v0.9.12 的开发构建。*
 
 [English](README.md) · [日本語](README.ja-JP.md) · [Tiếng Việt](README.vi.md) · [Bahasa Indonesia](README.id.md) · [한국어](README.ko-KR.md) · [Español](README.es-419.md) · [Português](README.pt-BR.md) · [Русский](README.ru.md) · [Українська](README.uk.md) · [Français](README.fr.md) · [Deutsch](README.de.md) · [繁體中文](README.zh-TW.md) · [हिन्दी](README.hi.md) · [Türkçe](README.tr.md) · [Italiano](README.it.md) · [Polski](README.pl.md) · [العربية](README.ar.md) · [Català](README.ca.md)
 
@@ -21,18 +23,20 @@ curl -fsSL https://codewhale.net/install.sh | sh
 "$HOME/.local/bin/codewhale"
 ```
 
+安装器会选择最新的已发布版本。[更新日志](CHANGELOG.md)也描述了下一版本尚未发布的候选构建;只有在该版本正式发布后,已发布的下载包才会包含这些变更。
+
 Windows 请使用 [GitHub Releases](https://github.com/Hmbown/CodeWhale/releases/latest)
 中的安装器或压缩包。已有的直接安装使用 `codewhale update`;它会显示当前可执行文件路径,
 并保留比已发布版本更新的构建。npm 和 Cargo 是次要打包选项。
 迁移与 PATH 排查见[安装指南](docs/zh_hans/INSTALL.md)。
 
-首次运行会帮助你连接提供商,也可以选择保持离线。Codewhale 还支持 Cargo、Docker、Nix、Scoop、预构建压缩包、Android/Termux 和 CNB 镜像。请参阅[安装指南](docs/INSTALL.md)。
+首次运行会帮助你连接提供商,也可以离线配置 Codewhale。要获得模型回复,必须连接托管模型或本地模型。Codewhale 还支持 npm 和 Cargo 作为次要打包方式,以及 Docker、Nix、Scoop、Android/Termux 和可选的 CNB 镜像。对于现有的软件包管理器安装,系统会提供迁移说明。请参阅[安装与 PATH 帮助](docs/INSTALL.md)。
 
 每种 shell 只需一条命令即可启用 Tab 补全——`codewhale completion bash|zsh|fish|powershell|elvish`。请参阅 [shell 补全](docs/INSTALL.md#8-shell-completions)。
 
 ## 使用
 
-像与队友交流一样向 Codewhale 描述任务:
+在项目文件夹中打开终端并运行 `codewhale`。使用 `/provider` 选择提供商,使用 `/model` 选择模型,然后描述一项具体任务:
 
 ```text
 Fix the failing tests and explain what changed.
@@ -44,16 +48,24 @@ Fix the failing tests and explain what changed.
 codewhale exec "fix the failing tests and explain what changed"
 ```
 
-Codewhale 可以读取你的代码仓库、编辑文件、运行命令、检查结果,并持续推进目标。由你决定授予它多少访问权限。
+Codewhale 可以读取你的代码仓库、编辑文件、运行命令、检查结果,并持续推进目标。使用 `/mode plan` 可以在不修改文件、不执行 shell 命令的情况下进行探索;希望它实施修改时,使用 `/mode work`。按 `Shift+Tab` 可选择 Ask、Auto-Review 或 Full Access;[模式与权限指南](docs/MODES.md)说明了各自允许的操作。
+
+## 终端、应用与 Computer Use
+
+终端和图形客户端连接到 Codewhale Runtime,由它运行智能体及其工具:
+
+- **终端:** `codewhale` 打开交互界面;`codewhale exec` 可从脚本或 CI 作业中运行任务。
+- **本地浏览器:** `codewhale web` 打开随附的[本地 Web 客户端](docs/WEB.md),使用同一个 Runtime。
+- **Codewhale Web 和桌面应用:** 仍在开发中的图形工作台。其可用情况见[产品页面](https://codewhale.net/en/product)。
 
-## GUI 前端
+**Computer Use 提供观察其他应用并与之交互的工具。** 当前源码已包含此插件。使用前请查看它请求的访问权限并启用它;仍须满足操作系统权限和平台要求。请参阅随附的 [Computer Use 指南](crates/tui/plugins/computer-use/README.md)和[插件设置](docs/PLUGINS.md)。
 
-更喜欢图形界面?社区维护的 CodeWhale for VS Code 扩展把同一个智能体放进 VS Code 侧边栏——聊天、线程会话、实时 diff 与任务管理,全部基于同一个 Runtime API,会话与终端保持同步。可从 [VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=HengQuWorld.brotherwhale-vscode) 安装;源码见 [GitHub](https://github.com/HengQuWorld/CodeWhale-VSCode)。
+在 VS Code 中,社区维护的 CodeWhale 扩展通过侧边栏连接本地 Runtime。可从 [VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=HengQuWorld.brotherwhale-vscode) 安装;源码见 [GitHub](https://github.com/HengQuWorld/CodeWhale-VSCode)。
 
 ## 为什么选择 Codewhale
 
-- **使用你想要的模型。** 连接托管提供商,或通过 Ollama、vLLM、SGLang 使用本地模型。使用 `/model` 切换提供商和模型。
-- **掌控始终在你手中。** Plan 模式为只读。Ask、Auto-Review 和 Full Access 会清晰展示审批行为。`/undo` 可撤销上一轮操作,`/restore` 可将工作区恢复到较早的快照。
+- **选择你的模型。** 连接托管提供商,或通过 Ollama、vLLM、SGLang 使用本地模型。使用 `/provider` 切换提供商,使用 `/model` 选择模型。
+- **掌控始终在你手中。** 检查拟执行的操作及其造成的文件变更。审批设置决定何时需要审查;Full Access 仍须遵守不可逾越的策略边界。`/undo` 和 `/restore` 可帮助恢复工作区变更。
 - **让长时间任务井然有序。** 保存会话、设置持久的 `/goal`、在工作流运行前进行审查,并协调多个智能体,同时不让其内部指令混入你的对话记录。
 - **扩展你已有的智能体。** 连接 MCP 服务器和技能、配置钩子,并将智能体角色作为可读文件保存在项目或个人设置中。
 
@@ -72,10 +84,11 @@ Codewhale 在你的机器上运行,并仅拥有你授予的访问权限。审
 - [MCP](docs/MCP.md)、[钩子](docs/HOOKS.md)和[配置](docs/CONFIGURATION.md)
 - [本地 Web 客户端](docs/WEB.md)
 - [全部文档](docs)
+- [仓库结构与贡献指南](CONTRIBUTING.md#project-structure)
 
 ## 加入社区
 
-当人们使用 Codewhale、反馈不顺手之处并帮助修复问题时,它就会变得更好。如果缺少某个提供商、工作流体验不佳,或终端界面妨碍了你,请[提交 issue](https://github.com/Hmbown/CodeWhale/issues)。如果你知道如何改进,请[提交 pull request](CONTRIBUTING.md)。我们欢迎首次贡献,贡献者也会保留已合入工作的署名。
+**欢迎提交错误报告、功能建议和 pull request**,无论你已使用 Codewhale 数月,还是刚刚开始尝试。如果缺少某个提供商、工作流体验不佳,或终端界面妨碍了你,请[提交 issue](https://github.com/Hmbown/CodeWhale/issues/new/choose) 或[提交 pull request](CONTRIBUTING.md),一起改进。我们欢迎首次贡献,贡献者也会保留已合入工作的署名。
 
 加入 [Discord](https://discord.gg/37gfS3ksug),或在微信添加 Hunter(`hunterbown`)并申请加入 Whale Brothers 群。
 
diff --git a/README.zh-TW.md b/README.zh-TW.md
index 1bd86cde02..294f1f2a5e 100644
--- a/README.zh-TW.md
+++ b/README.zh-TW.md
@@ -1,9 +1,11 @@
-
+
 # Codewhale
 
-Codewhale 是一款在終端機中使用的開源程式設計代理,以 Rust 打造,並與使用者一起透過公開協作持續改進。
+Codewhale 是一款開源代理,可使用你選擇的託管模型或本機模型讀取專案、編輯檔案、執行指令,並檢查自己的工作。從終端機中的一項任務開始。對於較大的工作,可以將部分任務交給使用不同模型、擔任不同角色的代理。
 
-![Codewhale 在終端機中執行](assets/screenshot.webp)
+![Codewhale 在終端機中執行](web/public/codewhale-tui-171acee.png)
+
+*終端機預覽截圖來自 v0.9.12 的開發建置版本。*
 
 [English](README.md) · [简体中文](README.zh-CN.md) · [日本語](README.ja-JP.md) · [Tiếng Việt](README.vi.md) · [Bahasa Indonesia](README.id.md) · [한국어](README.ko-KR.md) · [Español](README.es-419.md) · [Português](README.pt-BR.md) · [Русский](README.ru.md) · [Українська](README.uk.md) · [Français](README.fr.md) · [Deutsch](README.de.md) · [हिन्दी](README.hi.md) · [Türkçe](README.tr.md) · [Italiano](README.it.md) · [Polski](README.pl.md) · [العربية](README.ar.md) · [Català](README.ca.md)
 
@@ -21,15 +23,17 @@ curl -fsSL https://codewhale.net/install.sh | sh
 "$HOME/.local/bin/codewhale"
 ```
 
+安裝程式會選擇最新的已發布版本。[更新日誌](CHANGELOG.md)也描述了下一版本尚未發布的候選建置;只有在該版本正式發布後,已發布的下載檔才會包含這些變更。
+
 Windows 請從 [GitHub Releases](https://github.com/Hmbown/CodeWhale/releases/latest) 下載對應的安裝程式或封存檔。已有的直接安裝使用 `codewhale update`;若只想檢查,使用 `codewhale update --check`。更新器會顯示執行檔路徑,並保留較新的建置版本。npm 和 Cargo 是次要套件安裝方式;套件管理器安裝的遷移與 PATH 設定請參閱[安裝指南](docs/INSTALL.md)。
 
-第一次執行時,系統會協助你連線至供應商,也可以選擇保持離線。Codewhale 亦支援 Cargo、Docker、Nix、Scoop、預先建置的封存檔、Android/Termux 與 CNB 映像。請參閱[安裝指南](docs/INSTALL.md)。
+第一次執行時,系統會協助你連線至供應商,也可以離線設定 Codewhale。要取得模型回覆,必須連線至託管模型或本機模型。Codewhale 也支援 npm 和 Cargo 作為次要套件安裝方式,以及 Docker、Nix、Scoop、Android/Termux 與選用的 CNB 鏡像。對於既有的套件管理器安裝,系統會提供遷移說明。請參閱[安裝與 PATH 說明](docs/INSTALL.md)。
 
 每種 shell 只需一個指令即可啟用 Tab 自動完成——`codewhale completion bash|zsh|fish|powershell|elvish`。請參閱 [shell 自動完成](docs/INSTALL.md#8-shell-completions)。
 
 ## 使用
 
-像和隊友交談一樣告訴 Codewhale 你的需求:
+在專案資料夾中開啟終端機並執行 `codewhale`。使用 `/provider` 選擇供應商,使用 `/model` 選擇模型,接著描述一項具體任務:
 
 ```text
 Fix the failing tests and explain what changed.
@@ -41,16 +45,24 @@ Fix the failing tests and explain what changed.
 codewhale exec "fix the failing tests and explain what changed"
 ```
 
-Codewhale 可以讀取你的程式碼儲存庫、編輯檔案、執行指令、檢查結果,並持續朝目標推進。你可以決定要授予它多少存取權限。
+Codewhale 可以讀取你的程式碼儲存庫、編輯檔案、執行指令、檢查結果,並持續朝目標推進。使用 `/mode plan` 可以在不修改檔案、不執行 shell 指令的情況下探索;希望它進行修改時,使用 `/mode work`。按 `Shift+Tab` 可選擇 Ask、Auto-Review 或 Full Access;[模式與權限指南](docs/MODES.md)說明了各自允許的操作。
+
+## 終端機、應用程式與 Computer Use
+
+終端機和圖形用戶端連線至 Codewhale Runtime,由它執行代理及其工具:
+
+- **終端機:** `codewhale` 開啟互動介面;`codewhale exec` 可從指令碼或 CI 工作中執行任務。
+- **本機瀏覽器:** `codewhale web` 開啟隨附的[本機網頁用戶端](docs/WEB.md),使用同一個 Runtime。
+- **Codewhale 網頁與桌面應用程式:** 仍在開發中的圖形工作台。其可用情況見[產品頁面](https://codewhale.net/en/product)。
 
-## GUI 前端
+**Computer Use 提供觀察其他應用程式並與之互動的工具。** 目前的原始碼已包含此外掛程式。使用前請檢視它要求的存取權限並啟用它;仍須符合作業系統權限與平台要求。請參閱隨附的 [Computer Use 指南](crates/tui/plugins/computer-use/README.md)與[外掛程式設定](docs/PLUGINS.md)。
 
-偏好圖形介面?社群維護的 CodeWhale for VS Code 擴充功能把同一個智慧體放進 VS Code 側邊欄——聊天、執行緒對話、即時 diff 與任務管理,全部基於同一個 Runtime API,並與終端保持同步。可從 [VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=HengQuWorld.brotherwhale-vscode) 安裝;原始碼見 [GitHub](https://github.com/HengQuWorld/CodeWhale-VSCode)。
+在 VS Code 中,社群維護的 CodeWhale 擴充功能透過側邊欄連線至本機 Runtime。可從 [VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=HengQuWorld.brotherwhale-vscode) 安裝;原始碼見 [GitHub](https://github.com/HengQuWorld/CodeWhale-VSCode)。
 
 ## 為何選擇 Codewhale
 
-- **使用你想要的模型。** 連線至託管供應商,或透過 Ollama、vLLM、SGLang 使用本機模型。使用 `/model` 切換供應商與模型。
-- **掌控權始終在你手中。** Plan 模式為唯讀。Ask、Auto-Review 與 Full Access 會清楚呈現核准行為。`/undo` 可復原上一輪操作,`/restore` 可將工作區還原至較早的快照。
+- **選擇你的模型。** 連線至託管供應商,或透過 Ollama、vLLM、SGLang 使用本機模型。使用 `/provider` 切換供應商,使用 `/model` 選擇模型。
+- **掌控權始終在你手中。** 檢查擬執行的操作及其造成的檔案變更。核准設定決定何時需要審查;Full Access 仍須遵守不可逾越的政策邊界。`/undo` 和 `/restore` 可協助復原工作區變更。
 - **讓長時間工作井然有序。** 儲存工作階段、設定持久的 `/goal`、在工作流程執行前加以審查,並協調多個代理,同時避免其內部指示混入你的對話記錄。
 - **擴充你已有的代理。** 連接 MCP 伺服器與技能、設定掛鉤,並將代理角色以可讀檔案保存在專案或個人設定中。
 
@@ -69,10 +81,11 @@ Codewhale 在你的電腦上執行,且只擁有你授予的存取權限。核
 - [MCP](docs/MCP.md)、[掛鉤](docs/HOOKS.md)與[設定](docs/CONFIGURATION.md)
 - [本機網頁用戶端](docs/WEB.md)
 - [所有文件](docs)
+- [儲存庫結構與貢獻指南](CONTRIBUTING.md#project-structure)
 
 ## 加入社群
 
-當人們使用 Codewhale、回報不順手之處並協助修正問題時,它就會變得更好。如果缺少某個供應商、工作流程操作不便,或終端機介面妨礙了你,請[提出 issue](https://github.com/Hmbown/CodeWhale/issues)。如果你知道如何改善,請[提出 pull request](CONTRIBUTING.md)。我們歡迎首次貢獻,貢獻者也會保留已合併工作的署名。
+**歡迎回報錯誤、提出功能建議及提交 pull request**,無論你已使用 Codewhale 數月,還是第一次嘗試。如果缺少某個供應商、工作流程操作不便,或終端機介面妨礙了你,請[提出 issue](https://github.com/Hmbown/CodeWhale/issues/new/choose) 或[提交 pull request](CONTRIBUTING.md),一起改善。我們歡迎首次貢獻,貢獻者也會保留已合併工作的署名。
 
 加入 [Discord](https://discord.gg/37gfS3ksug),或在微信加入 Hunter(`hunterbown`)並申請加入 Whale Brothers 群組。
 
diff --git a/assets/fanout.gif b/assets/fanout.gif
deleted file mode 100644
index c0a0c47702..0000000000
Binary files a/assets/fanout.gif and /dev/null differ
diff --git a/assets/screenshot.webp b/assets/screenshot.webp
deleted file mode 100644
index f0ef4c5f7e..0000000000
Binary files a/assets/screenshot.webp and /dev/null differ
diff --git a/computer/snapshots/cloud-agent/Dockerfile b/computer/snapshots/cloud-agent/Dockerfile
index 735223f425..28c6ce4c90 100644
--- a/computer/snapshots/cloud-agent/Dockerfile
+++ b/computer/snapshots/cloud-agent/Dockerfile
@@ -18,13 +18,11 @@
 #   daytona snapshot create codewhale-cloud-agent \
 #     -f Dockerfile --cpu 4 --memory 8 --disk 10   (plan max; resources bind to the snapshot)
 #
-# Current dispatcher boundary: this is an image definition, not a wired cloud
-# execution path. `crates/tui/src/cloud_dispatch.rs` currently creates a
-# Daytona sandbox with a name and labels only; it does not select this snapshot,
-# clone a repository, inject any sandbox environment, or execute `codewhale`.
-# A manual image build or Daytona probe is therefore not Cloud Agent launch
-# evidence. Do not add a create-time provider-key shortcut while that product
-# wiring is built.
+# Dispatcher source selects this snapshot name, sends account identity, and
+# uses the toolbox to clone and execute commands. That wiring does not prove
+# provider-credential resolution or end-to-end Cloud Agent acceptance. See
+# README.md beside this Dockerfile for the source and image evidence limits.
+# Never substitute a create-time provider-key shortcut for that missing bridge.
 
 FROM debian:bookworm-slim
 
diff --git a/computer/snapshots/cloud-agent/README.md b/computer/snapshots/cloud-agent/README.md
index ec6c8bac99..34aabadbce 100644
--- a/computer/snapshots/cloud-agent/README.md
+++ b/computer/snapshots/cloud-agent/README.md
@@ -23,7 +23,8 @@ digest-pinned Linux binary; nothing else in the image runs agent logic.
   `CODEWHALE_HOME=/home/agent/.codewhale`. `/work` and `/workspace` exist and
   are owned by `agent`.
 - Entrypoint: `sleep infinity` (Daytona injects its own toolbox daemon).
-- No provider credentials are baked in or supplied at sandbox create time.
+- No provider credentials are baked in. Provider credentials must not be
+  supplied at sandbox create time.
   Daytona create-time environment is server-visible, so a provider secret
   must never appear in `daytona create -e …` or an SDK `envVars` payload.
 
@@ -31,20 +32,23 @@ The pins are recorded as OCI labels (`org.opencontainers.image.revision`,
 `net.codewhale.binary.sha256`, ...) so a running Computer can be audited
 against the release it claims to run.
 
-## Current dispatcher state — not a runtime contract
-
-The current product dispatcher wiring for this snapshot is absent.
-`crates/tui/src/cloud_dispatch.rs` creates a Daytona sandbox with a generated
-name and labels, then records its ID. It does **not** select
-`codewhale-cloud-agent`, clone into `/workspace`, inject sandbox environment,
-call the Daytona toolbox execution endpoint, or run `codewhale`. It also does
-not contain a server-side account-token-to-provider-credential resolution path.
-
-This directory is consequently an image definition and a bounded manual
-inspection aid, not an end-to-end Cloud Agent implementation. A snapshot build,
-manual `daytona create`, or manual `codewhale exec` proves only the specific
-image/operator step observed; none is launch proof for dispatcher, entitlement,
-credential custody, Engine execution, lifecycle, metering, or customer use.
+## Dispatcher wiring and acceptance limits
+
+The image definition lives only in this directory. The launcher in
+`crates/tui/src/cloud_dispatch.rs` selects `codewhale-cloud-agent` (or
+`CODEWHALE_DISPATCH_SNAPSHOT`), sends the account machine token in
+`CODEWHALE_API_KEY`, applies job labels, and uses the Daytona toolbox to clone
+and execute commands. `crates/tui/src/dispatch_runner.rs` drives that lifecycle.
+See the [dispatch guide](../../../docs/DAYTONA_CLOUD_DISPATCH.md) for the
+command surface.
+
+This describes source wiring. The image remains pinned to the Engine version
+listed above, and this repository does not establish a server-side
+account-token-to-provider-credential resolution path. A snapshot build or
+manual `codewhale exec` is evidence for that specific operation; it does not
+qualify account entitlement, provider credential custody, dispatcher execution,
+metering or customer use. The historical manual image receipt below is not
+acceptance of the current dispatcher.
 
 ## Provider credentials inside the Computer
 
diff --git a/config.example.toml b/config.example.toml
index 2647e233e2..1ea3d84a39 100644
--- a/config.example.toml
+++ b/config.example.toml
@@ -107,6 +107,15 @@ reasoning_effort = "max"
 #   pin_last_prompt = true
 #   show_tool_details = false
 
+# Signed catalog overlays are optional and inactive without approved trust keys.
+# Explicit route/model selections and provider-owned rosters keep priority.
+# CODEWHALE_DISABLE_CLOUD_FACTS=1 disables cache, local files and network too.
+[cloud_facts]
+enabled = false
+channel = "stable"
+ttl_hours = 6
+# url = "https://codewhale.net/api/facts/v1/{channel}"
+
 # ─────────────────────────────────────────────────────────────────────────────────
 # Startup update check
 # ─────────────────────────────────────────────────────────────────────────────────
@@ -206,6 +215,33 @@ memory_path = "~/.codewhale/memory.md"
 # max_reprompts = 2
 # reprompt_message = "So, what's up ? Keep running !"
 
+# ───────────────────────────────────────────────────────────────────────────
+# Model-bound key redaction ([redaction])
+# ───────────────────────────────────────────────────────────────────────────
+# Codewhale masks credential-looking values in tool output before it reaches
+# the model (the "model boundary"), so a file that contains a configured API
+# key, a bare provider token, or a credential-shaped opaque string never leaks
+# those bytes to the model. Leave this enabled unless the model must read and
+# edit files that contain real credentials.
+#
+# Disabling is a security decision, so it is never a plain flag:
+#   * Set model_bound = "disabled" here, restart Codewhale, and the startup
+#     gate asks twice - a first confirmation, then a red "are you really
+#     sure?" stage. Only the second confirmation takes effect, and it applies
+#     on later launches while model_bound stays "disabled".
+#   * Going back to "enabled" - or rewriting config.toml after the
+#     confirmation - invalidates it: requesting "disabled" again always
+#     asks for a fresh confirmation.
+#   * Until a confirmation exists - including in non-interactive/headless
+#     runs, which never confirm anything - masking stays on regardless of
+#     this key. Choosing "keep masking on" on the gate leaves the key
+#     untouched, so the next launch asks again.
+#   * The value is forgiving: false/"off" mean "disabled"; true/"on" mean
+#     "enabled".
+# [redaction]
+# model_bound = "enabled"    # mask keys before they reach the model (default)
+# model_bound = "disabled"   # request the opt-out (restart + confirm required)
+
 # Native tool catalog controls (#2076). By default only the core tool surface
 # is loaded into the model context; less common native tools are discoverable
 # through ToolSearch and loaded on first use.
@@ -218,6 +254,10 @@ memory_path = "~/.codewhale/memory.md"
 # supported ranges (1..=10 and 2..=10) with a warning.
 # user_input_max_questions = 6
 # user_input_max_options = 4
+#
+# Seconds a question or an approval decision waits before it cancels (#6003).
+# 0 disables the timeout entirely; values above 86400 (24h) are clamped.
+# user_input_timeout_seconds = 300
 
 # ─────────────────────────────────────────────────────────────────────────────────
 # Product telemetry — opt-in, off by default
@@ -424,26 +464,6 @@ sandbox_mode = "workspace-write" # read-only | workspace-write | danger-full-acc
 # The backend uses a 30-second HTTP timeout. Background, interactive, and
 # TTY modes are not supported with external backends — all commands run
 # synchronously via HTTP.
-#
-# ShannonNet backend: each shell command becomes a signed capability
-# invocation on a ShannonNet worker (which may run on another tailnet node).
-# At session start Codewhale resolves its durable `codewhale` Agent, creates
-# a Task World named after the workspace, and attaches the capability; every
-# command then leaves a signed receipt (`shannon trace`). Requires the
-# `shannon` CLI on PATH (or `$SHANNON`) and an admitted provider for the
-# capability (`shannon cap advertise`).
-#
-#   sandbox_backend = "shannon"
-#   sandbox_shannon_home = "~/.shannon"           # default: $SHANNON_HOME or ~/.shannon
-#   sandbox_shannon_capability = "cap://sandbox/exec"  # default
-#   sandbox_shannon_sync = true                   # default: ship the workspace's
-#       # non-ignored files into the worker's per-World session container before
-#       # each command (a full archive first, then only changes), so remote builds
-#       # and tests run on the files just edited here. false runs each command in
-#       # a throwaway container against the worker's own read-only checkout.
-#
-# Env-var overrides: CODEWHALE_SANDBOX_SHANNON_HOME, CODEWHALE_SANDBOX_SHANNON_CAPABILITY,
-# CODEWHALE_SANDBOX_SHANNON_SYNC.
 # ─────────────────────────────────────────────────────────────────────────────────
 # Bubblewrap (Linux only, additional filesystem isolation)
 # ─────────────────────────────────────────────────────────────────────────────────
@@ -613,6 +633,7 @@ max_subagents = 10 # optional (default 64, clamped to 1-128)
 
 # OpenRouter — multi-provider gateway (https://openrouter.ai)
 [providers.openrouter]
+# vendor = "deepinfra" # exact upstream slug; disables OpenRouter fallbacks
 # api_key = "YOUR_OPENROUTER_API_KEY"
 # base_url = "https://openrouter.ai/api/v1"
 # model = "deepseek/deepseek-v4-pro"
@@ -1104,11 +1125,10 @@ alternate_screen = "auto"   # auto/always start on the alternate screen; never s
 mouse_capture = true        # true copies only transcript user/assistant text; false uses raw terminal selection/copy
 terminal_probe_timeout_ms = 500 # optional startup terminal-mode timeout (100-5000ms)
 stream_chunk_timeout_secs = 900 # optional SSE idle timeout per chunk (0 = default, 1-3600)
-# R1 turn budgets. An agent loop with no finite bound can spend real money
-# forever, so every one of these is finite by default and finite at its
-# maximum. `0` is an invalid value that falls back to the default — it is
-# never a "0 means unlimited" sentinel.
-max_model_steps = 200        # model steps one turn may take (0 = default, 1-100000)
+# Model steps are uncapped by default. Uncomment to install an explicit
+# ceiling; omission or 0 means no step limit, positive values clamp to 1-100000.
+# max_model_steps = 1000
+# Wall-clock and stream budgets remain finite; 0 selects their default.
 turn_wall_clock_secs = 3600  # cumulative wall clock for one turn, excluding time
                              # blocked on a human approval (0 = default, 30-86400)
 stream_max_content_mb = 10   # per-step cap on streamed content (0 = default, 1-512)
@@ -1126,6 +1146,17 @@ osc8_links = true            # emit OSC 8 escapes around URLs (Cmd+click in iTer
 # git_branch, last_tool_elapsed, rate_limit — they drove nothing. Old files
 # keep loading; the retired keys are ignored.
 # status_items = ["mode", "model", "context_percent", "cost", "tokens"]
+# Size presets for the two rows themselves (#5950) — composition stays in
+# status_items; these only decide how much of a row paints:
+# posture_bar = "full"          # full | compact | hidden (default full)
+#                               # compact keeps the posture chips (and the cap
+#                               # warning) and drops the clocks, counts and hint;
+#                               # hidden gives the row to the transcript.
+# metrics_line = "full"         # full | compact | hidden (default full)
+#                               # compact keeps the route, context reading, cost
+#                               # and balance and drops the telemetry and the
+#                               # help hint; hidden gives the row to the transcript.
+#                               # Also settable at runtime: /config posture_bar compact
 # notification_condition = "unfocused" # unfocused | always | never
 #                                    "unfocused" = notify only after this terminal has been
 #                                    in the background for two seconds (default);
@@ -1195,6 +1226,26 @@ initial_delay = 1.0
 max_delay = 60.0
 exponential_base = 2.0
 
+# ─────────────────────────────────────────────────────────────────────────────────
+# Goal loop (`[goal]`) — operate-mode persistent goals
+# ─────────────────────────────────────────────────────────────────────────────────
+# Operate-mode goals run to their completion gate with no default token, time,
+# or continuation ceiling. Token/time budgets, when supplied, are telemetry
+# only and do not stop a goal. The keys below are the opt-in circuit breakers.
+# [goal]
+# Optional safety backstop on automatic goal continuation passes.
+# Default: 0 (unlimited). Set a positive value to opt into a ceiling.
+# max_continuations = 100
+# Optional cancellable quiet period between successful turns, useful for
+# coordinator goals that poll on a cadence instead of keeping one provider
+# turn open. Default: 0 (continue immediately). Cap: 86400 (24h).
+# continuation_delay_seconds = 300
+# Per-turn step allowance while a goal is active (#5994): larger but still
+# finite. Default: 1000 (0/absent resolves to 1000). Range: 1..=100,000.
+# Bounds each turn, never the number of continuation passes; explicit
+# per-invocation ceilings (exec --max-turns, worker caps) always win.
+# max_steps = 1000
+
 # ─────────────────────────────────────────────────────────────────────────────────
 # Context Compaction
 # ─────────────────────────────────────────────────────────────────────────────────
diff --git a/crates/agent/src/lib.rs b/crates/agent/src/lib.rs
index 51e6accfe5..c7e429310e 100644
--- a/crates/agent/src/lib.rs
+++ b/crates/agent/src/lib.rs
@@ -149,7 +149,7 @@ pub struct ModelRegistry {
 /// Creates a registry pre-populated with all built-in models and their aliases.
 impl Default for ModelRegistry {
     fn default() -> Self {
-        let models = vec![
+        let mut models = vec![
             ModelInfo {
                 id: "deepseek-v4-pro".to_string(),
                 provider: ProviderKind::Deepseek,
@@ -1188,82 +1188,6 @@ impl Default for ModelRegistry {
                 supports_tools: true,
                 supports_reasoning: true,
             },
-            // OpenCode Go Chat Completions models (https://opencode.ai/docs/go/).
-            // Go models documented only on `/messages` are intentionally not
-            // advertised by this OpenAI-compatible provider slice.
-            ModelInfo {
-                id: "deepseek-v4-pro".to_string(),
-                provider: ProviderKind::OpencodeGo,
-                aliases: vec!["opencode-go/deepseek-v4-pro".to_string()],
-                supports_tools: true,
-                supports_reasoning: true,
-            },
-            ModelInfo {
-                id: "grok-4.5".to_string(),
-                provider: ProviderKind::OpencodeGo,
-                aliases: vec!["opencode-go/grok-4.5".to_string()],
-                supports_tools: true,
-                supports_reasoning: true,
-            },
-            // No glm-5.3 row (2026-08-03): OpenCode Go publishes no glm-5.3
-            // model. The Z.ai/OpenRouter glm-5.3 rows inherit glm-5.2 metadata;
-            // that inheritance is not evidence this gateway serves it.
-            ModelInfo {
-                id: "glm-5.2".to_string(),
-                provider: ProviderKind::OpencodeGo,
-                aliases: vec!["opencode-go/glm-5.2".to_string()],
-                supports_tools: true,
-                supports_reasoning: true,
-            },
-            ModelInfo {
-                id: "glm-5.1".to_string(),
-                provider: ProviderKind::OpencodeGo,
-                aliases: vec!["opencode-go/glm-5.1".to_string()],
-                supports_tools: true,
-                supports_reasoning: true,
-            },
-            ModelInfo {
-                id: "kimi-k3".to_string(),
-                provider: ProviderKind::OpencodeGo,
-                aliases: vec!["opencode-go/kimi-k3".to_string()],
-                supports_tools: true,
-                supports_reasoning: true,
-            },
-            ModelInfo {
-                id: "kimi-k2.7-code".to_string(),
-                provider: ProviderKind::OpencodeGo,
-                aliases: vec!["opencode-go/kimi-k2.7-code".to_string()],
-                supports_tools: true,
-                supports_reasoning: true,
-            },
-            ModelInfo {
-                id: "kimi-k2.6".to_string(),
-                provider: ProviderKind::OpencodeGo,
-                aliases: vec!["opencode-go/kimi-k2.6".to_string()],
-                supports_tools: true,
-                supports_reasoning: true,
-            },
-            ModelInfo {
-                id: "deepseek-v4-flash".to_string(),
-                provider: ProviderKind::OpencodeGo,
-                aliases: vec!["opencode-go/deepseek-v4-flash".to_string()],
-                supports_tools: true,
-                supports_reasoning: true,
-            },
-            ModelInfo {
-                id: "mimo-v2.5".to_string(),
-                provider: ProviderKind::OpencodeGo,
-                aliases: vec!["opencode-go/mimo-v2.5".to_string()],
-                supports_tools: true,
-                supports_reasoning: true,
-            },
-            ModelInfo {
-                id: "mimo-v2.5-pro".to_string(),
-                provider: ProviderKind::OpencodeGo,
-                aliases: vec!["opencode-go/mimo-v2.5-pro".to_string()],
-                supports_tools: true,
-                supports_reasoning: true,
-            },
             // Meta Model API / Muse Spark. Keep these in step with
             // `DEFAULT_META_MODEL` in config's provider_defaults and with the
             // bundled models.dev catalog: this registry resolves the `muse`
@@ -1437,6 +1361,33 @@ impl Default for ModelRegistry {
                 supports_reasoning: false,
             },
         ];
+        // The shared Chat roster owns Go compatibility; Messages and Responses
+        // models must not enter this provider's registry through a second list.
+        models.extend(codewhale_config::OPENCODE_GO_CHAT_MODELS.iter().map(|&id| {
+            // Preserve the existing reviewed flags. Roster membership alone
+            // proves neither capability; false withholds a positive assertion
+            // for new models because ModelInfo cannot express unknown.
+            let reviewed_capabilities = matches!(
+                id,
+                "deepseek-v4-pro"
+                    | "grok-4.5"
+                    | "glm-5.2"
+                    | "glm-5.1"
+                    | "kimi-k3"
+                    | "kimi-k2.7-code"
+                    | "kimi-k2.6"
+                    | "deepseek-v4-flash"
+                    | "mimo-v2.5"
+                    | "mimo-v2.5-pro"
+            );
+            ModelInfo {
+                id: id.to_string(),
+                provider: ProviderKind::OpencodeGo,
+                aliases: vec![format!("opencode-go/{id}")],
+                supports_tools: reviewed_capabilities,
+                supports_reasoning: reviewed_capabilities,
+            }
+        }));
         Self::new(models)
     }
 }
@@ -1522,9 +1473,9 @@ impl ModelRegistry {
                     fallback_chain,
                 });
             }
-            // OpenCode Go's catalog spans Chat Completions and Anthropic
-            // Messages, while Codewhale's provider slice intentionally speaks
-            // Chat only. Resolve a hinted Go model through the shared Chat
+            // OpenCode Go's catalog spans Chat Completions, Messages, and
+            // Responses, while this provider slice intentionally speaks Chat
+            // only. Resolve a hinted Go model through the shared Chat
             // allowlist and never fall through to a same-named global alias on
             // OpenRouter or MiniMax.
             if provider_hint == Some(ProviderKind::OpencodeGo)
@@ -2391,51 +2342,77 @@ mod tests {
             .map(|model| model.id.as_str())
             .collect();
 
+        // Literal expectations independently catch an incomplete shared roster
+        // and prevent new compatibility entries from claiming capabilities.
+        let expected = [
+            ("deepseek-v4-pro", true),
+            ("grok-4.5", true),
+            ("glm-5.2", true),
+            ("glm-5.1", true),
+            ("kimi-k3", true),
+            ("kimi-k2.7-code", true),
+            ("kimi-k2.6", true),
+            ("deepseek-v4-flash", true),
+            ("mimo-v2.5", true),
+            ("mimo-v2.5-pro", true),
+            ("glm-5.3-flash", false),
+            ("glm-5.3", false),
+            ("longcat-2.0", false),
+            ("deepseek-v4-flash-vision-exp", false),
+            ("hy4-preview", false),
+            ("hy3", false),
+            ("omen-alpha", false),
+        ];
         assert_eq!(
             models,
-            vec![
-                "deepseek-v4-pro",
-                "grok-4.5",
-                "glm-5.2",
-                "glm-5.1",
-                "kimi-k3",
-                "kimi-k2.7-code",
-                "kimi-k2.6",
-                "deepseek-v4-flash",
-                "mimo-v2.5",
-                "mimo-v2.5-pro",
-            ]
+            expected.iter().map(|(id, _)| *id).collect::>()
         );
 
         let default = registry.resolve_ok(None, Some(ProviderKind::OpencodeGo));
         assert_eq!(default.resolved.provider, ProviderKind::OpencodeGo);
         assert_eq!(default.resolved.id, "deepseek-v4-pro");
 
-        for model in ["grok-4.5", "kimi-k3"] {
+        for (model, expected_capabilities) in expected {
             for requested in [model.to_string(), format!("opencode-go/{model}")] {
                 let resolved =
                     registry.resolve_ok(Some(&requested), Some(ProviderKind::OpencodeGo));
                 assert_eq!(resolved.resolved.provider, ProviderKind::OpencodeGo);
                 assert_eq!(resolved.resolved.id, model);
                 assert!(!resolved.used_fallback);
+                assert_eq!(
+                    resolved.resolved.aliases,
+                    vec![format!("opencode-go/{model}")],
+                    "{requested}"
+                );
+                assert_eq!(
+                    resolved.resolved.supports_tools, expected_capabilities,
+                    "{requested} tool support"
+                );
+                assert_eq!(
+                    resolved.resolved.supports_reasoning, expected_capabilities,
+                    "{requested} reasoning support"
+                );
             }
         }
 
-        for messages_only in [
+        for non_chat in [
             "minimax-m3",
             "minimax-m2.7",
             "minimax-m2.5",
             "qwen3.7-max",
             "qwen3.7-plus",
             "qwen3.6-plus",
+            "qwen3.8-max",
+            "qwen3.8-flash",
+            "grok-4.6",
+            "gpt-5.6-luna",
+            "muse-spark-1.3-contributor",
+            "muse-spark-1.2-contributor",
         ] {
-            for requested in [
-                messages_only.to_string(),
-                format!("opencode-go/{messages_only}"),
-            ] {
+            for requested in [non_chat.to_string(), format!("opencode-go/{non_chat}")] {
                 let rejected = registry
                     .resolve(Some(&requested), Some(ProviderKind::OpencodeGo))
-                    .expect_err("Messages-only id must not fall back on the Chat-only route");
+                    .expect_err("Messages/Responses id must not fall back on the Chat-only route");
                 assert_eq!(
                     rejected,
                     ModelResolutionError::ModelNotAvailableForProvider {
diff --git a/crates/app-server/src/chat_completions.rs b/crates/app-server/src/chat_completions.rs
index cf072f8ddd..a824d9b8c4 100644
--- a/crates/app-server/src/chat_completions.rs
+++ b/crates/app-server/src/chat_completions.rs
@@ -17,11 +17,12 @@ use axum::http::{HeaderName, StatusCode};
 use axum::response::IntoResponse;
 use codewhale_agent::ModelRegistry;
 use codewhale_config::{
-    ConfigApiKeyValueKind, ConfigToml, ProviderKind, auth_mode_disables_api_key,
-    classify_config_api_key_value, is_upstream_auth_header,
+    ConfigApiKeyValueKind, ConfigToml, ProviderKind, apply_openrouter_vendor,
+    auth_mode_disables_api_key, classify_config_api_key_value, is_upstream_auth_header,
     provider::WireFormat,
     provider_base_url_is_official, provider_preserves_custom_base_url_model,
     route::{LogicalModelRef, RouteError, RouteRequest, RouteResolver},
+    validate_openrouter_vendor,
 };
 use serde_json::Value;
 
@@ -314,6 +315,28 @@ pub(crate) async fn chat_completions_handler(
 
     // Resolve endpoint.
     let config = state.config.read().await;
+    let vendor = config
+        .providers
+        .for_provider(config.provider)
+        .vendor
+        .as_deref()
+        .unwrap_or_default();
+    let openrouter_vendor = match validate_openrouter_vendor(vendor) {
+        Ok(vendor) if vendor.is_none() || config.provider == ProviderKind::Openrouter => vendor,
+        _ => {
+            return (
+                StatusCode::BAD_REQUEST,
+                Json(serde_json::json!({
+                    "error": {
+                        "message": "vendor is supported only for OpenRouter and must be a slug without whitespace or control characters",
+                        "type": "invalid_request_error",
+                        "code": "invalid_vendor"
+                    }
+                })),
+            )
+                .into_response();
+        }
+    };
     let endpoint = match resolve_endpoint(&config, &state.registry, request_model) {
         Ok(endpoint) => endpoint,
         Err(error) => {
@@ -353,6 +376,9 @@ pub(crate) async fn chat_completions_handler(
     // byte-for-byte passthrough values, while known aliases become their exact
     // provider wire ids before forwarding.
     body["model"] = serde_json::Value::String(endpoint.model.clone());
+    // The operator pin overrides caller ordering/fallback preferences while
+    // retaining caller restrictions such as only, ignore, and privacy policy.
+    apply_openrouter_vendor(&mut body, openrouter_vendor);
 
     let url = upstream_url(&endpoint, &body);
 
@@ -677,6 +703,96 @@ api_key = {provider_api_key:?}
         serde_json::from_slice(&bytes).expect("json response")
     }
 
+    #[tokio::test]
+    async fn openrouter_vendor_forwarding_preserves_pin_and_caller_restrictions() {
+        install_crypto_provider();
+        let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
+        let mock_url = format!("http://{}", listener.local_addr().unwrap());
+        let (captured_tx, mut captured_rx) = mpsc::unbounded_channel::();
+        let upstream = axum::Router::new().route(
+            "/v1/chat/completions",
+            axum::routing::post(move |Json(body): Json| {
+                let captured = captured_tx.clone();
+                async move {
+                    captured.send(body).unwrap();
+                    Json(serde_json::json!({"choices": []}))
+                }
+            }),
+        );
+        let upstream_task = tokio::spawn(async move {
+            axum::serve(listener, upstream).await.unwrap();
+        });
+
+        for (provider, vendor, status) in [
+            ("openrouter", "deepinfra/turbo", StatusCode::OK),
+            ("openrouter", "", StatusCode::OK),
+            ("openrouter", "bad vendor fixture", StatusCode::BAD_REQUEST),
+            ("arcee", "deepinfra/turbo", StatusCode::BAD_REQUEST),
+            ("arcee", "", StatusCode::OK),
+        ] {
+            let tmp = tempfile::tempdir().unwrap();
+            let config_path = tmp.path().join("config.toml");
+            let openrouter_vendor = if provider == "openrouter" {
+                vendor
+            } else {
+                "dormant/pin"
+            };
+            let arcee_vendor = if provider == "arcee" { vendor } else { "" };
+            fs::write(&config_path, format!(
+                "provider = {provider:?}\n\
+                 [providers.openrouter]\nbase_url = {mock_url:?}\napi_key = \"fixture-openrouter-key\"\nvendor = {openrouter_vendor:?}\n\
+                 [providers.arcee]\nbase_url = {mock_url:?}\napi_key = \"fixture-arcee-key\"\nvendor = {arcee_vendor:?}\n"
+            )).unwrap();
+            let state = build_state(Some(config_path), None).unwrap();
+            let app = app_router(state, &[]);
+            let caller_policy = serde_json::json!({
+                "order": ["caller/escape"],
+                "allow_fallbacks": true,
+                "only": ["caller/restriction"],
+                "ignore": ["caller/blocked"],
+                "zdr": true,
+                "data_collection": "deny",
+                "require_parameters": true
+            });
+            let body = serde_json::json!({
+                "model": "fixture/model",
+                "messages": [{"role": "user", "content": "hello"}],
+                "provider": caller_policy
+            });
+            let response = app
+                .oneshot(
+                    Request::builder()
+                        .method(Method::POST)
+                        .uri("/v1/chat/completions")
+                        .header("content-type", "application/json")
+                        .body(Body::from(serde_json::to_vec(&body).unwrap()))
+                        .unwrap(),
+                )
+                .await
+                .unwrap();
+            assert_eq!(response.status(), status, "{provider}: {vendor}");
+            if status == StatusCode::BAD_REQUEST {
+                let error = response_body_json(response).await;
+                assert_eq!(error["error"]["code"], "invalid_vendor");
+                assert!(!error.to_string().contains(vendor));
+                assert!(
+                    captured_rx.try_recv().is_err(),
+                    "invalid config reached upstream"
+                );
+            } else {
+                let forwarded = captured_rx.try_recv().expect("captured forwarded request");
+                let mut expected = caller_policy;
+                if provider == "openrouter" && !vendor.is_empty() {
+                    expected["order"] = serde_json::json!([vendor]);
+                    expected["allow_fallbacks"] = serde_json::json!(false);
+                }
+                assert_eq!(forwarded["provider"], expected, "{provider}: {vendor}");
+                assert_eq!(forwarded["model"], "fixture/model");
+            }
+        }
+        upstream_task.abort();
+    }
+
     #[tokio::test]
     async fn forwards_messages_and_tools() {
         install_crypto_provider();
diff --git a/crates/app-server/src/lib.rs b/crates/app-server/src/lib.rs
index 373ab3c4c5..a7fa4b61aa 100644
--- a/crates/app-server/src/lib.rs
+++ b/crates/app-server/src/lib.rs
@@ -1,3 +1,4 @@
+use codewhale_protocol::runtime::{MAX_RUNTIME_IMAGE_BODY_BYTES, RuntimeImageInput};
 use std::collections::{HashMap, VecDeque};
 use std::net::SocketAddr;
 use std::path::{Path, PathBuf};
@@ -36,7 +37,7 @@ pub mod daemon_socket;
 
 /// Legacy DeepSeek-era naming kept for external compatibility.
 ///
-/// CodeWhale began life as a DeepSeek CLI; existing health probes, SDK
+/// CodeWhale began life as DeepSeek-TUI; existing health probes, SDK
 /// harnesses, and on-disk layouts still key off these names. Every remaining
 /// legacy reference in this crate routes through this shim so a future
 /// coordinated migration touches exactly one place (repo policy: preserve
@@ -306,8 +307,12 @@ struct ThreadIdParams {
 
 #[derive(Debug, Deserialize)]
 struct ThreadMessageParams {
+    #[serde(default, rename = "maxOutputTokens", alias = "max_output_tokens")]
+    max_output_tokens: Option,
     thread_id: String,
     input: String,
+    #[serde(default)]
+    images: Vec,
 }
 
 #[derive(Debug, Deserialize)]
@@ -353,9 +358,19 @@ async fn shutdown_signal() {
 
 fn app_router(state: AppState, cors_origins: &[String]) -> Router {
     let protected_routes = Router::new()
-        .route("/thread", post(thread_handler))
+        .route(
+            "/thread",
+            post(thread_handler).layer(axum::extract::DefaultBodyLimit::max(
+                MAX_RUNTIME_IMAGE_BODY_BYTES,
+            )),
+        )
         .route("/app", post(app_handler))
-        .route("/prompt", post(prompt_handler))
+        .route(
+            "/prompt",
+            post(prompt_handler).layer(axum::extract::DefaultBodyLimit::max(
+                MAX_RUNTIME_IMAGE_BODY_BYTES,
+            )),
+        )
         .route("/tool", post(tool_handler))
         .route("/jobs", get(jobs_handler))
         .route("/mcp/startup", post(mcp_startup_handler))
@@ -521,6 +536,12 @@ enum ParsedStdioLine {
 }
 
 fn parse_stdio_line(line: &str) -> ParsedStdioLine {
+    if line.len() > MAX_RUNTIME_IMAGE_BODY_BYTES {
+        return ParsedStdioLine::Rejected(jsonrpc_error(
+            None,
+            JsonRpcError::invalid_params("request exceeds the 8 MiB transport limit"),
+        ));
+    }
     if line.trim().is_empty() {
         return ParsedStdioLine::Blank;
     }
@@ -648,8 +669,16 @@ async fn thread_handler(State(state): State, Json(req): Json (StatusCode::OK, Json(res)).into_response(),
             Err(err) => http_error_from_jsonrpc(err).into_response(),
         };
@@ -1113,9 +1142,11 @@ async fn handle_thread_request(
 /// One turn's worth of routing decisions, shared by every surface that runs
 /// a turn through the bridge.
 struct BridgedTurn<'a> {
+    max_output_tokens: Option,
     /// Client-facing thread id; the bridge maps it to a runtime thread.
     thread_key: &'a str,
     input: &'a str,
+    images: &'a [RuntimeImageInput],
     /// Model for the runtime thread when this call is the one that creates
     /// it. An existing thread keeps the model it was created with.
     model_override: Option,
@@ -1153,6 +1184,33 @@ async fn run_bridged_turn(
     // access. The cache slot itself stays unlocked, so config updates and
     // bridge invalidation are never queued behind a streaming turn.
     let mut bridge = bridge.lock().await;
+    if turn.max_output_tokens.is_some() {
+        let info = bridge
+            .request_json(
+                bridge.authed(
+                    bridge
+                        .client
+                        .get(format!("{}/v1/runtime/info", bridge.base_url)),
+                ),
+            )
+            .await
+            .map_err(|err| JsonRpcError::runtime_unavailable(err.to_string()))?;
+        if info
+            .pointer("/capabilities/turn_output_token_limit")
+            .and_then(Value::as_bool)
+            != Some(true)
+        {
+            return Err(JsonRpcError::invalid_params(
+                "Runtime does not support maxOutputTokens",
+            ));
+        }
+        if !bridge.thread_map.contains_key(turn.thread_key) {
+            bridge
+                .require_output_limited_model(hint.as_ref().and_then(|hint| hint.model.as_deref()))
+                .await
+                .map_err(|err| JsonRpcError::invalid_params(err.to_string()))?;
+        }
+    }
     let runtime_thread_id = bridge
         .ensure_runtime_thread(turn.thread_key, hint)
         .await
@@ -1164,6 +1222,8 @@ async fn run_bridged_turn(
         .message_thread(
             &runtime_thread_id,
             turn.input,
+            turn.images,
+            turn.max_output_tokens,
             writer,
             registration,
             transcript,
@@ -1205,8 +1265,10 @@ async fn run_prompt_turn(
         state,
         writer,
         BridgedTurn {
+            max_output_tokens: req.max_output_tokens,
             thread_key: &thread_key,
             input: &req.prompt,
+            images: &req.images,
             model_override: req.model.clone(),
             // `thread/interrupt` addresses client-facing thread ids. A
             // one-shot prompt has none to hand back, and a caller-supplied
@@ -1256,6 +1318,8 @@ async fn run_http_thread_message(
     state: &AppState,
     thread_id: String,
     input: String,
+    images: Vec,
+    max_output_tokens: Option,
 ) -> std::result::Result {
     let mut transcript = TurnTranscript::default();
     let mut sink = tokio::io::sink();
@@ -1263,8 +1327,10 @@ async fn run_http_thread_message(
         state,
         &mut sink,
         BridgedTurn {
+            max_output_tokens,
             thread_key: &thread_id,
             input: &input,
+            images: &images,
             model_override: None,
             interruptible: false,
             ephemeral: false,
@@ -1300,8 +1366,10 @@ async fn handle_stdio_thread_message(
         state,
         writer,
         BridgedTurn {
+            max_output_tokens: parsed.max_output_tokens,
             thread_key: &parsed.thread_id,
             input: &parsed.input,
+            images: &parsed.images,
             model_override: None,
             interruptible: true,
             ephemeral: false,
@@ -1500,6 +1568,73 @@ impl RuntimeBridge {
         serde_json::from_str(&body).with_context(|| format!("invalid runtime API json: {body}"))
     }
 
+    /// Read the existing Runtime catalog before a one-shot prompt can create
+    /// its thread. Existing threads are checked by canonical turn admission.
+    async fn require_output_limited_model(&self, requested_model: Option<&str>) -> Result<()> {
+        let providers = self
+            .request_json(self.authed(self.client.get(format!("{}/v1/providers", self.base_url))))
+            .await?;
+        let current = providers
+            .get("current")
+            .and_then(Value::as_str)
+            .context("Runtime provider is unavailable")?;
+        let provider = providers
+            .get("providers")
+            .and_then(Value::as_array)
+            .and_then(|providers| {
+                providers
+                    .iter()
+                    .find(|provider| provider.get("id").and_then(Value::as_str) == Some(current))
+            })
+            .context("Runtime provider is unavailable")?;
+        let model = requested_model
+            .or_else(|| provider.get("default_model").and_then(Value::as_str))
+            .context("maxOutputTokens requires an exact model")?;
+        if model.trim().is_empty() || model.eq_ignore_ascii_case("auto") {
+            bail!("maxOutputTokens requires an exact model");
+        }
+        if !current
+            .bytes()
+            .all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit() || byte == b'-')
+        {
+            bail!("Runtime provider identity is invalid");
+        }
+        let mut cursor = None;
+        let mut seen = std::collections::HashSet::new();
+        loop {
+            let mut url =
+                reqwest::Url::parse(&format!("{}/v1/providers/{current}/models", self.base_url))?;
+            url.query_pairs_mut().append_pair("limit", "250");
+            if let Some(cursor) = cursor.as_deref() {
+                url.query_pairs_mut().append_pair("cursor", cursor);
+            }
+            let catalog = self.request_json(self.authed(self.client.get(url))).await?;
+            if let Some(entry) =
+                catalog
+                    .get("models")
+                    .and_then(Value::as_array)
+                    .and_then(|models| {
+                        models
+                            .iter()
+                            .find(|entry| entry.get("id").and_then(Value::as_str) == Some(model))
+                    })
+            {
+                if entry.get("output_token_limit").and_then(Value::as_str) == Some("supported") {
+                    return Ok(());
+                }
+                bail!("The selected Runtime model does not support maxOutputTokens");
+            }
+            let next = catalog
+                .get("nextCursor")
+                .and_then(Value::as_str)
+                .context("Output-limit support is unknown for the selected Runtime model")?;
+            if !seen.insert(next.to_string()) {
+                bail!("Runtime model catalog cursor repeated");
+            }
+            cursor = Some(next.to_string());
+        }
+    }
+
     async fn ensure_runtime_thread(
         &mut self,
         stdio_thread_id: &str,
@@ -1557,17 +1692,43 @@ impl RuntimeBridge {
         &mut self,
         thread_id: &str,
         input: &str,
+        images: &[RuntimeImageInput],
+        max_output_tokens: Option,
         writer: &mut W,
         registration: Option<(TurnRegistry, String)>,
         mut transcript: Option<&mut TurnTranscript>,
     ) -> Result {
+        let mut request = json!({ "prompt": input });
+        if !images.is_empty() {
+            let info = self
+                .request_json(
+                    self.authed(
+                        self.client
+                            .get(format!("{}/v1/runtime/info", self.base_url)),
+                    ),
+                )
+                .await?;
+            if info
+                .pointer("/capabilities/turn_image_inputs")
+                .and_then(Value::as_bool)
+                != Some(true)
+            {
+                bail!(
+                    "Runtime image input is unavailable; update the Runtime before sending attachments"
+                );
+            }
+            request["images"] = json!(images);
+        }
+        if let Some(limit) = max_output_tokens {
+            request["maxOutputTokens"] = json!(limit);
+        }
         let turn = self
             .request_json(
                 self.authed(
                     self.client
                         .post(format!("{}/v1/threads/{thread_id}/turns", self.base_url)),
                 )
-                .json(&json!({ "prompt": input })),
+                .json(&request),
             )
             .await?;
         let turn_id = turn
@@ -1943,6 +2104,7 @@ async fn dispatch_stdio_request_with_writer(
                 result: json!({
                     "transport": transport.label(),
                     "families": ["thread/*", "app/*", "prompt/*"],
+                    "turn_image_inputs": true,
                     "methods": methods,
                 }),
                 should_exit: false,
@@ -1950,6 +2112,7 @@ async fn dispatch_stdio_request_with_writer(
         }
         "thread/capabilities" => StdioDispatchResult {
             result: json!({
+                "turn_image_inputs": true,
                 "methods": [
                     "thread/request",
                     "thread/create",
@@ -1972,11 +2135,22 @@ async fn dispatch_stdio_request_with_writer(
         },
         "thread/request" => {
             let request: ThreadRequest = parse_params(params)?;
-            if let ThreadRequest::Message { thread_id, input } = request {
+            if let ThreadRequest::Message {
+                thread_id,
+                input,
+                images,
+                max_output_tokens,
+            } = request
+            {
                 let response = handle_stdio_thread_message(
                     state,
                     writer,
-                    ThreadMessageParams { thread_id, input },
+                    ThreadMessageParams {
+                        thread_id,
+                        input,
+                        images,
+                        max_output_tokens,
+                    },
                 )
                 .await?;
                 return Ok(StdioDispatchResult {
@@ -3397,6 +3571,153 @@ mod tests {
         assert_eq!(response.result["interrupted"], json!(false));
     }
 
+    #[tokio::test]
+    async fn output_cap_bridge_checks_support_before_creation_and_forwards_each_surface() {
+        use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
+        #[derive(Clone)]
+        struct Fixture {
+            supported: Arc,
+            created: Arc,
+            requests: Arc>>,
+        }
+        async fn info(State(f): State, headers: axum::http::HeaderMap) -> Json {
+            assert_eq!(
+                headers.get(header::AUTHORIZATION).unwrap(),
+                "Bearer fixture-output-cap"
+            );
+            Json(
+                json!({"capabilities":{"turn_output_token_limit":f.supported.load(Ordering::SeqCst)}}),
+            )
+        }
+        async fn providers() -> Json {
+            Json(
+                json!({"current":"custom","providers":[{"id":"custom","default_model":"fixture-model"}]}),
+            )
+        }
+        async fn models() -> Json {
+            Json(
+                json!({"models":[{"id":"fixture-model","output_token_limit":"supported"},{"id":"uncapped-transport","output_token_limit":"unsupported"}]}),
+            )
+        }
+        async fn create_thread(State(f): State) -> Json {
+            let n = f.created.fetch_add(1, Ordering::SeqCst);
+            Json(json!({"id":format!("thr_cap_{n}")}))
+        }
+        async fn create_turn(State(f): State, Json(body): Json) -> Json {
+            f.requests.lock().await.push(body);
+            Json(json!({"turn":{"id":"turn_cap"}}))
+        }
+        async fn events() -> impl IntoResponse {
+            (
+                [(header::CONTENT_TYPE, "text/event-stream")],
+                sse_frame(
+                    "turn.completed",
+                    json!({
+                        "seq":1,"turn_id":"turn_cap","payload":{"turn":{"status":"completed"}}
+                    }),
+                ),
+            )
+        }
+        let fixture = Fixture {
+            supported: Arc::new(AtomicBool::new(false)),
+            created: Arc::new(AtomicUsize::new(0)),
+            requests: Arc::new(Mutex::new(Vec::new())),
+        };
+        let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
+        let addr = listener.local_addr().unwrap();
+        let router = Router::new()
+            .route("/v1/runtime/info", get(info))
+            .route("/v1/providers", get(providers))
+            .route("/v1/providers/custom/models", get(models))
+            .route("/v1/threads", post(create_thread))
+            .route("/v1/threads/{id}/turns", post(create_turn))
+            .route("/v1/threads/{id}/events", get(events))
+            .with_state(fixture.clone());
+        let server = tokio::spawn(async move { axum::serve(listener, router).await.unwrap() });
+        let (state, _tmp) = capability_test_state();
+        let mut bridge = RuntimeBridge::from_base_url_for_test(format!("http://{addr}"));
+        bridge.auth_token = Some("fixture-output-cap".into());
+        *state.runtime_bridge.lock().await = Some(Arc::new(Mutex::new(bridge)));
+        let result = dispatch_stdio_request(
+            &state,
+            "prompt/run",
+            json!({"prompt":"review","maxOutputTokens":1500}),
+        )
+        .await;
+        assert!(result.unwrap_err().message.contains("does not support"));
+        assert_eq!(fixture.created.load(Ordering::SeqCst), 0);
+        assert!(fixture.requests.lock().await.is_empty());
+        fixture.supported.store(true, Ordering::SeqCst);
+        for model in ["auto", "uncapped-transport", "unknown-model"] {
+            assert!(
+                dispatch_stdio_request(
+                    &state,
+                    "prompt/run",
+                    json!({"prompt":"review","model":model,"maxOutputTokens":1500})
+                )
+                .await
+                .is_err()
+            );
+        }
+        assert_eq!(fixture.created.load(Ordering::SeqCst), 0);
+        for (method, params) in [
+            (
+                "prompt/run",
+                json!({"prompt":"review","maxOutputTokens":1500}),
+            ),
+            (
+                "thread/message",
+                json!({"thread_id":"stdio-cap","input":"review","maxOutputTokens":1500}),
+            ),
+            (
+                "thread/request",
+                json!({"kind":"message","thread_id":"request-cap","input":"review","maxOutputTokens":1500}),
+            ),
+        ] {
+            dispatch_stdio_request(&state, method, params)
+                .await
+                .expect("existing app-server caller forwards allowance");
+        }
+        run_http_thread_message(
+            &state,
+            "http-cap".into(),
+            "review".into(),
+            Vec::new(),
+            std::num::NonZeroU32::new(1500),
+        )
+        .await
+        .unwrap();
+        let requests = fixture.requests.lock().await.clone();
+        assert_eq!(requests.len(), 4);
+        assert!(
+            requests
+                .iter()
+                .all(|request| request["maxOutputTokens"] == 1500)
+        );
+        let count = fixture.created.load(Ordering::SeqCst);
+        for invalid in [
+            json!(0),
+            json!(-1),
+            json!(1.5),
+            json!("1500"),
+            json!(4_294_967_296u64),
+        ] {
+            assert!(
+                dispatch_stdio_request(
+                    &state,
+                    "prompt/run",
+                    json!({"prompt":"review","maxOutputTokens":invalid})
+                )
+                .await
+                .is_err()
+            );
+        }
+        assert_eq!(fixture.created.load(Ordering::SeqCst), count);
+        assert_eq!(fixture.requests.lock().await.len(), 4);
+        server.abort();
+        let _ = server.await;
+    }
+
     #[tokio::test]
     async fn stdio_runtime_bridge_streams_response_delta_events() {
         async fn create_turn(AxumPath(thread_id): AxumPath) -> Json {
@@ -3461,7 +3782,7 @@ mod tests {
         let (mut reader, mut writer) = tokio::io::duplex(4096);
 
         let result = bridge
-            .message_thread("thr_test", "hello", &mut writer, None, None)
+            .message_thread("thr_test", "hello", &[], None, &mut writer, None, None)
             .await
             .expect("message_thread should succeed");
         drop(writer);
@@ -3753,9 +4074,15 @@ mod tests {
         let (base_url, prompts, server) = spawn_stub_runtime().await;
         seed_bridge_at(&state, base_url).await;
 
-        let response = run_http_thread_message(&state, "thr_http".to_string(), "go".to_string())
-            .await
-            .expect("http thread message");
+        let response = run_http_thread_message(
+            &state,
+            "thr_http".to_string(),
+            "go".to_string(),
+            Vec::new(),
+            None,
+        )
+        .await
+        .expect("http thread message");
 
         assert_eq!(response.status, "completed");
         assert_eq!(response.thread_id, "thr_http");
@@ -3778,9 +4105,15 @@ mod tests {
         let (state, _tmp) = capability_test_state();
         seed_bridge_at(&state, "http://127.0.0.1:9".to_string()).await;
 
-        let err = run_http_thread_message(&state, "thr_http".to_string(), "go".to_string())
-            .await
-            .expect_err("no runtime means no turn");
+        let err = run_http_thread_message(
+            &state,
+            "thr_http".to_string(),
+            "go".to_string(),
+            Vec::new(),
+            None,
+        )
+        .await
+        .expect_err("no runtime means no turn");
         assert_eq!(err.code, RUNTIME_UNAVAILABLE_CODE);
     }
 
@@ -4161,4 +4494,84 @@ mod tests {
         assert!(DEFAULT_CORS_ORIGINS.contains(&"http://localhost:5173"));
         assert!(DEFAULT_CORS_ORIGINS.contains(&"tauri://localhost"));
     }
+    #[tokio::test]
+    async fn runtime_image_daemon_bridge_checks_transport_and_forwards_exact_wire() {
+        async fn capture(
+            State(seen): State>>>,
+            Json(body): Json,
+        ) -> (StatusCode, Json) {
+            seen.lock().await.push(body);
+            (
+                StatusCode::BAD_REQUEST,
+                Json(json!({"error":"fixture stops before an Engine"})),
+            )
+        }
+        for supported in [false, true] {
+            let seen = Arc::new(Mutex::new(Vec::new()));
+            let app = Router::new()
+                .route(
+                    "/v1/runtime/info",
+                    get(move || async move {
+                        Json(json!({"capabilities":{"turn_image_inputs":supported}}))
+                    }),
+                )
+                .route("/v1/threads/{id}/turns", post(capture))
+                .with_state(seen.clone());
+            let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
+            let addr = listener.local_addr().unwrap();
+            let server = tokio::spawn(async move {
+                axum::serve(listener, app).await.unwrap();
+            });
+            let mut bridge = RuntimeBridge::from_base_url_for_test(format!("http://{addr}"));
+            let images = vec![RuntimeImageInput {
+                mime: "image/png".into(),
+                data_base64: "fixture-bytes-validated-by-Core".into(),
+            }];
+            let mut writer = tokio::io::sink();
+            assert!(
+                bridge
+                    .message_thread(
+                        "thr_fixture",
+                        "look",
+                        &images,
+                        None,
+                        &mut writer,
+                        None,
+                        None
+                    )
+                    .await
+                    .is_err()
+            );
+            let requests = seen.lock().await;
+            assert_eq!(requests.len(), usize::from(supported));
+            if supported {
+                assert_eq!(requests[0], json!({"prompt":"look","images":images}));
+            }
+            server.abort();
+        }
+    }
+
+    #[test]
+    fn runtime_image_daemon_all_input_families_preserve_images() {
+        let image = json!({"mime":"image/png","dataBase64":"AQ=="});
+        let thread: ThreadMessageParams = serde_json::from_value(
+            json!({"thread_id":"thr_fixture","input":"look","images":[image.clone()]}),
+        )
+        .unwrap();
+        let prompt: PromptRequest =
+            serde_json::from_value(json!({"prompt":"look","images":[image.clone()]})).unwrap();
+        let generic: ThreadRequest = serde_json::from_value(
+            json!({"kind":"message","thread_id":"thr_fixture","input":"look","images":[image]}),
+        )
+        .unwrap();
+        assert_eq!(thread.images, prompt.images);
+        let ThreadRequest::Message { images, .. } = generic else {
+            panic!("message");
+        };
+        assert_eq!(thread.images, images);
+        assert!(matches!(
+            parse_stdio_line(&" ".repeat(MAX_RUNTIME_IMAGE_BODY_BYTES + 1)),
+            ParsedStdioLine::Rejected(_)
+        ));
+    }
 }
diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml
index ed7573447d..c79c2e0364 100644
--- a/crates/cli/Cargo.toml
+++ b/crates/cli/Cargo.toml
@@ -11,11 +11,13 @@ description = "Agentic terminal facade for open-source and open-weight coding mo
 workspace = true
 
 [features]
+default = ["mimalloc-allocator"]
+mimalloc-allocator = ["dep:mimalloc"]
 # Opt-in global-allocator swap (#5872): build on rusty_alloc (the pure-Rust
 # mimalloc v2.4.5 remake — no C compiler, no build script) instead of the
 # default mimalloc. Off by default; the default build is unchanged.
-# Build with: cargo build -p codewhale-cli --features rusty-alloc
-rusty-alloc = ["dep:rusty_alloc-api"]
+# Build with: cargo build -p codewhale-cli --no-default-features --features rusty-alloc
+rusty-alloc = ["dep:rusty_alloc-api", "codewhale-tui/rusty-alloc"]
 
 [[bin]]
 name = "codewhale"
@@ -25,7 +27,7 @@ path = "src/main.rs"
 anyhow.workspace = true
 clap.workspace = true
 clap_complete.workspace = true
-codewhale-tui = { path = "../tui", version = "0.9.13" }
+codewhale-tui = { path = "../tui", version = "0.9.13", default-features = false }
 codewhale-agent = { path = "../agent", version = "0.9.13" }
 codewhale-app-server = { path = "../app-server", version = "0.9.13" }
 codewhale-config = { path = "../config", version = "0.9.13" }
@@ -48,7 +50,7 @@ reqwest = { workspace = true, features = ["blocking"] }
 rustls.workspace = true
 semver.workspace = true
 tokio.workspace = true
-mimalloc.workspace = true
+mimalloc = { workspace = true, optional = true }
 rusty_alloc-api = { workspace = true, optional = true }
 sha2.workspace = true
 tempfile.workspace = true
diff --git a/crates/cli/src/cloud.rs b/crates/cli/src/cloud.rs
index 49c8d624e5..b52cc4cdf2 100644
--- a/crates/cli/src/cloud.rs
+++ b/crates/cli/src/cloud.rs
@@ -721,7 +721,7 @@ fn run_with(
         CloudCommand::Login(login) => {
             let device = client.start_device()?;
             validate_user_code(&device.user_code)?;
-            let verification_uri = validate_verification_url(
+            validate_verification_url(
                 &device.verification_uri,
                 api_base,
                 &device.user_code,
@@ -735,7 +735,7 @@ fn run_with(
             )?;
             writeln!(out, "Codewhale account sign-in")?;
             writeln!(out, "Code: {}", device.user_code)?;
-            writeln!(out, "Open: {verification_uri}")?;
+            writeln!(out, "Open: {verification_uri_complete}")?;
             writeln!(out, "Profile: {}", printable(profile))?;
             if !login.no_open && !opener(verification_uri_complete) {
                 writeln!(
diff --git a/crates/cli/src/cloud/tests.rs b/crates/cli/src/cloud/tests.rs
index 0e20190b25..c104e099ef 100644
--- a/crates/cli/src/cloud/tests.rs
+++ b/crates/cli/src/cloud/tests.rs
@@ -336,68 +336,90 @@ fn user_codes_and_key_inputs_match_the_server_contract() {
 
 #[test]
 fn device_flow_handles_pending_then_authorized_without_printing_tokens() {
-    let (temp, config) = test_config();
-    let _keep_temp = temp;
-    let (secrets, _) = test_secrets();
-    let transport = FakeTransport::new(vec![
-        response(
-            200,
-            json!({
-                "deviceCode": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
-                "userCode": "ABCD-EFGH-JKLM",
-                "verificationUri": "https://app.codewhale.net/cli/authorize",
-                "verificationUriComplete": "https://app.codewhale.net/cli/authorize?user_code=ABCD-EFGH-JKLM",
-                "expiresIn": 600,
-                "interval": 1
+    for (no_open, browser_opens) in [(false, true), (false, false), (true, false)] {
+        let (temp, config) = test_config();
+        let _keep_temp = temp;
+        let (secrets, _) = test_secrets();
+        let transport = FakeTransport::new(vec![
+            response(
+                200,
+                json!({
+                    "deviceCode": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
+                    "userCode": "ABCD-EFGH-JKLM",
+                    "verificationUri": "https://app.codewhale.net/cli/authorize",
+                    "verificationUriComplete": "https://app.codewhale.net/cli/authorize?user_code=ABCD-EFGH-JKLM",
+                    "expiresIn": 600,
+                    "interval": 1
+                }),
+            ),
+            response(202, json!({ "status": "authorization_pending" })),
+            response(
+                200,
+                auth_json("access-never-print", "refresh-never-print", "acct-123"),
+            ),
+            response(200, account("acct-123")),
+        ]);
+        let mut output = Vec::new();
+        let mut key_reader = |_| bail!("key reader should not be called");
+        let mut opened = Vec::new();
+        let mut opener = |url: String| {
+            opened.push(url);
+            browser_opens
+        };
+        let mut sleeper = |_| {};
+        run_with(
+            command(if no_open {
+                &["codewhale", "cloud", "login", "--no-open"]
+            } else {
+                &["codewhale", "cloud", "login"]
             }),
-        ),
-        response(202, json!({ "status": "authorization_pending" })),
-        response(
-            200,
-            auth_json("access-never-print", "refresh-never-print", "acct-123"),
-        ),
-        response(200, account("acct-123")),
-    ]);
-    let mut output = Vec::new();
-    let mut key_reader = |_| bail!("key reader should not be called");
-    let mut opened = Vec::new();
-    let mut opener = |url: String| {
-        opened.push(url);
-        true
-    };
-    let mut sleeper = |_| {};
-    run_with(
-        command(&["codewhale", "cloud", "login"]),
-        "work",
-        "https://api.codewhale.net",
-        &config,
-        &secrets,
-        &secrets,
-        &machine::MachineKeyEnv::default(),
-        &transport,
-        &mut output,
-        &mut key_reader,
-        &mut opener,
-        &mut sleeper,
-    )
-    .unwrap();
+            "work",
+            "https://api.codewhale.net",
+            &config,
+            &secrets,
+            &secrets,
+            &machine::MachineKeyEnv::default(),
+            &transport,
+            &mut output,
+            &mut key_reader,
+            &mut opener,
+            &mut sleeper,
+        )
+        .unwrap();
 
-    let output = String::from_utf8(output).unwrap();
-    assert!(output.contains("ABCD-EFGH-JKLM"));
-    assert!(output.contains("Account ID: acct-123"));
-    assert!(output.contains("Profile: work"));
-    // No-brand invariant: login signs in the account; the internal
-    // cloud-agent credential is never taught here.
-    assert!(!output.to_lowercase().contains("daytona"), "{output}");
-    assert!(!output.contains("set-slot"), "{output}");
-    assert!(!output.contains("access-never-print"));
-    assert!(!output.contains("refresh-never-print"));
-    assert_eq!(opened.len(), 1);
-    let requests = transport.requests();
-    assert_eq!(requests[0].path, "/api/cli/device/start");
-    assert_eq!(requests[1].path, "/api/cli/device/token");
-    assert_eq!(requests[2].path, "/api/cli/device/token");
-    assert_eq!(requests[3].path, "/api/me");
+        let output = String::from_utf8(output).unwrap();
+        assert_eq!(
+            output.lines().find(|line| line.starts_with("Open: ")),
+            Some("Open: https://app.codewhale.net/cli/authorize?user_code=ABCD-EFGH-JKLM")
+        );
+        assert!(output.contains("ABCD-EFGH-JKLM"));
+        assert!(output.contains("Account ID: acct-123"));
+        assert!(output.contains("Profile: work"));
+        // No-brand invariant: login signs in the account; the internal
+        // cloud-agent credential is never taught here.
+        assert!(!output.to_lowercase().contains("daytona"), "{output}");
+        assert!(!output.contains("set-slot"), "{output}");
+        assert!(!output.contains("access-never-print"));
+        assert!(!output.contains("refresh-never-print"));
+        assert!(!output.contains("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"));
+        if no_open {
+            assert!(opened.is_empty());
+        } else {
+            assert_eq!(
+                opened,
+                ["https://app.codewhale.net/cli/authorize?user_code=ABCD-EFGH-JKLM"]
+            );
+        }
+        assert_eq!(
+            output.contains("Browser could not be opened; use the URL and code above."),
+            !no_open && !browser_opens
+        );
+        let requests = transport.requests();
+        assert_eq!(requests[0].path, "/api/cli/device/start");
+        assert_eq!(requests[1].path, "/api/cli/device/token");
+        assert_eq!(requests[2].path, "/api/cli/device/token");
+        assert_eq!(requests[3].path, "/api/me");
+    }
 }
 
 #[test]
diff --git a/crates/cli/src/config_bundles.rs b/crates/cli/src/config_bundles.rs
index 544d3b6d09..e892d0225b 100644
--- a/crates/cli/src/config_bundles.rs
+++ b/crates/cli/src/config_bundles.rs
@@ -445,6 +445,8 @@ fn is_machine_bound_top_level_key(key: &str) -> bool {
                     | "project_instruction_imports"
                     | "projects"
                     | "requirements_path"
+                    | "route_preferences_version"
+                    | "route_preferences_migration"
                     | "runtime_api"
                     | "workspace"
             )
@@ -849,7 +851,14 @@ pub fn export_bundle(
     let mut global = BundleTable::default();
     let mut project = BundleTable::default();
 
-    for (key, value) in config_document(config)? {
+    let mut document = config_document(config)?;
+    if scope == BundleScope::Global {
+        // Root model aliases are route-relative on import; reconcile them with
+        // the canonical provider slots so the bundle never conflicts with
+        // itself, without reparsing unrelated preserved extras as `Config`.
+        codewhale_tui::route_preferences::scrub_root_model_aliases_for_export(&mut document)?;
+    }
+    for (key, value) in document {
         if let Some(value) = sanitize_export_value(&key, &value) {
             match export_section_for(&key, scope) {
                 ExportSection::Preferences => {
@@ -996,55 +1005,34 @@ pub struct ImportReceipt {
 /// Apply a validated bundle to `store` transactionally.
 ///
 /// The current document is backed up to `.bundle-backup--`,
-/// entries are applied through `ConfigStore::set_value`, and any failure
+/// the prepared candidate is committed through one `ConfigStore::save`, and any failure
 /// restores the backup before returning the error. The receipt redacts by
 /// construction: it carries only key paths and counts, never values.
+#[cfg(test)]
 pub fn apply_bundle(
     bundle: &PortableBundle,
     store: &mut codewhale_config::ConfigStore,
     scope: BundleScope,
-    workspace: &Path,
+    _workspace: &Path,
 ) -> Result {
-    apply_bundle_with(bundle, store, scope, workspace, apply_entries)
+    let prepared = prepare_import(bundle, store, scope)?;
+    apply_prepared_bundle(prepared, store, save_candidate)
 }
 
-fn apply_bundle_with(
-    bundle: &PortableBundle,
+struct PreparedImport {
+    plan: ImportPlan,
+    candidate: ConfigToml,
+}
+
+fn apply_prepared_bundle(
+    prepared: PreparedImport,
     store: &mut codewhale_config::ConfigStore,
-    scope: BundleScope,
-    workspace: &Path,
     apply: F,
 ) -> Result
 where
-    F: FnOnce(
-        &PortableBundle,
-        &mut codewhale_config::ConfigStore,
-        BundleScope,
-        &Path,
-        &mut bool,
-    ) -> Result<()>,
+    F: FnOnce(ConfigToml, &mut codewhale_config::ConfigStore, &mut bool) -> Result<()>,
 {
-    // Scope isolation is structural: project entries belong only in a
-    // project document, global entries only in the user-global one. A bundle
-    // carrying the other scope's section is refused up front rather than
-    // silently writing across the boundary.
-    match scope {
-        BundleScope::Global if !bundle.project.entries.is_empty() => {
-            bail!(
-                "bundle carries [project] entries; import it with --project from the workspace instead"
-            );
-        }
-        BundleScope::Project if !bundle.global.entries.is_empty() => {
-            bail!(
-                "bundle carries [global] entries; importing them into a project document would leak machine state"
-            );
-        }
-        _ => {}
-    }
-    // A project-scoped import must target an actual workspace document — the
-    // user-global file is never a landing zone for [project] entries.
-    validate_scope_target(scope, store.path())?;
-    let plan = plan_import(bundle, &store.config, scope);
+    let PreparedImport { plan, candidate } = prepared;
     if !plan.conflicting.is_empty() {
         bail!(
             "bundle contains conflicting or rejected entries: {}; remove duplicate keys or credential-shaped entries and re-export",
@@ -1071,7 +1059,7 @@ where
     };
 
     let mut target_written = false;
-    let apply_result = apply(bundle, store, scope, workspace, &mut target_written);
+    let apply_result = apply(candidate, store, &mut target_written);
     if let Err(error) = apply_result {
         store.config = original_config;
         let rollback = rollback_import_target(&target, backup_path.as_deref(), target_written)
@@ -1125,14 +1113,41 @@ fn rollback_import_target(
     }
 }
 
-fn apply_entries(
+/// Build the exact candidate used for both preview and commit. Legacy route
+/// migration remains in memory until the existing ConfigStore CAS save.
+fn prepare_import(
     bundle: &PortableBundle,
-    store: &mut codewhale_config::ConfigStore,
+    store: &codewhale_config::ConfigStore,
     scope: BundleScope,
-    workspace: &Path,
-    target_written: &mut bool,
-) -> Result<()> {
-    let mut candidate = store.config.clone();
+) -> Result {
+    match scope {
+        BundleScope::Global if !bundle.project.entries.is_empty() => bail!(
+            "bundle carries [project] entries; import it with --project from the workspace instead"
+        ),
+        BundleScope::Project if !bundle.global.entries.is_empty() => bail!(
+            "bundle carries [global] entries; importing them into a project document would leak machine state"
+        ),
+        _ => {}
+    }
+    validate_scope_target(scope, store.path())?;
+    let mut plan = plan_import(bundle, &store.config, scope);
+    if !plan.conflicting.is_empty() || (plan.is_no_op() && plan.skipped.is_empty()) {
+        return Ok(PreparedImport {
+            plan,
+            candidate: store.config.clone(),
+        });
+    }
+    let rendered;
+    let original = if let Some(original) = store.original_body() {
+        original
+    } else {
+        rendered = store.rendered_body()?;
+        &rendered
+    };
+    let mut document = codewhale_tui::route_preferences::prepare_document(store.path(), original)?;
+    let mut candidate = config_from_document(&document.to_string())?;
+    let mut model_edits = Vec::<(String, String, String)>::new();
+    let mut selected_provider = None;
     for (section, table) in [
         ("preferences", &bundle.preferences),
         ("profiles", &bundle.profiles),
@@ -1140,60 +1155,147 @@ fn apply_entries(
         ("project", &bundle.project),
         ("global", &bundle.global),
     ] {
-        let applies = match section {
-            "project" => scope == BundleScope::Project,
-            "global" => scope == BundleScope::Global,
-            _ => true,
-        };
-        if !applies {
+        if !section_applies(section, scope) {
             continue;
         }
         for (key, value) in &table.entries {
-            if key == "provider" {
-                continue;
-            }
             let dotted = format!("{section}.{key}");
             if nonportable_path_reason(key).is_some()
                 || value_rejection_reason(key, value).is_some()
             {
                 bail!("refusing to import non-portable config path {dotted}");
             }
+            if key == "provider" {
+                selected_provider = Some(
+                    value
+                        .as_str()
+                        .ok_or_else(|| anyhow!("config entry {dotted:?} must be a string"))?,
+                );
+                continue;
+            }
+            collect_model_edits(&dotted, key, value, &mut model_edits)?;
+            if codewhale_tui::route_preferences::is_route_key(key) {
+                continue;
+            }
             apply_config_value(&mut candidate, key, value)?;
         }
     }
-    // Apply provider selection after provider tables so an exact named custom
-    // provider exported with its definition can validate successfully.
-    for (section, table) in [
-        ("preferences", &bundle.preferences),
-        ("profiles", &bundle.profiles),
-        ("plugins", &bundle.plugins),
-        ("project", &bundle.project),
-        ("global", &bundle.global),
-    ] {
-        if !section_applies(section, scope) {
-            continue;
+    document = toml::to_string(&toml::Value::Table(config_document(&candidate)?))?
+        .parse()
+        .map_err(|_| anyhow!("could not prepare imported configuration; contents omitted"))?;
+    // Definitions precede the exact final selector. Root aliases then target
+    // that selected route, so an old provider slot cannot mask an imported model.
+    if let Some(provider) = selected_provider {
+        codewhale_tui::route_preferences::set_document(
+            store.path(),
+            &mut document,
+            "provider",
+            provider,
+        )?;
+    }
+    model_edits.sort_by_key(|(_, key, _)| !key.starts_with("providers."));
+    for (_, key, value) in &model_edits {
+        codewhale_tui::route_preferences::set_document(store.path(), &mut document, key, value)?;
+    }
+    let final_value: toml::Value = toml::from_str(&document.to_string())?;
+    for (dotted, key, value) in &model_edits {
+        let mut replay = document.clone();
+        codewhale_tui::route_preferences::set_document(store.path(), &mut replay, key, value)?;
+        if toml::from_str::(&replay.to_string())? != final_value {
+            plan.conflicting.push(dotted.clone());
         }
-        if let Some(value) = table.entries.get("provider") {
-            apply_config_value(&mut candidate, "provider", value)?;
+    }
+    candidate = config_from_document(&document.to_string())?;
+    // Run the same validation/serialization as the final save before consent
+    // or backup creation. This clone never writes or replaces the CAS snapshot.
+    let mut validation_store = store.clone();
+    validation_store.config = candidate.clone();
+    validation_store.rendered_body()?;
+    if config_document(&candidate)? == config_document(&store.config)? {
+        plan.skipped.append(&mut plan.added);
+        plan.skipped.append(&mut plan.changed);
+    } else {
+        // A raw root alias may already match while its canonical provider
+        // slot differs. Such an import is a real change, not a skipped write.
+        if plan.is_no_op() {
+            for (dotted, _, _) in &model_edits {
+                plan.skipped.retain(|key| key != dotted);
+                plan.changed.push(dotted.clone());
+            }
+        }
+        let original_value: toml::Value = toml::from_str(original)?;
+        if original_value.get("route_preferences_version").is_none()
+            && final_value.get("route_preferences_version").is_some()
+        {
+            plan.added
+                .push("global.route_preferences_version (local migration)".to_string());
+        }
+    }
+    for keys in [
+        &mut plan.added,
+        &mut plan.changed,
+        &mut plan.skipped,
+        &mut plan.conflicting,
+    ] {
+        keys.sort();
+        keys.dedup();
+    }
+    Ok(PreparedImport { plan, candidate })
+}
+
+fn collect_model_edits(
+    dotted: &str,
+    key: &str,
+    value: &toml::Value,
+    edits: &mut Vec<(String, String, String)>,
+) -> Result<()> {
+    if key != "provider" && codewhale_tui::route_preferences::is_route_key(key) {
+        let value = value
+            .as_str()
+            .ok_or_else(|| anyhow!("config entry {dotted:?} must be a string"))?;
+        edits.push((dotted.to_string(), key.to_string(), value.to_string()));
+    } else if (key == "providers" || key.starts_with("providers."))
+        && let Some(table) = value.as_table()
+    {
+        for (child, value) in table {
+            collect_model_edits(
+                &format!("{dotted}.{child}"),
+                &format!("{key}.{child}"),
+                value,
+                edits,
+            )?;
         }
     }
+    Ok(())
+}
+
+fn config_from_document(body: &str) -> Result {
+    let mut config: ConfigToml = toml::from_str(body).map_err(|_| {
+        anyhow!("imported configuration has an invalid TOML type; contents omitted")
+    })?;
+    let document: toml::Value = toml::from_str(body)?;
+    if let Some(provider) = document.get("provider").and_then(toml::Value::as_str) {
+        config.bind_persisted_provider_id(provider)?;
+    }
+    Ok(config)
+}
+
+fn save_candidate(
+    candidate: ConfigToml,
+    store: &mut codewhale_config::ConfigStore,
+    target_written: &mut bool,
+) -> Result<()> {
     store.config = candidate;
     store.save().context("saving imported bundle")?;
     *target_written = true;
-    let _ = workspace;
     Ok(())
 }
 
 fn apply_config_value(config: &mut ConfigToml, key: &str, value: &toml::Value) -> Result<()> {
-    if key == "provider"
-        || key == "auth.mode"
-        || key == "hook_sinks.unix_socket_path"
-        || key.starts_with("providers.")
-    {
+    if key == "auth.mode" || key == "hook_sinks.unix_socket_path" || key.starts_with("providers.") {
         return config.set_value(key, &render_toml_value(value)?);
     }
 
-    let selected_provider_id = config.selected_provider_id.clone();
     let mut document = config_document(config)?;
     if let Some(current) = document.get_mut(key) {
         deep_merge_toml_value(current, value);
@@ -1205,10 +1307,7 @@ fn apply_config_value(config: &mut ConfigToml, key: &str, value: &toml::Value) -
     // tables or strings.
     let text = toml::to_string(&toml::Value::Table(document))
         .with_context(|| format!("config entry {key:?} could not be serialized"))?;
-    let mut updated: ConfigToml = toml::from_str(&text)
-        .map_err(|_| anyhow!("config entry {key:?} has an invalid TOML type"))?;
-    updated.selected_provider_id = selected_provider_id;
-    *config = updated;
+    *config = config_from_document(&text)?;
     Ok(())
 }
 
@@ -1365,7 +1464,7 @@ pub struct ExportArgs {
 pub fn run_import(
     args: &ImportArgs,
     store: &mut codewhale_config::ConfigStore,
-    workspace: &Path,
+    _workspace: &Path,
 ) -> Result<()> {
     let scope = if args.project {
         BundleScope::Project
@@ -1409,7 +1508,8 @@ pub fn run_import(
     };
 
     let bundle = parse_bundle_bytes(&raw, source_label)?;
-    let plan = plan_import(&bundle, &store.config, scope);
+    let prepared = prepare_import(&bundle, store, scope)?;
+    let plan = &prepared.plan;
 
     println!("import plan ({} scope, {source_label}):", scope.label());
     println!("  added:       {}", plan.added.len());
@@ -1435,8 +1535,8 @@ pub fn run_import(
         return Ok(());
     }
 
-    require_import_consent(args.yes, &plan)?;
-    let receipt = apply_bundle(&bundle, store, scope, workspace)?;
+    require_import_consent(args.yes, plan)?;
+    let receipt = apply_prepared_bundle(prepared, store, save_candidate)?;
     if receipt.plan.is_no_op() {
         println!("nothing to apply; config already matches the bundle (idempotent re-import)");
         return Ok(());
@@ -1944,11 +2044,458 @@ verbosity = "verbose"
         let store = isolated_store();
         let before = std::fs::read_to_string(store.path()).expect("read config");
         let bundle = sample_bundle();
-        let _plan = plan_import(&bundle, &store.config, BundleScope::Global);
+        let _prepared = prepare_import(&bundle, &store, BundleScope::Global)
+            .expect("prepare import without writing");
         let after = std::fs::read_to_string(store.path()).expect("read config");
         assert_eq!(before, after, "planning must not write");
     }
 
+    #[test]
+    fn route_import_prepares_migration_once_and_overrides_a_masking_provider_slot() {
+        use crate::tests::{ScopedEnvVar, env_lock};
+        let _env = env_lock();
+        let home = tempfile::tempdir().expect("isolated home");
+        let _home = ScopedEnvVar::set("CODEWHALE_HOME", &home.path().to_string_lossy());
+        let _config_override = ScopedEnvVar::remove("CODEWHALE_CONFIG_PATH");
+        let _legacy_override = ScopedEnvVar::remove("DEEPSEEK_CONFIG_PATH");
+        let path = home.path().join("config.toml");
+        let original = "provider = 'zai'\ndefault_text_model = 'GLM-5.3'\n[providers.zai]\nmodel = 'GLM-5.2'\n";
+        std::fs::write(&path, original).expect("seed config");
+        let settings_path = home.path().join("settings.toml");
+        let old_settings = "default_provider = 'deepseek'\n[provider_models]\ndeepseek = 'deepseek-v4-pro'\nzai = 'GLM-5.4'\n";
+        std::fs::write(&settings_path, old_settings).expect("seed legacy choices");
+        let bundle = parse_bundle_str(
+            "schema_version = 1\nkind = 'codewhale.portable-config'\n[global]\nprovider = 'zai'\ndefault_text_model = 'GLM-5.3'\n",
+            "route.toml",
+        ).expect("route bundle");
+        let mut store = ConfigStore::load(Some(path.clone())).expect("store");
+        let prepared = prepare_import(&bundle, &store, BundleScope::Global).expect("preview");
+        assert!(
+            prepared
+                .plan
+                .changed
+                .iter()
+                .any(|key| key == "global.default_text_model")
+        );
+        assert!(
+            !prepared.plan.is_no_op(),
+            "the old slot still masks the root value"
+        );
+        assert_eq!(std::fs::read_to_string(&path).unwrap(), original);
+        assert_eq!(
+            std::fs::read_to_string(&settings_path).unwrap(),
+            old_settings
+        );
+
+        // Consent commits exactly the preview even if the archived input moves.
+        let later_settings = "default_provider = 'openai'\n";
+        std::fs::write(&settings_path, later_settings).unwrap();
+        apply_prepared_bundle(prepared, &mut store, save_candidate).expect("commit preview");
+        let saved: toml::Value = toml::from_str(&std::fs::read_to_string(&path).unwrap()).unwrap();
+        assert_eq!(saved["provider"].as_str(), Some("zai"));
+        assert_eq!(saved["providers"]["zai"]["model"].as_str(), Some("GLM-5.3"));
+        assert_eq!(
+            saved["providers"]["deepseek"]["model"].as_str(),
+            Some("deepseek-v4-pro")
+        );
+        assert_eq!(saved["route_preferences_version"].as_integer(), Some(1));
+        assert_eq!(
+            std::fs::read_to_string(settings_path).unwrap(),
+            later_settings
+        );
+        assert_eq!(
+            codewhale_tui::route_preferences::get(&path, "provider")
+                .unwrap()
+                .as_deref(),
+            Some("zai")
+        );
+        assert_eq!(
+            codewhale_tui::route_preferences::get(&path, "model")
+                .unwrap()
+                .as_deref(),
+            Some("GLM-5.3")
+        );
+        let again = prepare_import(&bundle, &store, BundleScope::Global).expect("repeat preview");
+        assert!(again.plan.is_no_op(), "{:?}", again.plan);
+    }
+
+    #[test]
+    fn conflicting_import_model_aliases_fail_before_any_write() {
+        let dir = tempfile::tempdir().expect("config dir");
+        let path = dir.path().join("config.toml");
+        let original = "provider = 'zai'\n[providers.zai]\nmodel = 'GLM-5.2'\n";
+        std::fs::write(&path, original).unwrap();
+        let mut store = ConfigStore::load(Some(path.clone())).unwrap();
+        let bundle = parse_bundle_str(
+            "schema_version = 1\nkind = 'codewhale.portable-config'\n[global]\ndefault_text_model = 'GLM-5.3'\n[global.providers.zai]\nmodel = 'GLM-5.4'\n",
+            "conflict.toml",
+        ).unwrap();
+        let prepared = prepare_import(&bundle, &store, BundleScope::Global).unwrap();
+        assert!(
+            prepared
+                .plan
+                .conflicting
+                .iter()
+                .any(|key| key == "global.providers.zai.model")
+        );
+        let error = apply_prepared_bundle(prepared, &mut store, save_candidate)
+            .expect_err("conflicting aliases must be refused");
+        assert!(error.to_string().contains("conflicting"));
+        assert!(!error.to_string().contains("GLM-5.4"));
+        assert_eq!(std::fs::read_to_string(path).unwrap(), original);
+        assert_eq!(
+            std::fs::read_dir(dir.path()).unwrap().count(),
+            1,
+            "no backup or staged write before validation"
+        );
+    }
+
+    #[test]
+    fn prepared_import_keeps_configstore_cas_against_concurrent_edits() {
+        let dir = tempfile::tempdir().expect("config dir");
+        let path = dir.path().join("config.toml");
+        std::fs::write(&path, "provider = 'deepseek'\n").unwrap();
+        let mut store = ConfigStore::load(Some(path.clone())).unwrap();
+        let prepared = prepare_import(&sample_bundle(), &store, BundleScope::Global).unwrap();
+        let concurrent = "provider = 'openai'\n# concurrent writer\n";
+        std::fs::write(&path, concurrent).unwrap();
+        apply_prepared_bundle(prepared, &mut store, save_candidate)
+            .expect_err("stale preview must fail closed");
+        assert_eq!(std::fs::read_to_string(path).unwrap(), concurrent);
+    }
+
+    #[test]
+    fn route_migration_receipts_are_local_and_not_portable() {
+        let bundle = parse_bundle_str(
+            "schema_version = 1\nkind = 'codewhale.portable-config'\n[global]\nroute_preferences_version = 1\n[global.route_preferences_migration]\nprevious_provider = 'zai'\n",
+            "receipt.toml",
+        ).unwrap();
+        assert_eq!(find_rejected_entries(&bundle).len(), 2);
+        let config: ConfigToml = toml::from_str(
+            "route_preferences_version = 1\n[route_preferences_migration]\nprevious_provider = 'zai'\n",
+        ).unwrap();
+        let exported =
+            export_bundle(&config, BundleScope::Global, BundleMetadata::default()).unwrap();
+        assert!(
+            !exported
+                .global
+                .entries
+                .contains_key("route_preferences_version")
+        );
+        assert!(
+            !exported
+                .global
+                .entries
+                .contains_key("route_preferences_migration")
+        );
+    }
+
+    #[test]
+    fn canonical_route_export_omits_shadowed_roots_and_round_trips_provider_slots() {
+        let config: ConfigToml = toml::from_str(
+            "provider = 'zai'\ndefault_text_model = 'deepseek-v4-pro'\nmodel = 'old-root-model'\n[providers.zai]\nmodel = 'GLM-5.3'\n[providers.deepseek]\nmodel = 'deepseek-v4-flash'\n[providers.openai]\nmodel = 'gpt-4.1'\n",
+        ).unwrap();
+        let bundle =
+            export_bundle(&config, BundleScope::Global, BundleMetadata::default()).unwrap();
+        assert!(!bundle.global.entries.contains_key("model"));
+        assert!(!bundle.global.entries.contains_key("default_text_model"));
+        let dir = tempfile::tempdir().expect("config dir");
+        let mut store = ConfigStore::load(Some(dir.path().join("config.toml"))).unwrap();
+        let receipt = apply_bundle(&bundle, &mut store, BundleScope::Global, dir.path())
+            .expect("canonical export must import without alias conflicts");
+        assert!(receipt.plan.conflicting.is_empty());
+        assert_eq!(store.config.provider_id(), "zai");
+        assert_eq!(
+            store.config.get_value("providers.zai.model").as_deref(),
+            Some("GLM-5.3")
+        );
+        assert_eq!(
+            store
+                .config
+                .get_value("providers.deepseek.model")
+                .as_deref(),
+            Some("deepseek-v4-flash")
+        );
+        assert_eq!(
+            store.config.get_value("providers.openai.model").as_deref(),
+            Some("gpt-4.1")
+        );
+        let again = export_bundle(
+            &store.config,
+            BundleScope::Global,
+            BundleMetadata::default(),
+        )
+        .unwrap();
+        assert_eq!(
+            serialize_bundle(&again).unwrap(),
+            serialize_bundle(&bundle).unwrap()
+        );
+
+        let root_only: ConfigToml =
+            toml::from_str("default_text_model = 'deepseek-v4-pro'\n").unwrap();
+        let legacy =
+            export_bundle(&root_only, BundleScope::Global, BundleMetadata::default()).unwrap();
+        assert_eq!(
+            legacy
+                .global
+                .entries
+                .get("default_text_model")
+                .and_then(toml::Value::as_str),
+            Some("deepseek-v4-pro")
+        );
+        let literal_custom = config_from_document(
+            "provider = 'custom'\nbase_url = 'https://literal.example.test/v1'\ndefault_text_model = 'LiteralRootModel'\n",
+        ).unwrap();
+        let literal_export = export_bundle(
+            &literal_custom,
+            BundleScope::Global,
+            BundleMetadata::default(),
+        )
+        .unwrap();
+        assert_eq!(
+            literal_export
+                .global
+                .entries
+                .get("default_text_model")
+                .and_then(toml::Value::as_str),
+            Some("LiteralRootModel")
+        );
+    }
+
+    #[test]
+    fn export_reconciles_a_legacy_root_default_model_with_the_deepseek_slot() {
+        // Migration writes the canonical slot but never removes a legacy root
+        // `default_model`; on import that alias also targets the DeepSeek slot,
+        // so a raw export would conflict with itself.
+        let config: ConfigToml = toml::from_str(
+            "provider = 'deepseek'\ndefault_model = 'deepseek-v4-flash'\n[providers.deepseek]\nmodel = 'deepseek-v4-pro'\n",
+        )
+        .unwrap();
+        let bundle =
+            export_bundle(&config, BundleScope::Global, BundleMetadata::default()).unwrap();
+        assert!(!bundle.global.entries.contains_key("default_model"));
+        let providers = bundle
+            .global
+            .entries
+            .get("providers")
+            .and_then(toml::Value::as_table)
+            .expect("providers table");
+        assert_eq!(
+            providers["deepseek"]["model"].as_str(),
+            Some("deepseek-v4-pro")
+        );
+
+        let dir = tempfile::tempdir().expect("config dir");
+        let mut store = ConfigStore::load(Some(dir.path().join("config.toml"))).unwrap();
+        let receipt = apply_bundle(&bundle, &mut store, BundleScope::Global, dir.path())
+            .expect("export must import without alias conflicts");
+        assert!(receipt.plan.conflicting.is_empty(), "{:?}", receipt.plan);
+        assert_eq!(
+            store
+                .config
+                .get_value("providers.deepseek.model")
+                .as_deref(),
+            Some("deepseek-v4-pro")
+        );
+
+        // Without a canonical slot the root alias folds into the DeepSeek slot.
+        let config: ConfigToml = toml::from_str(
+            "provider = 'zai'\ndefault_model = 'deepseek-v4-flash'\n[providers.zai]\nmodel = 'GLM-5.3'\n",
+        )
+        .unwrap();
+        let bundle =
+            export_bundle(&config, BundleScope::Global, BundleMetadata::default()).unwrap();
+        assert!(!bundle.global.entries.contains_key("default_model"));
+        let providers = bundle
+            .global
+            .entries
+            .get("providers")
+            .and_then(toml::Value::as_table)
+            .expect("providers table");
+        assert_eq!(
+            providers["deepseek"]["model"].as_str(),
+            Some("deepseek-v4-flash")
+        );
+        assert_eq!(providers["zai"]["model"].as_str(), Some("GLM-5.3"));
+    }
+
+    #[test]
+    fn export_preserves_the_deepseek_root_fallback_when_another_route_is_active() {
+        // With Z.ai active, a DeepSeek-id root `default_text_model` is
+        // DeepSeek's saved fallback, not shadowed state; the active route's
+        // canonical slot must not cause it to be dropped.
+        let config: ConfigToml = toml::from_str(
+            "provider = 'zai'\ndefault_text_model = 'deepseek-v4-flash'\n[providers.zai]\nmodel = 'GLM-5.3'\n",
+        )
+        .unwrap();
+        let bundle =
+            export_bundle(&config, BundleScope::Global, BundleMetadata::default()).unwrap();
+        assert!(!bundle.global.entries.contains_key("default_text_model"));
+        let providers = bundle
+            .global
+            .entries
+            .get("providers")
+            .and_then(toml::Value::as_table)
+            .expect("providers table");
+        assert_eq!(
+            providers["deepseek"]["model"].as_str(),
+            Some("deepseek-v4-flash")
+        );
+        assert_eq!(providers["zai"]["model"].as_str(), Some("GLM-5.3"));
+
+        let dir = tempfile::tempdir().expect("config dir");
+        let mut store = ConfigStore::load(Some(dir.path().join("config.toml"))).unwrap();
+        let receipt = apply_bundle(&bundle, &mut store, BundleScope::Global, dir.path())
+            .expect("export must import without alias conflicts");
+        assert!(receipt.plan.conflicting.is_empty(), "{:?}", receipt.plan);
+        assert_eq!(
+            store
+                .config
+                .get_value("providers.deepseek.model")
+                .as_deref(),
+            Some("deepseek-v4-flash")
+        );
+
+        // A root alias the active route still consumes stays shadowed state and
+        // is dropped; a canonical DeepSeek leaf wins over a duplicate root
+        // fallback.
+        let config: ConfigToml = toml::from_str(
+            "provider = 'zai'\ndefault_text_model = 'deepseek-v4-pro'\nmodel = 'GLM-5.1'\n[providers.zai]\nmodel = 'GLM-5.3'\n[providers.deepseek]\nmodel = 'deepseek-v4-flash'\n",
+        )
+        .unwrap();
+        let bundle =
+            export_bundle(&config, BundleScope::Global, BundleMetadata::default()).unwrap();
+        assert!(!bundle.global.entries.contains_key("model"));
+        assert!(!bundle.global.entries.contains_key("default_text_model"));
+        let providers = bundle
+            .global
+            .entries
+            .get("providers")
+            .and_then(toml::Value::as_table)
+            .expect("providers table");
+        assert_eq!(
+            providers["deepseek"]["model"].as_str(),
+            Some("deepseek-v4-flash")
+        );
+    }
+
+    #[test]
+    fn imports_preserve_exact_builtin_shadowing_custom_provider_identity() {
+        let dir = tempfile::tempdir().expect("config dir");
+        let path = dir.path().join("config.toml");
+        std::fs::write(
+            &path,
+            "provider = 'OpenAI'\n[providers.OpenAI]\nkind = 'openai-compatible'\nbase_url = 'https://custom.example.test/v1'\nmodel = 'LiteralOldModel'\n[providers.openai]\nmodel = 'gpt-4.1'\n",
+        ).unwrap();
+        let mut store = ConfigStore::load(Some(path.clone())).unwrap();
+        for (entries, expected_model) in [
+            ("verbosity = 'quiet'\n", "LiteralOldModel"),
+            (
+                "provider = 'OpenAI'\nmodel = 'LiteralNewModel'\noutput_mode = 'plain'\n",
+                "LiteralNewModel",
+            ),
+        ] {
+            let bundle = parse_bundle_str(
+                &format!(
+                    "schema_version = 1\nkind = 'codewhale.portable-config'\n[global]\n{entries}"
+                ),
+                "custom.toml",
+            )
+            .unwrap();
+            apply_bundle(&bundle, &mut store, BundleScope::Global, dir.path()).unwrap();
+            let saved: toml::Value =
+                toml::from_str(&std::fs::read_to_string(&path).unwrap()).unwrap();
+            assert_eq!(saved["provider"].as_str(), Some("OpenAI"));
+            assert_eq!(
+                saved["providers"]["OpenAI"]["model"].as_str(),
+                Some(expected_model)
+            );
+            assert_eq!(
+                saved["providers"]["OpenAI"]["base_url"].as_str(),
+                Some("https://custom.example.test/v1")
+            );
+            assert_eq!(
+                saved["providers"]["openai"]["model"].as_str(),
+                Some("gpt-4.1")
+            );
+            store.reload().unwrap();
+            assert_eq!(
+                store.config.provider,
+                codewhale_config::ProviderKind::Custom
+            );
+            assert_eq!(store.config.provider_id(), "OpenAI");
+            assert_eq!(
+                codewhale_tui::route_preferences::get(&path, "provider")
+                    .unwrap()
+                    .as_deref(),
+                Some("OpenAI")
+            );
+        }
+    }
+
+    #[test]
+    fn imports_preserve_regional_selector_and_canonical_model_slot() {
+        let dir = tempfile::tempdir().expect("config dir");
+        let path = dir.path().join("config.toml");
+        std::fs::write(
+            &path,
+            "provider = 'deepseek-cn'\n[providers.deepseek_cn]\nmodel = 'deepseek-v4-pro'\n[providers.deepseek]\nmodel = 'deepseek-v4-pro'\n",
+        ).unwrap();
+        let mut store = ConfigStore::load(Some(path.clone())).unwrap();
+        for (entries, expected_model) in [
+            ("verbosity = 'quiet'\n", "deepseek-v4-pro"),
+            (
+                "'providers.deepseek_cn.model' = 'deepseek-v4-flash'\n",
+                "deepseek-v4-flash",
+            ),
+        ] {
+            let bundle = parse_bundle_str(
+                &format!(
+                    "schema_version = 1\nkind = 'codewhale.portable-config'\n[global]\n{entries}"
+                ),
+                "regional.toml",
+            )
+            .unwrap();
+            apply_bundle(&bundle, &mut store, BundleScope::Global, dir.path()).unwrap();
+            let saved: toml::Value =
+                toml::from_str(&std::fs::read_to_string(&path).unwrap()).unwrap();
+            assert_eq!(saved["provider"].as_str(), Some("deepseek-cn"));
+            assert_eq!(
+                saved["providers"]["deepseek_cn"]["model"].as_str(),
+                Some(expected_model)
+            );
+            assert_eq!(
+                saved["providers"]["deepseek"]["model"].as_str(),
+                Some("deepseek-v4-pro")
+            );
+            store.reload().unwrap();
+            assert_eq!(store.config.provider_id(), "deepseek-cn");
+            assert_eq!(
+                codewhale_tui::route_preferences::get(&path, "model")
+                    .unwrap()
+                    .as_deref(),
+                Some(expected_model)
+            );
+            let mut export_config = store.config.clone();
+            export_config.default_text_model = Some("stale-regional-root".to_string());
+            let exported = export_bundle(
+                &export_config,
+                BundleScope::Global,
+                BundleMetadata::default(),
+            )
+            .unwrap();
+            assert!(!exported.global.entries.contains_key("default_text_model"));
+            assert_eq!(
+                exported
+                    .global
+                    .entries
+                    .get("provider")
+                    .and_then(toml::Value::as_str),
+                Some("deepseek-cn")
+            );
+        }
+    }
+
     #[test]
     fn apply_is_idempotent_on_reimport() {
         let mut store = isolated_store();
@@ -2083,17 +2630,14 @@ output_mode = "plain"
         let path = dir.path().join("config.toml");
         let mut store = ConfigStore::load(Some(path.clone())).expect("missing config loads");
 
-        let error = apply_bundle_with(
-            &sample_bundle(),
-            &mut store,
-            BundleScope::Global,
-            dir.path(),
-            |bundle, store, scope, workspace, target_written| {
-                apply_entries(bundle, store, scope, workspace, target_written)?;
+        let prepared =
+            prepare_import(&sample_bundle(), &store, BundleScope::Global).expect("prepare import");
+        let error =
+            apply_prepared_bundle(prepared, &mut store, |candidate, store, target_written| {
+                save_candidate(candidate, store, target_written)?;
                 bail!("forced failure after the new document was saved")
-            },
-        )
-        .expect_err("forced post-save failure must roll back");
+            })
+            .expect_err("forced post-save failure must roll back");
 
         assert!(error.to_string().contains("rolled back"), "{error:#}");
         assert!(
diff --git a/crates/cli/src/lib.rs b/crates/cli/src/lib.rs
index 5e828dd8e2..ac9e0825a0 100644
--- a/crates/cli/src/lib.rs
+++ b/crates/cli/src/lib.rs
@@ -170,8 +170,10 @@ struct Cli {
     session_id: Option,
     #[arg(short = 'p', long = "prompt", value_name = "PROMPT")]
     prompt_flag: Option,
-    /// Per-run config override (`KEY=VALUE`), repeatable. Applied in memory
-    /// after load and never saved — `config set` persists instead. Long-only:
+    /// Per-run config override (`KEY=VALUE`), repeatable, never saved.
+    /// Runtime keys: provider, model/default_text_model, verbosity,
+    /// approval_policy, sandbox_mode, telemetry. Dedicated flags win;
+    /// managed policy still applies. `config set` persists instead. Long-only:
     /// short `-c` is already `--continue`.
     #[arg(long = "set", value_name = "KEY=VALUE")]
     overrides: Vec,
@@ -1875,6 +1877,55 @@ fn config_store_path_for_dispatch(
     explicit_path
 }
 
+/// Runtime `--set` uses the dedicated flag handoff, so the existing loader
+/// owns profile, provider, managed-policy and requirements precedence. Keep
+/// config read/write commands on their separate, never-saved store overlay.
+fn apply_runtime_set_overrides(cli: &mut Cli) -> Result<()> {
+    let mut values = CliRuntimeOverrides::default();
+    let mut provider = None;
+    for spec in &cli.overrides {
+        let (key, value) = spec
+            .split_once('=')
+            .context("invalid --set: expected KEY=VALUE (value omitted)")?;
+        match key.trim() {
+            "provider" => {
+                provider = Some(
+                    parse_provider_identifier(value)
+                        .map_err(|_| anyhow!("invalid --set provider (value omitted)"))?,
+                );
+            }
+            "model" | "default_text_model" => values.model = Some(value.to_string()),
+            "verbosity" => values.verbosity = Some(value.to_string()),
+            "approval_policy" => values.approval_policy = Some(value.to_string()),
+            "sandbox_mode" => values.sandbox_mode = Some(value.to_string()),
+            "telemetry" => {
+                let mut config = ConfigToml::default();
+                config
+                    .set_value("telemetry", value)
+                    .map_err(|_| anyhow!("invalid --set telemetry: expected a boolean"))?;
+                values.telemetry = config.telemetry;
+            }
+            _ => bail!(
+                "unsupported runtime --set key (value omitted): supported keys are provider, \
+                 model, default_text_model, verbosity, approval_policy, sandbox_mode and \
+                 telemetry; use the dedicated option or config set for other keys"
+            ),
+        }
+        if value.trim().is_empty() {
+            bail!("invalid runtime --set: value must not be empty");
+        }
+    }
+    // A dedicated flag is more specific than a generic --set for the same
+    // field. Repeated --set keys otherwise keep their last value.
+    cli.provider = cli.provider.take().or(provider);
+    cli.model = cli.model.take().or(values.model);
+    cli.verbosity = cli.verbosity.take().or(values.verbosity);
+    cli.approval_policy = cli.approval_policy.take().or(values.approval_policy);
+    cli.sandbox_mode = cli.sandbox_mode.take().or(values.sandbox_mode);
+    cli.telemetry = cli.telemetry.or(values.telemetry);
+    Ok(())
+}
+
 fn run() -> Result<()> {
     let matches = Cli::command().get_matches();
     let project_bundle_scope = config_command_targets_project(&matches);
@@ -1888,6 +1939,20 @@ fn run() -> Result<()> {
         return run_lane_log_proxy_command(args);
     }
 
+    if !cli.overrides.is_empty() && matches!(command, Some(Commands::Auth(_))) {
+        bail!("--set is not supported by auth commands; use a saved config");
+    }
+    if !cli.overrides.is_empty()
+        && matches!(&command, Some(Commands::AppServer(args)) if !args.http && !args.mobile)
+    {
+        bail!(
+            "--set is not supported by the legacy app-server transport; use app-server --http or a saved config"
+        );
+    }
+    if !matches!(command, Some(Commands::Config(_))) {
+        apply_runtime_set_overrides(&mut cli)?;
+    }
+
     let pipe_api_key_handoff = matches!(
         &command,
         Some(Commands::Auth(AuthArgs {
@@ -1943,10 +2008,12 @@ fn run() -> Result<()> {
              use the subcommand's own flag (for example `codewhale exec --session-id `)."
         );
     }
-    // Per-run `--set KEY=VALUE` overlays: validated and applied in memory,
-    // never saved. Mutating config subcommands refuse them below rather than
-    // letting a per-run value leak into the file.
-    apply_per_run_overrides(&mut store, &cli.overrides)?;
+    // Only config inspection needs the store overlay. Runtime overrides use
+    // the dedicated flags above and must never enter a store that another
+    // command (or legacy credential migration) can save.
+    if matches!(command, Some(Commands::Config(_))) {
+        apply_per_run_overrides(&mut store, &cli.overrides)?;
+    }
 
     match command {
         Some(Commands::Run(args)) => {
@@ -4544,6 +4611,34 @@ fn run_config_command(
     }
     match command {
         ConfigCommand::Get { key } => {
+            if per_run_overrides.is_empty() && codewhale_tui::route_preferences::is_route_key(&key)
+            {
+                if let Some(value) = codewhale_tui::route_preferences::get(store.path(), &key)? {
+                    println!("{value}");
+                    return Ok(());
+                }
+                bail!("key not found: {key}");
+            }
+            if codewhale_config::notifications::in_namespace(&key) {
+                let config = codewhale_config::notifications::from_extras(&store.config.extras)?;
+                let keys = if key.eq_ignore_ascii_case("notifications") {
+                    codewhale_config::notifications::NotificationSetting::ALL.to_vec()
+                } else {
+                    vec![codewhale_config::notifications::NotificationSetting::required(&key)?]
+                };
+                for setting in keys {
+                    if key.eq_ignore_ascii_case("notifications") {
+                        println!(
+                            "notifications.{} = {}",
+                            setting.key(),
+                            config.display(setting)
+                        );
+                    } else {
+                        println!("{}", config.display(setting));
+                    }
+                }
+                return Ok(());
+            }
             if let Some(value) = store.config.get_display_value(&key) {
                 if key == "telemetry" {
                     println!(
@@ -4559,6 +4654,20 @@ fn run_config_command(
             bail!("key not found: {key}");
         }
         ConfigCommand::Set { key, value } => {
+            if codewhale_tui::route_preferences::is_route_key(&key) {
+                codewhale_tui::route_preferences::set(store.path(), &key, &value)?;
+                store.reload()?;
+                println!("set {key}");
+                return Ok(());
+            }
+            if codewhale_config::notifications::in_namespace(&key) {
+                let setting = codewhale_config::notifications::NotificationSetting::required(&key)?;
+                codewhale_config::notifications::NotificationConfigUpdate::parse(setting, &value)?
+                    .persist(store.path())?;
+                store.reload()?;
+                println!("set notifications.{}", setting.key());
+                return Ok(());
+            }
             store.config.set_value(&key, &value)?;
             if key == "telemetry" {
                 let enabled = store
@@ -4598,6 +4707,19 @@ fn run_config_command(
             Ok(())
         }
         ConfigCommand::Unset { key } => {
+            if codewhale_tui::route_preferences::is_route_key(&key) {
+                codewhale_tui::route_preferences::unset(store.path(), &key)?;
+                store.reload()?;
+                println!("unset {key}");
+                return Ok(());
+            }
+            if codewhale_config::notifications::in_namespace(&key) {
+                let setting = codewhale_config::notifications::NotificationSetting::required(&key)?;
+                setting.unset(store.path())?;
+                store.reload()?;
+                println!("unset notifications.{}", setting.key());
+                return Ok(());
+            }
             store.config.unset_value(&key)?;
             store.save()?;
             println!("unset {key}");
@@ -4683,7 +4805,17 @@ fn run_config_doctor(store: &ConfigStore) -> Result<()> {
     let mut warnings = 0;
     let mut errors: Vec = Vec::new();
 
-    let mut unknown: Vec<&String> = store.config.extras.keys().collect();
+    let mut unknown: Vec<&String> = store
+        .config
+        .extras
+        .keys()
+        .filter(|key| {
+            !matches!(
+                key.as_str(),
+                "route_preferences_version" | "route_preferences_migration"
+            )
+        })
+        .collect();
     unknown.sort();
     for key in unknown {
         println!("warning: unrecognized key `{key}` (preserved, never applied)");
@@ -4814,17 +4946,36 @@ fn run_model_command(
             // re-deriving one from an empty flag set. Re-deriving is what made
             // a Z.ai config report `provider: deepseek` (#4832).
             if queried.is_none() && subcommand_provider.is_none() {
-                let source = resolved_runtime.model_source;
+                let saved = if matches!(resolved_runtime.provider_source, ProviderSource::Config)
+                    && !matches!(
+                        resolved_runtime.model_source,
+                        codewhale_config::ModelSource::Cli | codewhale_config::ModelSource::Env
+                    ) {
+                    Some(codewhale_tui::route_preferences::selected_route(
+                        store.path(),
+                    )?)
+                } else {
+                    None
+                };
+                let provider = saved
+                    .as_ref()
+                    .map_or(resolved_runtime.provider.as_str(), |(provider, _, _)| {
+                        provider.as_str()
+                    });
+                let model = saved
+                    .as_ref()
+                    .map_or(resolved_runtime.model.as_str(), |(_, model, _)| {
+                        model.as_str()
+                    });
+                let source = saved
+                    .as_ref()
+                    .map_or(resolved_runtime.model_source, |(_, _, source)| *source);
                 println!(
                     "requested: {}",
-                    if source.is_explicit() {
-                        resolved_runtime.model.as_str()
-                    } else {
-                        ""
-                    }
+                    if source.is_explicit() { model } else { "" }
                 );
-                println!("resolved: {}", resolved_runtime.model);
-                println!("provider: {}", resolved_runtime.provider.as_str());
+                println!("resolved: {model}");
+                println!("provider: {provider}");
                 println!("used_fallback: {}", !source.is_explicit());
                 println!(
                     "provider_source: {}",
@@ -4872,8 +5023,8 @@ fn run_model_command(
                 bail!("Model name cannot be empty");
             }
             let canonical = canonical_model_for_set(trimmed);
-            store.config.default_text_model = Some(canonical.to_string());
-            store.save()?;
+            codewhale_tui::route_preferences::set(store.path(), "model", canonical)?;
+            store.reload()?;
             println!("Default model set to '{canonical}'");
             Ok(())
         }
@@ -6361,6 +6512,84 @@ verbosity = "project-imported"
         ));
     }
 
+    #[test]
+    fn durable_cli_route_edits_use_canonical_config_and_keep_temporary_overrides_unsaved() {
+        let _env = env_lock();
+        let home = tempfile::tempdir().expect("isolated home");
+        let _home = ScopedEnvVar::set("CODEWHALE_HOME", &home.path().to_string_lossy());
+        let _config_path = ScopedEnvVar::remove("CODEWHALE_CONFIG_PATH");
+        let _legacy_config_path = ScopedEnvVar::remove("DEEPSEEK_CONFIG_PATH");
+        let path = home.path().join("config.toml");
+        std::fs::write(&path, "provider = \"deepseek\"\ndefault_text_model = \"deepseek-v4-pro\"\n[providers.zai]\nmodel = \"GLM-5.2\"\n").unwrap();
+        let settings_path = home.path().join("settings.toml");
+        let settings = "default_provider = \"zai\"\n[provider_models]\nzai = \"GLM-5.3\"\n";
+        std::fs::write(&settings_path, settings).unwrap();
+        let mut store = ConfigStore::load(Some(path.clone())).unwrap();
+        let runtime = resolved_runtime_for_test(ProviderKind::Deepseek, ProviderSource::Config);
+        run_model_command(
+            &mut store,
+            ModelCommand::Set {
+                model: "GLM-5.2".into(),
+            },
+            None,
+            &runtime,
+        )
+        .unwrap();
+        assert_eq!(store.config.provider, ProviderKind::Zai);
+        assert_eq!(store.config.providers.zai.model.as_deref(), Some("GLM-5.2"));
+        assert_eq!(
+            store.config.extras["route_preferences_version"].as_integer(),
+            Some(1)
+        );
+
+        run_config_command(
+            &mut store,
+            ConfigCommand::Set {
+                key: "default_text_model".into(),
+                value: "GLM-5.1".into(),
+            },
+            false,
+            &[],
+        )
+        .unwrap();
+        assert_eq!(store.config.providers.zai.model.as_deref(), Some("GLM-5.1"));
+        assert_eq!(
+            codewhale_tui::route_preferences::get(&path, "model")
+                .unwrap()
+                .as_deref(),
+            Some("GLM-5.1")
+        );
+        run_config_command(
+            &mut store,
+            ConfigCommand::Unset {
+                key: "providers.zai.model".into(),
+            },
+            false,
+            &[],
+        )
+        .unwrap();
+        assert!(store.config.providers.zai.model.is_none());
+        assert_eq!(std::fs::read_to_string(settings_path).unwrap(), settings);
+
+        let before = std::fs::read(&path).unwrap();
+        let overrides = vec!["model=temporary-model".to_string()];
+        assert!(
+            run_config_command(
+                &mut store,
+                ConfigCommand::Set {
+                    key: "model".into(),
+                    value: "GLM-5.2".into(),
+                },
+                false,
+                &overrides
+            )
+            .is_err()
+        );
+        apply_per_run_overrides(&mut store, &overrides).unwrap();
+        assert_eq!(store.config.model.as_deref(), Some("temporary-model"));
+        assert_eq!(std::fs::read(&path).unwrap(), before);
+    }
+
     #[test]
     fn model_set_canonicalizes_deepseek_vision_aliases() {
         for alias in ["flash-vision", "deepseek-v4flashvisionexp"] {
diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs
index fd8081fb46..eb7386f361 100644
--- a/crates/cli/src/main.rs
+++ b/crates/cli/src/main.rs
@@ -1,7 +1,7 @@
-// Default allocator: mimalloc. `--features rusty-alloc` swaps it for
-// rusty_alloc (pure-Rust mimalloc v2.4.5 remake, #5872); the default build
-// is unchanged.
-#[cfg(not(feature = "rusty-alloc"))]
+// Default allocator: mimalloc. `--no-default-features --features rusty-alloc`
+// selects the Rust allocator without building the C allocator (#5872).
+// With neither feature the standard library system allocator is used.
+#[cfg(all(feature = "mimalloc-allocator", not(feature = "rusty-alloc")))]
 #[global_allocator]
 static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;
 
diff --git a/crates/cli/src/metrics.rs b/crates/cli/src/metrics.rs
index b0263ae238..7cc1143f83 100644
--- a/crates/cli/src/metrics.rs
+++ b/crates/cli/src/metrics.rs
@@ -7,16 +7,17 @@
 //! - `~/.codewhale/sessions/`   — saved session JSON files (tool call history)
 //! - `~/.codewhale/tasks/runtime/events/` — runtime thread JSONL event streams
 //!
-//! An install that never migrated off the DeepSeek-era `~/.deepseek` root still
-//! reads there, but only for a path that actually exists — see
-//! `resolve_state_file`.
+//! Default-root audit history includes retained rotations and legacy receipts,
+//! excluding records copied across roots. An explicit `CODEWHALE_HOME` never
+//! reads outside that root.
 
-use std::collections::HashMap;
+use std::collections::{HashMap, HashSet};
 use std::path::{Path, PathBuf};
 
 use anyhow::Result;
 use chrono::{DateTime, Duration, Utc};
 use serde_json::Value;
+use sha2::{Digest, Sha256};
 
 // ──────────────────────────────────────────────────────────────────────────────
 // Public entry-point
@@ -37,7 +38,7 @@ pub fn run(args: MetricsArgs) -> Result<()> {
     // streams off `/runtime`. Resolving the home is fallible, and a
     // rollup of zeros is indistinguishable from real emptiness, so a home we
     // cannot resolve is an error rather than a silent all-zero report.
-    let audit_log = resolve_state_file("audit.log")?;
+    let audit_roots = resolve_audit_roots()?;
     let sessions = codewhale_config::resolve_state_dir("sessions")?;
     let runtime_events = codewhale_config::resolve_state_dir("tasks")?
         .join("runtime")
@@ -45,7 +46,7 @@ pub fn run(args: MetricsArgs) -> Result<()> {
 
     // Collect data from every source; treat missing files as empty.
     let mut rollup = Rollup::default();
-    read_audit_log(&audit_log, args.since, &mut rollup);
+    read_audit_history(&audit_roots, args.since, &mut rollup);
     read_session_files(&sessions, args.since, &mut rollup);
     read_runtime_events(&runtime_events, args.since, &mut rollup);
 
@@ -171,22 +172,73 @@ impl CompactionStats {
     }
 }
 
-/// Sub-agent spawn stats.
+/// Sub-agent lifecycle receipt counts; these are not unique worker totals.
 #[derive(Debug, Default, serde::Serialize)]
 pub struct AgentStats {
     pub spawns: u64,
     pub successes: u64,
     pub failures: u64,
+    pub cancelled: u64,
+    pub interrupted: u64,
+    pub budget_exhausted: u64,
+    /// Terminal receipts with missing, malformed, or unrecognized outcomes.
+    pub unknown_outcomes: u64,
 }
 
 impl AgentStats {
-    fn success_rate_pct(&self) -> Option {
-        let judged = self.successes + self.failures;
-        if judged == 0 {
-            None
+    fn record_completion(&mut self, event: &Value) {
+        // Runtime's worker_status owns the outcome. A completed status item
+        // means its receipt settled, not that the worker succeeded. Preserve
+        // explicit unknown values instead of falling back to a legacy boolean.
+        let status = event
+            .pointer("/details/worker_status")
+            .or_else(|| event.pointer("/payload/worker_status"))
+            .or_else(|| event.pointer("/details/status"))
+            .or_else(|| event.pointer("/payload/status"));
+        let count = if let Some(status) = status {
+            match status.as_str() {
+                Some("completed") => &mut self.successes,
+                Some("failed") => &mut self.failures,
+                Some("cancelled") => &mut self.cancelled,
+                Some("interrupted") => &mut self.interrupted,
+                Some("budget_exhausted") => &mut self.budget_exhausted,
+                _ => &mut self.unknown_outcomes,
+            }
         } else {
-            Some(self.successes as f64 / judged as f64 * 100.0)
+            match event
+                .pointer("/details/success")
+                .or_else(|| event.pointer("/payload/success"))
+                .and_then(Value::as_bool)
+            {
+                Some(true) => &mut self.successes,
+                Some(false) => &mut self.failures,
+                None => &mut self.unknown_outcomes,
+            }
+        };
+        *count = count.saturating_add(1);
+    }
+
+    fn summary(&self) -> String {
+        let outcomes = [
+            (self.successes, "completed"),
+            (self.failures, "failed"),
+            (self.cancelled, "cancelled"),
+            (self.interrupted, "interrupted"),
+            (self.budget_exhausted, "budget exhausted"),
+            (self.unknown_outcomes, "outcome unconfirmed"),
+        ]
+        .into_iter()
+        .filter(|(count, _)| *count > 0)
+        .map(|(count, label)| format!("{} {label}", fmt_num(count)))
+        .collect::>();
+        if self.spawns == 0 && outcomes.is_empty() {
+            return "Sub-agents: (no data)".to_string();
+        }
+        let mut summary = format!("Sub-agents: {} spawn receipts", fmt_num(self.spawns));
+        if !outcomes.is_empty() {
+            summary.push_str(&format!("; outcomes: {}", outcomes.join(", ")));
         }
+        summary
     }
 }
 
@@ -204,6 +256,44 @@ pub struct CredentialStats {
     pub clears: u64,
 }
 
+/// Runtime receipts for model-client dispatch and provider-reported usage.
+///
+/// These are deliberately not billing records: the terminal diagnostics count
+/// parent model-client calls, while `turn.usage` exists only when a provider
+/// supplied usage for one call. Client-internal HTTP retries and invoices are
+/// outside both receipts.
+#[derive(Debug, Default, serde::Serialize)]
+pub struct RuntimeRequestStats {
+    /// Distinct durable `turn.completed` receipts with a usable `(thread, turn)` identity.
+    pub terminal_turn_receipts: u64,
+    /// Terminal receipts carrying the optional request diagnostics projection.
+    pub diagnostics_turn_receipts: u64,
+    /// Terminal receipts from older or partial logs with no diagnostics projection.
+    pub diagnostics_unavailable_turn_receipts: u64,
+    /// Present-but-incomplete diagnostics are unknown rather than zero.
+    pub diagnostics_incomplete_turn_receipts: u64,
+    /// Terminal receipts omitted because their identity could not be verified.
+    pub terminal_receipts_without_identity: u64,
+    /// Repeated terminal snapshots for one `(thread, turn)` omitted from the rollup.
+    pub duplicate_terminal_receipts_skipped: u64,
+    /// Parent model-client calls recorded by terminal diagnostics, not HTTP retries or invoices.
+    pub model_requests_started: u64,
+    pub transparent_stream_retries: u64,
+    pub stream_resumes: u64,
+    /// Distinct `turn.usage` receipts with a verified runtime event identity.
+    pub provider_usage_receipts: u64,
+    /// `turn.usage` records that could not be identified, so their values are unknown.
+    pub provider_usage_receipts_without_identity: u64,
+    /// Repeated runtime event identities omitted from provider usage totals.
+    pub duplicate_provider_usage_receipts_skipped: u64,
+    /// `turn.usage` records missing either required token total are not treated as zero.
+    pub provider_usage_receipts_incomplete: u64,
+    /// Provider-reported per-request input tokens only; terminal cumulative snapshots are excluded.
+    pub provider_reported_input_tokens: u64,
+    /// Provider-reported per-request output tokens only; terminal cumulative snapshots are excluded.
+    pub provider_reported_output_tokens: u64,
+}
+
 /// Top-level rollup.
 #[derive(Debug, Default, serde::Serialize)]
 pub struct Rollup {
@@ -217,12 +307,19 @@ pub struct Rollup {
     pub agents: AgentStats,
     pub capacity: CapacityStats,
     pub credentials: CredentialStats,
+    pub runtime_requests: RuntimeRequestStats,
     /// Total lines read across all sources.
     pub total_lines: u64,
     /// Lines successfully parsed.
     pub parsed_lines: u64,
 }
 
+#[derive(Default)]
+struct RuntimeEventDedup {
+    terminal_turns: HashSet<(String, String)>,
+    event_records: HashSet<(String, u64)>,
+}
+
 impl Rollup {
     fn touch_ts(&mut self, ts: &DateTime) {
         match self.earliest_ts {
@@ -250,8 +347,37 @@ impl Rollup {
 // Source readers
 // ──────────────────────────────────────────────────────────────────────────────
 
-/// Read one-JSON-line-per-event audit log.
-fn read_audit_log(path: &Path, since: Option>, rollup: &mut Rollup) {
+/// Read both retained generations from each root. A copied legacy record is
+/// counted once across roots, while repeated records within one root retain
+/// their multiplicity. No source log is rewritten or removed.
+fn read_audit_history(roots: &[PathBuf], since: Option>, rollup: &mut Rollup) {
+    let mut earlier_roots = HashMap::new();
+    for root in roots {
+        let mut root_counts = HashMap::new();
+        for name in ["audit.log.1", "audit.log"] {
+            read_audit_log(
+                &root.join(name),
+                since,
+                rollup,
+                &earlier_roots,
+                &mut root_counts,
+            );
+        }
+        for (record, count) in root_counts {
+            let prior = earlier_roots.entry(record).or_insert(0);
+            *prior = (*prior).max(count);
+        }
+    }
+}
+
+/// Read one JSON event per line, excluding copies already seen in other roots.
+fn read_audit_log(
+    path: &Path,
+    since: Option>,
+    rollup: &mut Rollup,
+    earlier_roots: &HashMap<[u8; 32], u64>,
+    root_counts: &mut HashMap<[u8; 32], u64>,
+) {
     let content = match std::fs::read_to_string(path) {
         Ok(c) => c,
         Err(e) if e.kind() == std::io::ErrorKind::NotFound => return,
@@ -280,6 +406,16 @@ fn read_audit_log(path: &Path, since: Option>, rollup: &mut Rollup
             }
         };
 
+        // Copy migration preserves the complete event, including its timestamp.
+        // Count occurrences so two identical legitimate records in one source
+        // are not collapsed into one merely because another root also exists.
+        let fingerprint: [u8; 32] = Sha256::digest(v.to_string().as_bytes()).into();
+        let count = root_counts.entry(fingerprint).or_insert(0);
+        *count += 1;
+        if *count <= earlier_roots.get(&fingerprint).copied().unwrap_or(0) {
+            continue;
+        }
+
         // Parse timestamp — field is "ts" in audit log.
         let ts = parse_ts_field(&v, "ts");
 
@@ -362,16 +498,7 @@ fn read_audit_log(path: &Path, since: Option>, rollup: &mut Rollup
                 rollup.agents.spawns += 1;
             }
             "agent.completed" | "subagent.completed" => {
-                let success = v
-                    .pointer("/details/success")
-                    .or_else(|| v.pointer("/payload/success"))
-                    .and_then(|b| b.as_bool())
-                    .unwrap_or(true);
-                if success {
-                    rollup.agents.successes += 1;
-                } else {
-                    rollup.agents.failures += 1;
-                }
+                rollup.agents.record_completion(&v);
             }
             e if e.starts_with("capacity.") => {
                 rollup.capacity.total += 1;
@@ -564,16 +691,22 @@ fn read_runtime_events(events_dir: &Path, since: Option>, rollup:
         }
     };
 
+    let mut dedup = RuntimeEventDedup::default();
     for entry in rd.flatten() {
         let path = entry.path();
         if path.extension().map(|e| e != "jsonl").unwrap_or(true) {
             continue;
         }
-        read_events_jsonl(&path, since, rollup);
+        read_events_jsonl(&path, since, rollup, &mut dedup);
     }
 }
 
-fn read_events_jsonl(path: &Path, since: Option>, rollup: &mut Rollup) {
+fn read_events_jsonl(
+    path: &Path,
+    since: Option>,
+    rollup: &mut Rollup,
+    dedup: &mut RuntimeEventDedup,
+) {
     let content = match std::fs::read_to_string(path) {
         Ok(c) => c,
         Err(e) => {
@@ -618,6 +751,8 @@ fn read_events_jsonl(path: &Path, since: Option>, rollup: &mut Rol
         let event = v.get("event").and_then(|e| e.as_str()).unwrap_or("");
 
         match event {
+            "turn.completed" => record_terminal_request_diagnostics(&v, rollup, dedup),
+            "turn.usage" => record_provider_usage_receipt(&v, rollup, dedup),
             "tool.started" | "tool.completed" | "tool.failed" => {
                 let tool_name = v
                     .pointer("/payload/tool_name")
@@ -653,15 +788,7 @@ fn read_events_jsonl(path: &Path, since: Option>, rollup: &mut Rol
                 rollup.agents.spawns += 1;
             }
             "agent.completed" | "subagent.completed" => {
-                let success = v
-                    .pointer("/payload/success")
-                    .and_then(|b| b.as_bool())
-                    .unwrap_or(true);
-                if success {
-                    rollup.agents.successes += 1;
-                } else {
-                    rollup.agents.failures += 1;
-                }
+                rollup.agents.record_completion(&v);
             }
             e if e.starts_with("capacity.") => {
                 rollup.capacity.total += 1;
@@ -680,6 +807,126 @@ fn read_events_jsonl(path: &Path, since: Option>, rollup: &mut Rol
     }
 }
 
+fn runtime_event_identity(v: &Value) -> Option<(String, u64)> {
+    Some((
+        v.get("thread_id")?.as_str()?.to_string(),
+        v.get("seq")?.as_u64()?,
+    ))
+}
+
+fn terminal_turn_identity(v: &Value) -> Option<(String, String)> {
+    Some((
+        v.get("thread_id")?.as_str()?.to_string(),
+        v.get("turn_id")?.as_str()?.to_string(),
+    ))
+}
+
+fn record_terminal_request_diagnostics(
+    v: &Value,
+    rollup: &mut Rollup,
+    dedup: &mut RuntimeEventDedup,
+) {
+    let Some(identity) = terminal_turn_identity(v) else {
+        rollup.runtime_requests.terminal_receipts_without_identity = rollup
+            .runtime_requests
+            .terminal_receipts_without_identity
+            .saturating_add(1);
+        return;
+    };
+    if !dedup.terminal_turns.insert(identity) {
+        rollup.runtime_requests.duplicate_terminal_receipts_skipped = rollup
+            .runtime_requests
+            .duplicate_terminal_receipts_skipped
+            .saturating_add(1);
+        return;
+    }
+
+    let stats = &mut rollup.runtime_requests;
+    stats.terminal_turn_receipts = stats.terminal_turn_receipts.saturating_add(1);
+    let Some(diagnostics) = v.pointer("/payload/turn/modelRequestDiagnostics") else {
+        stats.diagnostics_unavailable_turn_receipts = stats
+            .diagnostics_unavailable_turn_receipts
+            .saturating_add(1);
+        return;
+    };
+    let Some(model_requests_started) = diagnostics
+        .get("modelRequestsStarted")
+        .and_then(Value::as_u64)
+    else {
+        stats.diagnostics_incomplete_turn_receipts =
+            stats.diagnostics_incomplete_turn_receipts.saturating_add(1);
+        return;
+    };
+    let Some(transparent_stream_retries) = diagnostics
+        .get("transparentStreamRetries")
+        .and_then(Value::as_u64)
+    else {
+        stats.diagnostics_incomplete_turn_receipts =
+            stats.diagnostics_incomplete_turn_receipts.saturating_add(1);
+        return;
+    };
+    let Some(stream_resumes) = diagnostics.get("streamResumes").and_then(Value::as_u64) else {
+        stats.diagnostics_incomplete_turn_receipts =
+            stats.diagnostics_incomplete_turn_receipts.saturating_add(1);
+        return;
+    };
+
+    stats.diagnostics_turn_receipts = stats.diagnostics_turn_receipts.saturating_add(1);
+    stats.model_requests_started = stats
+        .model_requests_started
+        .saturating_add(model_requests_started);
+    stats.transparent_stream_retries = stats
+        .transparent_stream_retries
+        .saturating_add(transparent_stream_retries);
+    stats.stream_resumes = stats.stream_resumes.saturating_add(stream_resumes);
+}
+
+fn record_provider_usage_receipt(v: &Value, rollup: &mut Rollup, dedup: &mut RuntimeEventDedup) {
+    let Some(identity) = runtime_event_identity(v) else {
+        rollup
+            .runtime_requests
+            .provider_usage_receipts_without_identity = rollup
+            .runtime_requests
+            .provider_usage_receipts_without_identity
+            .saturating_add(1);
+        return;
+    };
+    if !dedup.event_records.insert(identity) {
+        rollup
+            .runtime_requests
+            .duplicate_provider_usage_receipts_skipped = rollup
+            .runtime_requests
+            .duplicate_provider_usage_receipts_skipped
+            .saturating_add(1);
+        return;
+    }
+
+    let stats = &mut rollup.runtime_requests;
+    let Some(input_tokens) = v
+        .pointer("/payload/usage/input_tokens")
+        .and_then(Value::as_u64)
+    else {
+        stats.provider_usage_receipts_incomplete =
+            stats.provider_usage_receipts_incomplete.saturating_add(1);
+        return;
+    };
+    let Some(output_tokens) = v
+        .pointer("/payload/usage/output_tokens")
+        .and_then(Value::as_u64)
+    else {
+        stats.provider_usage_receipts_incomplete =
+            stats.provider_usage_receipts_incomplete.saturating_add(1);
+        return;
+    };
+    stats.provider_usage_receipts = stats.provider_usage_receipts.saturating_add(1);
+    stats.provider_reported_input_tokens = stats
+        .provider_reported_input_tokens
+        .saturating_add(input_tokens);
+    stats.provider_reported_output_tokens = stats
+        .provider_reported_output_tokens
+        .saturating_add(output_tokens);
+}
+
 // ──────────────────────────────────────────────────────────────────────────────
 // Output formatters
 // ──────────────────────────────────────────────────────────────────────────────
@@ -786,19 +1033,7 @@ fn print_human(rollup: &Rollup) {
     }
 
     // ── Sub-agents ─────────────────────────────────────────────────────────
-    if rollup.agents.spawns > 0 {
-        let rate_str = match rollup.agents.success_rate_pct() {
-            Some(pct) => format!(", {pct:.1}% success"),
-            None => String::new(),
-        };
-        println!(
-            "Sub-agents: {} spawns{}",
-            fmt_num(rollup.agents.spawns),
-            rate_str
-        );
-    } else {
-        println!("Sub-agents: (no data)");
-    }
+    println!("{}", rollup.agents.summary());
 
     // ── Capacity interventions ─────────────────────────────────────────────
     if rollup.capacity.total > 0 {
@@ -819,6 +1054,55 @@ fn print_human(rollup: &Rollup) {
         println!("Capacity interventions: (no data)");
     }
 
+    // ── Runtime request and provider-usage receipts ───────────────────────
+    let runtime = &rollup.runtime_requests;
+    if runtime.terminal_turn_receipts == 0 {
+        println!("Runtime requests: (no terminal receipts; model-client counts unknown)");
+    } else if runtime.diagnostics_turn_receipts == 0 {
+        println!(
+            "Runtime requests: (diagnostics unavailable for all {} terminal receipts)",
+            fmt_num(runtime.terminal_turn_receipts)
+        );
+    } else {
+        println!(
+            "Runtime requests: {} model-client calls, {} stream resumes, {} transparent retries (diagnostics for {}/{} terminal receipts; status events excluded)",
+            fmt_num(runtime.model_requests_started),
+            fmt_num(runtime.stream_resumes),
+            fmt_num(runtime.transparent_stream_retries),
+            fmt_num(runtime.diagnostics_turn_receipts),
+            fmt_num(runtime.terminal_turn_receipts),
+        );
+    }
+    if runtime.provider_usage_receipts == 0 {
+        println!("Provider usage receipts: (none recorded; this is not zero usage)");
+    } else {
+        println!(
+            "Provider usage receipts: {} records, {} input tokens, {} output tokens",
+            fmt_num(runtime.provider_usage_receipts),
+            fmt_num(runtime.provider_reported_input_tokens),
+            fmt_num(runtime.provider_reported_output_tokens),
+        );
+    }
+    if runtime.diagnostics_unavailable_turn_receipts > 0
+        || runtime.diagnostics_incomplete_turn_receipts > 0
+        || runtime.terminal_receipts_without_identity > 0
+        || runtime.provider_usage_receipts_without_identity > 0
+        || runtime.provider_usage_receipts_incomplete > 0
+        || runtime.duplicate_terminal_receipts_skipped > 0
+        || runtime.duplicate_provider_usage_receipts_skipped > 0
+    {
+        println!(
+            "Runtime receipt coverage: {} diagnostics unavailable, {} diagnostics incomplete, {} terminal receipts without identity, {} usage receipts without identity, {} usage receipts incomplete, {} duplicate terminal receipts skipped, {} duplicate usage receipts skipped",
+            fmt_num(runtime.diagnostics_unavailable_turn_receipts),
+            fmt_num(runtime.diagnostics_incomplete_turn_receipts),
+            fmt_num(runtime.terminal_receipts_without_identity),
+            fmt_num(runtime.provider_usage_receipts_without_identity),
+            fmt_num(runtime.provider_usage_receipts_incomplete),
+            fmt_num(runtime.duplicate_terminal_receipts_skipped),
+            fmt_num(runtime.duplicate_provider_usage_receipts_skipped),
+        );
+    }
+
     // ── Credentials ────────────────────────────────────────────────────────
     if rollup.credentials.saves > 0 || rollup.credentials.clears > 0 {
         println!(
@@ -832,26 +1116,18 @@ fn print_human(rollup: &Rollup) {
 // Helpers
 // ──────────────────────────────────────────────────────────────────────────────
 
-/// Resolve a file that lives directly in the state root, preferring the
-/// canonical Codewhale root.
-///
-/// This is the file-shaped twin of `codewhale_config::resolve_state_dir` (and
-/// of `default_config_path`, which resolves `config.toml` the same way): the
-/// primary path wins whenever it exists, the legacy DeepSeek path is used only
-/// when it is the *only* one present, and with neither present the primary is
-/// returned so an empty rollup names the canonical location. An explicit
-/// `CODEWHALE_HOME` is an isolation boundary and never falls back.
-///
-/// The two roots are never unioned. `ensure_state_dir` may migrate legacy state
-/// by *copying* it (`StateMigrationKind::Copied` leaves the legacy tree in
-/// place), so summing both roots would double-count every migrated record.
-fn resolve_state_file(name: &str) -> Result {
-    let primary = codewhale_config::codewhale_home()?.join(name);
-    if codewhale_config::codewhale_home_is_explicit() || primary.exists() {
-        return Ok(primary);
-    }
-    let legacy = codewhale_config::legacy_deepseek_home()?.join(name);
-    Ok(if legacy.exists() { legacy } else { primary })
+/// An explicit home is an isolation boundary. Default installs can have
+/// distinct audit histories in both roots, even after a copied migration.
+fn resolve_audit_roots() -> Result> {
+    let primary = codewhale_config::codewhale_home()?;
+    let mut roots = vec![primary];
+    if !codewhale_config::codewhale_home_is_explicit() {
+        let legacy = codewhale_config::legacy_deepseek_home()?;
+        if !roots.contains(&legacy) {
+            roots.push(legacy);
+        }
+    }
+    Ok(roots)
 }
 
 /// Parse a timestamp from a JSON value field (tries RFC3339).
@@ -880,6 +1156,156 @@ fn fmt_num(n: u64) -> String {
 mod tests {
     use super::*;
 
+    fn read_audit_test_log(path: &Path, since: Option>, rollup: &mut Rollup) {
+        super::read_audit_log(path, since, rollup, &HashMap::new(), &mut HashMap::new());
+    }
+
+    fn read_runtime_test_log(path: &Path, since: Option>, rollup: &mut Rollup) {
+        super::read_events_jsonl(path, since, rollup, &mut RuntimeEventDedup::default());
+    }
+
+    fn runtime_event(
+        seq: u64,
+        timestamp: &str,
+        thread_id: &str,
+        turn_id: Option<&str>,
+        event: &str,
+        payload: Value,
+    ) -> Value {
+        serde_json::json!({
+            "schema_version": 4,
+            "seq": seq,
+            "timestamp": timestamp,
+            "thread_id": thread_id,
+            "turn_id": turn_id,
+            "event": event,
+            "payload": payload,
+        })
+    }
+
+    fn write_runtime_events(events: &[Value]) -> tempfile::NamedTempFile {
+        use std::io::Write;
+
+        let mut tmp = tempfile::NamedTempFile::new().unwrap();
+        for event in events {
+            writeln!(tmp, "{event}").unwrap();
+        }
+        tmp
+    }
+
+    #[test]
+    fn runtime_worker_completion_uses_owner_outcome_not_completed_receipt_status() {
+        let statuses = [
+            serde_json::json!("completed"),
+            serde_json::json!("failed"),
+            serde_json::json!("cancelled"),
+            serde_json::json!("interrupted"),
+            serde_json::json!("budget_exhausted"),
+            Value::Null,
+        ];
+        let events: Vec<_> = statuses
+            .into_iter()
+            .enumerate()
+            .map(|(seq, worker_status)| {
+                runtime_event(
+                    seq as u64,
+                    "2026-09-08T10:00:00Z",
+                    "thread-a",
+                    Some("turn-a"),
+                    "agent.completed",
+                    serde_json::json!({
+                        "item": { "kind": "status", "status": "completed" },
+                        "agent_id": format!("worker-{seq}"),
+                        "worker_status": worker_status,
+                        "parent_run_id": "run-a",
+                        "spawn_depth": 1,
+                        "continuable": false,
+                    }),
+                )
+            })
+            .collect();
+        let tmp = write_runtime_events(&events);
+        let mut rollup = Rollup::default();
+        read_runtime_test_log(tmp.path(), None, &mut rollup);
+        let agents = &rollup.agents;
+        assert_eq!(agents.successes, 1, "a settled item is not worker success");
+        assert_eq!(agents.failures, 1);
+        assert_eq!(agents.cancelled, 1);
+        assert_eq!(agents.interrupted, 1);
+        assert_eq!(agents.budget_exhausted, 1);
+        assert_eq!(agents.unknown_outcomes, 1);
+        assert_eq!(agents.spawns, 0);
+        let summary = agents.summary();
+        assert!(summary.contains("1 failed"));
+        assert!(summary.contains("1 outcome unconfirmed"));
+        assert!(
+            !summary.contains("no data"),
+            "terminal-only windows have data"
+        );
+        assert!(
+            !summary.contains("%"),
+            "partial receipts are not a success rate"
+        );
+    }
+
+    #[test]
+    fn runtime_legacy_worker_receipts_require_explicit_success_evidence() {
+        let payloads = [
+            serde_json::json!({ "success": true }),
+            serde_json::json!({ "success": false }),
+            serde_json::json!({}),
+            serde_json::json!({ "success": "true" }),
+            serde_json::json!({ "worker_status": "failed", "success": true }),
+            serde_json::json!({ "worker_status": null, "success": true }),
+            serde_json::json!({ "worker_status": "running", "success": true }),
+            serde_json::json!({ "worker_status": { "completed": true }, "success": true }),
+            serde_json::json!({ "worker_status": "future_outcome", "success": true }),
+        ];
+        let events: Vec<_> = payloads
+            .into_iter()
+            .enumerate()
+            .map(|(seq, payload)| {
+                runtime_event(
+                    seq as u64,
+                    "2026-09-08T10:00:00Z",
+                    "thread-a",
+                    Some("turn-a"),
+                    "agent.completed",
+                    payload,
+                )
+            })
+            .collect();
+        let tmp = write_runtime_events(&events);
+        let mut rollup = Rollup::default();
+        read_runtime_test_log(tmp.path(), None, &mut rollup);
+        assert_eq!(rollup.agents.successes, 1);
+        assert_eq!(rollup.agents.failures, 2);
+        assert_eq!(rollup.agents.unknown_outcomes, 6);
+    }
+
+    #[test]
+    fn audit_worker_receipts_share_typed_and_legacy_outcome_rules() {
+        let events = [
+            serde_json::json!({ "event": "agent.completed", "details": { "worker_status": "failed", "success": true } }),
+            serde_json::json!({ "event": "subagent.completed", "payload": { "status": "cancelled", "success": true } }),
+            serde_json::json!({ "event": "subagent.completed", "details": { "status": "completed" } }),
+            serde_json::json!({ "event": "agent.completed", "details": { "success": false } }),
+            serde_json::json!({ "event": "agent.completed", "payload": { "success": true } }),
+            serde_json::json!({ "event": "agent.completed", "details": { "worker_status": null, "success": true } }),
+            serde_json::json!({ "event": "agent.completed", "details": {} }),
+        ];
+        let tmp = write_runtime_events(&events);
+        let mut rollup = Rollup::default();
+        read_audit_test_log(tmp.path(), None, &mut rollup);
+        assert_eq!(rollup.agents.successes, 2);
+        assert_eq!(rollup.agents.failures, 2);
+        assert_eq!(rollup.agents.cancelled, 1);
+        assert_eq!(rollup.agents.unknown_outcomes, 2);
+        let json = serde_json::to_value(&rollup).unwrap();
+        assert_eq!(json["agents"]["unknown_outcomes"], 2);
+        assert_eq!(json["agents"]["cancelled"], 1);
+    }
+
     // ── Duration parser ──
 
     #[test]
@@ -958,7 +1384,7 @@ mod tests {
     fn audit_log_empty_file() {
         let mut rollup = Rollup::default();
         // Non-existent path — should not panic, rollup stays empty.
-        read_audit_log(Path::new("/nonexistent/audit.log"), None, &mut rollup);
+        read_audit_test_log(Path::new("/nonexistent/audit.log"), None, &mut rollup);
         assert_eq!(rollup.total_lines, 0);
     }
 
@@ -980,7 +1406,7 @@ mod tests {
         writeln!(tmp, "{line2}").unwrap();
 
         let mut rollup = Rollup::default();
-        read_audit_log(tmp.path(), None, &mut rollup);
+        read_audit_test_log(tmp.path(), None, &mut rollup);
 
         assert_eq!(rollup.parsed_lines, 2);
         assert_eq!(rollup.tools["exec_shell"].calls, 1);
@@ -1000,7 +1426,7 @@ mod tests {
         .unwrap();
 
         let mut rollup = Rollup::default();
-        read_audit_log(tmp.path(), None, &mut rollup);
+        read_audit_test_log(tmp.path(), None, &mut rollup);
 
         // 2 lines total, 1 malformed skipped, 1 parsed.
         assert_eq!(rollup.total_lines, 2);
@@ -1027,7 +1453,7 @@ mod tests {
 
         let cutoff: DateTime = "2026-01-01T00:00:00Z".parse().unwrap();
         let mut rollup = Rollup::default();
-        read_audit_log(tmp.path(), Some(cutoff), &mut rollup);
+        read_audit_test_log(tmp.path(), Some(cutoff), &mut rollup);
 
         // Only the newer line should be counted.
         assert_eq!(rollup.parsed_lines, 1);
@@ -1043,6 +1469,243 @@ mod tests {
         assert_eq!(rollup.total_tool_calls(), 5_130);
     }
 
+    // ── Runtime request and provider-usage receipts ──
+
+    #[test]
+    fn runtime_receipts_separate_terminal_requests_from_per_request_usage() {
+        let timestamp = "2026-09-08T10:00:00Z";
+        let terminal = runtime_event(
+            2,
+            timestamp,
+            "thread-a",
+            Some("turn-a"),
+            "turn.completed",
+            serde_json::json!({
+                "turn": {
+                    "usage": { "input_tokens": 10_000, "output_tokens": 9_000 },
+                    "modelRequestDiagnostics": {
+                        "modelRequestsStarted": 2,
+                        "transparentStreamRetries": 1,
+                        "streamResumes": 1,
+                    },
+                },
+            }),
+        );
+        let usage_one = runtime_event(
+            3,
+            timestamp,
+            "thread-a",
+            Some("turn-a"),
+            "turn.usage",
+            serde_json::json!({ "usage": { "input_tokens": 7, "output_tokens": 2 } }),
+        );
+        let usage_two = runtime_event(
+            4,
+            timestamp,
+            "thread-a",
+            Some("turn-a"),
+            "turn.usage",
+            serde_json::json!({ "usage": { "input_tokens": 11, "output_tokens": 3 } }),
+        );
+        let duplicate_terminal = runtime_event(
+            5,
+            timestamp,
+            "thread-a",
+            Some("turn-a"),
+            "turn.completed",
+            serde_json::json!({
+                "turn": {
+                    "modelRequestDiagnostics": {
+                        "modelRequestsStarted": 99,
+                        "transparentStreamRetries": 99,
+                        "streamResumes": 99,
+                    },
+                },
+            }),
+        );
+        let legacy_terminal = runtime_event(
+            6,
+            timestamp,
+            "thread-a",
+            Some("turn-b"),
+            "turn.completed",
+            serde_json::json!({ "turn": { "usage": { "input_tokens": 50, "output_tokens": 5 } } }),
+        );
+        let status = runtime_event(
+            7,
+            timestamp,
+            "thread-a",
+            Some("turn-a"),
+            "item.completed",
+            serde_json::json!({ "item": { "kind": "status" } }),
+        );
+        let tmp = write_runtime_events(&[
+            terminal,
+            usage_one,
+            usage_two,
+            duplicate_terminal,
+            legacy_terminal,
+            status,
+        ]);
+        let mut rollup = Rollup::default();
+        read_runtime_test_log(tmp.path(), None, &mut rollup);
+
+        let runtime = &rollup.runtime_requests;
+        assert_eq!(runtime.terminal_turn_receipts, 2);
+        assert_eq!(runtime.diagnostics_turn_receipts, 1);
+        assert_eq!(runtime.diagnostics_unavailable_turn_receipts, 1);
+        assert_eq!(runtime.duplicate_terminal_receipts_skipped, 1);
+        assert_eq!(runtime.model_requests_started, 2);
+        assert_eq!(runtime.transparent_stream_retries, 1);
+        assert_eq!(runtime.stream_resumes, 1);
+        assert_eq!(runtime.provider_usage_receipts, 2);
+        assert_eq!(runtime.provider_reported_input_tokens, 18);
+        assert_eq!(runtime.provider_reported_output_tokens, 5);
+        assert_ne!(runtime.provider_reported_input_tokens, 10_018);
+        assert_eq!(runtime.model_requests_started, 2, "status is not a request");
+    }
+
+    #[test]
+    fn runtime_usage_receipts_deduplicate_by_runtime_event_identity() {
+        let usage = runtime_event(
+            20,
+            "2026-09-08T10:00:00Z",
+            "thread-a",
+            Some("turn-a"),
+            "turn.usage",
+            serde_json::json!({ "usage": { "input_tokens": 7, "output_tokens": 2 } }),
+        );
+        let tmp = write_runtime_events(&[usage.clone(), usage]);
+        let mut rollup = Rollup::default();
+        read_runtime_test_log(tmp.path(), None, &mut rollup);
+
+        let runtime = &rollup.runtime_requests;
+        assert_eq!(runtime.provider_usage_receipts, 1);
+        assert_eq!(runtime.provider_reported_input_tokens, 7);
+        assert_eq!(runtime.provider_reported_output_tokens, 2);
+        assert_eq!(runtime.duplicate_provider_usage_receipts_skipped, 1);
+    }
+
+    #[test]
+    fn runtime_receipt_coverage_marks_unidentified_or_incomplete_old_records_unknown() {
+        let terminal_without_identity = serde_json::json!({
+            "timestamp": "2026-09-08T10:00:00Z",
+            "event": "turn.completed",
+            "payload": {
+                "turn": {
+                    "modelRequestDiagnostics": {
+                        "modelRequestsStarted": 3,
+                        "transparentStreamRetries": 1,
+                        "streamResumes": 2,
+                    },
+                },
+            },
+        });
+        let usage_without_identity = serde_json::json!({
+            "timestamp": "2026-09-08T10:00:00Z",
+            "event": "turn.usage",
+            "payload": { "usage": { "input_tokens": 9, "output_tokens": 4 } },
+        });
+        let incomplete_diagnostics = runtime_event(
+            30,
+            "2026-09-08T10:00:00Z",
+            "thread-a",
+            Some("turn-b"),
+            "turn.completed",
+            serde_json::json!({
+                "turn": { "modelRequestDiagnostics": { "modelRequestsStarted": 3 } },
+            }),
+        );
+        let incomplete_usage = runtime_event(
+            31,
+            "2026-09-08T10:00:00Z",
+            "thread-a",
+            Some("turn-b"),
+            "turn.usage",
+            serde_json::json!({ "usage": { "input_tokens": 9 } }),
+        );
+        let tmp = write_runtime_events(&[
+            terminal_without_identity,
+            usage_without_identity,
+            incomplete_diagnostics,
+            incomplete_usage,
+        ]);
+        let mut rollup = Rollup::default();
+        read_runtime_test_log(tmp.path(), None, &mut rollup);
+
+        let runtime = &rollup.runtime_requests;
+        assert_eq!(runtime.terminal_receipts_without_identity, 1);
+        assert_eq!(runtime.terminal_turn_receipts, 1);
+        assert_eq!(runtime.diagnostics_incomplete_turn_receipts, 1);
+        assert_eq!(runtime.model_requests_started, 0);
+        assert_eq!(runtime.provider_usage_receipts_without_identity, 1);
+        assert_eq!(runtime.provider_usage_receipts_incomplete, 1);
+        assert_eq!(runtime.provider_usage_receipts, 0);
+        assert_eq!(runtime.provider_reported_input_tokens, 0);
+    }
+
+    #[test]
+    fn runtime_receipts_respect_since_cutoff_without_crossing_snapshot_boundaries() {
+        let old_terminal = runtime_event(
+            40,
+            "2026-09-01T10:00:00Z",
+            "thread-a",
+            Some("turn-old"),
+            "turn.completed",
+            serde_json::json!({
+                "turn": { "modelRequestDiagnostics": {
+                    "modelRequestsStarted": 4,
+                    "transparentStreamRetries": 1,
+                    "streamResumes": 2,
+                } },
+            }),
+        );
+        let old_usage = runtime_event(
+            41,
+            "2026-09-01T10:00:00Z",
+            "thread-a",
+            Some("turn-old"),
+            "turn.usage",
+            serde_json::json!({ "usage": { "input_tokens": 40, "output_tokens": 4 } }),
+        );
+        let new_terminal = runtime_event(
+            42,
+            "2026-09-08T10:00:00Z",
+            "thread-a",
+            Some("turn-new"),
+            "turn.completed",
+            serde_json::json!({
+                "turn": { "modelRequestDiagnostics": {
+                    "modelRequestsStarted": 1,
+                    "transparentStreamRetries": 0,
+                    "streamResumes": 0,
+                } },
+            }),
+        );
+        let new_usage = runtime_event(
+            43,
+            "2026-09-08T10:00:00Z",
+            "thread-a",
+            Some("turn-new"),
+            "turn.usage",
+            serde_json::json!({ "usage": { "input_tokens": 10, "output_tokens": 1 } }),
+        );
+        let tmp = write_runtime_events(&[old_terminal, old_usage, new_terminal, new_usage]);
+        let mut rollup = Rollup::default();
+        read_runtime_test_log(
+            tmp.path(),
+            Some("2026-09-08T00:00:00Z".parse().unwrap()),
+            &mut rollup,
+        );
+
+        let runtime = &rollup.runtime_requests;
+        assert_eq!(runtime.terminal_turn_receipts, 1);
+        assert_eq!(runtime.model_requests_started, 1);
+        assert_eq!(runtime.provider_usage_receipts, 1);
+        assert_eq!(runtime.provider_reported_input_tokens, 10);
+        assert_eq!(runtime.provider_reported_output_tokens, 1);
+    }
+
     // ── State-root resolution ──
     //
     // These pin *which* files the rollup reads. Before the fix the reader
@@ -1072,70 +1735,101 @@ mod tests {
     }
 
     #[test]
-    fn state_files_resolve_under_the_codewhale_root_on_a_clean_install() {
+    fn default_audit_history_includes_both_roots_without_requiring_existing_files() {
         let (home, _lock, _env) = isolated_home();
         assert_eq!(
-            resolve_state_file("audit.log").expect("resolves"),
-            home.path().join(".codewhale").join("audit.log"),
-            "the reader must land on the root the audit writer actually writes"
+            resolve_audit_roots().expect("resolves"),
+            vec![
+                home.path().join(".codewhale"),
+                home.path().join(".deepseek")
+            ],
         );
     }
 
     #[test]
-    fn a_legacy_file_is_used_only_when_it_is_the_one_that_exists() {
-        let (home, _lock, _env) = isolated_home();
-        let legacy = home.path().join(".deepseek");
-        std::fs::create_dir_all(&legacy).expect("legacy root");
-        std::fs::write(legacy.join("audit.log"), b"{}\n").expect("legacy log");
-
-        assert_eq!(
-            resolve_state_file("audit.log").expect("resolves"),
+    fn copied_audit_history_keeps_unique_legacy_and_rotated_records() {
+        let dir = tempfile::TempDir::new().expect("tempdir");
+        let primary = dir.path().join("primary");
+        let legacy = dir.path().join("legacy");
+        std::fs::create_dir_all(&primary).unwrap();
+        std::fs::create_dir_all(&legacy).unwrap();
+        let shared = r#"{"ts":"2026-09-01T00:00:00Z","event":"credential.save","details":{}}"#;
+        let old = r#"{"ts":"2026-08-01T00:00:00Z","event":"credential.clear","details":{}}"#;
+        let new = r#"{"ts":"2026-09-02T00:00:00Z","event":"credential.save","details":{}}"#;
+        std::fs::write(primary.join("audit.log.1"), format!("{shared}\n{shared}\n")).unwrap();
+        std::fs::write(primary.join("audit.log"), format!("{new}\nmalformed\n")).unwrap();
+        std::fs::write(
             legacy.join("audit.log"),
-            "real DeepSeek-era receipts must not be dropped on the floor"
+            format!("{shared}\n{shared}\n{shared}\n"),
+        )
+        .unwrap();
+        std::fs::write(legacy.join("audit.log.1"), format!("{old}\n")).unwrap();
+        let roots = [primary, legacy];
+        let before: Vec<_> = roots
+            .iter()
+            .flat_map(|root| {
+                ["audit.log.1", "audit.log"].map(|name| {
+                    let path = root.join(name);
+                    (path.clone(), std::fs::read(path).unwrap())
+                })
+            })
+            .collect();
+        let mut rollup = Rollup::default();
+        read_audit_history(&roots, None, &mut rollup);
+        assert_eq!(
+            rollup.credentials.saves, 4,
+            "maximum occurrence count across copied roots"
         );
-
-        // Once the canonical file exists it wins outright; the two roots are
-        // never summed, because legacy state may have been migrated by copy.
-        let primary = home.path().join(".codewhale");
-        std::fs::create_dir_all(&primary).expect("primary root");
-        std::fs::write(primary.join("audit.log"), b"{}\n").expect("primary log");
         assert_eq!(
-            resolve_state_file("audit.log").expect("resolves"),
-            primary.join("audit.log")
+            rollup.credentials.clears, 1,
+            "unique old rotation is retained"
+        );
+        assert_eq!(rollup.parsed_lines, 5);
+        for (path, bytes) in before {
+            assert_eq!(
+                std::fs::read(path).unwrap(),
+                bytes,
+                "source history is read-only"
+            );
+        }
+        let mut recent = Rollup::default();
+        read_audit_history(
+            &roots,
+            Some("2026-09-01T00:00:00Z".parse().unwrap()),
+            &mut recent,
         );
+        assert_eq!(recent.credentials.saves, 4);
+        assert_eq!(recent.credentials.clears, 0);
     }
 
     #[test]
-    fn an_explicit_codewhale_home_is_an_isolation_boundary() {
+    fn an_explicit_codewhale_home_is_an_audit_isolation_boundary() {
         let (home, _lock, _env) = isolated_home();
         let legacy = home.path().join(".deepseek");
-        std::fs::create_dir_all(&legacy).expect("legacy root");
-        std::fs::write(legacy.join("audit.log"), b"{}\n").expect("legacy log");
-
-        let explicit = tempfile::TempDir::new().expect("tempdir");
+        std::fs::create_dir_all(&legacy).unwrap();
+        std::fs::write(legacy.join("audit.log"), r#"{"event":"credential.save"}"#).unwrap();
+        let explicit = tempfile::TempDir::new().unwrap();
         let _pin =
             crate::tests::ScopedEnvVar::set("CODEWHALE_HOME", &explicit.path().to_string_lossy());
-
-        assert_eq!(
-            resolve_state_file("audit.log").expect("resolves"),
-            explicit.path().join("audit.log"),
-            "an explicit home must never reach outside its own root"
-        );
+        let roots = resolve_audit_roots().unwrap();
+        assert_eq!(roots, vec![explicit.path().to_path_buf()]);
+        let mut rollup = Rollup::default();
+        read_audit_history(&roots, None, &mut rollup);
+        assert_eq!(rollup.parsed_lines, 0);
     }
 
     #[test]
     fn the_legacy_deepseek_home_variable_is_no_longer_honoured() {
-        // docs/CONFIGURATION.md tells upgraders to rename DEEPSEEK_HOME to
-        // CODEWHALE_HOME; every other subsystem already ignores it, and this
-        // reader was the last consumer of the legacy alias.
         let (home, _lock, _env) = isolated_home();
-        let stale = tempfile::TempDir::new().expect("tempdir");
+        let stale = tempfile::TempDir::new().unwrap();
         let _stale =
             crate::tests::ScopedEnvVar::set("DEEPSEEK_HOME", &stale.path().to_string_lossy());
-
         assert_eq!(
-            resolve_state_file("audit.log").expect("resolves"),
-            home.path().join(".codewhale").join("audit.log")
+            resolve_audit_roots().unwrap(),
+            vec![
+                home.path().join(".codewhale"),
+                home.path().join(".deepseek")
+            ],
         );
     }
 }
diff --git a/crates/cli/tests/notification_config.rs b/crates/cli/tests/notification_config.rs
new file mode 100644
index 0000000000..72e77e4c7b
--- /dev/null
+++ b/crates/cli/tests/notification_config.rs
@@ -0,0 +1,89 @@
+//! Real dispatcher regression: nested config edits retain TOML types and
+//! unrelated bytes. Only config commands run; no provider, player or OS banner.
+use std::{
+    fs,
+    path::Path,
+    process::{Command, Output},
+};
+
+fn run(root: &Path, args: &[&str]) -> Output {
+    Command::new(env!("CARGO_BIN_EXE_codewhale"))
+        .env_clear()
+        .env("HOME", root)
+        .env("USERPROFILE", root)
+        .env("CODEWHALE_HOME", root.join("state"))
+        .env("CODEWHALE_SECRET_BACKEND", "file")
+        .current_dir(root)
+        .arg("--config")
+        .arg(root.join("config.toml"))
+        .arg("config")
+        .args(args)
+        .output()
+        .unwrap()
+}
+fn ok(root: &Path, args: &[&str]) -> String {
+    let out = run(root, args);
+    assert!(
+        out.status.success(),
+        "{args:?}: {}",
+        String::from_utf8_lossy(&out.stderr)
+    );
+    String::from_utf8(out.stdout).unwrap()
+}
+
+#[test]
+fn nested_notification_cli_set_get_unset_is_typed_lossless_and_validated() {
+    let temp = tempfile::tempdir().unwrap();
+    let root = temp.path();
+    let path = root.join("config.toml");
+    fs::write(&path, "# keep operator note\n\"notifications.quiet\" = \"false\"\n[notifications]\nfuture = \"keep\"\n[notifications.events]\ninput-needed = false\n").unwrap();
+    for (key, value) in [
+        ("sound", "whale"),
+        ("quiet", "true"),
+        ("threshold_secs", "17"),
+        ("events.approval-needed", "false"),
+        ("event_sound.events", r#"["model-notify", "input-needed"]"#),
+        ("sound_file", "custom call.wav"),
+    ] {
+        ok(root, &["set", &format!("notifications.{key}"), value]);
+    }
+    assert_eq!(ok(root, &["get", "notifications.sound"]).trim(), "whale");
+    assert_eq!(ok(root, &["get", "notifications.quiet"]).trim(), "true");
+    let saved = fs::read_to_string(&path).unwrap();
+    assert!(saved.contains("# keep operator note"));
+    let raw: toml::Value = toml::from_str(&saved).unwrap();
+    assert_eq!(raw["notifications"]["quiet"].as_bool(), Some(true));
+    assert_eq!(
+        raw["notifications"]["threshold_secs"].as_integer(),
+        Some(17)
+    );
+    assert_eq!(
+        raw["notifications"]["event_sound"]["events"]
+            .as_array()
+            .unwrap()
+            .len(),
+        2
+    );
+    assert_eq!(
+        raw["notifications"]["events"]["input-needed"].as_bool(),
+        Some(false)
+    );
+    assert_eq!(raw["notifications"]["future"].as_str(), Some("keep"));
+    assert!(!raw.as_table().unwrap().contains_key("notifications.quiet"));
+    for (key, value) in [
+        ("notifications", "false"),
+        ("notifications.quiet", "maybe"),
+        ("notifications.threshold_secs", "18446744073709551615"),
+        ("notifications.event_sound.events", r#"["unknown"]"#),
+    ] {
+        assert!(!run(root, &["set", key, value]).status.success());
+        assert_eq!(fs::read_to_string(&path).unwrap(), saved);
+    }
+    ok(root, &["unset", "notifications.quiet"]);
+    assert_eq!(ok(root, &["get", "notifications.quiet"]).trim(), "false");
+    assert_eq!(ok(root, &["get", "notifications.sound"]).trim(), "whale");
+    assert!(
+        ok(root, &["get", "notifications"])
+            .contains("notifications.events.approval-needed = false")
+    );
+}
diff --git a/crates/cli/tests/runtime_set_dispatch.rs b/crates/cli/tests/runtime_set_dispatch.rs
new file mode 100644
index 0000000000..dadc02fd26
--- /dev/null
+++ b/crates/cli/tests/runtime_set_dispatch.rs
@@ -0,0 +1,430 @@
+//! Exercise the public CLI boundary: the runtime reloads configuration after
+//! dispatch, so testing the outer store alone cannot catch lost overrides.
+
+use std::fs;
+use std::io::{BufRead, BufReader, Read, Write};
+use std::net::TcpListener;
+use std::path::PathBuf;
+use std::process::{Command, Output, Stdio};
+use std::time::Duration;
+
+use serde_json::{Value, json};
+use tempfile::TempDir;
+
+const CONFIG: &str = r#"
+provider = "deepseek"
+default_text_model = "deepseek-v4-flash"
+sandbox_mode = "workspace-write"
+approval_policy = "never"
+telemetry = false
+
+[profiles.review]
+provider = "openrouter"
+default_text_model = "profile-model"
+sandbox_mode = "danger-full-access"
+approval_policy = "untrusted"
+"#;
+
+struct Fixture {
+    root: TempDir,
+    config: PathBuf,
+}
+
+impl Fixture {
+    fn new(config: &str) -> Self {
+        let root = TempDir::new().unwrap();
+        let config_path = root.path().join("config.toml");
+        fs::write(&config_path, config).unwrap();
+        Self {
+            root,
+            config: config_path,
+        }
+    }
+
+    fn command(&self, args: &[&str]) -> Command {
+        let mut command = Command::new(env!("CARGO_BIN_EXE_codewhale"));
+        command
+            .current_dir(self.root.path())
+            .env_clear()
+            .env("HOME", self.root.path().join("home"))
+            .env("USERPROFILE", self.root.path().join("home"))
+            .env("CODEWHALE_HOME", self.root.path().join("state"))
+            .env("CODEWHALE_SECRET_BACKEND", "file")
+            .env("CODEWHALE_TELEMETRY", "0")
+            .stdin(Stdio::null())
+            .arg("--config")
+            .arg(&self.config)
+            .arg("--no-project-config")
+            .args(args);
+        // Doctor may inspect rustc. Keep rustup's initialization outside the
+        // sealed fixture, while withholding all provider/account variables.
+        for name in ["PATH", "RUSTUP_HOME", "SystemRoot", "WINDIR"] {
+            if let Some(value) = std::env::var_os(name) {
+                command.env(name, value);
+            }
+        }
+        command
+    }
+
+    fn run(&self, args: &[&str]) -> Output {
+        self.command(args).output().expect("run public codewhale")
+    }
+
+    fn doctor(&self, args: &[&str]) -> Value {
+        let output = self
+            .command(args)
+            .args(["doctor", "--json"])
+            .output()
+            .unwrap();
+        success(&output);
+        serde_json::from_slice(&output.stdout).unwrap()
+    }
+
+    fn unchanged(&self) {
+        assert_eq!(fs::read_to_string(&self.config).unwrap(), CONFIG);
+        assert!(
+            !self.root.path().join("state").exists(),
+            "diagnostic must not create state"
+        );
+    }
+}
+
+fn success(output: &Output) {
+    assert!(
+        output.status.success(),
+        "status: {}\nstdout: {}\nstderr: {}",
+        output.status,
+        String::from_utf8_lossy(&output.stdout),
+        String::from_utf8_lossy(&output.stderr)
+    );
+}
+
+fn route(report: &Value) -> (&str, &str) {
+    let route = &report["setup"]["provider_model"];
+    (
+        route["provider"]["id"].as_str().unwrap(),
+        route["model"]["resolved"].as_str().unwrap(),
+    )
+}
+
+fn posture<'a>(report: &'a Value, key: &str) -> &'a Value {
+    &report["setup"]["runtime_posture"][key]["value"]
+}
+
+#[test]
+fn runtime_set_reaches_the_public_runtime_and_does_not_persist() {
+    let fixture = Fixture::new(CONFIG);
+    let baseline = fixture.doctor(&[]);
+    assert_eq!(route(&baseline), ("deepseek", "deepseek-v4-flash"));
+    assert_eq!(posture(&baseline, "sandbox_mode"), "workspace-write");
+    let report = fixture.doctor(&[
+        "--set",
+        "sandbox_mode=read-only",
+        "--set",
+        "approval_policy=on-request",
+        "--set",
+        "model=deepseek-v4-pro",
+        "--set",
+        "telemetry=false",
+    ]);
+    assert_eq!(route(&report), ("deepseek", "deepseek-v4-pro"));
+    assert_eq!(posture(&report, "sandbox_mode"), "read-only");
+    assert_eq!(posture(&report, "approval_policy"), "on-request");
+    assert_eq!(posture(&report, "telemetry"), false);
+    let after = fixture.doctor(&[]);
+    assert_eq!(route(&after), route(&baseline));
+    assert_eq!(posture(&after, "sandbox_mode"), "workspace-write");
+    fixture.unchanged();
+}
+
+#[test]
+fn profile_defaults_survive_unrelated_overrides_and_yield_to_explicit_routes() {
+    let fixture = Fixture::new(CONFIG);
+    let report = fixture.doctor(&["--profile", "review", "--set", "sandbox_mode=read-only"]);
+    assert_eq!(route(&report), ("openrouter", "profile-model"));
+    assert_eq!(posture(&report, "approval_policy"), "untrusted");
+    assert_eq!(posture(&report, "sandbox_mode"), "read-only");
+    let report = fixture.doctor(&[
+        "--profile",
+        "review",
+        "--set",
+        "provider=deepseek",
+        "--set",
+        "default_text_model=deepseek-v4-pro",
+    ]);
+    assert_eq!(route(&report), ("deepseek", "deepseek-v4-pro"));
+    assert_eq!(posture(&report, "sandbox_mode"), "danger-full-access");
+    fixture.unchanged();
+}
+
+#[test]
+fn dedicated_flags_win_in_either_order_and_repeated_aliases_use_the_last_value() {
+    let fixture = Fixture::new(CONFIG);
+    let flags = [
+        "--provider",
+        "deepseek",
+        "--model",
+        "deepseek-v4-flash",
+        "--sandbox-mode",
+        "workspace-write",
+        "--approval-policy",
+        "on-request",
+    ];
+    let overrides = [
+        "--set",
+        "provider=openrouter",
+        "--set",
+        "model=other-model",
+        "--set",
+        "sandbox_mode=read-only",
+        "--set",
+        "approval_policy=never",
+    ];
+    for args in [
+        flags.iter().chain(&overrides).copied().collect::>(),
+        overrides.iter().chain(&flags).copied().collect::>(),
+    ] {
+        let report = fixture.doctor(&args);
+        assert_eq!(route(&report), ("deepseek", "deepseek-v4-flash"));
+        assert_eq!(posture(&report, "sandbox_mode"), "workspace-write");
+        assert_eq!(posture(&report, "approval_policy"), "on-request");
+    }
+    let report = fixture.doctor(&[
+        "--set",
+        "model=deepseek-v4-flash",
+        "--set",
+        "default_text_model=deepseek-v4-pro",
+        "--set",
+        "sandbox_mode=workspace-write",
+        "--set",
+        "sandbox_mode=read-only",
+    ]);
+    assert_eq!(route(&report), ("deepseek", "deepseek-v4-pro"));
+    assert_eq!(posture(&report, "sandbox_mode"), "read-only");
+    fixture.unchanged();
+}
+
+#[test]
+fn managed_policy_keeps_authority_over_runtime_set_and_dedicated_flags() {
+    let fixture = Fixture::new(CONFIG);
+    let managed = fixture.root.path().join("managed.toml");
+    fs::write(&managed, "provider = \"deepseek\"\ndefault_text_model = \"deepseek-v4-flash\"\nsandbox_mode = \"read-only\"\napproval_policy = \"untrusted\"\n").unwrap();
+    let output = fixture
+        .command(&[
+            "--profile",
+            "review",
+            "--set",
+            "provider=openrouter",
+            "--set",
+            "model=other-model",
+            "--set",
+            "sandbox_mode=danger-full-access",
+            "--approval-policy",
+            "never",
+        ])
+        .env("CODEWHALE_MANAGED_CONFIG_PATH", &managed)
+        .args(["doctor", "--json"])
+        .output()
+        .unwrap();
+    success(&output);
+    let report: Value = serde_json::from_slice(&output.stdout).unwrap();
+    assert_eq!(route(&report), ("deepseek", "deepseek-v4-flash"));
+    assert_eq!(posture(&report, "sandbox_mode"), "read-only");
+    assert_eq!(posture(&report, "approval_policy"), "untrusted");
+    fixture.unchanged();
+}
+
+#[test]
+fn managed_requirements_reject_an_incompatible_temporary_sandbox() {
+    let fixture = Fixture::new(CONFIG);
+    let requirements = fixture.root.path().join("requirements.toml");
+    fs::write(&requirements, "allowed_sandbox_modes = [\"read-only\"]\n").unwrap();
+    let output = fixture
+        .command(&[
+            "--set",
+            "sandbox_mode=danger-full-access",
+            "doctor",
+            "--json",
+        ])
+        .env("CODEWHALE_REQUIREMENTS_PATH", requirements)
+        .output()
+        .unwrap();
+    assert!(!output.status.success());
+    let report: Value = serde_json::from_slice(&output.stdout).unwrap();
+    assert_eq!(report["error"]["kind"], "config_validation");
+    fixture.unchanged();
+}
+
+#[test]
+fn unsupported_values_auth_and_legacy_transports_fail_before_config_or_secret_access() {
+    const SENTINEL: &str = "synthetic-override-secret";
+    let fixture = Fixture::new("invalid = [synthetic-config-secret\n");
+    for key in [
+        "api_key",
+        "auth.mode",
+        "base_url",
+        "providers.openai.api_key",
+        "not_a_key",
+    ] {
+        let output = fixture.run(&["--set", &format!("{key}={SENTINEL}"), "doctor", "--json"]);
+        assert!(!output.status.success());
+        assert!(String::from_utf8_lossy(&output.stderr).contains("unsupported runtime --set key"));
+        assert!(!String::from_utf8_lossy(&output.stderr).contains(SENTINEL));
+        assert!(!String::from_utf8_lossy(&output.stderr).contains("synthetic-config-secret"));
+    }
+    for args in [
+        vec!["auth", "status"],
+        vec!["auth", "print-api-key", "--provider", "deepseek"],
+        vec!["app-server"],
+        vec!["app-server", "--stdio"],
+        vec!["app-server", "--socket"],
+    ] {
+        let output = fixture
+            .command(&["--set", "sandbox_mode=read-only"])
+            .args(&args)
+            .output()
+            .unwrap();
+        assert!(!output.status.success());
+        assert!(
+            String::from_utf8_lossy(&output.stderr).contains("--set is not supported"),
+            "{output:?}"
+        );
+        assert!(!String::from_utf8_lossy(&output.stderr).contains("synthetic-config-secret"));
+    }
+    for spec in [
+        "missing-equals",
+        "model=",
+        "model=   ",
+        "telemetry=maybe",
+        "provider=bad/id",
+    ] {
+        let output = fixture.run(&["--set", spec, "doctor", "--json"]);
+        assert!(!output.status.success());
+        assert!(String::from_utf8_lossy(&output.stderr).contains("invalid"));
+        assert!(!String::from_utf8_lossy(&output.stderr).contains("synthetic-config-secret"));
+    }
+    assert!(!fixture.root.path().join("state").exists());
+}
+
+#[test]
+fn runtime_values_cannot_leak_through_a_command_that_saves_the_store() {
+    let fixture = Fixture::new(CONFIG);
+    let output = fixture.run(&[
+        "--set",
+        "sandbox_mode=read-only",
+        "--set",
+        "provider=openrouter",
+        "--set",
+        "model=temporary-model",
+        "model",
+        "set",
+        "deepseek-v4-pro",
+    ]);
+    success(&output);
+    let saved: Value = serde_json::to_value(
+        toml::from_str::(&fs::read_to_string(&fixture.config).unwrap()).unwrap(),
+    )
+    .unwrap();
+    assert_eq!(saved["provider"], "deepseek");
+    assert_eq!(saved["sandbox_mode"], "workspace-write");
+    // Selected models persist in the canonical per-provider slot; root
+    // default_text_model is legacy fallback only.
+    assert_eq!(saved["providers"]["deepseek"]["model"], "deepseek-v4-pro");
+    assert!(
+        !fs::read_to_string(&fixture.config)
+            .unwrap()
+            .contains("temporary-model")
+    );
+}
+
+#[test]
+fn named_provider_and_model_reach_an_actual_exec_request_with_a_profile() {
+    let listener = TcpListener::bind("127.0.0.1:0").unwrap();
+    let endpoint = format!("http://{}/v1", listener.local_addr().unwrap());
+    let mock = std::thread::spawn(move || {
+        let (stream, _) = listener.accept().unwrap();
+        stream
+            .set_read_timeout(Some(Duration::from_secs(10)))
+            .unwrap();
+        let mut reader = BufReader::new(stream);
+        let mut request = String::new();
+        reader.read_line(&mut request).unwrap();
+        assert_eq!(request.trim(), "POST /v1/chat/completions HTTP/1.1");
+        let mut length = None;
+        loop {
+            let mut line = String::new();
+            assert!(reader.read_line(&mut line).unwrap() > 0);
+            if line == "\r\n" {
+                break;
+            }
+            if let Some((name, value)) = line.split_once(':')
+                && name.eq_ignore_ascii_case("content-length")
+            {
+                length = Some(value.trim().parse::().unwrap());
+            }
+        }
+        let length = length.expect("request content length");
+        assert!(length <= 1024 * 1024);
+        let mut body = vec![0; length];
+        reader.read_exact(&mut body).unwrap();
+        let body: Value = serde_json::from_slice(&body).unwrap();
+        let response = format!(
+            "data: {}\n\ndata: [DONE]\n\n",
+            json!({
+                "id": "fixture", "object": "chat.completion.chunk", "model": "temporary-model",
+                "choices": [{"index": 0, "delta": {"content": "LOCAL_SET_ACCEPTED"}, "finish_reason": "stop"}]
+            })
+        );
+        write!(reader.get_mut(), "HTTP/1.1 200 OK\r\nContent-Type: text/event-stream\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}", response.len(), response).unwrap();
+        body
+    });
+    let config = format!(
+        "{CONFIG}\n[providers.fixture_route]\nkind = \"openai-compatible\"\nbase_url = \"{endpoint}\"\nmodel = \"saved-model\"\napi_key = \"synthetic-fixture-key\"\n"
+    );
+    let fixture = Fixture::new(&config);
+    let output = fixture.run(&[
+        "--profile",
+        "review",
+        "--set",
+        "provider=fixture_route",
+        "--set",
+        "model=temporary-model",
+        "--set",
+        "sandbox_mode=read-only",
+        "exec",
+        "--max-turns",
+        "1",
+        "--output-format",
+        "stream-json",
+        "reply briefly",
+    ]);
+    success(&output);
+    let request = mock.join().unwrap();
+    assert_eq!(request["model"], "temporary-model");
+    assert!(
+        !request["tools"]
+            .as_array()
+            .unwrap()
+            .iter()
+            .any(|tool| tool["function"]["name"] == "Bash")
+    );
+    let events: Vec = String::from_utf8(output.stdout)
+        .unwrap()
+        .lines()
+        .map(|line| serde_json::from_str(line).unwrap())
+        .collect();
+    assert!(
+        events
+            .iter()
+            .any(|event| event["type"] == "content" && event["content"] == "LOCAL_SET_ACCEPTED"),
+        "{events:?}"
+    );
+    assert_eq!(events.last().unwrap()["type"], "done");
+    let receipt = events
+        .iter()
+        .find(|event| event["type"] == "metadata")
+        .unwrap();
+    assert_eq!(receipt["meta"]["provider_id"], "fixture_route");
+    assert_eq!(receipt["meta"]["model"], "temporary-model");
+    assert_eq!(fs::read_to_string(&fixture.config).unwrap(), config);
+}
diff --git a/crates/cloud-facts/Cargo.toml b/crates/cloud-facts/Cargo.toml
new file mode 100644
index 0000000000..cc3d5db1a6
--- /dev/null
+++ b/crates/cloud-facts/Cargo.toml
@@ -0,0 +1,28 @@
+[package]
+name = "codewhale-cloud-facts"
+version.workspace = true
+edition.workspace = true
+rust-version.workspace = true
+license.workspace = true
+repository.workspace = true
+description = "Fetch + verified disk cache for the Codewhale cloud facts channel (facts/v1)"
+
+[lints]
+workspace = true
+
+[dependencies]
+codewhale-config = { path = "../config", version = "0.9.13" }
+codewhale-release = { path = "../release", version = "0.9.13" }
+reqwest.workspace = true
+libc.workspace = true
+semver.workspace = true
+serde.workspace = true
+serde_json.workspace = true
+tokio.workspace = true
+tracing.workspace = true
+
+[dev-dependencies]
+tempfile.workspace = true
+
+[target.'cfg(windows)'.dependencies]
+windows-sys = { version = "0.61.2", features = ["Win32_Foundation", "Win32_Storage_FileSystem"] }
diff --git a/crates/cloud-facts/examples/fetch_live.rs b/crates/cloud-facts/examples/fetch_live.rs
new file mode 100644
index 0000000000..9b00ac6be6
--- /dev/null
+++ b/crates/cloud-facts/examples/fetch_live.rs
@@ -0,0 +1,63 @@
+//! Dogfood/proof harness: run the cloud facts client once against a real
+//! endpoint with an isolated `CODEWHALE_HOME` and print the resulting status.
+//!
+//! ```sh
+//! CODEWHALE_HOME=$(mktemp -d) CODEWHALE_CLOUD_FACTS=1 \
+//!   CODEWHALE_CLOUD_FACTS_URL=http://localhost:3000/api/facts/v1/{channel} \
+//!   cargo run -p codewhale-cloud-facts --example fetch_live
+//! ```
+//!
+//! The flag stays off by default; this example only honours the env override.
+
+use codewhale_cloud_facts::{Settings, maybe_load_persisted_cache, refresh, status};
+use codewhale_config::catalog::now_unix;
+use codewhale_config::cloud_facts::overlay;
+
+#[tokio::main(flavor = "current_thread")]
+async fn main() {
+    let settings = Settings::default().resolve();
+    println!(
+        "enabled={} channel={} url={}",
+        settings.enabled,
+        settings.channel,
+        settings.url()
+    );
+    codewhale_cloud_facts::configure(&settings);
+    let seeded = maybe_load_persisted_cache(&settings);
+    println!("seeded_from_disk={seeded:?}");
+    println!("before: {}", status().label(now_unix()));
+    match refresh(&settings, true).await {
+        Ok(outcome) => println!("refresh: {outcome:?}"),
+        Err(err) => println!("refresh error: {err}"),
+    }
+    let st = status();
+    println!("after:  {}", st.label(now_unix()));
+    println!(
+        "status_json={}",
+        serde_json::to_string(&st).unwrap_or_default()
+    );
+    if let Some(facts) = overlay::overlay() {
+        println!(
+            "overlay: channel={} v{} key={} sha256={} patches={} defaults={} announcements={} dropped={}",
+            facts.channel,
+            facts.facts_version,
+            facts.key_id,
+            facts.sha256,
+            facts.models.len(),
+            facts.provider_defaults.len(),
+            facts.announcements.len(),
+            facts.dropped.len()
+        );
+        if let Some(release) = &facts.release {
+            println!(
+                "release: latest={} yanked={:?}",
+                release.latest, release.yanked
+            );
+        }
+        for a in &facts.announcements {
+            println!("announcement[{}] {:?}: {}", a.id, a.level, a.text);
+        }
+    } else {
+        println!("overlay: none (bundled facts in use)");
+    }
+}
diff --git a/crates/cloud-facts/src/lib.rs b/crates/cloud-facts/src/lib.rs
new file mode 100644
index 0000000000..8178e48e7b
--- /dev/null
+++ b/crates/cloud-facts/src/lib.rs
@@ -0,0 +1,953 @@
+//! Cloud facts client: fetch `https://codewhale.net/api/facts/v1/`,
+//! verify the Ed25519 envelope against the keys pinned in
+//! `codewhale_config::cloud_facts::keys`, cache it under
+//! `$CODEWHALE_HOME/facts/cloud-facts.json`, and install the scoped view as the
+//! process-wide overlay. Modeled on the TUI's `models_dev_live` producer.
+//!
+//! Guarantees:
+//! - Never a startup dependency: [`maybe_load_persisted_cache`] is a bounded
+//!   synchronous disk read; all network happens in [`spawn_background_refresh`].
+//! - Off by default (`[cloud_facts].enabled = false`); `CODEWHALE_CLOUD_FACTS=1`
+//!   flips it, `CODEWHALE_DISABLE_CLOUD_FACTS=1` beats everything, CI markers
+//!   suppress the fetch.
+//! - The disk cache is re-verified on every load; untrusted bytes are cleared while the rollback floor is retained.
+//! - The fetch sends only a fixed user agent and `If-None-Match`; no
+//!   identifiers, cookies, or query parameters (PRD §5).
+//! - With no active pinned key the layer is inert even when enabled.
+
+use std::io::Read as _;
+use std::path::{Path, PathBuf};
+use std::sync::Arc;
+use std::time::Duration;
+
+use codewhale_config::catalog::now_unix;
+use codewhale_config::cloud_facts::{
+    CloudFactsState, CloudFactsStatus, FactsOrigin, FactsRejection, TrustedKey, VerifiedFacts,
+    overlay, scoped_view, verify_envelope,
+};
+use codewhale_config::persistence::atomic_write;
+use serde::{Deserialize, Serialize};
+
+/// `{channel}` is replaced with the channel slug.
+pub const DEFAULT_URL_TEMPLATE: &str = "https://codewhale.net/api/facts/v1/{channel}";
+/// Refresh interval for a verified payload (6 h).
+pub const DEFAULT_TTL_SECS: u64 = 6 * 60 * 60;
+/// Bounded HTTP budget.
+pub const FETCH_TIMEOUT: Duration = Duration::from_secs(10);
+pub const CONNECT_TIMEOUT: Duration = Duration::from_secs(5);
+/// Largest response body accepted.
+pub const MAX_BODY_BYTES: usize = codewhale_config::cloud_facts::MAX_ENVELOPE_BYTES;
+/// Fixed, identifier-free user agent.
+pub const USER_AGENT: &str = concat!("CodeWhale/", env!("CARGO_PKG_VERSION"), " (+cloud-facts)");
+/// State subdir + file under `$CODEWHALE_HOME`.
+pub const STATE_SUBDIR: &str = "facts";
+pub const CACHE_FILE: &str = "cloud-facts.json";
+const CACHE_SCHEMA_VERSION: u32 = 2;
+const MAX_SOURCE_BYTES: usize = 4096;
+const MAX_CACHE_BYTES: usize = MAX_BODY_BYTES * 6 + 32 * 1024;
+static REFRESH_LOCK: tokio::sync::Mutex<()> = tokio::sync::Mutex::const_new(());
+const BACKOFF_BASE_SECS: u64 = 10 * 60;
+
+/// Env: `1`/`0` overrides `[cloud_facts].enabled`.
+pub const ENV_ENABLED: &str = "CODEWHALE_CLOUD_FACTS";
+/// Env: hard kill switch (truthy) — beats config and `ENV_ENABLED`.
+pub const ENV_DISABLE: &str = "CODEWHALE_DISABLE_CLOUD_FACTS";
+/// Env: full URL override (may contain `{channel}`).
+pub const ENV_URL: &str = "CODEWHALE_CLOUD_FACTS_URL";
+/// Env: channel slug override.
+pub const ENV_CHANNEL: &str = "CODEWHALE_CLOUD_FACTS_CHANNEL";
+/// Env: read the envelope from a local file instead of the network.
+pub const ENV_PATH: &str = "CODEWHALE_CLOUD_FACTS_PATH";
+const CI_MARKERS: &[&str] = &[
+    "CI",
+    "GITHUB_ACTIONS",
+    "GITLAB_CI",
+    "BUILDKITE",
+    "CIRCLECI",
+    "JENKINS_URL",
+    "TEAMCITY_VERSION",
+    "TF_BUILD",
+];
+
+/// Resolved runtime settings (config + env).
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct Settings {
+    pub enabled: bool,
+    pub channel: String,
+    pub url: Option,
+    pub ttl_secs: u64,
+    /// Explicit cache file (tests); otherwise `$CODEWHALE_HOME/facts/cloud-facts.json`.
+    pub cache_path: Option,
+    /// Local envelope path (`ENV_PATH`); skips the network.
+    pub local_path: Option,
+}
+
+impl Default for Settings {
+    fn default() -> Self {
+        Self {
+            enabled: false,
+            channel: "stable".to_string(),
+            url: None,
+            ttl_secs: DEFAULT_TTL_SECS,
+            cache_path: None,
+            local_path: None,
+        }
+    }
+}
+
+fn env_truthy(name: &str) -> Option {
+    let value = std::env::var(name).ok()?;
+    match value.trim().to_ascii_lowercase().as_str() {
+        "1" | "true" | "yes" | "on" => Some(true),
+        "0" | "false" | "no" | "off" => Some(false),
+        _ => None,
+    }
+}
+
+impl Settings {
+    /// Apply env overrides on top of config-derived settings.
+    #[must_use]
+    pub fn resolve(mut self) -> Self {
+        if let Some(enabled) = env_truthy(ENV_ENABLED) {
+            self.enabled = enabled;
+        }
+        if let Ok(channel) = std::env::var(ENV_CHANNEL) {
+            let channel = channel.trim();
+            if valid_channel(channel) {
+                self.channel = channel.to_string();
+            }
+        }
+        if let Ok(url) = std::env::var(ENV_URL) {
+            let url = url.trim();
+            if !url.is_empty() {
+                self.url = Some(url.to_string());
+            }
+        }
+        if let Ok(path) = std::env::var(ENV_PATH) {
+            let path = path.trim();
+            if !path.is_empty() {
+                self.local_path = Some(PathBuf::from(path));
+            }
+        }
+        if hard_disabled() {
+            self.enabled = false;
+        }
+        self.ttl_secs = self.ttl_secs.max(60);
+        self
+    }
+
+    /// The effective envelope URL.
+    #[must_use]
+    pub fn url(&self) -> String {
+        self.url
+            .as_deref()
+            .unwrap_or(DEFAULT_URL_TEMPLATE)
+            .replace("{channel}", &self.channel)
+    }
+
+    fn cache_file(&self) -> Option {
+        self.cache_path.clone().or_else(|| {
+            let path = cache_path()?;
+            if self.channel == "stable" {
+                Some(path)
+            } else {
+                Some(path.with_file_name(format!("cloud-facts-{}.json", self.channel)))
+            }
+        })
+    }
+}
+
+/// Channel slugs are `[a-z0-9][a-z0-9-]{0,31}`.
+#[must_use]
+pub fn valid_channel(slug: &str) -> bool {
+    let bytes = slug.as_bytes();
+    (1..=32).contains(&bytes.len())
+        && (bytes[0].is_ascii_lowercase() || bytes[0].is_ascii_digit())
+        && bytes
+            .iter()
+            .all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || *b == b'-')
+}
+
+/// Production network policy. Local fixtures inject their transport explicitly.
+#[must_use]
+pub fn fetch_suppressed() -> bool {
+    CI_MARKERS.iter().any(|name| {
+        env_truthy(name).unwrap_or_else(|| std::env::var(name).is_ok_and(|v| !v.trim().is_empty()))
+    })
+}
+
+/// Default cache path under the CodeWhale state root.
+#[must_use]
+pub fn cache_path() -> Option {
+    codewhale_config::resolve_state_dir(STATE_SUBDIR)
+        .ok()
+        .map(|dir| dir.join(CACHE_FILE))
+}
+
+fn hard_disabled() -> bool {
+    overlay::hard_disabled()
+}
+
+fn source(settings: &Settings) -> Result {
+    if !valid_channel(&settings.channel) {
+        return Err(RefreshError::InvalidSettings("invalid channel".into()));
+    }
+    if let Some(path) = &settings.local_path {
+        let path = if path.is_absolute() {
+            path.clone()
+        } else {
+            std::env::current_dir()
+                .map_err(|e| RefreshError::Io(e.to_string()))?
+                .join(path)
+        };
+        let value = format!("file:{}", path.display());
+        if value.len() > MAX_SOURCE_BYTES {
+            return Err(RefreshError::TooLarge(value.len()));
+        }
+        return Ok(value);
+    }
+    let raw = settings.url();
+    if raw.len() > MAX_SOURCE_BYTES {
+        return Err(RefreshError::TooLarge(raw.len()));
+    }
+    let url = reqwest::Url::parse(&raw)
+        .map_err(|_| RefreshError::InvalidSettings("invalid URL".into()))?;
+    if !matches!(url.scheme(), "http" | "https")
+        || url.host_str().is_none()
+        || !url.username().is_empty()
+        || url.password().is_some()
+        || url.fragment().is_some()
+        || url.query().is_some()
+    {
+        return Err(RefreshError::InvalidSettings(
+            "URL must be HTTP(S), without credentials, query or fragment".into(),
+        ));
+    }
+    Ok(url.to_string())
+}
+
+fn source_identity(settings: &Settings, keys: &[TrustedKey]) -> Result {
+    // Trust inputs are public pins, not credentials. Including them invalidates
+    // a previously issued ticket when a test or a future reload changes trust.
+    Ok(format!(
+        "{}\n{}\n{}\n{}\n{:?}",
+        settings.channel,
+        source(settings)?,
+        settings.ttl_secs,
+        codewhale_config::cloud_facts::current_version(),
+        keys
+    ))
+}
+
+/// Publish admitted settings synchronously, before spawning work. Refreshing
+/// an old Settings value can never re-enable or change this authority.
+pub fn configure(settings: &Settings) {
+    configure_with_keys(settings, codewhale_config::cloud_facts::TRUSTED_KEYS);
+}
+
+fn configure_with_keys(settings: &Settings, keys: &[TrustedKey]) {
+    if !settings.enabled || hard_disabled() {
+        overlay::configure(false, "");
+        return;
+    }
+    let identity = match source_identity(settings, keys) {
+        Ok(identity) => identity,
+        Err(_) => {
+            overlay::configure(false, "");
+            return;
+        }
+    };
+    if let Some(ticket) = overlay::configure(true, &identity)
+        && !keys
+            .iter()
+            .any(|key| key.status == codewhale_config::cloud_facts::KeyStatus::Active)
+    {
+        overlay::publish(
+            &ticket,
+            None,
+            state_status(CloudFactsState::Inert, None, ""),
+        );
+    }
+}
+
+fn ticket(
+    settings: &Settings,
+    keys: &[TrustedKey],
+) -> Result {
+    if !settings.enabled || hard_disabled() {
+        return Err(RefreshError::Disabled);
+    }
+    if !keys
+        .iter()
+        .any(|key| key.status == codewhale_config::cloud_facts::KeyStatus::Active)
+    {
+        return Err(RefreshError::Inert);
+    }
+    overlay::current_ticket(&source_identity(settings, keys)?).ok_or(RefreshError::Superseded)
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize, Default)]
+struct PersistedCache {
+    schema_version: u32,
+    channel: String,
+    url: String,
+    #[serde(default)]
+    source_identity: String,
+    fetched_at: u64,
+    #[serde(default)]
+    etag: Option,
+    #[serde(default)]
+    highest_seen_version: Option,
+    #[serde(default)]
+    backoff_until: Option,
+    #[serde(default)]
+    failures: u32,
+    #[serde(default)]
+    envelope: String,
+}
+
+fn read_bounded_regular(path: &Path, limit: usize) -> Result, RefreshError> {
+    let mut options = std::fs::OpenOptions::new();
+    options.read(true);
+    #[cfg(unix)]
+    {
+        use std::os::unix::fs::OpenOptionsExt as _;
+        options.custom_flags(libc::O_NOFOLLOW | libc::O_CLOEXEC | libc::O_NONBLOCK);
+    }
+    #[cfg(windows)]
+    {
+        use std::os::windows::fs::OpenOptionsExt as _;
+        options.custom_flags(0x0020_0000);
+    }
+    let file = options
+        .open(path)
+        .map_err(|e| RefreshError::Io(e.to_string()))?;
+    let metadata = file
+        .metadata()
+        .map_err(|e| RefreshError::Io(e.to_string()))?;
+    let mut regular = metadata.is_file();
+    #[cfg(unix)]
+    {
+        use std::os::unix::fs::MetadataExt as _;
+        regular &= metadata.nlink() == 1;
+    }
+    #[cfg(windows)]
+    {
+        use std::os::windows::fs::MetadataExt as _;
+        use std::os::windows::io::AsRawHandle as _;
+        use windows_sys::Win32::Storage::FileSystem::{
+            BY_HANDLE_FILE_INFORMATION, GetFileInformationByHandle,
+        };
+        let mut information: BY_HANDLE_FILE_INFORMATION = unsafe { std::mem::zeroed() };
+        // SAFETY: this is the live handle already opened without following
+        // reparse points; `information` is writable for the synchronous call.
+        let inspected =
+            unsafe { GetFileInformationByHandle(file.as_raw_handle(), &mut information) };
+        regular &= metadata.file_attributes() & 0x0000_0400 == 0
+            && inspected != 0
+            && information.nNumberOfLinks == 1;
+    }
+    if !regular {
+        return Err(RefreshError::Io(
+            "facts file must be a regular file with one link".into(),
+        ));
+    }
+    if metadata.len() > limit as u64 {
+        return Err(RefreshError::TooLarge(limit.saturating_add(1)));
+    }
+    let mut bytes = Vec::new();
+    file.take(limit.saturating_add(1) as u64)
+        .read_to_end(&mut bytes)
+        .map_err(|e| RefreshError::Io(e.to_string()))?;
+    if bytes.len() > limit {
+        return Err(RefreshError::TooLarge(bytes.len()));
+    }
+    Ok(bytes)
+}
+
+fn load_cache(path: &Path) -> Option {
+    let bytes = read_bounded_regular(path, MAX_CACHE_BYTES).ok()?;
+    let cache: PersistedCache = serde_json::from_slice(&bytes).ok()?;
+    (cache.schema_version == CACHE_SCHEMA_VERSION
+        && valid_channel(&cache.channel)
+        && cache.envelope.len() <= MAX_BODY_BYTES
+        && cache.url.len() <= MAX_SOURCE_BYTES
+        && cache.source_identity.len() <= 16 * MAX_SOURCE_BYTES
+        && cache
+            .etag
+            .as_ref()
+            .is_none_or(|etag| etag.len() <= MAX_SOURCE_BYTES))
+    .then_some(cache)
+}
+
+fn save_cache(path: &Path, cache: &PersistedCache) {
+    if cache.envelope.len() > MAX_BODY_BYTES
+        || cache.url.len() > MAX_SOURCE_BYTES
+        || cache.source_identity.len() > 16 * MAX_SOURCE_BYTES
+        || cache
+            .etag
+            .as_ref()
+            .is_some_and(|etag| etag.len() > MAX_SOURCE_BYTES)
+    {
+        return;
+    }
+    if let Ok(bytes) = serde_json::to_vec(cache)
+        && bytes.len() <= MAX_CACHE_BYTES
+        && let Err(err) = atomic_write(path, &bytes)
+    {
+        tracing::debug!(target: "cloud_facts", error = %err, "cache write failed");
+    }
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub enum RefreshError {
+    Disabled,
+    Inert,
+    Suppressed,
+    Superseded,
+    InvalidSettings(String),
+    BackingOff { until: u64 },
+    Network(String),
+    HttpStatus(u16),
+    TooLarge(usize),
+    Rejected(FactsRejection),
+    Io(String),
+}
+impl std::fmt::Display for RefreshError {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        match self {
+            Self::Disabled => write!(f, "cloud facts disabled"),
+            Self::Inert => write!(f, "no active trusted key"),
+            Self::Suppressed => write!(f, "production network fetch suppressed by CI"),
+            Self::Superseded => write!(f, "facts settings changed before publication"),
+            Self::InvalidSettings(e) => write!(f, "invalid settings: {e}"),
+            Self::BackingOff { until } => write!(f, "backing off until {until}"),
+            Self::Network(e) => write!(f, "network: {e}"),
+            Self::HttpStatus(code) => write!(f, "HTTP {code}"),
+            Self::TooLarge(n) => write!(f, "response too large ({n} bytes)"),
+            Self::Rejected(e) => write!(f, "{e}"),
+            Self::Io(e) => write!(f, "io: {e}"),
+        }
+    }
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub enum RefreshOutcome {
+    NotModified { facts_version: Option },
+    Updated { facts_version: u64 },
+    Fresh { facts_version: Option },
+    NoFacts,
+}
+
+fn state_status(
+    state: CloudFactsState,
+    etag: Option,
+    source_label: &str,
+) -> CloudFactsStatus {
+    CloudFactsStatus {
+        state,
+        last_attempt: Some(now_unix()),
+        etag,
+        source_label: source_label.into(),
+    }
+}
+
+fn verify_with(
+    bytes: &[u8],
+    settings: &Settings,
+    highest: Option,
+    keys: &[TrustedKey],
+    now: u64,
+) -> Result {
+    verify_envelope(
+        bytes,
+        &settings.channel,
+        &codewhale_config::cloud_facts::current_version(),
+        highest.max(overlay::highest_seen(&settings.channel)),
+        keys,
+        now,
+    )
+}
+
+fn publish_verified(
+    ticket: &overlay::OverlayTicket,
+    verified: &VerifiedFacts,
+    origin: FactsOrigin,
+    cache: &PersistedCache,
+    path: Option<&Path>,
+    now: u64,
+) -> Result {
+    let scoped = scoped_view(
+        verified,
+        &codewhale_config::cloud_facts::current_version(),
+        now,
+    );
+    let (patches, defaults, announcements) = scoped.item_counts();
+    let version = scoped.facts_version;
+    let status = state_status(
+        CloudFactsState::Verified {
+            channel: scoped.channel.clone(),
+            facts_version: version,
+            key_id: scoped.key_id.clone(),
+            fetched_at: cache.fetched_at,
+            origin,
+            stale: scoped.stale,
+            patches,
+            defaults,
+            announcements,
+        },
+        cache.etag.clone(),
+        &cache.url,
+    );
+    if overlay::publish_with(ticket, Some(scoped), status, || {
+        if let Some(path) = path {
+            save_cache(path, cache);
+        }
+    }) {
+        Ok(version)
+    } else {
+        Err(RefreshError::Superseded)
+    }
+}
+
+fn cache_for(settings: &Settings, keys: &[TrustedKey]) -> Result {
+    let identity = source_identity(settings, keys)?;
+    let mut cache = settings
+        .cache_file()
+        .as_deref()
+        .and_then(load_cache)
+        .filter(|cache| cache.channel == settings.channel)
+        .unwrap_or_default();
+    // The persisted high-water hint is unsigned. Recover any stronger floor
+    // from the authenticated body before a source switch discards its bytes,
+    // including refreshes that did not first seed the process overlay.
+    if !cache.envelope.is_empty()
+        && let Ok(verified) =
+            verify_with(cache.envelope.as_bytes(), settings, None, keys, now_unix())
+    {
+        cache.highest_seen_version = cache
+            .highest_seen_version
+            .max(Some(verified.facts.facts_version));
+    }
+    if cache.source_identity != identity {
+        // Retain the channel rollback floor while discarding another source's
+        // validators, body and retry state. Channels use separate default files.
+        let floor = cache.highest_seen_version;
+        cache = PersistedCache {
+            highest_seen_version: floor,
+            ..PersistedCache::default()
+        };
+    }
+    cache.highest_seen_version = cache
+        .highest_seen_version
+        .max(overlay::highest_seen(&settings.channel));
+    // Local cache metadata is only a hint. A forged/future timestamp must
+    // not grant an indefinitely fresh view or suppress all future refreshes.
+    let now = now_unix();
+    if cache.fetched_at > now {
+        cache.fetched_at = 0;
+        cache.backoff_until = None;
+    }
+    let max_backoff = (BACKOFF_BASE_SECS << 9).min(settings.ttl_secs);
+    if cache
+        .backoff_until
+        .is_some_and(|until| until > now.saturating_add(max_backoff))
+    {
+        cache.backoff_until = None;
+    }
+    cache.schema_version = CACHE_SCHEMA_VERSION;
+    cache.channel = settings.channel.clone();
+    cache.source_identity = identity;
+    cache.url = source(settings)?;
+    Ok(cache)
+}
+
+pub fn maybe_load_persisted_cache(settings: &Settings) -> Option {
+    maybe_load_persisted_cache_with_keys(settings, codewhale_config::cloud_facts::TRUSTED_KEYS)
+}
+fn maybe_load_persisted_cache_with_keys(settings: &Settings, keys: &[TrustedKey]) -> Option {
+    let ticket = ticket(settings, keys).ok()?;
+    let cache = cache_for(settings, keys).ok()?;
+    if cache.envelope.is_empty() {
+        return None;
+    }
+    let now = now_unix();
+    match verify_with(
+        cache.envelope.as_bytes(),
+        settings,
+        cache.highest_seen_version,
+        keys,
+        now,
+    ) {
+        Ok(mut verified) => {
+            verified.stale |= now.saturating_sub(cache.fetched_at) >= settings.ttl_secs;
+            publish_verified(
+                &ticket,
+                &verified,
+                FactsOrigin::DiskCache,
+                &cache,
+                None,
+                now,
+            )
+            .ok()
+        }
+        Err(reason) => {
+            // Retain the channel's rollback floor, replacing the rejected body
+            // through the same generation-guarded cache publication boundary.
+            let mut rejected = cache.clone();
+            rejected.envelope.clear();
+            rejected.etag = None;
+            overlay::publish_with(
+                &ticket,
+                None,
+                state_status(
+                    CloudFactsState::Rejected {
+                        reason: reason.to_string(),
+                        at: now,
+                    },
+                    None,
+                    &cache.url,
+                ),
+                || {
+                    if let Some(path) = settings.cache_file() {
+                        save_cache(&path, &rejected);
+                    }
+                },
+            );
+            None
+        }
+    }
+}
+
+enum Fetched {
+    NotModified,
+    NotFound,
+    Body {
+        bytes: Vec,
+        etag: Option,
+    },
+}
+
+async fn fetch(url: String, etag: Option) -> Result {
+    let client = codewhale_release::tls::reqwest_client_builder()
+        .timeout(FETCH_TIMEOUT)
+        .connect_timeout(CONNECT_TIMEOUT)
+        .user_agent(USER_AGENT)
+        .redirect(reqwest::redirect::Policy::none())
+        .build()
+        .map_err(|e| RefreshError::Network(e.to_string()))?;
+    let mut request = client.get(url).header("Accept", "application/json");
+    if let Some(etag) = etag {
+        request = request.header("If-None-Match", etag);
+    }
+    let mut response = request
+        .send()
+        .await
+        .map_err(|e| RefreshError::Network(e.to_string()))?;
+    let status = response.status().as_u16();
+    if status == 304 {
+        return Ok(Fetched::NotModified);
+    }
+    if status == 404 {
+        return Ok(Fetched::NotFound);
+    }
+    if !(200..300).contains(&status) {
+        return Err(RefreshError::HttpStatus(status));
+    }
+    if response
+        .content_length()
+        .is_some_and(|len| len > MAX_BODY_BYTES as u64)
+    {
+        return Err(RefreshError::TooLarge(MAX_BODY_BYTES + 1));
+    }
+    let etag = response
+        .headers()
+        .get("etag")
+        .and_then(|v| v.to_str().ok())
+        .filter(|value| value.len() <= MAX_SOURCE_BYTES)
+        .map(str::to_string);
+    let mut bytes = Vec::new();
+    while let Some(chunk) = response
+        .chunk()
+        .await
+        .map_err(|e| RefreshError::Network(e.to_string()))?
+    {
+        let size = bytes.len().saturating_add(chunk.len());
+        if size > MAX_BODY_BYTES {
+            return Err(RefreshError::TooLarge(size));
+        }
+        bytes.extend_from_slice(&chunk);
+    }
+    Ok(Fetched::Body { bytes, etag })
+}
+
+/// Uses only admitted settings; callers must configure synchronously first.
+pub async fn refresh(settings: &Settings, force: bool) -> Result {
+    refresh_with_keys(settings, force, codewhale_config::cloud_facts::TRUSTED_KEYS).await
+}
+async fn refresh_with_keys(
+    settings: &Settings,
+    force: bool,
+    keys: &[TrustedKey],
+) -> Result {
+    refresh_using(settings, force, keys, None, fetch_suppressed(), fetch).await
+}
+
+// Tests inject an explicit transport/policy, leaving the production CI gate
+// intact. Their dependencies cannot override settings/trust admission.
+async fn refresh_using(
+    settings: &Settings,
+    force: bool,
+    keys: &[TrustedKey],
+    admitted: Option,
+    suppress_network: bool,
+    transport: F,
+) -> Result
+where
+    F: FnOnce(String, Option) -> Fut,
+    Fut: std::future::Future>,
+{
+    let ticket = admitted.map(Ok).unwrap_or_else(|| ticket(settings, keys))?;
+    let _refresh = REFRESH_LOCK.lock().await;
+    // A queued old request must fail without even reading a file.
+    let _ = self::ticket(settings, keys)?;
+    if !overlay::is_current(&ticket) {
+        return Err(RefreshError::Superseded);
+    }
+    let mut cache = cache_for(settings, keys)?;
+    let path = settings.cache_file();
+    let now = now_unix();
+    let fetched = if let Some(local) = &settings.local_path {
+        read_bounded_regular(local, MAX_BODY_BYTES).map(|bytes| Fetched::Body { bytes, etag: None })
+    } else {
+        if suppress_network {
+            return Err(RefreshError::Suppressed);
+        }
+        if !force {
+            if let Some(until) = cache.backoff_until
+                && now < until
+            {
+                return Err(RefreshError::BackingOff { until });
+            }
+            if !cache.envelope.is_empty()
+                && now.saturating_sub(cache.fetched_at) < settings.ttl_secs
+                && let Ok(verified) = verify_with(
+                    cache.envelope.as_bytes(),
+                    settings,
+                    cache.highest_seen_version,
+                    keys,
+                    now,
+                )
+            {
+                let version = publish_verified(
+                    &ticket,
+                    &verified,
+                    FactsOrigin::DiskCache,
+                    &cache,
+                    None,
+                    now,
+                )?;
+                return Ok(RefreshOutcome::Fresh {
+                    facts_version: Some(version),
+                });
+            }
+        }
+        transport(
+            cache.url.clone(),
+            cache.etag.clone().filter(|_| !cache.envelope.is_empty()),
+        )
+        .await
+    };
+    let mut not_modified = false;
+    let (bytes, etag) = match fetched {
+        Ok(Fetched::NotModified) => {
+            not_modified = true;
+            (cache.envelope.as_bytes().to_vec(), cache.etag.clone())
+        }
+        Ok(Fetched::NotFound) => {
+            cache.envelope.clear();
+            cache.etag = None;
+            cache.failures = 0;
+            cache.backoff_until = None;
+            cache.fetched_at = now;
+            if !overlay::publish_with(
+                &ticket,
+                None,
+                state_status(CloudFactsState::BundledOnly, None, &cache.url),
+                || {
+                    if let Some(path) = &path {
+                        save_cache(path, &cache);
+                    }
+                },
+            ) {
+                return Err(RefreshError::Superseded);
+            }
+            return Ok(RefreshOutcome::NoFacts);
+        }
+        Ok(Fetched::Body { bytes, etag }) => (bytes, etag),
+        Err(err) => {
+            cache.failures = cache.failures.saturating_add(1);
+            cache.backoff_until = Some(
+                now.saturating_add(
+                    (BACKOFF_BASE_SECS << cache.failures.min(10).saturating_sub(1))
+                        .min(settings.ttl_secs),
+                ),
+            );
+            // Reverify retained facts on failure too; the status must never
+            // hide a revoked or expired overlay behind a prior successful fetch.
+            let kept = verify_with(
+                cache.envelope.as_bytes(),
+                settings,
+                cache.highest_seen_version,
+                keys,
+                now,
+            )
+            .ok()
+            .map(|mut verified| {
+                verified.stale |= now.saturating_sub(cache.fetched_at) >= settings.ttl_secs;
+                scoped_view(
+                    &verified,
+                    &codewhale_config::cloud_facts::current_version(),
+                    now,
+                )
+            });
+            let keeping = kept
+                .as_ref()
+                .filter(|facts| !facts.stale)
+                .map(|facts| facts.facts_version);
+            if !overlay::publish_with(
+                &ticket,
+                kept,
+                state_status(
+                    CloudFactsState::Failed {
+                        last_error: err.to_string(),
+                        at: now,
+                        keeping,
+                    },
+                    cache.etag.clone(),
+                    &cache.url,
+                ),
+                || {
+                    if let Some(path) = &path {
+                        save_cache(path, &cache);
+                    }
+                },
+            ) {
+                return Err(RefreshError::Superseded);
+            }
+            return Err(err);
+        }
+    };
+    // 304 is a transport optimization, never a trust decision. This also
+    // rejects a 304 without an authenticated matching cached envelope.
+    match verify_with(
+        &bytes,
+        settings,
+        cache.highest_seen_version,
+        keys,
+        now_unix(),
+    ) {
+        Ok(verified) => {
+            cache.fetched_at = now_unix();
+            cache.etag = etag;
+            cache.failures = 0;
+            cache.backoff_until = None;
+            cache.highest_seen_version = Some(
+                cache
+                    .highest_seen_version
+                    .unwrap_or(0)
+                    .max(verified.facts.facts_version),
+            );
+            cache.envelope =
+                String::from_utf8(bytes).map_err(|e| RefreshError::Io(e.to_string()))?;
+            let origin = if settings.local_path.is_some() {
+                FactsOrigin::LocalFile
+            } else {
+                FactsOrigin::Network
+            };
+            let version = publish_verified(
+                &ticket,
+                &verified,
+                origin,
+                &cache,
+                path.as_deref(),
+                now_unix(),
+            )?;
+            Ok(if not_modified {
+                RefreshOutcome::NotModified {
+                    facts_version: Some(version),
+                }
+            } else {
+                RefreshOutcome::Updated {
+                    facts_version: version,
+                }
+            })
+        }
+        Err(reason) => {
+            // Drop the now-untrusted body, retain rollback floor, and do not
+            // let the next response reuse its ETag.
+            cache.envelope.clear();
+            cache.etag = None;
+            let status = match &reason {
+                FactsRejection::NotApplicable { applies_to } => CloudFactsState::NotApplicable {
+                    applies_to: applies_to.clone(),
+                },
+                _ => CloudFactsState::Rejected {
+                    reason: reason.to_string(),
+                    at: now_unix(),
+                },
+            };
+            if !overlay::publish_with(
+                &ticket,
+                None,
+                state_status(status, None, &cache.url),
+                || {
+                    if let Some(path) = &path {
+                        save_cache(path, &cache);
+                    }
+                },
+            ) {
+                return Err(RefreshError::Superseded);
+            }
+            Err(RefreshError::Rejected(reason))
+        }
+    }
+}
+
+pub fn spawn_background_refresh(
+    settings: Settings,
+    on_update: Option>,
+) {
+    let Ok(admitted) = ticket(&settings, codewhale_config::cloud_facts::TRUSTED_KEYS) else {
+        return;
+    };
+    if settings.local_path.is_none() && fetch_suppressed() {
+        return;
+    }
+    tokio::spawn(async move {
+        let before = overlay::snapshot().generation;
+        let outcome = refresh_using(
+            &settings,
+            false,
+            codewhale_config::cloud_facts::TRUSTED_KEYS,
+            Some(admitted),
+            fetch_suppressed(),
+            fetch,
+        )
+        .await;
+        tracing::debug!(target: "cloud_facts", ?outcome, "cloud facts refresh settled");
+        if overlay::snapshot().generation != before
+            && let Some(hook) = on_update
+        {
+            hook();
+        }
+    });
+}
+
+#[must_use]
+pub fn status() -> CloudFactsStatus {
+    overlay::status()
+}
+
+#[cfg(test)]
+mod tests;
diff --git a/crates/cloud-facts/src/tests.rs b/crates/cloud-facts/src/tests.rs
new file mode 100644
index 0000000000..7ccb689ca6
--- /dev/null
+++ b/crates/cloud-facts/src/tests.rs
@@ -0,0 +1,831 @@
+use std::path::PathBuf;
+use std::sync::{Arc, Mutex, MutexGuard, OnceLock};
+
+use codewhale_config::cloud_facts::{CloudFactsState, KeyStatus, TrustedKey, overlay};
+use tokio::io::{AsyncReadExt as _, AsyncWriteExt as _};
+use tokio::net::TcpListener;
+
+use super::*;
+
+// All tests that read or mutate the process overlay/environment hold `lock()`.
+struct TestEnv {
+    name: &'static str,
+    previous: Option,
+}
+impl TestEnv {
+    fn set(name: &'static str, value: &str) -> Self {
+        let previous = std::env::var_os(name);
+        // SAFETY: the test module serializes environment-dependent work.
+        unsafe { std::env::set_var(name, value) };
+        Self { name, previous }
+    }
+}
+impl Drop for TestEnv {
+    fn drop(&mut self) {
+        // SAFETY: the test module serializes environment-dependent work.
+        unsafe {
+            match &self.previous {
+                Some(value) => std::env::set_var(self.name, value),
+                None => std::env::remove_var(self.name),
+            }
+        }
+    }
+}
+
+/// Cross-language fixture signed with the TEST-ONLY key.
+const FIXTURE_V7: &str = include_str!("../../../docs/cloud-facts/fixtures/envelope-stable-v7.json");
+const FIXTURE_FUTURE_V8: &str =
+    include_str!("../../../docs/cloud-facts/fixtures/envelope-future-only-v8.json");
+
+fn test_keys() -> &'static [TrustedKey] {
+    static KEYS: OnceLock> = OnceLock::new();
+    KEYS.get_or_init(|| {
+        vec![TrustedKey {
+            key_id: "cwf-test-only",
+            public_key: [
+                243, 225, 75, 13, 110, 14, 162, 181, 4, 77, 69, 100, 179, 72, 105, 64, 8, 185, 46,
+                62, 48, 131, 121, 35, 42, 55, 216, 23, 50, 219, 39, 181,
+            ],
+            status: KeyStatus::Active,
+        }]
+    })
+}
+
+/// The overlay/status are process-wide; serialize tests that touch them.
+fn lock() -> MutexGuard<'static, ()> {
+    static LOCK: OnceLock> = OnceLock::new();
+    LOCK.get_or_init(|| Mutex::new(()))
+        .lock()
+        .unwrap_or_else(|p| p.into_inner())
+}
+
+fn settings(dir: &tempfile::TempDir, url: Option) -> Settings {
+    Settings {
+        enabled: true,
+        channel: "stable".into(),
+        url,
+        ttl_secs: 3600,
+        cache_path: Some(dir.path().join("facts").join(CACHE_FILE)),
+        local_path: None,
+    }
+}
+
+// Explicit fixture transport: CI still blocks the production refresh path.
+fn refresh_fixture<'a>(
+    settings: &'a Settings,
+    force: bool,
+    keys: &'a [TrustedKey],
+) -> impl std::future::Future> + 'a {
+    configure_with_keys(settings, keys);
+    refresh_using(settings, force, keys, None, false, fetch)
+}
+
+/// One canned HTTP response per connection; records the request line/headers.
+struct MockServer {
+    url: String,
+    requests: Arc>>,
+}
+
+/// `(status, headers, body)` canned HTTP response.
+type CannedResponse = (u16, Vec<(&'static str, String)>, String);
+
+async fn mock_server(responses: Vec) -> MockServer {
+    let listener = TcpListener::bind("127.0.0.1:0").await.expect("bind");
+    let addr = listener.local_addr().expect("addr");
+    let requests = Arc::new(Mutex::new(Vec::new()));
+    let seen = Arc::clone(&requests);
+    tokio::spawn(async move {
+        let mut responses = responses.into_iter();
+        while let Ok((mut stream, _)) = listener.accept().await {
+            let mut buf = vec![0u8; 8192];
+            let n = stream.read(&mut buf).await.unwrap_or(0);
+            let head = String::from_utf8_lossy(&buf[..n]).into_owned();
+            seen.lock().unwrap().push(head);
+            let (status, headers, body) =
+                responses
+                    .next()
+                    .unwrap_or((500, vec![], "no more canned responses".into()));
+            let reason = match status {
+                200 => "OK",
+                304 => "Not Modified",
+                404 => "Not Found",
+                _ => "Error",
+            };
+            let mut out = format!("HTTP/1.1 {status} {reason}\r\nConnection: close\r\n");
+            for (k, v) in headers {
+                out.push_str(&format!("{k}: {v}\r\n"));
+            }
+            out.push_str(&format!("Content-Length: {}\r\n\r\n{}", body.len(), body));
+            let _ = stream.write_all(out.as_bytes()).await;
+            let _ = stream.shutdown().await;
+        }
+    });
+    MockServer {
+        url: format!("http://{addr}/api/facts/v1/{{channel}}"),
+        requests,
+    }
+}
+
+fn rt() -> tokio::runtime::Runtime {
+    tokio::runtime::Builder::new_current_thread()
+        .enable_all()
+        .build()
+        .expect("runtime")
+}
+
+#[test]
+fn flag_off_means_no_client_no_file_and_off_status() {
+    let _lock = lock();
+    overlay::clear();
+    let dir = tempfile::tempdir().unwrap();
+    let mut s = settings(&dir, Some("http://127.0.0.1:1/{channel}".into()));
+    s.enabled = false;
+    configure_with_keys(&s, test_keys());
+    assert_eq!(maybe_load_persisted_cache_with_keys(&s, test_keys()), None);
+    assert_eq!(status().state, CloudFactsState::Off);
+    let err = rt()
+        .block_on(refresh_fixture(&s, true, test_keys()))
+        .unwrap_err();
+    assert_eq!(err, RefreshError::Disabled);
+    assert!(
+        !dir.path().join("facts").exists(),
+        "flag off must write nothing"
+    );
+    assert!(overlay::overlay().is_none());
+}
+
+#[test]
+fn enabled_with_no_active_key_is_inert_and_never_fetches() {
+    let _lock = lock();
+    overlay::clear();
+    let dir = tempfile::tempdir().unwrap();
+    let s = settings(&dir, Some("http://127.0.0.1:1/{channel}".into()));
+    configure_with_keys(&s, &[]);
+    assert_eq!(maybe_load_persisted_cache_with_keys(&s, &[]), None);
+    assert_eq!(status().state, CloudFactsState::Inert);
+    let err = rt().block_on(refresh_fixture(&s, true, &[])).unwrap_err();
+    assert_eq!(err, RefreshError::Inert);
+    assert!(!dir.path().join("facts").exists());
+}
+
+#[test]
+fn network_200_verifies_installs_caches_and_304_keeps_it() {
+    let _lock = lock();
+    overlay::clear();
+    let rt = rt();
+    let server = rt.block_on(mock_server(vec![
+        (
+            200,
+            vec![
+                ("ETag", "\"stable-v7-abc\"".into()),
+                ("Content-Type", "application/json".into()),
+            ],
+            FIXTURE_V7.into(),
+        ),
+        (
+            304,
+            vec![("ETag", "\"stable-v7-abc\"".into())],
+            String::new(),
+        ),
+    ]));
+    let dir = tempfile::tempdir().unwrap();
+    let s = settings(&dir, Some(server.url.clone()));
+
+    let outcome = rt.block_on(refresh_fixture(&s, true, test_keys())).unwrap();
+    assert_eq!(outcome, RefreshOutcome::Updated { facts_version: 7 });
+    let st = status();
+    assert!(
+        matches!(
+            st.state,
+            CloudFactsState::Verified {
+                facts_version: 7,
+                origin: FactsOrigin::Network,
+                patches: 5,
+                defaults: 1,
+                announcements: 1,
+                ..
+            }
+        ),
+        "{st:?}"
+    );
+    assert_eq!(st.etag.as_deref(), Some("\"stable-v7-abc\""));
+    let overlay = overlay::overlay().expect("overlay installed");
+    assert_eq!(overlay.facts_version, 7);
+    assert_eq!(
+        overlay::cloud_default_model("deepseek")
+            .map(|(m, _)| m)
+            .as_deref(),
+        Some("deepseek-v4-pro")
+    );
+
+    // Cache file exists, is secret-free, and carries the envelope + etag.
+    let cache = std::fs::read_to_string(s.cache_path.as_ref().unwrap()).unwrap();
+    assert!(cache.contains("stable-v7-abc"));
+    assert!(cache.contains("cwf-test-only"));
+    for needle in ["api_key", "authorization", "bearer", "password"] {
+        assert!(!cache.to_lowercase().contains(&format!("\"{needle}\"")));
+    }
+
+    // Second fetch sends If-None-Match and keeps the overlay on 304.
+    let outcome = rt.block_on(refresh_fixture(&s, true, test_keys())).unwrap();
+    assert_eq!(
+        outcome,
+        RefreshOutcome::NotModified {
+            facts_version: Some(7)
+        }
+    );
+    let requests = server.requests.lock().unwrap();
+    assert_eq!(requests.len(), 2);
+    assert!(
+        requests[1]
+            .to_lowercase()
+            .contains("if-none-match: \"stable-v7-abc\""),
+        "{}",
+        requests[1]
+    );
+    for req in requests.iter() {
+        assert!(
+            req.contains(&format!("User-Agent: {USER_AGENT}"))
+                || req.to_lowercase().contains("user-agent: codewhale/")
+        );
+        assert!(!req.to_lowercase().contains("cookie"));
+        assert!(
+            req.lines()
+                .next()
+                .unwrap()
+                .contains("/api/facts/v1/stable HTTP/1.1"),
+            "{}",
+            req.lines().next().unwrap()
+        );
+    }
+    assert!(matches!(
+        status().state,
+        CloudFactsState::Verified {
+            facts_version: 7,
+            ..
+        }
+    ));
+    overlay::clear();
+}
+
+#[test]
+fn persisted_cache_round_trips_and_a_tampered_cache_is_rejected_and_cleared() {
+    let _lock = lock();
+    overlay::clear();
+    let rt = rt();
+    let server = rt.block_on(mock_server(vec![(200, vec![], FIXTURE_V7.into())]));
+    let dir = tempfile::tempdir().unwrap();
+    let s = settings(&dir, Some(server.url.clone()));
+    rt.block_on(refresh_fixture(&s, true, test_keys())).unwrap();
+    overlay::clear();
+
+    // A newly admitted startup seeds the overlay without a network call.
+    configure_with_keys(&s, test_keys());
+    assert_eq!(
+        maybe_load_persisted_cache_with_keys(&s, test_keys()),
+        Some(7)
+    );
+    assert!(matches!(
+        status().state,
+        CloudFactsState::Verified {
+            origin: FactsOrigin::DiskCache,
+            ..
+        }
+    ));
+    assert!(overlay::overlay().is_some());
+    overlay::clear();
+
+    // Tamper one payload byte on disk.
+    let path = s.cache_path.clone().unwrap();
+    let text = std::fs::read_to_string(&path).unwrap();
+    let mut cache: serde_json::Value = serde_json::from_str(&text).unwrap();
+    let env = cache["envelope"]
+        .as_str()
+        .unwrap()
+        .replace("\"facts_version\": 7", "\"facts_version\": 9");
+    cache["envelope"] = serde_json::Value::String(env);
+    std::fs::write(&path, serde_json::to_vec(&cache).unwrap()).unwrap();
+    configure_with_keys(&s, test_keys());
+    assert_eq!(maybe_load_persisted_cache_with_keys(&s, test_keys()), None);
+    assert!(
+        matches!(status().state, CloudFactsState::Rejected { .. }),
+        "{:?}",
+        status().state
+    );
+    let cleared = load_cache(&path).expect("retain the rollback floor");
+    assert!(cleared.envelope.is_empty());
+    assert_eq!(cleared.highest_seen_version, Some(7));
+    assert!(overlay::overlay().is_none());
+}
+
+#[test]
+fn local_path_loads_without_network_and_scope_rejection_is_reported() {
+    let _lock = lock();
+    overlay::clear();
+    let dir = tempfile::tempdir().unwrap();
+    let local = dir.path().join("envelope.json");
+    std::fs::write(&local, FIXTURE_V7).unwrap();
+    let mut s = settings(&dir, Some("http://127.0.0.1:1/{channel}".into()));
+    s.local_path = Some(local.clone());
+    let outcome = rt()
+        .block_on(refresh_fixture(&s, true, test_keys()))
+        .unwrap();
+    assert_eq!(outcome, RefreshOutcome::Updated { facts_version: 7 });
+    assert!(matches!(
+        status().state,
+        CloudFactsState::Verified {
+            origin: FactsOrigin::LocalFile,
+            ..
+        }
+    ));
+
+    std::fs::write(&local, FIXTURE_FUTURE_V8).unwrap();
+    let err = rt()
+        .block_on(refresh_fixture(&s, true, test_keys()))
+        .unwrap_err();
+    assert!(matches!(
+        err,
+        RefreshError::Rejected(FactsRejection::NotApplicable { .. })
+    ));
+    assert!(matches!(
+        status().state,
+        CloudFactsState::NotApplicable { .. }
+    ));
+    overlay::clear();
+}
+
+#[test]
+fn server_errors_keep_prior_facts_and_persist_backoff() {
+    let _lock = lock();
+    overlay::clear();
+    let rt = rt();
+    let server = rt.block_on(mock_server(vec![
+        (200, vec![], FIXTURE_V7.into()),
+        (500, vec![], "boom".into()),
+        (200, vec![], "x".repeat(MAX_BODY_BYTES + 1)),
+    ]));
+    let dir = tempfile::tempdir().unwrap();
+    let s = settings(&dir, Some(server.url.clone()));
+    rt.block_on(refresh_fixture(&s, true, test_keys())).unwrap();
+
+    let err = rt
+        .block_on(refresh_fixture(&s, true, test_keys()))
+        .unwrap_err();
+    assert_eq!(err, RefreshError::HttpStatus(500));
+    assert!(
+        matches!(
+            status().state,
+            CloudFactsState::Failed {
+                keeping: Some(7),
+                ..
+            }
+        ),
+        "{:?}",
+        status().state
+    );
+    assert!(
+        overlay::overlay().is_some(),
+        "prior verified facts survive a failure"
+    );
+
+    // Backoff is persisted and honoured by non-forced refreshes.
+    let cache: serde_json::Value =
+        serde_json::from_str(&std::fs::read_to_string(s.cache_path.as_ref().unwrap()).unwrap())
+            .unwrap();
+    assert!(cache["backoff_until"].as_u64().unwrap() > now_unix());
+    let err = rt
+        .block_on(refresh_fixture(&s, false, test_keys()))
+        .unwrap_err();
+    assert!(matches!(err, RefreshError::BackingOff { .. }));
+
+    // Oversized body is refused before verification.
+    let err = rt
+        .block_on(refresh_fixture(&s, true, test_keys()))
+        .unwrap_err();
+    assert!(matches!(err, RefreshError::TooLarge(_)));
+    assert!(overlay::overlay().is_some());
+    overlay::clear();
+}
+
+#[test]
+fn not_found_means_no_facts_not_failure() {
+    let _lock = lock();
+    overlay::clear();
+    let rt = rt();
+    let server = rt.block_on(mock_server(vec![(
+        404,
+        vec![],
+        "{\"error\":\"no-facts\"}".into(),
+    )]));
+    let dir = tempfile::tempdir().unwrap();
+    let s = settings(&dir, Some(server.url.clone()));
+    let outcome = rt.block_on(refresh_fixture(&s, true, test_keys())).unwrap();
+    assert_eq!(outcome, RefreshOutcome::NoFacts);
+    assert_eq!(status().state, CloudFactsState::BundledOnly);
+    assert!(overlay::overlay().is_none());
+}
+
+#[test]
+fn settings_resolve_url_template_and_channel_validation() {
+    let s = Settings {
+        channel: "beta".into(),
+        ..Settings::default()
+    };
+    assert_eq!(s.url(), "https://codewhale.net/api/facts/v1/beta");
+    assert!(valid_channel("stable"));
+    assert!(valid_channel("beta-2"));
+    assert!(!valid_channel("-bad"));
+    assert!(!valid_channel("Stable"));
+    assert!(!valid_channel(""));
+    assert_eq!(
+        Settings::default().cache_path.as_deref(),
+        None::<&std::path::Path>,
+        "default settings resolve the cache under CODEWHALE_HOME"
+    );
+    let _ = PathBuf::new();
+}
+
+#[test]
+fn disable_rejects_late_200_and_304_and_preserves_cache_bytes() {
+    let _lock = lock();
+    for not_modified in [false, true] {
+        overlay::clear();
+        let dir = tempfile::tempdir().unwrap();
+        let s = settings(&dir, Some("https://fixture.invalid/{channel}".into()));
+        configure_with_keys(&s, test_keys());
+        rt().block_on(async {
+            refresh_using(&s, true, test_keys(), None, false, |_, _| async {
+                Ok(Fetched::Body {
+                    bytes: FIXTURE_V7.as_bytes().to_vec(),
+                    etag: Some("v7".into()),
+                })
+            })
+            .await
+            .unwrap();
+            let before = std::fs::read(s.cache_path.as_ref().unwrap()).unwrap();
+            let admitted = ticket(&s, test_keys()).unwrap();
+            let (entered_tx, entered_rx) = tokio::sync::oneshot::channel();
+            let (release_tx, release_rx) = tokio::sync::oneshot::channel();
+            let refresh = refresh_using(
+                &s,
+                true,
+                test_keys(),
+                Some(admitted),
+                false,
+                |_, _| async move {
+                    entered_tx.send(()).unwrap();
+                    release_rx.await.unwrap();
+                    Ok(if not_modified {
+                        Fetched::NotModified
+                    } else {
+                        Fetched::Body {
+                            bytes: FIXTURE_V7.as_bytes().to_vec(),
+                            etag: Some("late".into()),
+                        }
+                    })
+                },
+            );
+            let disable = async {
+                entered_rx.await.unwrap();
+                let mut disabled = s.clone();
+                disabled.enabled = false;
+                configure_with_keys(&disabled, test_keys());
+                release_tx.send(()).unwrap();
+            };
+            let (result, ()) = tokio::join!(refresh, disable);
+            assert!(
+                std::fs::read(s.cache_path.as_ref().unwrap()).unwrap() == before,
+                "a disabled refresh must not change cache bytes"
+            );
+            assert_eq!(result, Err(RefreshError::Superseded));
+            assert!(overlay::overlay().is_none());
+            assert_eq!(status().state, CloudFactsState::Off);
+            assert_eq!(
+                refresh_using(&s, true, test_keys(), None, false, |_, _| async {
+                    panic!("old settings cannot create transport")
+                })
+                .await,
+                Err(RefreshError::Superseded)
+            );
+        });
+    }
+}
+
+#[test]
+fn changed_source_drops_etag_and_304_reverifies_cached_envelope() {
+    let _lock = lock();
+    overlay::clear();
+    let dir = tempfile::tempdir().unwrap();
+    let mut s = settings(&dir, Some("https://first.invalid/{channel}".into()));
+    configure_with_keys(&s, test_keys());
+    rt().block_on(async {
+        refresh_using(&s, true, test_keys(), None, false, |_, _| async {
+            Ok(Fetched::Body {
+                bytes: FIXTURE_V7.as_bytes().to_vec(),
+                etag: Some("first-v7".into()),
+            })
+        })
+        .await
+        .unwrap();
+        s.url = Some("https://second.invalid/{channel}".into());
+        configure_with_keys(&s, test_keys());
+        refresh_using(&s, true, test_keys(), None, false, |_, etag| async move {
+            assert!(
+                etag.is_none(),
+                "validators must not cross source boundaries"
+            );
+            Ok(Fetched::Body {
+                bytes: FIXTURE_V7.as_bytes().to_vec(),
+                etag: Some("second-v7".into()),
+            })
+        })
+        .await
+        .unwrap();
+        let path = s.cache_path.as_ref().unwrap();
+        let mut cache = load_cache(path).unwrap();
+        assert_eq!(cache.highest_seen_version, Some(7));
+        cache.highest_seen_version = Some(8);
+        save_cache(path, &cache);
+        let result = refresh_using(&s, true, test_keys(), None, false, |_, etag| async move {
+            assert_eq!(etag.as_deref(), Some("second-v7"));
+            Ok(Fetched::NotModified)
+        })
+        .await;
+        assert!(
+            matches!(result, Err(RefreshError::Rejected(_))),
+            "{result:?}"
+        );
+        assert!(overlay::overlay().is_none());
+        let rejected = load_cache(path).unwrap();
+        assert_eq!(rejected.highest_seen_version, Some(8));
+        assert!(rejected.envelope.is_empty());
+        assert!(rejected.etag.is_none());
+    });
+}
+
+#[test]
+fn fixture_transport_is_explicit_and_production_suppression_still_wins() {
+    let _lock = lock();
+    overlay::clear();
+    let dir = tempfile::tempdir().unwrap();
+    let s = settings(&dir, Some("https://fixture.invalid/{channel}".into()));
+    configure_with_keys(&s, test_keys());
+    let _ci = TestEnv::set("CI", "true");
+    rt().block_on(async {
+        assert_eq!(
+            refresh_with_keys(&s, true, test_keys()).await,
+            Err(RefreshError::Suppressed)
+        );
+        let suppressed = refresh_using(&s, true, test_keys(), None, true, |_, _| async {
+            panic!("production suppression precedes transport")
+        })
+        .await;
+        assert_eq!(suppressed, Err(RefreshError::Suppressed));
+        let fixture = refresh_using(&s, true, test_keys(), None, false, |_, _| async {
+            Ok(Fetched::NotFound)
+        })
+        .await;
+        assert_eq!(fixture, Ok(RefreshOutcome::NoFacts));
+    });
+}
+
+#[test]
+fn bounded_files_reject_oversize_outer_cache_and_nonregular_inputs() {
+    let _lock = lock();
+    let dir = tempfile::tempdir().unwrap();
+    let path = dir.path().join("large");
+    std::fs::File::create(&path)
+        .unwrap()
+        .set_len((MAX_CACHE_BYTES + 1) as u64)
+        .unwrap();
+    assert!(matches!(
+        read_bounded_regular(&path, MAX_CACHE_BYTES),
+        Err(RefreshError::TooLarge(_))
+    ));
+    assert!(load_cache(&path).is_none());
+    assert!(read_bounded_regular(dir.path(), MAX_BODY_BYTES).is_err());
+    #[cfg(unix)]
+    {
+        use std::os::unix::fs::symlink;
+        let target = dir.path().join("target");
+        std::fs::write(&target, FIXTURE_V7).unwrap();
+        let linked = dir.path().join("symlink");
+        symlink(&target, &linked).unwrap();
+        assert!(read_bounded_regular(&linked, MAX_BODY_BYTES).is_err());
+        let hardlink = dir.path().join("hardlink");
+        std::fs::hard_link(&target, &hardlink).unwrap();
+        assert!(read_bounded_regular(&target, MAX_BODY_BYTES).is_err());
+        let fifo = dir.path().join("fifo");
+        let raw = std::ffi::CString::new(fifo.as_os_str().as_encoded_bytes()).unwrap();
+        assert_eq!(unsafe { libc::mkfifo(raw.as_ptr(), 0o600) }, 0);
+        assert!(read_bounded_regular(&fifo, MAX_BODY_BYTES).is_err());
+    }
+}
+
+#[test]
+fn chunked_fetch_stops_at_body_limit_without_waiting_for_end_of_stream() {
+    let _lock = lock();
+    rt().block_on(async {
+        let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
+        let url = format!("http://{}/facts", listener.local_addr().unwrap());
+        let server = tokio::spawn(async move {
+            let (mut stream, _) = listener.accept().await.unwrap();
+            let mut head = [0u8; 4096];
+            let _ = stream.read(&mut head).await;
+            stream
+                .write_all(b"HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n")
+                .await
+                .unwrap();
+            let chunk = vec![b'x'; 8192];
+            for _ in 0..(MAX_BODY_BYTES / chunk.len() + 1) {
+                if stream.write_all(b"2000\r\n").await.is_err() {
+                    return;
+                }
+                if stream.write_all(&chunk).await.is_err() {
+                    return;
+                }
+                if stream.write_all(b"\r\n").await.is_err() {
+                    return;
+                }
+            }
+            std::future::pending::<()>().await;
+        });
+        let result = tokio::time::timeout(Duration::from_secs(3), fetch(url, None))
+            .await
+            .expect("oversized stream must be rejected before timeout/end-of-body");
+        assert!(matches!(result, Err(RefreshError::TooLarge(_))));
+        server.abort();
+    });
+}
+
+#[test]
+fn hard_disable_blocks_local_cache_transport_and_late_publication() {
+    let _lock = lock();
+    overlay::clear();
+    let dir = tempfile::tempdir().unwrap();
+    let local = dir.path().join("local.json");
+    std::fs::write(&local, FIXTURE_V7).unwrap();
+    let mut s = settings(&dir, None);
+    s.local_path = Some(local);
+    rt().block_on(refresh_fixture(&s, true, test_keys()))
+        .unwrap();
+    let path = s.cache_path.as_ref().unwrap();
+    let before = std::fs::read(path).unwrap();
+    let prior = ticket(&s, test_keys()).unwrap();
+    let _disable = TestEnv::set(ENV_DISABLE, "1");
+    assert!(overlay::overlay().is_none());
+    assert_eq!(maybe_load_persisted_cache_with_keys(&s, test_keys()), None);
+    let result = rt().block_on(refresh_using(
+        &s,
+        true,
+        test_keys(),
+        Some(prior),
+        false,
+        |_, _| async { panic!("hard disable must precede local reads and transport") },
+    ));
+    assert_eq!(result, Err(RefreshError::Disabled));
+    assert_eq!(std::fs::read(path).unwrap(), before);
+    configure_with_keys(&s, test_keys());
+    assert_eq!(status().state, CloudFactsState::Off);
+    assert!(!s.resolve().enabled);
+}
+
+#[test]
+fn source_switch_and_disable_keep_channel_floor_after_cache_removal() {
+    let _lock = lock();
+    overlay::clear();
+    let dir = tempfile::tempdir().unwrap();
+    let mut s = settings(&dir, Some("https://first.invalid/{channel}".into()));
+    configure_with_keys(&s, test_keys());
+    rt().block_on(refresh_using(
+        &s,
+        true,
+        test_keys(),
+        None,
+        false,
+        |_, _| async {
+            Ok(Fetched::Body {
+                bytes: FIXTURE_V7.as_bytes().to_vec(),
+                etag: Some("first-v7".into()),
+            })
+        },
+    ))
+    .unwrap();
+    std::fs::remove_file(s.cache_path.as_ref().unwrap()).unwrap();
+    overlay::clear();
+    s.url = Some("https://second.invalid/{channel}".into());
+    configure_with_keys(&s, test_keys());
+    assert_eq!(overlay::highest_seen("stable"), Some(7));
+    let cache = cache_for(&s, test_keys()).unwrap();
+    assert_eq!(cache.highest_seen_version, Some(7));
+    assert!(cache.envelope.is_empty());
+    assert!(cache.etag.is_none());
+}
+
+#[test]
+fn forged_cache_timestamps_cannot_make_facts_fresh_or_block_refresh() {
+    let _lock = lock();
+    overlay::clear();
+    let dir = tempfile::tempdir().unwrap();
+    let s = settings(&dir, Some("https://fixture.invalid/{channel}".into()));
+    configure_with_keys(&s, test_keys());
+    rt().block_on(async {
+        refresh_using(&s, true, test_keys(), None, false, |_, _| async {
+            Ok(Fetched::Body {
+                bytes: FIXTURE_V7.as_bytes().to_vec(),
+                etag: Some("v7".into()),
+            })
+        })
+        .await
+        .unwrap();
+        let path = s.cache_path.as_ref().unwrap();
+        let mut cache = load_cache(path).unwrap();
+        cache.fetched_at = u64::MAX;
+        cache.backoff_until = Some(u64::MAX);
+        save_cache(path, &cache);
+        assert_eq!(
+            maybe_load_persisted_cache_with_keys(&s, test_keys()),
+            Some(7)
+        );
+        assert!(
+            overlay::overlay().is_none(),
+            "future local metadata cannot grant authority"
+        );
+        let result = refresh_using(&s, false, test_keys(), None, false, |_, etag| async move {
+            assert_eq!(etag.as_deref(), Some("v7"));
+            Ok(Fetched::NotModified)
+        })
+        .await;
+        assert_eq!(
+            result,
+            Ok(RefreshOutcome::NotModified {
+                facts_version: Some(7)
+            })
+        );
+        assert!(overlay::overlay().is_some());
+        let repaired = load_cache(path).unwrap();
+        assert!(repaired.fetched_at <= now_unix());
+        assert!(repaired.backoff_until.is_none());
+        assert_eq!(repaired.highest_seen_version, Some(7));
+    });
+}
+
+#[test]
+fn cache_restart_recovers_authenticated_floor_before_source_switch() {
+    const CHILD_CACHE: &str = "CODEWHALE_TEST_CLOUD_RESTART_CACHE";
+    let _lock = lock();
+    if let Some(path) = std::env::var_os(CHILD_CACHE) {
+        // This child runs only this test, so there is no inherited process
+        // overlay to mask a lost disk rollback receipt.
+        assert_eq!(overlay::highest_seen("stable"), None);
+        let s = Settings {
+            enabled: true,
+            url: Some("https://second.invalid/{channel}".into()),
+            cache_path: Some(path.into()),
+            ..Settings::default()
+        };
+        configure_with_keys(&s, test_keys());
+        let cache = cache_for(&s, test_keys()).unwrap();
+        assert_eq!(cache.highest_seen_version, Some(7));
+        assert!(cache.envelope.is_empty());
+        assert!(cache.etag.is_none());
+        return;
+    }
+    overlay::clear();
+    let dir = tempfile::tempdir().unwrap();
+    let s = settings(&dir, Some("https://first.invalid/{channel}".into()));
+    configure_with_keys(&s, test_keys());
+    rt().block_on(refresh_using(
+        &s,
+        true,
+        test_keys(),
+        None,
+        false,
+        |_, _| async {
+            Ok(Fetched::Body {
+                bytes: FIXTURE_V7.as_bytes().to_vec(),
+                etag: Some("first-v7".into()),
+            })
+        },
+    ))
+    .unwrap();
+    let path = s.cache_path.as_ref().unwrap();
+    let mut cache = load_cache(path).unwrap();
+    cache.highest_seen_version = Some(0);
+    save_cache(path, &cache);
+    let child = std::process::Command::new(std::env::current_exe().unwrap())
+        .args([
+            "--exact",
+            "tests::cache_restart_recovers_authenticated_floor_before_source_switch",
+            "--nocapture",
+        ])
+        .env(CHILD_CACHE, path)
+        .output()
+        .unwrap();
+    assert!(
+        child.status.success(),
+        "fresh-process check failed: {}{}",
+        String::from_utf8_lossy(&child.stdout),
+        String::from_utf8_lossy(&child.stderr)
+    );
+}
diff --git a/crates/config/Cargo.toml b/crates/config/Cargo.toml
index 266121cceb..3d1383970b 100644
--- a/crates/config/Cargo.toml
+++ b/crates/config/Cargo.toml
@@ -12,6 +12,10 @@ workspace = true
 
 [dependencies]
 anyhow.workspace = true
+base64 = "0.22.1"
+chrono.workspace = true
+ring = "0.17.14"
+semver.workspace = true
 codewhale-execpolicy = { path = "../execpolicy", version = "0.9.13" }
 codewhale-paths = { path = "../paths", version = "0.9.13" }
 codewhale-secrets = { path = "../secrets", version = "0.9.13" }
diff --git a/crates/config/src/catalog.rs b/crates/config/src/catalog.rs
index 125bf2e7db..854ba654eb 100644
--- a/crates/config/src/catalog.rs
+++ b/crates/config/src/catalog.rs
@@ -44,6 +44,8 @@ use serde_json::Value;
 use crate::models_dev::{ModelsDevCatalog, ModelsDevCost, ModelsDevLimit, ModelsDevModalities};
 use crate::route::{ModelId, ProviderId, ProviderModelOffering, RouteLimits, WireModelId};
 
+pub mod configured;
+
 /// Provenance of a catalog row. Drives layer precedence and UI provenance.
 #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
 #[serde(tag = "kind", rename_all = "snake_case")]
@@ -68,6 +70,14 @@ pub enum CatalogSource {
     CodewhaleBundled { revision: String },
     /// Live Codewhale signed catalog fetched from CWC / `CODEWHALE_CATALOG_URL`.
     CodewhaleLive { revision: String, fetched_at: u64 },
+    /// Signed field patch, below provider-owned rows and explicit overrides.
+    CloudFacts {
+        facts_version: u64,
+        key_id: String,
+        fetched_at: u64,
+        #[serde(default, skip_serializing_if = "Option::is_none")]
+        valid_until: Option,
+    },
 }
 
 /// One catalog-layer offering row.
@@ -100,6 +110,9 @@ pub struct CatalogOffering {
     /// Provider-scoped pricing, when known.
     #[serde(default, skip_serializing_if = "Option::is_none")]
     pub cost: Option,
+    /// Price authority stays separate when a layer changes only capabilities.
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub cost_source: Option,
     /// Input/output modalities for this offering, when known. Carried as the
     /// raw Models.dev shape so a factual `text` vs `multimodal` label can be
     /// derived without guessing; `None` means the layer did not state it (an
@@ -128,6 +141,11 @@ pub struct CatalogOffering {
 }
 
 impl CatalogOffering {
+    #[must_use]
+    pub fn pricing_source(&self) -> &CatalogSource {
+        self.cost_source.as_ref().unwrap_or(&self.source)
+    }
+
     /// The provider id as a route newtype.
     #[must_use]
     pub fn provider_id(&self) -> ProviderId {
@@ -318,6 +336,7 @@ fn offerings_from_models_dev(
                 structured_output: model.structured_output,
                 reasoning_options: model.reasoning_options.clone(),
                 source: source.clone(),
+                cost_source: None,
             });
         }
     }
@@ -635,6 +654,7 @@ impl CatalogSnapshot {
 ///  0 bundled              committed models.dev-shaped snapshot
 ///  5 codewhale bundled    Codewhale-owned offline snapshot
 /// 10 live models.dev      models.dev refresh
+/// 15 cloud facts          verified field patches (default off)
 /// 20 provider             per-provider /v1/models refresh
 /// 25 codewhale live       signed CWC catalog (authority)
 /// 30 config               config.toml [providers.*] overrides
@@ -650,7 +670,7 @@ pub struct CatalogCompiler {
     bundled: Vec,
     codewhale_bundled: Vec,
     models_dev_live: Vec,
-    live: Vec,
+    cloud_facts: Option<(crate::cloud_facts::ScopedFacts, u64)>,
     provider_live: Vec,
     codewhale_live: Vec,
     config: Vec,
@@ -704,11 +724,28 @@ impl CatalogCompiler {
     /// Add live (combined models.dev + provider) rows.
     ///
     /// Prefer [`Self::with_models_dev_live`] / [`Self::with_provider_live`].
-    /// Kept so existing callers still compile; these rows sit between
-    /// models.dev live and provider live.
+    /// Source ownership places each row on the corresponding side of the
+    /// signed cloud layer; a legacy provider row never becomes a lower layer.
     #[must_use]
     pub fn with_live(mut self, rows: Vec) -> Self {
-        self.live.extend(rows);
+        for row in rows {
+            if matches!(row.source, CatalogSource::ModelsDevLive { .. }) {
+                self.models_dev_live.push(row);
+            } else {
+                self.provider_live.push(row);
+            }
+        }
+        self
+    }
+
+    /// Apply signed facts between generic catalogs and provider-owned rows.
+    #[must_use]
+    pub fn with_cloud_facts(
+        mut self,
+        facts: &crate::cloud_facts::ScopedFacts,
+        fetched_at: u64,
+    ) -> Self {
+        self.cloud_facts = Some((facts.clone(), fetched_at));
         self
     }
 
@@ -749,8 +786,15 @@ impl CatalogCompiler {
             .into_iter()
             .chain(self.codewhale_bundled)
             .chain(self.models_dev_live)
-            .chain(self.live)
-            .chain(self.provider_live)
+        {
+            merged.insert(row.merge_key(), row);
+        }
+        if let Some((facts, fetched_at)) = self.cloud_facts {
+            crate::cloud_facts::catalog_patch::apply_model_patches(&mut merged, &facts, fetched_at);
+        }
+        for row in self
+            .provider_live
+            .into_iter()
             .chain(self.codewhale_live)
             .chain(self.config)
             .chain(self.overrides)
diff --git a/crates/config/src/catalog/configured.rs b/crates/config/src/catalog/configured.rs
new file mode 100644
index 0000000000..1a9d0882d3
--- /dev/null
+++ b/crates/config/src/catalog/configured.rs
@@ -0,0 +1,196 @@
+//! Persisted, operator-declared input to the existing catalog. These records
+//! describe one exact route; they never create credentials or model aliases.
+
+use std::collections::{BTreeMap, BTreeSet};
+
+use serde::{Deserialize, Deserializer, Serialize};
+
+use super::{CatalogOffering, CatalogSource, base_url_fingerprint};
+use crate::models_dev::{ModelsDevCost, ModelsDevLimit, ModelsDevModalities};
+
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
+pub struct ConfiguredModel {
+    pub provider: String,
+    pub base_url: String,
+    /// Exact, case-sensitive wire identity. The label is never sent instead.
+    pub id: String,
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub display_name: Option,
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    #[serde(deserialize_with = "deserialize_limit")]
+    pub limit: Option,
+    /// USD per million tokens, using the same shape as the catalog.
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    #[serde(deserialize_with = "deserialize_cost")]
+    pub cost: Option,
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    #[serde(deserialize_with = "deserialize_modalities")]
+    pub modalities: Option,
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub attachment: Option,
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub reasoning: Option,
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub tool_call: Option,
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub structured_output: Option,
+    /// Keep future metadata intact through typed config saves.
+    #[serde(flatten)]
+    pub extras: BTreeMap,
+}
+
+impl ConfiguredModel {
+    pub fn matches_route(&self, provider: &str, base_url: &str) -> bool {
+        !base_url.contains(['@', '?', '#'])
+            && self.provider == provider
+            && base_url_fingerprint(&self.base_url) == base_url_fingerprint(base_url)
+    }
+
+    /// Unknown fields stay unknown; no sibling, alias, or provider fact is
+    /// inherited. Source is assigned here and cannot be supplied by the file.
+    pub fn to_catalog_offering(&self) -> CatalogOffering {
+        CatalogOffering {
+            provider: self.provider.clone(),
+            wire_model_id: self.id.clone(),
+            endpoint_key: "chat".into(),
+            limit: self.limit.clone(),
+            cost: self.cost.clone(),
+            modalities: self.modalities.clone(),
+            attachment: self.attachment,
+            reasoning: self.reasoning,
+            tool_call: self.tool_call,
+            structured_output: self.structured_output,
+            source: CatalogSource::ConfigOverride,
+            ..CatalogOffering::default()
+        }
+    }
+}
+
+pub fn validate_configured_models(models: &[ConfiguredModel]) -> anyhow::Result<()> {
+    let mut identities = BTreeSet::new();
+    for (index, model) in models.iter().enumerate() {
+        // Error messages name fields, never echo untrusted values or URLs.
+        let invalid = |field| anyhow::anyhow!("custom_models[{index}].{field} is invalid");
+        for (field, value) in [("provider", &model.provider), ("id", &model.id)] {
+            if value.is_empty()
+                || value.len() > 256
+                || value.chars().any(char::is_whitespace)
+                || value.chars().any(char::is_control)
+            {
+                return Err(invalid(field));
+            }
+        }
+        if model.id.eq_ignore_ascii_case("auto") {
+            return Err(invalid("id"));
+        }
+        let Some((scheme, rest)) = model.base_url.split_once("://") else {
+            return Err(invalid("base_url"));
+        };
+        if !matches!(scheme.to_ascii_lowercase().as_str(), "http" | "https")
+            || rest.split('/').next().is_none_or(str::is_empty)
+            || model.base_url.contains(['@', '?', '#'])
+            || model
+                .base_url
+                .chars()
+                .any(|c| c.is_whitespace() || c.is_control())
+        {
+            return Err(invalid("base_url"));
+        }
+        if model
+            .display_name
+            .as_ref()
+            .is_some_and(|name| name.trim().is_empty() || name.chars().any(char::is_control))
+        {
+            return Err(invalid("display_name"));
+        }
+        if let Some(limit) = &model.limit
+            && ([limit.context, limit.input, limit.output]
+                .into_iter()
+                .flatten()
+                .any(|value| value == 0 || value > u64::from(u32::MAX))
+                || limit.context.is_some_and(|context| {
+                    limit.input.is_some_and(|input| input > context)
+                        || limit.output.is_some_and(|output| output > context)
+                }))
+        {
+            return Err(invalid("limit"));
+        }
+        if model
+            .cost
+            .as_ref()
+            .is_some_and(|cost| !crate::pricing::catalog_cost_is_valid(cost))
+        {
+            return Err(invalid("cost"));
+        }
+        if model.extras.keys().any(|key| {
+            matches!(
+                key.as_str(),
+                "source"
+                    | "canonical_model"
+                    | "aliases"
+                    | "api_key"
+                    | "auth"
+                    | "headers"
+                    | "endpoint_key"
+                    | "default_for_provider"
+            )
+        }) {
+            return Err(invalid("metadata authority"));
+        }
+        if !identities.insert((
+            model.provider.clone(),
+            base_url_fingerprint(&model.base_url),
+            model.id.clone(),
+        )) {
+            return Err(invalid("duplicate route"));
+        }
+    }
+    Ok(())
+}
+
+pub fn deserialize_configured_models<'de, D>(
+    deserializer: D,
+) -> Result>, D::Error>
+where
+    D: Deserializer<'de>,
+{
+    let models = Option::>::deserialize(deserializer)?;
+    validate_configured_models(models.as_deref().unwrap_or_default())
+        .map_err(serde::de::Error::custom)?;
+    Ok(models)
+}
+
+// Nested units and limits have precise meanings. Reject unrecognized keys
+// instead of silently dropping a currency, tier, or other pricing condition.
+fn deserialize_known<'de, D: Deserializer<'de>, T: serde::de::DeserializeOwned>(
+    d: D,
+    fields: &[&str],
+) -> Result, D::Error> {
+    let value = Option::::deserialize(d)?;
+    value
+        .map(|value| {
+            if value
+                .as_table()
+                .is_none_or(|table| table.keys().any(|key| !fields.contains(&key.as_str())))
+            {
+                return Err(serde::de::Error::custom(
+                    "unsupported nested custom model metadata field",
+                ));
+            }
+            value
+                .try_into()
+                .map_err(|_| serde::de::Error::custom("invalid custom model metadata"))
+        })
+        .transpose()
+}
+fn deserialize_limit<'de, D: Deserializer<'de>>(d: D) -> Result, D::Error> {
+    deserialize_known(d, &["context", "input", "output"])
+}
+fn deserialize_cost<'de, D: Deserializer<'de>>(d: D) -> Result, D::Error> {
+    deserialize_known(d, &["input", "output", "cache_read", "cache_write"])
+}
+fn deserialize_modalities<'de, D: Deserializer<'de>>(
+    d: D,
+) -> Result, D::Error> {
+    deserialize_known(d, &["input", "output"])
+}
diff --git a/crates/config/src/cloud_facts/catalog_patch.rs b/crates/config/src/cloud_facts/catalog_patch.rs
new file mode 100644
index 0000000000..972be8888b
--- /dev/null
+++ b/crates/config/src/cloud_facts/catalog_patch.rs
@@ -0,0 +1,178 @@
+//! Apply cloud model patches to a catalog layer map (layer 15: above bundled
+//! and live models.dev, below provider `/v1/models`, config, and user rows).
+//!
+//! Patch semantics:
+//! - `Upsert`: only the fields the patch sets shadow the row; a patch for a
+//!   row that does not exist is materialized only when it carries a context
+//!   window (otherwise skipped with a receipt).
+//! - `Deprecate`: annotates (the note is carried in `reasoning_options` as a
+//!   `{"cloud_facts": {...}}` marker); never removes.
+//! - `Hide`: removes the row only when it came from the bundled or live
+//!   models.dev layers. Provider-live/config/user rows are never hidden.
+
+use std::collections::BTreeMap;
+
+use serde_json::json;
+
+use super::scope::ScopedFacts;
+use super::types::{ModelFact, ModelOp};
+use crate::catalog::{CatalogOffering, CatalogSource};
+use crate::models_dev::ModelsDevCost;
+
+/// Merge key used by the catalog compiler.
+type Key = (String, String);
+
+/// Receipt for one patch that changed nothing.
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct SkippedPatch {
+    pub provider: String,
+    pub id: String,
+    pub reason: String,
+}
+
+/// Apply every model patch in `facts` to `rows`, returning skip receipts.
+pub fn apply_model_patches(
+    rows: &mut BTreeMap,
+    facts: &ScopedFacts,
+    fetched_at: u64,
+) -> Vec {
+    let mut skipped = Vec::new();
+    if !facts.is_current_at(crate::catalog::now_unix()) {
+        return skipped;
+    }
+    let source = CatalogSource::CloudFacts {
+        facts_version: facts.facts_version,
+        key_id: facts.key_id.clone(),
+        fetched_at,
+        valid_until: facts.valid_until,
+    };
+    for patch in &facts.models {
+        let key = (patch.provider.clone(), patch.id.clone());
+        if rows.get(&key).is_some_and(|row| {
+            !matches!(
+                row.source,
+                CatalogSource::Bundled
+                    | CatalogSource::CodewhaleBundled { .. }
+                    | CatalogSource::ModelsDevLive { .. }
+                    | CatalogSource::CloudFacts { .. }
+            )
+        }) {
+            skipped.push(SkippedPatch {
+                provider: patch.provider.clone(),
+                id: patch.id.clone(),
+                reason: "patch ignored: row comes from a higher layer".into(),
+            });
+            continue;
+        }
+        match patch.op {
+            ModelOp::Hide => match rows.get(&key) {
+                Some(row)
+                    if matches!(
+                        row.source,
+                        CatalogSource::Bundled
+                            | CatalogSource::CodewhaleBundled { .. }
+                            | CatalogSource::ModelsDevLive { .. }
+                    ) =>
+                {
+                    rows.remove(&key);
+                }
+                Some(_) => skipped.push(SkippedPatch {
+                    provider: patch.provider.clone(),
+                    id: patch.id.clone(),
+                    reason: "hide ignored: row comes from a higher layer".into(),
+                }),
+                None => skipped.push(SkippedPatch {
+                    provider: patch.provider.clone(),
+                    id: patch.id.clone(),
+                    reason: "hide ignored: no such row".into(),
+                }),
+            },
+            ModelOp::Deprecate => match rows.get_mut(&key) {
+                Some(row) => {
+                    annotate(row, patch, "deprecated");
+                }
+                None => skipped.push(SkippedPatch {
+                    provider: patch.provider.clone(),
+                    id: patch.id.clone(),
+                    reason: "deprecate ignored: no such row".into(),
+                }),
+            },
+            ModelOp::Upsert => {
+                if let Some(row) = rows.get_mut(&key) {
+                    // A capability patch must not refresh or relabel inherited prices.
+                    let inherited_price_source = row.pricing_source().clone();
+                    patch_fields(row, patch);
+                    row.cost_source = Some(if patch.pricing.is_some() {
+                        source.clone()
+                    } else {
+                        inherited_price_source
+                    });
+                    row.source = source.clone();
+                } else if patch.context_window.is_some() {
+                    let mut row = CatalogOffering {
+                        provider: patch.provider.clone(),
+                        wire_model_id: patch.id.clone(),
+                        endpoint_key: "chat".to_string(),
+                        source: source.clone(),
+                        ..CatalogOffering::default()
+                    };
+                    patch_fields(&mut row, patch);
+                    if patch.pricing.is_some() {
+                        row.cost_source = Some(source.clone());
+                    }
+                    rows.insert(key, row);
+                } else {
+                    skipped.push(SkippedPatch {
+                        provider: patch.provider.clone(),
+                        id: patch.id.clone(),
+                        reason: "upsert ignored: new row needs context_window".into(),
+                    });
+                }
+            }
+        }
+    }
+    skipped
+}
+
+fn patch_fields(row: &mut CatalogOffering, patch: &ModelFact) {
+    if patch.context_window.is_some() || patch.max_output.is_some() {
+        let mut limit = row.limit.clone().unwrap_or_default();
+        if let Some(context) = patch.context_window {
+            limit.context = Some(context);
+        }
+        if let Some(output) = patch.max_output {
+            limit.output = Some(output);
+        }
+        row.limit = Some(limit);
+    }
+    if let Some(pricing) = &patch.pricing {
+        // A price block has one authority. Missing classes stay unknown instead
+        // of silently mixing an old row's prices with newly signed rates.
+        row.cost = Some(ModelsDevCost {
+            input: pricing.input_per_m,
+            output: pricing.output_per_m,
+            cache_read: pricing.cache_read_per_m,
+            cache_write: None,
+        });
+    }
+    if patch.reasoning.is_some() {
+        row.reasoning = patch.reasoning;
+    }
+    if patch.display_name.is_some() || patch.note.is_some() {
+        annotate(row, patch, "upsert");
+    }
+}
+
+fn annotate(row: &mut CatalogOffering, patch: &ModelFact, kind: &str) {
+    row.reasoning_options
+        .retain(|value| value.get("cloud_facts").is_none());
+    row.reasoning_options.push(json!({
+        "cloud_facts": {
+            "op": kind,
+            "display_name": patch.display_name,
+            "deprecated_at": patch.deprecated_at,
+            "replacement": patch.replacement,
+            "note": patch.note,
+        }
+    }));
+}
diff --git a/crates/config/src/cloud_facts/keys.rs b/crates/config/src/cloud_facts/keys.rs
new file mode 100644
index 0000000000..4d8c8eba4a
--- /dev/null
+++ b/crates/config/src/cloud_facts/keys.rs
@@ -0,0 +1,59 @@
+//! Trust anchors for the cloud facts channel.
+//!
+//! Keys are pinned in the binary. The Supabase `facts_key` table and the
+//! website mirror (`web/lib/cloud-facts/keys.ts`) are informational; a facts
+//! envelope is accepted only when its signature verifies under an `Active` key
+//! listed here. `web/scripts/check-cloud-facts.mjs` fails CI if this table and
+//! the TypeScript mirror diverge.
+//!
+//! Rotation (two-release rule): pin the new key here → ship → sign with both
+//! keys (`sigs`) → mark the old key `Retired` → ship → drop it. Compromise:
+//! revoke every release signed by the key server-side, ship a binary without
+//! the key. There is deliberately no in-band "distrust this key" message.
+
+/// Domain separator prefixed to every signed message.
+///
+/// Message = `DOMAIN || key_id || 0x00 || payload_bytes`.
+pub const DOMAIN: &[u8] = b"codewhale-facts/v1\0";
+
+/// Transport envelope version this client understands.
+pub const ENVELOPE_VERSION: u64 = 1;
+
+/// Highest signed-payload `schema_version` this client understands. Newer
+/// payloads are rejected as `SchemaTooNew` and the bundled facts stay in use.
+pub const SUPPORTED_SCHEMA_VERSION: u32 = 1;
+
+/// Hard cap on the decoded payload; enforced before any crypto runs.
+pub const MAX_PAYLOAD_BYTES: usize = 512 * 1024;
+
+/// Hard cap on the raw envelope document (payload base64 + metadata).
+pub const MAX_ENVELOPE_BYTES: usize = 768 * 1024;
+
+/// Whether a pinned key may still authenticate new releases.
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum KeyStatus {
+    /// Accepts signatures.
+    Active,
+    /// Still listed so `/status` can name it, but no longer accepted.
+    Retired,
+}
+
+/// One pinned Ed25519 verifying key.
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub struct TrustedKey {
+    /// `cwf-