mirror of
https://github.com/codewhale-hq/Codewhale.git
synced 2026-11-07 10:36:05 -10:00
Restore missing plugin stages only from receipt-matching bytes under the existing state writer, with generation and policy checks. Refuse changed existing stages. Reuse normal reload/MCP invalidation for explicit restage; add read-only shell status and bounded authenticated DSH asset grants. Expose current-server MCP backoff without treating an absent backoff as health. Share profile-scoped Rust account custody across terminal and native hosts, keep local provider setup optional, and make ordinary logout session-only. Fence browser imports and cleanup by exact revision; preserve ambiguous or newer sessions. Keep definitely unadopted device grants in cleanup custody. Validation: six-crate Mac all-target check exit0, 4,270 source entries unchanged, 35m37s, no disk stop. Grok Linux raw evidence verified: secrets account6 and plugins202 passed, one existing real-home rehearsal ignored; all4,270 entries exactly matched that Mac freeze. Existing command-crate boundary gate passed; 2,816 keys complete in all15 TUI locales. The subsequent two-file private account cleanup has independent source review and exact pre/post hash checks; Linux verification is in progress. Only that cleanup and the already-landed visual-change changelog entry differ from the passing Mac compile snapshot. No new tests or code comments added. Qualification is incomplete: production recovery/asset HTTP smoke, published plugin, Electron, live sign-in, cross-platform/package and final release gates remain. This local candidate commit is not a release or publication.
408 lines
2.5 MiB
Plaintext
408 lines
2.5 MiB
Plaintext
[
|
||
{
|
||
"id": "install",
|
||
"slug": "install",
|
||
"sources": [
|
||
{
|
||
"path": "docs/INSTALL.md",
|
||
"sha256": "ff4aa6d802e28c5ac6d8f5cbeab7935c6c4e35e0ea24375c11b5c6bc0a2d959c",
|
||
"content": "# Installing Codewhale\n\n> 阅读简体中文版:[zh_hans/INSTALL.md](zh_hans/INSTALL.md)\n\nCodewhale is an open-source coding agent that runs in your terminal. You give\nit a task (\"fix the failing test\", \"add a CLI flag\"). It reads your repository,\nedits files and runs commands. In the default **Ask** posture it applies file\nedits inside the workspace immediately (and shows you the diff), but asks before\nrunning shell commands, so commit or stash anything you care about first. It\nworks with many model providers. **DeepSeek** is the default.\n\nThe command is `codewhale`. `codew` is a shorter alias for the same program.\n\nThe installation paths were first checked with **v0.10.0** (released 2026-09-22) on a\nfresh **Ubuntu 24.04 x86_64** machine. The dated\n[install receipts](https://github.com/codewhale-hq/Codewhale/blob/37ecdfcc49bc68a9b0d058b97c3946e62c34bd31/docs/install-report/v0.10.0-2026-09-23/RECEIPTS.md) record the commands and output checked at that time. Steps that\ncould not be run on that machine are marked **(untested on this VM: reason)**.\nmacOS, Windows and Android are out of scope, apart from a few notes. A second\npass re-ran the installer, manual-download, archive and npm paths, the no-key\nchecks and zsh completion on **macOS 26.1 (Apple silicon)**; see\n[macOS notes](#macos-notes). Steps that need a model call were not re-run\nthere.\n\nInstall commands that use `latest` resolve to the latest **published** GitHub\nRelease or package. Between releases, `main` may already describe the next\nversion (for example a v0.10.3 source candidate before its tag). A\nprebuilt candidate is available through the official installer only after its\ntag, checksums and release assets exist. Contributors can build `main` now.\n\n## No account needed\n\nCodewhale needs no account. Keys you save with `/provider` or\n`codewhale auth set` stay on your machine; see\n[API keys and providers](#8-api-keys-and-providers). New Codewhale accounts\nare not open, so this guide does not cover account key storage.\n\n## Test and contribute to current source\n\n`latest` selects the latest published release. To test unreleased repairs,\nbuild canonical GitHub `main`, or CNB `main` after verifying its commit matches.\nThe source version number alone does not establish a published package.\n\n```bash\ngit clone --branch main https://github.com/codewhale-hq/CodeWhale.git\ncd CodeWhale\ncargo build --release --locked -p codewhale-cli\ntarget/release/codewhale --version\n```\n\nSee the Cargo section for build prerequisites; use the repository's stable Rust\nselection (declared minimum 1.89). Windows uses `target/release/codewhale.exe`\nand needs MSVC. A development build is separate from a signed release package.\nFor mainland-China downloads and source validation, see [CNB mirror](CNB_MIRROR.md).\nThe [Tencent Lighthouse bootstrap](../scripts/tencent-lighthouse/README.md) is\nbilingual. An npm registry mirror serves the wrapper; native binary downloads\nstill need an available release asset source.\n\n---\n\n## 60-second quickstart (Linux or macOS)\n\n```bash\n# 1. Install. Downloads two checksum-verified binaries into ~/.local/bin (no sudo).\ncurl -fsSL https://codewhale.net/install.sh | sh\n\n# 2. Make sure ~/.local/bin is on your PATH, now and in future terminals.\nprintf '%s\\n' '# >>> codewhale PATH >>>' 'export PATH=\"$HOME/.local/bin:$PATH\"' '# <<< codewhale PATH <<<' >> ~/.bashrc # zsh: use ~/.zshrc\nexport PATH=\"$HOME/.local/bin:$PATH\"\ncodewhale --version # -> codewhale 0.10.0 (1be1a703b975)\n\n# 3. Give it a DeepSeek API key (from https://platform.deepseek.com/api_keys).\ncodewhale auth set --provider deepseek # prompts for the key; nothing is echoed\ncodewhale auth status --provider deepseek # \"active source: secret store\"\n\n# 4. Run your first task inside a git repository.\ncd ~/your-project\ncodewhale\n```\n\nIn the TUI, type something concrete:\n\n```text\ncreate a Python file primes.py that prints the first 10 primes, run it, and show me the output\n```\n\nCodewhale writes the file, shows you a diff, then asks **APPROVAL: bash\npython3 primes.py – Do you want to proceed?** Press `y` to allow it once. It\nruns the command and reports the output. Press `Ctrl-D` (with an empty input\nbox) to quit. It prints the command to resume the session later.\n\n> **If nothing happens after you send your first message,** you have no key\n> configured. v0.10.0 doesn't warn you in that case. Press **F3**. If DeepSeek\n> shows `missing key`, press Enter, paste the key, then pick a model and\n> confirm.\n\n---\n\n## Contents\n\n1. [Before you start](#1-before-you-start)\n2. [Install: recommended installer](#2-recommended-installer-curl--sh)\n3. [Install: manual download from GitHub Releases](#3-manual-download-from-github-releases)\n4. [Install: npm](#4-npm)\n5. [Install: Cargo / build from source](#5-cargo-and-building-from-source)\n6. [Install: Homebrew (Linux) and Nix](#6-homebrew-on-linux-and-nix)\n7. [Updating and rolling back](#7-updating-and-rolling-back)\n8. [API keys and providers](#8-api-keys-and-providers)\n9. [Shell completions](#9-shell-completions)\n10. [Running it: TUI, headless, resume](#10-running-it)\n11. [Terminal notes (Ghostty and others)](#11-terminal-notes)\n12. [Uninstalling and what Codewhale leaves behind](#12-uninstalling)\n13. [Troubleshooting](#13-troubleshooting)\n14. [Appendix: other platforms (not re-tested in this revision)](#appendix-other-platforms-not-re-tested-in-this-revision)\n\n---\n\n## 1. Before you start\n\n| You need | Why |\n|---|---|\n| Linux x86_64 or arm64, or macOS | Prebuilt binaries exist for these. The Linux binaries are **static** (musl), so they have no glibc or libdbus dependency and run on any distro. |\n| HarmonyOS PC | Run the Linux ARM64 build inside the Linux subsystem (融合开发引擎), or build natively for `aarch64-unknown-linux-ohos` and sign it; see [HarmonyOS and OpenHarmony](HarmonyOS.md). |\n| `curl` (or `wget`) and `sha256sum` (or `shasum`) | The installer uses them to download and verify. |\n| A model provider key, e.g. [DeepSeek](https://platform.deepseek.com/api_keys) | Codewhale does nothing useful without a model. |\n| `git` (recommended) | Codewhale works best inside a git repository. |\n| Optional: Python 3, Node.js 20+ | If present, Codewhale enables its Python and JS execution tools (`codewhale doctor` lists them). |\n\n**Which install path?**\n\n* **Most people:** the [recommended installer](#2-recommended-installer-curl--sh).\n It's the fastest (about 6 s here), verifies checksums, and supports\n `codewhale update`.\n* **Air-gapped or security-reviewed machines:**\n [manual download](#3-manual-download-from-github-releases).\n* **You already manage CLI tools with npm:** [npm](#4-npm).\n* **No prebuilt binary for your platform, or you want to build it yourself:**\n [Cargo](#5-cargo-and-building-from-source).\n\nPick **one**. Several installs on one machine end up fighting over PATH (see\n[Troubleshooting](#13-troubleshooting)).\n\n**Privacy note:** Codewhale sends aggregate usage counts (PostHog) **by\ndefault**. To turn this off permanently:\n`codewhale config set telemetry false`, or export `CODEWHALE_TELEMETRY=0`,\nwhich always wins. The TUI also checks GitHub for updates at startup\n(`[update] check_for_updates` in `~/.codewhale/config.toml`).\n\n---\n\n<a id=\"recommended-official-github-releases\"></a>\n\n## 2. Recommended installer (`curl | sh`)\n\n**Prerequisites:** curl, `sha256sum` (Linux) or the built-in `shasum` (macOS),\na writable home directory. No sudo, no Node, no Rust.\n\n```bash\ncurl -fsSL https://codewhale.net/install.sh | sh\n```\n\nWhat it does (verified):\n\n* It detects your platform (`linux-x64`, `linux-arm64`, `macos-x64`,\n `macos-arm64`). It refuses Android/Termux and riscv64 with a clear message.\n* It downloads `codewhale-<platform>` and `codewhale-artifacts-sha256.txt` from\n the latest GitHub Release. When the `codewhale` and `codew` manifest entries\n have the same digest, it reuses that verified download for both command names;\n otherwise it downloads and verifies `codew-<platform>` separately. If either\n entry doesn't match, it stops before installing anything (`codewhale install:\n checksum mismatch for …`).\n* It installs `~/.local/bin/codewhale` and `~/.local/bin/codew` from the\n verified release bytes as two separately managed files. When both manifest\n entries match, the download staging step uses a hard link when possible and\n a temporary copy otherwise.\n* It **never uses sudo and never edits your shell profile.** It refuses to\n install into system or package-manager directories (`/usr/bin`,\n `~/.cargo/bin`, Homebrew and MacPorts bin directories, `node_modules`,\n `/nix/store`…), into your home directory or `/`, or into a directory that\n other users or groups can write (`chmod go-w <dir>` fixes one you own). It\n also refuses to overwrite a *different* existing `codewhale` unless you ask\n for it with `CODEWHALE_FORCE_REINSTALL=1` (see [re-running the\n installer](#re-running-the-installer)).\n\nExpected output:\n\n```\nInstalling Codewhale for linux-x64\nRelease assets: https://github.com/codewhale-hq/CodeWhale/releases/latest/download\nInstall dir: /home/you/.local/bin\nChecksums verified\nInstalled checksummed release commands:\n /home/you/.local/bin/codewhale\n /home/you/.local/bin/codew\n…\nPATH selects no codewhale command; this install is /home/you/.local/bin/codewhale\n…\nPut /home/you/.local/bin first on PATH in future shells (run once; this installer does not edit shell profiles):\n printf '%s\\n' '# >>> codewhale PATH >>>' 'export PATH=\"$HOME/.local/bin:$PATH\"' '# <<< codewhale PATH <<<' >> ~/.bashrc\nThe marker lines let codewhale uninstall remove exactly this block; a bare export line is yours and is never touched.\nThen run: . ~/.bashrc (or open a new terminal)\n…\n```\n\n### macOS notes\n\nRe-checked on macOS 26.1, Apple silicon (`macos-arm64`), with a fresh `HOME`:\n\n* The installer printed `Installing Codewhale for macos-arm64`, verified\n checksums with the system tools, and installed `codewhale` and `codew`\n (64 MiB each, Mach-O arm64) in 4.3 s. Both report\n `codewhale 0.10.0 (1be1a703b975)`. They ran without a Gatekeeper prompt.\n* When Node isn't on `PATH`, it also prints `Computer Use is included and needs\n Node.js 20 or newer on PATH.` The core TUI works without Node, but Computer Use\n and the JavaScript execution tool (`js_execution`) stay unavailable until Node\n is on `PATH`.\n* `codewhale doctor` behaves as on Linux (exit 0, `All checks complete!` with no\n key, file-based secret store under `~/.codewhale/secrets/`), except that it\n reports `✓ sandbox available: macos-seatbelt`.\n\n### Put it on your PATH\n\nIf the installer says `PATH selects no codewhale command`, `~/.local/bin` isn't\non your PATH **in this shell**. The `codewhale.net/install.sh` installer then\nprints the matching line from the block below for your `$SHELL` (zsh, bash,\nfish, or a POSIX `sh`; for any other shell, or a directory name with quotes,\n`$`, backticks or backslashes, it tells you to add the directory yourself). It\nnever edits a shell profile itself. The `install.sh` inside a release archive\nprints only the current-shell `export` line. On Ubuntu and Debian, `~/.profile` adds\n`~/.local/bin`, but only if the directory existed when you *logged in*. So:\n\n* a new SSH or login shell picks it up automatically;\n* a new terminal **window** on a desktop (GNOME Terminal, Ghostty, …) usually\n doesn't, until you log out and back in. I hit\n `bash: codewhale: command not found` in Ghostty right after installing.\n\nFix it once:\n\n```bash\nprintf '%s\\n' '# >>> codewhale PATH >>>' 'export PATH=\"$HOME/.local/bin:$PATH\"' '# <<< codewhale PATH <<<' >> ~/.bashrc # bash (macOS login bash: ~/.bash_profile, or ~/.profile if only that exists)\n# printf '%s\\n' '# >>> codewhale PATH >>>' 'export PATH=\"$HOME/.local/bin:$PATH\"' '# <<< codewhale PATH <<<' >> ~/.zshrc # zsh\n# fish_add_path ~/.local/bin # fish (untested on this VM)\nexport PATH=\"$HOME/.local/bin:$PATH\"; hash -r\ncommand -v codewhale codew\n```\n\nThe three-line block is marked so `codewhale uninstall` can remove exactly it later. A bare\n`export PATH=…` line is yours: uninstall never touches it, because other tools share `~/.local/bin`.\n\n### Options\n\n```bash\n# Choose the directory (must be absolute; created if missing)\ncurl -fsSL https://codewhale.net/install.sh | CODEWHALE_INSTALL_DIR=\"$HOME/.local/codewhale/bin\" sh\n# Install a specific release\ncurl -fsSL https://codewhale.net/install.sh | CODEWHALE_VERSION=v0.9.13 sh\n# Show help\ncurl -fsSL https://codewhale.net/install.sh | sh -s -- --help\n```\n\n### Verify\n\n```bash\ncodewhale --version # codewhale 0.10.0 (1be1a703b975)\ncodew --version # same\ncodewhale doctor # diagnostics; see the note in §8 about what it does NOT check\n```\n\n### Re-running the installer\n\n* Same version already installed: harmless. It prints\n `Already installed: …` and exits 0.\n* An earlier Codewhale already installed (a regular file you own that is a\n Codewhale release binary, with or without an install receipt): the\n installer **upgrades** it. The old `codewhale`, `codew` and receipt move to\n `~/.codewhale/recovery/<UTC stamp>/` (kept 30 days, never left beside the\n commands on PATH), the new release is installed with a fresh receipt, and the\n last lines print the undo command\n (`codewhale doctor --restore <stamp>`) and a short report of any other copies\n on the machine (`codewhale doctor --installs --brief`).\n* A symlink, a file owned by another user, or a file that is not a Codewhale\n binary at the destination is **refused** and left unchanged. Install into a\n fresh directory, or set `CODEWHALE_FORCE_REINSTALL=1` to move a regular file\n you own to the recovery folder and install anyway. A managed directory\n (npm, Cargo, Homebrew, system paths) is always refused.\n* Not a release build, at the same version (a local `cargo build` reports\n `0.10.2 (dev)`): `codewhale update` still reinstalls the release artifact\n over it, because that build has no release provenance to preserve.\n\n### Upgrade / uninstall\n\n* Upgrade: automatic for this install (see §7), or `codewhale update` now.\n* Uninstall: `~/.local/bin/codewhale uninstall` previews the receipt-owned\n files; add `--yes` to remove them. See §12 for data.\n\n---\n\n## 3. Manual download from GitHub Releases\n\nUse this when you want to see and verify every byte yourself. Releases:\n<https://github.com/codewhale-hq/CodeWhale/releases>. Each platform has **bare\nbinaries** (`codewhale-linux-x64`, `codew-linux-x64`, …) and an **archive**\n(`codewhale-linux-x64.tar.gz`) that holds the same two binaries plus an\n`install.sh`.\n\n### 3a. Bare binaries\n\n```bash\nmkdir -p ~/codewhale-dl && cd ~/codewhale-dl\nbase=https://github.com/codewhale-hq/CodeWhale/releases/latest/download\ncurl -fsSLO \"$base/codewhale-linux-x64\" # use linux-arm64 on ARM\ncurl -fsSLO \"$base/codew-linux-x64\"\ncurl -fsSLO \"$base/codewhale-artifacts-sha256.txt\"\nsha256sum -c codewhale-artifacts-sha256.txt --ignore-missing\n# codew-linux-x64: OK\n# codewhale-linux-x64: OK\nmkdir -p ~/.local/bin\ninstall -m 755 codewhale-linux-x64 ~/.local/bin/codewhale\ninstall -m 755 codew-linux-x64 ~/.local/bin/codew\n```\n\nThen [put `~/.local/bin` on PATH](#put-it-on-your-path) and run\n`codewhale --version`. On macOS the assets are `codewhale-macos-arm64` and\n`codew-macos-arm64` (`-macos-x64` on Intel), and the built-in `shasum` verifies\nthem (tested on macOS 26.1, Apple silicon):\n\n```bash\n/usr/bin/shasum -a 256 -c codewhale-artifacts-sha256.txt --ignore-missing\n# codew-macos-arm64: OK\n# codewhale-macos-arm64: OK\n```\n\nThe `codewhale-macos-arm64.tar.gz` archive verifies the same way against\n`codewhale-bundles-sha256.txt`, and its `./install.sh` installs into\n`~/.local/bin` (tested).\n\nTo pin a release, replace `latest/download` with `download/vX.Y.Z`, and take\nthe manifest from the same tag.\n\n### 3b. Archive\n\n```bash\ncd \"$(mktemp -d)\"\nbase=https://github.com/codewhale-hq/CodeWhale/releases/latest/download\ncurl -fsSLO \"$base/codewhale-linux-x64.tar.gz\"\ncurl -fsSLO \"$base/codewhale-bundles-sha256.txt\" # note: *bundles*, not *artifacts*\nsha256sum -c codewhale-bundles-sha256.txt --ignore-missing\n# codewhale-linux-x64.tar.gz: OK\ntar -xzf codewhale-linux-x64.tar.gz\ncd codewhale-linux-x64 && ./install.sh # -> ~/.local/bin; PREFIX=/some/dir ./install.sh -> /some/dir/bin\n```\n\nThe archive's `install.sh` never uses sudo, leaves differing existing files\nalone, never edits a shell profile, and asks nothing; it prints only the\ncurrent-shell `export` line.\n\n**Upgrade:** `codewhale update` works for both 3a and 3b, because they're\n\"direct binary\" installs. **Uninstall:** delete the two files (see §12).\n\n---\n\n## 4. npm\n\n**Prerequisites:** Node.js 18+ and npm, with a **global prefix you can write\nto**. npm installs the registry's latest published version, never an\nunpublished source candidate.\n\n```bash\nnpm install -g codewhale\ncodewhale --version\n```\n\nThe package is a small wrapper. Its `postinstall` step downloads the same\n`codewhale`/`codew` release binaries, checks them against the release's SHA-256\nmanifest, and links `codewhale` and `codew` into npm's global `bin`. The whole\nthing took 6 s here.\n\n**Windows npm sessions:** Node remains the native program's launcher for the\nwhole session. A process-name kill such as `taskkill /IM node.exe` or\n`Get-Process node | Stop-Process -Force` can interrupt this and other npm\nCodewhale sessions and prevent normal terminal cleanup. Stop only the server\nPID you started or the process owning its port, or use Codewhale's task\ncancellation. The Windows\nnative archive/installer avoids this npm-parent dependency; this does not\nremove Node requirements for optional JavaScript tools. Codewhale's Windows\nshell safety floor holds recognized image-wide Node kills even in Full Access.\nAn external hard kill or an arbitrary program that terminates the launcher\ncannot be made graceful by this shell-command check.\n\n### If you get `EACCES: permission denied`\n\nThat means Node is installed system-wide (apt, `/usr/local`, `/opt`), and your\nuser can't write to its global prefix:\n\n```\nnpm error code EACCES\nnpm error Error: EACCES: permission denied, mkdir '/opt/node22/lib/node_modules/codewhale'\n```\n\n**Don't use `sudo npm`.** Either use a per-user Node (nvm, fnm, volta), or\npoint npm at a directory you own. I tested the second option:\n\n```bash\nnpm config set prefix \"$HOME/.npm-global\"\necho 'export PATH=\"$HOME/.npm-global/bin:$PATH\"' >> ~/.bashrc\nexport PATH=\"$HOME/.npm-global/bin:$PATH\"\nnpm install -g codewhale\ncommand -v codewhale codew # ~/.npm-global/bin/codewhale, ~/.npm-global/bin/codew\n```\n\n### Notes\n\n* npm hides the download progress. Add `--foreground-scripts` to see it\n (`codewhale: selected GitHub Releases for v0.10.0 … done.`). The chosen\n source is also written to\n `$(npm prefix -g)/lib/node_modules/codewhale/bin/downloads/codewhale.source`.\n* The package uses 157 MB on disk.\n* On macOS 26.1 (Apple silicon, Homebrew Node 25) an install into a user-owned\n prefix (`npm install -g --prefix <dir> codewhale`) took 3 s and linked\n `codewhale` and `codew`, both `codewhale 0.10.0 (1be1a703b975)`.\n* **Upgrade:** `npm install -g codewhale@latest`. `codewhale update` refuses\n on npm installs. It prints migration instructions and exits 1 with\n `error: The package-managed executable was not changed.`\n* **Specific version:** `npm install -g [email protected]`.\n* **Uninstall:** `npm uninstall -g codewhale`. This removes only the program,\n not your data (§12).\n\n---\n\n<a id=\"4-install-via-cargo-any-tier-1-rust-target\"></a><a id=\"7-build-from-source\"></a>\n\n## 5. Cargo and building from source\n\nUse this if there's no prebuilt binary for your platform, or you want to\ncompile it yourself. One Cargo package is required:\n`codewhale-cli` installs the `codewhale` command. npm and prebuilt releases also\nexpose `codew` as a convenience name for the same compiled runtime; Cargo does\nnot create that alias, so add `alias codew=codewhale` to your shell rc if you\nwant the short name.\n\n### Prerequisites (Debian/Ubuntu)\n\n```bash\nsudo apt-get install -y build-essential pkg-config libdbus-1-dev git\n# Rust via rustup (the distro's cargo is too old for this edition-2024 workspace)\ncurl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y\nsource \"$HOME/.cargo/env\"\nrustc --version # the workspace declares rust-version = 1.89\n```\n\n`libdbus-1-dev` **is required**. Without it the build fails after about a\nminute with:\n\n```\nerror: failed to run custom build command for `libdbus-sys v0.2.7`\n The system library `dbus-1` required by crate `libdbus-sys` was not found.\n```\n\nFedora/RHEL: `sudo dnf install -y gcc make pkgconf-pkg-config dbus-devel`\n**(untested on this VM: Ubuntu only)**.\n\n### 5a. From crates.io\n\n```bash\ncargo install codewhale-cli --locked\ncodewhale --version\n```\n\nTested result: **works**, with current stable Rust (1.98.1).\n* It took **25 min 31 s** on 4 vCPU and 15 GB RAM, and pulled about 470 MB\n into `~/.cargo/registry`.\n* It installs one 122 MB file, `~/.cargo/bin/codewhale`. That's a normal\n glibc-linked binary, and it needs `libdbus-1` at runtime.\n* `codewhale --version` prints `codewhale 0.10.0`, with no commit hash.\n* Headless and TUI smoke tests passed.\n\n> **v0.10.0 declared \"Rust 1.88+\", which was wrong.** The workspace now\n> declares 1.89, the version CI's MSRV job builds. With 1.88.0 the v0.10.0\n> install fails in seconds:\n> `rustc 1.88.0 is not supported by the following package: [email protected] requires rustc 1.89`.\n> Use current stable (`rustup update stable`).\n\n### 5b. From a git checkout\n\n```bash\ngit clone --depth 1 --branch v0.10.0 https://github.com/codewhale-hq/CodeWhale.git\ncd CodeWhale\ncargo install --path crates/cli --locked # installs ~/.cargo/bin/codewhale\n```\n\nThings to know (observed):\n\n* The repo contains `rust-toolchain.toml` (`channel = \"stable\"`). The first\n `cargo` command inside the checkout **silently downloads the latest stable\n toolchain** (about 250 MB), whatever your default is.\n* The workspace treats every compiler warning as an error. With Rust 1.89\n the build **fails** after about 11 minutes with 8\n `error: this lint expectation is unfulfilled` errors in `codewhale-tui`. Use\n the stable toolchain the repo selects. Don't pass an older `+toolchain`.\n* The workspace release profile uses thin LTO. On a 4-vCPU / 15 GB VM, the\n `codewhale-tui` crate alone compiled for more than an hour, peaking at\n 6–8 GB of RAM. Budget 16 GB or more, and expect this to be the slowest install\n path by far. `target/` grew past 1.2 GB.\n\nTested result: **works** on the repo-selected stable Rust (1.98.1).\n* `Finished release profile … in 83m 12s`. A Nix build competed for CPU and\n memory for most of that time, so treat it as an upper bound.\n* `target/` ended at **3.1 GB**. Delete it afterwards with `cargo clean`.\n* The binary reports `codewhale 0.10.0 (dev)`, and `exec` worked.\n* Cargo prints `warning: default toolchain implicitly overridden with\n stable-x86_64-unknown-linux-gnu by rustup toolchain file`, which is harmless.\n* **Cargo builds provide no `codew`.**\n\n**Upgrade:** re-run the same `cargo install … --force` (for crates.io, you can\nadd `--version X.Y.Z`). `codewhale update` refuses Cargo installs.\n**Uninstall:** `cargo uninstall codewhale-cli`, then §12.\n\n---\n\n## 6. Homebrew on Linux and Nix\n\n### Homebrew on Linux: works, but not with the command the old docs gave\n\nPrerequisite: Homebrew itself. Its installer needs sudo **once**, to create\n`/home/linuxbrew/.linuxbrew`. Without sudo rights it stops with\n`Insufficient permissions to install Homebrew to \"/home/linuxbrew/.linuxbrew\"`.\nAsk an admin to run\n`sudo mkdir -p /home/linuxbrew/.linuxbrew && sudo chown $USER /home/linuxbrew/.linuxbrew`,\nthen re-run the installer. Afterwards, add\n`eval \"$(/home/linuxbrew/.linuxbrew/bin/brew shellenv bash)\"` to `~/.bashrc`,\nas its \"Next steps\" say.\n\n```bash\nbrew install Hmbown/deepseek-tui/codewhale # full name: taps and trusts in one step\n```\n\nThe two-step form (`brew tap Hmbown/deepseek-tui` then `brew install\ncodewhale`) **fails on Homebrew 7.x**:\n\n```\nError: Refusing to load formula hmbown/deepseek-tui/codewhale from untrusted tap hmbown/deepseek-tui.\nRun `brew trust --formula hmbown/deepseek-tui/codewhale` or `brew trust hmbown/deepseek-tui` to trust it.\n```\n\nRun `brew trust hmbown/deepseek-tui` first, or use the full name above.\n\nTested with Homebrew 7.0.6 against the v0.10.0 tap formula: the install took\n73 s. The tap formula tracks the latest release and downloads the official\nrelease binaries, so there's no compile. It provides **both** `codewhale` and\n`codew`. The `Hmbown/deepseek-tui` tap formula also depends on `node`, which\npulled in 31 bottles (~560 MB) on Linux. That `node` dependency belongs to\nthis tap formula only. The core TUI runs without Node; Computer Use and the JS\nexecution tool use it when it is on PATH.\n\n* **Upgrade:** `brew upgrade codewhale`. (`codewhale update` refuses, and\n suggests migrating.)\n* **Uninstall:** `brew uninstall codewhale && brew untap Hmbown/deepseek-tui`.\n This also autoremoves the tap's node dependency and anything else it pulled\n in. Homebrew's\n download cache (`~/.cache/Homebrew`, ~330 MB) stays until\n `brew cleanup --prune=all`.\n\n### Nix: partially tested\n\n```bash\n# flakes are still experimental; the tested setup enabled them once:\nmkdir -p ~/.config/nix\necho 'experimental-features = nix-command flakes' >> ~/.config/nix/nix.conf\nnix run github:codewhale-hq/CodeWhale -- --version\n# one-off alternative (untested on this VM): nix --extra-experimental-features 'nix-command flakes' run github:codewhale-hq/CodeWhale -- --version\n```\n\nNix 2.35 installed fine; single-user mode needs `/nix` created by root once.\nFlakes resolved. What I learned before stopping:\n\n* There's **no binary cache**, so this is a full source build of the\n **main branch**, not the v0.10.0 release. The binary reports\n `codewhale 0.10.0 (dev)`.\n* The build step took 30 min on 4 cores. The package then runs its **test\n suite** (`doCheck`), which recompiles the workspace in test mode. That took\n over 70 minutes and more than 8 GB of RAM for one rustc process, and I\n stopped it at 105 minutes. So `nix run` completing, `nix build`, and\n `nix profile install/remove` are **(untested on this VM: build did not\n finish in the time budget; the VM's proxy also required an\n `--override-input fenix …` workaround)**.\n* Nix provides only `codewhale`; there's no `codew` (it builds just the\n `codewhale-cli` package).\n\nUnless you already live in Nix, use §2 instead.\n\n---\n\n## 7. Updating and rolling back\n\nThese work for installs from §2 and §3 (direct binaries). Package-manager\ninstalls (npm, Cargo, Homebrew) must be updated with their own tool, and the\nEngine inside the Codewhale desktop app is updated by the app\n(Codewhale > Check for Updates); `codewhale update` refuses to write there.\n\n<a id=\"automatic-updates\"></a>\n\n### Automatic updates\n\nRelease-binary installs update themselves:\n\n1. When the TUI starts it checks for a newer release at most every 6 hours\n (`[update] check_interval_hours`). Offline or rate-limited checks keep the\n last answer and retry about an hour later, never on every launch.\n2. If one exists, it downloads and verifies it in the background\n (`codewhale update --stage`, a separate low-impact process) into\n `~/.codewhale/updates/staged/`. The header shows `↑ v0.10.4 ready`.\n3. The next time you start `codewhale`, before any session opens, the staged\n binary replaces the installed one. A running session is never touched.\n You see one line:\n `Updated to v0.10.4 · What's new: https://codewhale.net/en/changelog/0.10.4 · Undo: codewhale update --rollback`.\n\n**Until releases are signed, an update waits for your OK.** The download is\nchecksum-verified, but the checksum comes from the same place as the binary, so\nthe next start asks once (`Install v0.10.4 now? [y/N]`, no unless you type y); you can also run\n`/update install` or `codewhale update`. Say no and it does not ask again for\nthat version. Once a release carries a valid Ed25519 signature\n(`codewhale-artifacts-sha256.txt.sig`), it applies without asking. A build\nthat has the release key compiled in refuses any release whose signature is\nmissing or does not verify, both for `codewhale update` and at start, where the\nstaged binary is checked again against the signed manifest. The checksum\nmanifest always comes from GitHub Releases; the CNB mirror can only supply a\nbinary whose SHA-256 that manifest already lists. Non-interactive\nstarts never prompt and never apply an unsigned update.\n\nTurn it down or off:\n\n```bash\ncodewhale config set update.mode notify # tell me about updates, don't download\ncodewhale config set update.mode off # no update checks at all\nexport CODEWHALE_UPDATE_MODE=notify # same, per environment\n```\n\n`CODEWHALE_NO_UPDATE_CHECK=1` and CI environments also turn checks off.\n\n**Organizations** can set the mode for every user of a machine in a\nroot-owned, not group/world-writable policy file that wins over user settings:\n`/Library/Application Support/Codewhale/policy.toml` (macOS),\n`/etc/codewhale/policy.toml` (Linux and macOS) or\n`%ProgramData%\\Codewhale\\policy.toml` (Windows):\n\n```toml\n[update]\nmode = \"off\" # auto | notify | off\n```\n\nAn unreadable or malformed policy file means `off`. `codewhale doctor` shows\nthe mode in force and where it came from, any staged update, and the version\nkept for rollback.\n\nPackage-manager and desktop-app copies never download or replace anything;\nthe notice names their own command instead.\n\n```bash\ncodewhale update --check\n# Current binary: /home/you/.local/bin/codewhale\n# Current version: v0.9.13\n# Latest stable release: v0.10.0\n# Update available. Run `/home/you/.local/bin/codewhale update` to install v0.10.0.\ncodewhale update\n# Downloading codewhale-linux-x64...\n# SHA256 checksum verified against codewhale-artifacts-sha256.txt from GitHub Releases.\n# ✅ Successfully updated to v0.10.0!\n# Updated binaries:\n# - /home/you/.local/bin/codewhale (codewhale-linux-x64)\n# - /home/you/.local/bin/codew (codewhale-linux-x64)\n```\n\nIt updates `codewhale` **and** `codew` together. If `codew` is a different\nCodewhale build than `codewhale`, the old `codew` moves to the recovery folder\nand is replaced, instead of the update refusing. An install from before\nreceipts gets one (`.codewhale-install.sha256`), so `codewhale uninstall` can\nremove it later. It took 10 s here. Run it again and you get\n`Already up to date; no download needed.` Other options: `--beta` and\n`--proxy <URL>`.\n\nA build that is not a published release reports its provenance in\n`codewhale --version` (`0.10.2 (dev)` for a local build, a commit hash for a\nrelease). `codewhale update` compares the release, not just the version, so a\nsame-version `(dev)` build is replaced by the release artifact instead of\nreporting \"Already up to date\" forever.\n\n<a id=\"roll-back-to-a-previous-release\"></a>\n\n### Rolling back (e.g. to v0.9.13)\n\nEvery update, automatic or manual, keeps the version it replaced in\n`~/.codewhale/updates/previous/`. To go back one version:\n\n```bash\ncodewhale update --rollback\n# Rolled back to v0.10.3. v0.10.4 will not be installed automatically again; the next newer release will.\n```\n\nRun it again to swap forward. `codewhale update` never downgrades, and\n`CODEWHALE_VERSION=0.9.13 codewhale update` just says \"Already up to date\". To\ngo back further than one version, replace the files:\n\n```bash\ndir=\"$(dirname \"$(command -v codewhale)\")\" # the install PATH actually selects\nmkdir -p \"$dir/.keep\" && mv \"$dir/codewhale\" \"$dir/codew\" \"$dir/.keep/\"\ncurl -fsSL https://codewhale.net/install.sh | CODEWHALE_VERSION=v0.9.13 CODEWHALE_INSTALL_DIR=\"$dir\" sh \\\n && rm -r \"$dir/.keep\" \\\n || mv \"$dir/.keep/codewhale\" \"$dir/.keep/codew\" \"$dir/\" # restore on failure\nhash -r; codewhale --version # codewhale 0.9.13 (a0b81f619b66)\n```\n\nTested with both the default `~/.local/bin` and a custom\n`CODEWHALE_INSTALL_DIR`. Use it only for installer, manual or archive\ninstalls. Never point it at an npm, Cargo or Homebrew directory.\n\nOr keep both versions side by side, and put the old one first on PATH:\n\n```bash\ncurl -fsSL https://codewhale.net/install.sh | CODEWHALE_VERSION=v0.9.13 CODEWHALE_INSTALL_DIR=\"$HOME/.local/codewhale-0.9.13\" sh\nexport PATH=\"$HOME/.local/codewhale-0.9.13:$PATH\"; hash -r\n```\n\nTo return to the latest after an in-place rollback, run `codewhale update`.\n(Tested: 0.9.13 → 0.10.0.)\n\nnpm: `npm install -g [email protected]`. Cargo:\n`cargo install codewhale-cli --version 0.9.13 --locked --force` **(untested on\nthis VM: only 0.10.0 was built)**.\n\n<a id=\"clean-up-old-copies\"></a>\n\n### Clean up old copies\n\n`codewhale doctor` has an **Installations** section, and\nkeeps routine path and secret-backend details brief. Use `codewhale doctor --all`\nto show every resolved path and structural secret-backend detail.\n\n`codewhale doctor --installs` prints only that (`--json` for machines,\n`--brief` for nothing on a tidy machine). It lists every `codewhale`, `codew`,\n`codewhale-tui`, `deepseek` and `deepseek-tui` in your `PATH`, `~/.local/bin`,\n`~/.cargo/bin` and beside the running binary, says which one your shell runs,\nand classifies each: receipt-owned, unreceipted Codewhale, backup, managed by a\npackage manager (with that manager's exact removal command), part of the\ndesktop app, or left alone (symlinks, other programs such as `codewhale-go`,\nfiles of other users). It never runs a file it finds; a copy whose bytes\ndiffer from the running one is reported as \"version unknown\".\n\n`codewhale doctor --fix` shows one plan and asks once (`[Y/n]`). For\ninstallations it can:\n\n* move old backups (`codewhale.bak-…`, `codewhale-0.8.60.bak`,\n `codewhale.pre-v…`, `codewhale.old-<pid>`, `.codewhale-backup-*` folders and\n similar) into `~/.codewhale/recovery/<UTC stamp>/`;\n* make an unreceipted `codew` match the `codewhale` beside it (old copy to\n recovery) and record an install receipt for that directory.\n\nEvery move prints its own undo command; `codewhale doctor --restore <stamp>`\n(or `--restore` for the newest) puts the whole folder back. Recovery folders\nare deleted by a later `doctor --fix` after 30 days, and `doctor` shows when.\nNothing is deleted on the spot, user data (`~/.codewhale`, `~/.deepseek`) is\nnever touched, and package-managed, desktop-app and foreign files are only\nreported.\n\n<a id=\"release-signing\"></a>\n\n### Release signing (maintainers)\n\nThe Engine verifies `codewhale-artifacts-sha256.txt.sig`, a base64 Ed25519\nsignature over `codewhale-release-manifest-v1\\n<version>\\n<manifest bytes>`\n(`crates/release/src/signing.rs`). With no key configured every release reads\nas unsigned and automatic updates wait for each user's OK. One key signs both\nthe CLI manifest and the desktop appcast. To turn signing on:\n\n1. Create the key offline with OpenSSL 3 and keep the private half outside\n any repository:\n `openssl genpkey -algorithm ed25519 -out codewhale-release-ed25519.pem`\n2. Add the PEM file's contents as the GitHub Actions secret\n `CODEWHALE_RELEASE_SIGNING_KEY` on the Engine repository. The release\n workflow (`release-artifacts.yml`, assemble job) then runs\n `scripts/release/sign-release-manifest.sh` and publishes the `.sig`\n beside the manifest; without the secret it skips signing and says so.\n3. Print the public key:\n `CODEWHALE_RELEASE_SIGNING_KEY=\"$(cat codewhale-release-ed25519.pem)\" scripts/release/sign-release-manifest.sh --public-key`\n and add it to `TRUSTED_RELEASE_KEYS` in `crates/release/src/signing.rs`\n (or set `CODEWHALE_RELEASE_PUBLIC_KEY=<hex>` when building). Add the same\n 32 bytes to the desktop app's `TRUSTED_APPCAST_KEYS`.\n4. Ship a release built with the public key first; the release after it is the\n first one installed without asking.\n\nTo rotate, add the new public key next to the old one, release, then switch\nthe secret and remove the old key a release later.\n\n---\n\n## 8. API keys and providers\n\n### Where Codewhale looks for a key (first match wins)\n\n1. `--api-key <KEY>` on the command line\n2. `api_key` in `~/.codewhale/config.toml`\n3. the secret store written by `codewhale auth set`\n4. the environment variable (`DEEPSEEK_API_KEY` for DeepSeek)\n\nThis order matters. **A key in config or the secret store beats\n`DEEPSEEK_API_KEY`.** If you rotate your key by exporting a new env var, an\nold stored key keeps being used. I tested this: a wrong key in `config.toml`\nplus the correct env var gives `Authentication Fails … ****beef is invalid`.\n\n### Ways to set a DeepSeek key (all tested)\n\n**Environment variable.** Good for trying it out and for CI:\n```bash\nexport DEEPSEEK_API_KEY=sk-... # add to ~/.bashrc / ~/.zshenv to persist\n```\n\n**`auth set`.** Saves the key for every folder:\n```bash\ncodewhale auth set --provider deepseek # prompts: \"Enter API key for deepseek:\"\nprintf '%s\\n' \"$KEY\" | codewhale auth set --provider deepseek --api-key-stdin # scripted\n# -> saved API key for deepseek to file-based (\"/home/you/.codewhale/secrets/secrets.json\") (config contains metadata only)\n```\nThe file-based message prints the resolved secret-store path; an explicit\n`CODEWHALE_HOME` changes that location.\n\nOn Linux, the key is stored in **plaintext** in\n`~/.codewhale/secrets/secrets.json`, with mode 0600. It is not in an OS\nkeyring. Note that in v0.10.0, `auth set` also writes\n`default_text_model = \"deepseek-v4-pro\"` into your config, switching you from\nthe default `deepseek-flash` to the pricier Pro model. Change it back with\n`/model` in the TUI, or edit `~/.codewhale/config.toml`.\n\n**Inside the TUI.** Press **F3** (or type `/provider`), select DeepSeek, press\nEnter, paste the key (masked), pick a model, and confirm. This also writes the\nsecret store, and keeps `deepseek-flash`.\n\n**Config file.** `~/.codewhale/config.toml`:\n```toml\n[providers.deepseek]\napi_key = \"sk-...\"\n```\n\n### Check which key is active\n\n```bash\ncodewhale auth status --provider deepseek\n# active source: env (last4: ...xxxx) # or: secret store / config / missing\n# lookup order: config -> secret store -> env\ncodewhale doctor --probe-api\n# · Testing connection... ✓ API connection successful\n```\n\nPlain `codewhale doctor` names an environment key it can see\n(`deepseek: env_source=set via DEEPSEEK_API_KEY (value not shown; not checked offline)`)\nbut does not read the secret store or check the key; `--probe-api` does both.\nDoctor exits 0 even when no key is found, so scripts should use `auth status`.\n\n### Remove a stored key\n\n```bash\ncodewhale auth clear --provider deepseek\n# cleared API key for deepseek from config and secret store\n```\n\nIt doesn't unset `DEEPSEEK_API_KEY` in your shell, and it leaves the\n`default_text_model` line that `auth set` added.\n\n### Other providers\n\n`codewhale auth list` shows about 50 providers (OpenRouter, Anthropic, OpenAI,\nMoonshot, Ollama, …). The pattern is the same:\n`codewhale auth set --provider <name>`, or the provider's env var. Local models\n(Ollama, vLLM, SGLang) need no key. Only DeepSeek was tested here.\n\n---\n\n<a id=\"8-shell-completions\"></a>\n\n## 9. Shell completions\n\n```bash\n# bash (needs the bash-completion package)\nmkdir -p ~/.local/share/bash-completion/completions\ncodewhale completion bash > ~/.local/share/bash-completion/completions/codewhale\n\n# zsh\nmkdir -p ~/.zfunc\ncodewhale completion zsh > ~/.zfunc/_codewhale\n# in ~/.zshrc, if not already there:\n# fpath=(~/.zfunc $fpath)\n# autoload -Uz compinit && compinit\n\n# fish\nmkdir -p ~/.config/fish/completions\ncodewhale completion fish > ~/.config/fish/completions/codewhale.fish\n```\n\nEach script registers both `codewhale` and `codew`. `codewhale completions` is\nan alias. Open a new shell afterwards. Regenerate after upgrading.\n\nHow well they work in v0.10.0 (tested interactively):\n\n* **bash:** fully works (`codewhale comp<Tab>`, `codew auth <Tab><Tab>`).\n* **fish:** sub-commands complete with descriptions, but\n `codewhale completion <Tab>` offers files instead of shell names.\n* **zsh:** only the first word completes. After a sub-command\n (`codewhale auth <Tab>`), zsh wrongly lists the top-level commands again\n (same on macOS zsh 5.9, where it offers all 126 top-level entries).\n\nPowerShell and Elvish scripts are generated too **(untested on this VM: shells\nnot installed)**.\n\n---\n\n## 10. Running it\n\n### The TUI\n\n```bash\ncd your-git-repo\ncodewhale\n```\n\n* The composer is at the bottom. The footer shows the permission posture\n (`ask`), the mode (`work`) and the model (`DeepSeek · deepseek-flash`).\n* **Shift+Tab** cycles the permission posture: Ask → Auto-Review → Full Access.\n **Tab** (with an empty composer) cycles the mode: Plan → Work → Operate.\n* In **Ask**, file edits in the workspace are applied and shown as a diff.\n Shell commands stop at an **APPROVAL** prompt: `y` allow once, `a` allow for\n this session, `n` deny, `Esc` abort the turn.\n* Useful keys: **F1** help (or `/help`), **Ctrl-K** command palette, **F3**\n provider/model picker, **Ctrl-R** resume a past session, **Ctrl-U** clear the\n input (**Ctrl-Z** restores it), **Ctrl-C** cancel, clear the draft, or quit, **Ctrl-D** quit\n with an empty input. Full list: [KEYBINDINGS.md](KEYBINDINGS.md).\n* On exit it prints `To resume this session, run codewhale resume <id>`.\n\nCodewhale creates a `.codewhale/` directory in your repo. Ignore its contents\nbut keep the committable `constitution.json` (these are the same patterns\n`/init` writes):\n\n```gitignore\n**/.codewhale/*\n!**/.codewhale/constitution.json\n```\n\n### Headless (scripts, CI)\n\n```bash\ncodewhale exec \"Reply with exactly: pong\" # one-shot answer, no tools\ncodewhale exec --auto \"create primes.py that prints the first 10 primes and run it\" # tools, auto-approved\ncodewhale exec --json \"…\" # summary JSON (provider, model, usage, output)\ncodewhale exec --auto --output-format stream-json \"…\" # one JSON event per line\n```\n\n`--auto` enables Full Access by default: tools run without approval, shell\ncommands have no Codewhale sandbox, and trust mode permits file tools to write\noutside the workspace. Add `--sandbox workspace-write` to keep the shell\nsandbox and file-tool workspace restrictions, with trust mode off.\n\nPlain `exec` offers the model no tools. Only `--auto`, `--yolo`,\n`--allowed-tools` or resuming a session opens a tool surface; limits such as\n`--max-turns`, `--disallowed-tools`, `--sandbox` and the output format never\nadd tools (tool-only flags print a warning). If the provider stops a reply at\nits output limit, the model is asked to continue and the printed answer is the\nwhole reply. A plain run takes at most 8 model steps unless `--max-turns` sets\nanother limit; a reply still cut off at that limit fails the run.\n\n### Resuming\n\n```bash\ncodewhale resume <session-id> # or a unique prefix, e.g. e2525dfb\ncodewhale -c # continue the most recent session in this folder\ncodewhale sessions # list saved sessions\ncodewhale exec --continue \"…\" # headless follow-up to the latest session\ncodewhale exec --resume <id> \"…\"\n```\n\nTUI sessions, resumed exec sessions, and exec runs that offer tools (including\n`exec --auto`) are saved in every output format. `--output-format stream-json`\nalso saves answer-only runs. Use `codewhale receipts --last` for a saved run's\nreceipt. Other answer-only text and `--json` runs are saved only if conversation\ncompaction starts; without that, they create no session for `exec --continue`\nor a receipt.\n\n---\n\n## 11. Terminal notes\n\n### Ghostty (tested: Ghostty 1.3.1 on Linux/X11)\n\nEverything I checked worked in Ghostty with its default config\n(`TERM=xterm-ghostty`, `COLORTERM=truecolor`). Screenshots are kept with the\n[install receipts](https://github.com/codewhale-hq/Codewhale/tree/37ecdfcc49bc68a9b0d058b97c3946e62c34bd31/docs/install-report/v0.10.0-2026-09-23/screenshots).\n\n| Check | Result |\n|---|---|\n| Colours / truecolor gradient, box drawing, Unicode (✓ é 日本語) | ✅ |\n| Window resize (1504×886 → 800×500 → back) reflows cleanly | ✅ |\n| Mouse wheel scrolls the transcript, with a jump-to-bottom button | ✅ |\n| Paste (`Ctrl+Shift+V`), multi-line: inserted, not sent | ✅ |\n| F1, F3, Ctrl-K, Ctrl-R, Tab, Shift+Tab, Ctrl-U/Ctrl-Z, Ctrl-C, Ctrl-D | ✅ |\n| Window title shows state (`waiting on you…`, `✓ done`) | ✅ |\n| Exit restores the terminal (normal screen, cursor, no mouse-reporting garbage) | ✅ |\n\nGhostty on Linux starts a **non-login** shell, so it reads `~/.bashrc` and not\n`~/.profile`. That's why you need the PATH line in `~/.bashrc` (§2).\n\nYou may notice small dots and a faint label (e.g. `other · drift`) drifting\nacross empty space after a turn. That's Codewhale's decorative \"ambient life\"\nwhale, not a rendering bug.\n\n### Other terminals\n\ntmux eats **F1**, so use `/help` there. Some key chords (Ctrl-Shift-…, Ctrl-Tab)\nneed a terminal with an enhanced keyboard protocol; [KEYBINDINGS.md](KEYBINDINGS.md) lists\nportable alternatives. Windows users should use Windows Terminal\n**(untested on this VM)**.\n\n---\n\n## 12. Uninstalling\n\n### Step 1: forget stored keys (if you used `auth set` or F3)\n\n```bash\ncodewhale auth clear --provider deepseek\n```\n\n### Step 2: remove the program\n\n| Installed with | Remove with |\n|---|---|\n| current Unix installer (§2) | Run the intended executable by its full path with `uninstall` to inspect the plan, then `uninstall --yes` to remove it. |\n| Windows release archive (`install.ps1`) | `\"%USERPROFILE%\\bin\\codewhale.exe\" uninstall`, then `uninstall --yes`. Remove `%USERPROFILE%\\bin` from your user PATH if you added it only for Codewhale. |\n| Windows setup (NSIS) | Settings > Apps > CodeWhale > Uninstall, or `%LOCALAPPDATA%\\Programs\\CodeWhale\\Uninstall.exe`. `codewhale uninstall` prints this path. |\n| WinGet | `winget uninstall --id HunterBown.CodeWhale` |\n| Scoop | `scoop uninstall codewhale` |\n| older `curl \\| sh` install without a receipt | `~/.local/bin/codewhale uninstall --yes`: in a known older curl-installer directory (`~/.local/bin`, `~/.local/codewhale/bin`, `~/.local/codewhale-release.*`, or `%USERPROFILE%\\bin` on Windows) it records a receipt for the commands whose bytes match the running program, then removes only those. A symlink, a hard link, or a file you do not own among them stops it before any receipt is written. |\n| manual copy elsewhere (§3) | Inspect the exact command paths and provenance before removing them manually; automatic uninstall preserves them. |\n| npm | `npm uninstall -g codewhale` (`codewhale uninstall` prints the `--prefix` for this copy) |\n| Cargo | `cargo uninstall codewhale-cli` (`codewhale uninstall` prints the owning `--root`) |\n| Homebrew | `brew uninstall codewhale` (`codewhale uninstall` prints the owning formula; untap only when you no longer use the tap) |\n\nNot sure which one you have? Run `codewhale doctor --installs`: it lists every copy, who owns it and the exact command that removes it, without running or changing anything. `codewhale uninstall` (no `--yes`) shows the same classified list after its own plan.\n\nThe Unix installers and the Windows archive installer write `.codewhale-install.sha256` beside the commands. Uninstall accepts only existing regular, single-link files whose bytes match that receipt. On Unix, receipts, selected files and their retained parent directory must belong to the current user and must not be writable by group or others. Optional compiled-host files use their existing release receipt. All selected files pass preflight before any removal; the executing command is removed last. Modified files, symlinks, ambiguous receipts, shared writable directories, legacy unreceipted installs and unrelated files are preserved. No recursive directory removal is performed. A failed removal reports which entries were unlinked, any remaining recovery paths, and any durability failure. If the command receipt was unlinked while the executable remains, recovery republishes it only to a vacant receipt name.\n\nThe command runs before configuration, credential migration and telemetry, so malformed config does not block it. It preserves data, credentials, workspaces and services. For a receipted release copy it also removes this install's exact three-line `# >>> codewhale PATH >>>` block from your shell profiles, and the Codewhale-generated completion files at the documented paths (`~/.local/share/bash-completion/completions/codewhale`, `~/.zfunc/_codewhale`, `~/.config/fish/completions/codewhale.fish`, honouring `XDG_DATA_HOME` and `XDG_CONFIG_HOME`). Nothing else in a profile is touched, a symlinked or unreadable profile is left for you to edit by hand, and the completion files are kept while another Codewhale copy remains on PATH. If any of that cleanup fails, the command says so and exits non-zero even though the program is already gone. Package-managed copies get removal guidance; custom Cargo roots with local Cargo metadata are recognized. Other command copies still on PATH are listed without running them. Refresh the shell command cache with `hash -r` and inspect the remaining entries.\n\nSuccessful self-update renews an existing command receipt, and writes one for a pre-receipt directory whose commands it just installed; it never claims unknown siblings. This command does not remove a remote-setup service or a desktop application.\n\nOn Windows, `codewhale uninstall --yes` removes `codewhale.exe`, `codew.exe`, `codewhale.bat`, any receipted compiled-host files and the receipt itself. Windows cannot delete a running program, so the running `codewhale.exe` is renamed in place to a `.codewhale-host-recovery-…` name and deleted by a helper once the command exits; the output names that file in case it remains. `codewhale.old-…` files left by an earlier self-update are listed, not deleted. A known legacy directory can adopt matching command bytes into a receipt. Other unreceipted archive copies are preserved; reinstall with the current `install.ps1` to obtain a receipt before automatic removal. Windows removal has been compile-checked but not yet exercised on a Windows host.\n\nRepeating `uninstall` after removal is safe: the removed copy is gone, and any other copy only removes what its own receipt owns. Reinstalling into the same directory afterwards writes a fresh receipt.\n\n### Step 3: remove data. Program removal keeps it by default.\n\n`codewhale uninstall --purge-data` previews the data removal. Without an explicit\n`CODEWHALE_HOME`, the targets are `~/.codewhale` and the legacy\n`~/.deepseek/snapshots`. With an explicit `CODEWHALE_HOME`, only that directory\nis considered; the legacy snapshots are preserved. A custom target must be named\n`.codewhale` to qualify for automatic purge.\n\nThe purge requires a successful receipted program removal in the same `--yes`\nrun. A package-managed copy, preview or failed removal keeps the data. All targets\nare checked before program removal: symlinks, another user's directory, your home\nor its ancestors, and any target containing the installation are refused.\nSessions, workspace snapshots and recovery copies in those targets are deleted\nby the purge; review the preview and preserve anything needed before confirming.\n\n| Retained item | How to review it |\n|---|---|\n| OS keyring entries | Run `codewhale auth clear --provider <id>` before removing the program. |\n| Per-workspace `.codewhale` directories | Inspect the known workspace individually; uninstall does not search or remove them. |\n| A bare `export PATH=…` line you added yourself, and completion files outside the documented paths | Yours to review: uninstall only removes the marked block and the documented Codewhale-generated completion files. |\n| Other installed command copies | Use `codewhale doctor --installs`; each copy follows its own owner and receipt. |\n| Legacy snapshots with explicit `CODEWHALE_HOME` | Review them separately; explicit-home purge does not include them. |\n\nA reinstall restores the program, not purged sessions or snapshots. Installation,\nremote services and desktop applications have separate ownership; this command\nonly removes what its selected command installation owns.\n\n---\n\n## 13. Troubleshooting\n\nEvery error below was hit while writing this guide.\n\n**`bash: codewhale: command not found` right after installing.**\n`~/.local/bin` isn't on PATH in this terminal. See\n[Put it on your PATH](#put-it-on-your-path).\n\n**`npm error code EACCES … permission denied, mkdir '…/lib/node_modules/codewhale'`.**\nYour Node is system-owned. See [§4](#if-you-get-eacces-permission-denied).\nDon't use sudo.\n\n**`error: DeepSeek API key not found.` (from `codewhale exec`)**\nNo key anywhere. Follow the printed steps, or see §8.\n\n**The TUI shows your message but never answers.**\nNo key (v0.10.0 doesn't say so). Press F3 → DeepSeek → Enter → paste the key.\n\n**`error: Responses API request failed … Authentication Fails, Your api key: ****dead is invalid`.**\nThe key is wrong or revoked. Run `codewhale auth status --provider deepseek`\nto see *which* source is being used. Remember that config and the secret store\nbeat the env var. Fix with `codewhale auth set --provider deepseek`, or\n`codewhale auth clear --provider deepseek` to fall back to the env var. In the\nTUI, a bad key sends you to a \"Choose your model provider\" screen that marks\nDeepSeek `last check failed (authentication)`.\n\n**`error: Network error: SSE stream request failed after HTTP/1.1 fallback: Responses API request failed. … on Windows or proxy networks, try CODEWHALE_FORCE_HTTP1=1 …`.**\nDespite the wording, on Linux this usually just means **no connection to\n`api.deepseek.com`**. Check with `curl -sI https://api.deepseek.com` (a `401`\nresponse is fine; it means the host is reachable). If you're behind a proxy,\nmake sure `HTTPS_PROXY` is exported. `codewhale doctor --probe-api` only says\n`✗ API connection failed` for both bad keys and network problems.\n\n### A local proxy that closed after the session started\n\nreqwest reads `HTTP_PROXY`, `HTTPS_PROXY` and `ALL_PROXY` when a client is\nbuilt, and a session builds its clients once. Without a fallback, a local proxy\n(Clash, v2rayN, sing-box) that stops after Codewhale starts leaves every\nrequest dialing a port that no longer answers — including requests to providers\nthat need no proxy at all.\n\nCodewhale now watches that port on the paths listed below. While it refuses\nconnections, requests on those paths go direct; when it answers again, proxy\nrouting resumes. Detection runs in the background and takes effect within a few\nseconds of a port starting to refuse, so a request issued in that gap can still\nfail once. A port that drops packets instead of refusing them is not treated as\ndown at all.\n\nPaths that install the watch: the model client and the API-key verification\nprobe, the model-list and catalog clients, HTTP/1.1 fallback, web search, web\nrun, vision, finance, the MCP registry, `models.dev`, the profile constitution\ncheck, the TUI balance poll and the remote-control relay. `doctor --probe-api`\nand `doctor --probe-local` go through the model client, so they are covered.\n\nOther probes keep the previous behaviour and can still fail while a local proxy\nis down: `doctor`'s search probe and the Runtime API runtime-info probe read\nthe proxy environment themselves, `cloud-facts` refreshes the same way, and the\nsandbox client is not covered. The dev-server readiness probe is not affected at\nall because it always goes direct.\n\nOnly loopback proxies are watched, each on its own port: a live `HTTP_PROXY`\ndoes not keep a dead `HTTPS_PROXY` in the path, and a remote proxy in the same\nconfiguration is routed through exactly as before, never diverted. While a port\nanswers, `NO_PROXY`, proxy credentials and the `ALL_PROXY` / `HTTP_PROXY` /\n`HTTPS_PROXY` precedence are unchanged. Detection is a TCP probe rather than a\nproxy handshake, so a process that holds the port without forwarding traffic\nstill counts as up.\n\nThe proxy environment is read once, when the first client is built. Exporting a\ndifferent `HTTPS_PROXY` mid-session needs a restart.\n\nGoing direct means the connection leaves from your own address. If you rely on\nthe proxy to hide it, set the variable below instead of letting the fallback\nrun; `NO_PROXY` matches still go direct either way.\n\nSet `CODEWHALE_LOCAL_PROXY_FALLBACK=0` to keep the previous behaviour.\n\n**`codewhale install: refusing to replace existing ~/.local/bin/codewhale`.**\nA different version is already installed there. Run `codewhale update`, or\ndelete the two files first (§7 rollback), or install into a fresh\n`CODEWHALE_INSTALL_DIR`.\n\n**`codewhale install: checksum mismatch for codew-linux-x64`.**\nThe download was corrupted or tampered with. Nothing was installed. Retry, and\nif it repeats, don't use a mirror.\n\n**`error: The package-managed executable was not changed.` (from `codewhale update`)**\nYou installed with npm, Cargo or Homebrew. Update with that tool instead.\n\n**`error: failed to run custom build command for libdbus-sys` (Cargo).**\nRun `sudo apt-get install -y libdbus-1-dev pkg-config`.\n\n**`error: No saved sessions found for workspace …` (from `exec --continue`).**\nNo session has been saved in this workspace. Fresh answer-only text and `--json`\nexec runs are saved only if conversation compaction starts; use\n`--output-format stream-json` to always save them.\nTUI sessions and exec runs that offer tools are saved in every output format.\n\n**zsh completion suggests the wrong things after the first word.**\nKnown v0.10.0 bug. bash and fish are fine.\n\n**Getting help:** `codewhale doctor --json` produces a diagnostics bundle\nwithout secrets.\n\n---\n\n## Appendix: other platforms (not re-tested in this revision)\n\nThe sections below are carried over unchanged from the previous revision of\nthis page. They were **not re-run** for the v0.10.0 install test above\n(out of scope: Windows, macOS, Android/Termux, FreeBSD, mainland-China\nmirrors), apart from the macOS paths noted in [macOS notes](#macos-notes).\nKnown contradictions with the published v0.10.0 assets, found by inspecting\nthem ([details](https://github.com/codewhale-hq/Codewhale/blob/37ecdfcc49bc68a9b0d058b97c3946e62c34bd31/docs/install-report/v0.10.0-2026-09-23/DOC_DEFECTS.md), D15 and D16):\n\n* v0.10.0 publishes both `codewhale-windows-x64.zip` (with an `install.bat`\n that copies to `%USERPROFILE%\\bin`) and `codewhale-windows-x64-portable.zip`;\n the sections below mention only the first.\n* The standalone `codewhale.bat` launcher works only next to the x64 exe.\n\n### Supported platforms and assets\n\nThe [latest stable release](https://github.com/codewhale-hq/CodeWhale/releases/latest)\npublishes Linux x64/arm64, macOS x64/arm64, Windows x64/arm64, and Android arm64\nassets. A release that includes the OpenHarmony leg also publishes a native\nHarmonyOS/OpenHarmony arm64 tarball. Artifact presence is distinct from platform\nqualification.\nThe table below describes the current source tree's platform and secondary\npackaging support; `latest` installation still selects the published release.\nAndroid/Termux is preview pending real-device QA. Linux ARM64 is available from\nv0.8.8 onward. Linux RISC-V prebuilts are temporarily paused because the locked\n`rquickjs-sys` dependency does not ship `riscv64gc-unknown-linux-gnu` bindings.\n\n| Platform | Architecture | GitHub release asset | npm install | `cargo install` |\n| ------------ | ------------ | ----------------------------------------------------- | :---------: | :-------------: |\n| Linux | x64 (x86_64) | `codewhale-linux-x64`, `codew-linux-x64` | ✅ | ✅ |\n| Linux | arm64 | `codewhale-linux-arm64`, `codew-linux-arm64` | ✅ | ✅ |\n| Android / Termux | arm64 (aarch64) | `codewhale-android-arm64.tar.gz` (published in v0.9.12; device support is preview) | ⚠️⁴ preview | ⚠️⁴ preview |\n| HarmonyOS / OpenHarmony (native) | arm64 (aarch64) | `codewhale-ohos-arm64.tar.gz` (self-signed inside; only in releases that list it), otherwise build and sign — see [HarmonyOS](HarmonyOS.md) | ❌ | ❌ |\n| Linux | riscv64 | temporarily unsupported until upstream bindings land | ❌¹ | ❌³ |\n| macOS | x64 | `codewhale-macos-x64`, `codew-macos-x64` | ✅ | ✅ |\n| macOS | arm64 (M-series) | `codewhale-macos-arm64`, `codew-macos-arm64` | ✅ | ✅ |\n| Windows | x64 | `codewhale-windows-x64.exe`, `codew-windows-x64.exe` | ✅ | ✅ |\n| Windows | arm64 | `codewhale-windows-arm64.exe`, `codew-windows-arm64.exe` | ✅ | ✅ |\n| Linux x64 or arm64 on musl (Alpine) | native arch | matching static Linux asset | ✅ (static) | ✅ |\n| Other Linux (musl on other arches) | — | build from source | ❌¹ | ✅² |\n| FreeBSD 14+ | x64, arm64 | `cargo install codewhale-cli --locked` (no prebuilt; see § FreeBSD) | ❌ | ✅² |\n\n¹ The npm package will exit with a clear error and point you here.\n² Provided your toolchain can compile a recent Rust workspace; see\n [Build from source](#5-cargo-and-building-from-source) below.\n³ RISC-V source builds currently need upstream `rquickjs-sys` RISC-V bindings or\n a bindgen-enabled dependency build.\n⁴ The current npm wrapper recognizes Android arm64 and resolves\n the matching `codewhale` and `codew` Android assets. npm\n installation works only for a package version whose GitHub Release publishes\n those matching assets. The Android/Termux path remains preview-only until the\n real-device compile, startup, approval, file-tool, and update checks tracked\n in #4236 and #4242 are complete.\n\nAndroid / Termux is not the same target as Linux arm64. Do not install the\nLinux `codewhale-linux-arm64` archive in Termux; use the Termux-specific\nAndroid archive when a release or release candidate publishes one, or build\nfrom source inside Termux.\n\nThe HarmonyOS row is the native target only. Inside the Linux subsystem\n(融合开发引擎, an openEuler virtual machine) the ordinary Linux ARM64 asset works.\n\nThe current Linux **x64 and arm64** assets are **static musl builds**.\nThe x64 release path has used musl since v0.8.65; v0.9.6 extends the same build\nand static-launch check to arm64. These binaries have no glibc dependency and\nrun on their matching architecture across Ubuntu, Debian, RHEL/CentOS, and\nAlpine/musl. SQLite is bundled through `rusqlite`, so no separate `libsqlite3`\nruntime package is needed.\n\n#### Linux ARM64 portability\n\nLinux arm64 assets before v0.9.6 were GNU libc builds and could inherit the\nUbuntu 24.04 build host's `GLIBC_2.39` floor. Ubuntu 22.04 ships glibc 2.35, so\nthose older arm64 binaries can fail with errors such as:\n\n```text\nversion `GLIBC_2.39' not found\n```\n\nThe npm wrapper, `codewhale update`, and the Unix archive installer retain their\nGNU-binary preflight for older releases. The current arm64 build instead uses\n`aarch64-unknown-linux-musl`, so it has no `GLIBC_*` floor. If you are installing\nan earlier release on an older arm64 distribution, use:\n\n```bash\ncargo install codewhale-cli --locked # installs `codewhale`\n```\n\n> **Linux ARM64 note (v0.8.7 and earlier).** v0.8.7 and earlier do **not**\n> publish a Linux ARM64 prebuilt; users on HarmonyOS thin-and-light, Asahi\n> Linux, Raspberry Pi, AWS Graviton, etc. saw `Unsupported architecture: arm64`\n> from `npm i -g codewhale`. v0.8.8 publishes `codewhale-linux-arm64`, so a plain `npm i -g codewhale` works\n> on any glibc-based ARM64 Linux. If you're stuck on v0.8.7, jump to\n> [Build from source](#5-cargo-and-building-from-source) — `cargo install` works fine.\n> For HarmonyOS PC and OpenHarmony cross-build setup, see\n> [HarmonyOS and OpenHarmony](HarmonyOS.md).\n\n### Migrating from npm, Cargo, or another installation\n\n#### Migrating from npm, Cargo, or another installation\n\nPackage managers continue to own their files. `codewhale update` gives migration\ninstructions for npm, Cargo, Homebrew, and Omarchy instead of overwriting them.\nKnown system/package directories are also protected. A `CODEWHALE_INSTALL_METHOD=binary`\noverride cannot bypass a recognized managed path.\n\nRe-running the installer upgrades an earlier Codewhale in `~/.local/bin` in\nplace (old files go to the recovery folder). Create a fresh destination only\nwhen you want to keep every existing installation exactly as it is:\n\n```bash\nmkdir -p \"$HOME/.local\"\ncodewhale_install_dir=\"$(mktemp -d \"$HOME/.local/codewhale-release.XXXXXX\")\"\ncurl -fsSL https://codewhale.net/install.sh | CODEWHALE_INSTALL_DIR=\"$codewhale_install_dir\" sh\n\"$codewhale_install_dir/codewhale\" --version\nexport PATH=\"$codewhale_install_dir:$PATH\"\nhash -r\ncommand -v codewhale codew\n\"$codewhale_install_dir/codewhale\" update --check\n```\n\nAfter verifying the version and command paths, keep that directory first in your\nshell profile. In PowerShell, use `Get-Command codewhale, codew -All` to inspect\nresolution; run the selected executable using its full path. A successful update\nonly changes its own install directory, so another earlier PATH entry can still\nlaunch an older copy.\n\nModern matched `codewhale`, `codew`, and compatibility copies update from the\nsame verified bytes. Symlinks to the running binary are preserved. A sibling\nthat is a different Codewhale build is moved to the recovery folder and\nreplaced; an unrelated sibling is named in the error and left untouched. No\nsibling is executed merely to guess its owner.\n\nTo retain a secondary package-managed install, use its manager:\n\n```bash\nnpm install -g codewhale@latest\n# or\ncargo install codewhale-cli --locked --force\n```\n\nHomebrew uses `brew upgrade codewhale`; Omarchy uses `omarchy update`. These\ncommands update their own copies, so verify PATH again afterward.\n\n<a id=\"android--termux-arm64\"></a>\n\n### Android / Termux arm64 (preview)\n\nTermux runs on Android's Bionic libc and uses `$PREFIX` as its Unix prefix, so\nit needs a Termux-specific Android arm64 archive. The Linux arm64 release asset\ntargets standard Linux with musl; Android uses a distinct Rust target, so the\nLinux asset should not be used there.\n\nInstall the minimum archive/runtime tools first:\n\n```bash\npkg update\npkg install -y ca-certificates curl tar gzip coreutils\n```\n\nWhen the release includes `codewhale-android-arm64.tar.gz`, install it with the\narchive's bundled installer. Passing `PREFIX=\"$PREFIX\"` matters: the installer\ndefaults to `~/.local`, while Termux users normally expect commands under\n`$PREFIX/bin`.\n\n```bash\ncd \"$HOME\"\ncurl -L -O https://github.com/codewhale-hq/CodeWhale/releases/latest/download/codewhale-android-arm64.tar.gz\ncurl -L -O https://github.com/codewhale-hq/CodeWhale/releases/latest/download/codewhale-bundles-sha256.txt\nsha256sum -c codewhale-bundles-sha256.txt --ignore-missing\n\ntar xzf codewhale-android-arm64.tar.gz\ncd codewhale-android-arm64\nPREFIX=\"$PREFIX\" ./install.sh\nhash -r\n```\n\nIf you are validating from source or building a release candidate locally,\ninstall the build packages before running Cargo:\n\n```bash\npkg install -y rust clang pkg-config make git\ncargo install codewhale-cli --locked # installs `codewhale`\n```\n\nThe normal first-run setup path is implemented, but its Android interaction is\nstill part of the preview QA above. Prefer provider environment variables for\ntemporary credentials. `codewhale auth set` is available, but the Termux build\nhas no supported OS keyring integration and falls back to file-backed secrets\nby writing `~/.codewhale/config.toml` and mirroring keys to\n`~/.codewhale/secrets/secrets.json`. Both are plaintext files protected by\n`0600` permissions and are not encrypted at rest.\n\n```bash\ncodewhale auth set --provider deepseek\ncodewhale auth status\ncodewhale doctor\n```\n\nMaintainers should use this repeatable smoke checklist for a Termux / Android\narm64 release candidate:\n\n```bash\ncommand -v codewhale codew\ntest -x \"$PREFIX/bin/codewhale\"\ntest -x \"$PREFIX/bin/codew\"\n\ncodewhale --version\ncodewhale doctor\ncodewhale exec --auto \"run pwd\"\n```\n\nKnown limitations:\n\n- Commands inherit Android's per-app UID, SELinux, and seccomp protections and\n any permissions granted to Termux. Codewhale's bubblewrap\n child-process sandbox is Linux-only and is not built on Android, so approved\n commands receive no Codewhale-specific filesystem narrowing.\n- The Termux build has no supported Android Keystore or desktop Secret Service\n integration. Use `codewhale auth status` to confirm the active source and\n prefer provider environment variables when file-backed plaintext storage is\n not acceptable.\n- Terminal rendering varies by Android terminal app. The TUI always owns the\n alternate screen. If a terminal app cannot render the full-screen TUI,\n use `codewhale exec` for headless runs instead.\n\n### China / mirror-friendly install\n\nWhen installing from mainland China, configure mirrors for both **rustup**\n(the Rust toolchain installer) and **Cargo** (the package registry) to avoid\nTLS timeouts and download failures.\n\n**Step 1: Install Rust via a rustup mirror**\n\n```bash\n# PowerShell\n[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12\n(New-Object Net.WebClient).DownloadFile('https://win.rustup.rs/x86_64', 'rustup-init.exe')\n\n# git-bash / msys2\nexport RUSTUP_DIST_SERVER=https://mirrors.tuna.tsinghua.edu.cn/rustup\nexport RUSTUP_UPDATE_ROOT=https://mirrors.tuna.tsinghua.edu.cn/rustup/rustup\n./rustup-init.exe -y --default-toolchain stable\n\n# Linux / macOS\nexport RUSTUP_DIST_SERVER=https://mirrors.tuna.tsinghua.edu.cn/rustup\nexport RUSTUP_UPDATE_ROOT=https://mirrors.tuna.tsinghua.edu.cn/rustup/rustup\ncurl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable\n```\n\nIf the TUNA mirror is slow from your network, `rsproxy.cn` is another\nrustup mirror option for Linux/macOS:\n\n```bash\nexport RUSTUP_DIST_SERVER=https://rsproxy.cn\nexport RUSTUP_UPDATE_ROOT=https://rsproxy.cn/rustup\ncurl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable\n```\n\nThe `RUSTUP_DIST_SERVER` and `RUSTUP_UPDATE_ROOT` environment variables must\nbe set **before** running rustup-init; the toolchain download otherwise hits\nthe same TLS handshake problem as the installer.\n\n**Step 2: Configure Cargo registry mirror**\n\n```toml\n# ~/.cargo/config.toml\n[source.crates-io]\nreplace-with = \"tuna\"\n\n[source.tuna]\nregistry = \"sparse+https://mirrors.tuna.tsinghua.edu.cn/crates.io-index/\"\n```\n\n`rsproxy`, Tencent COS, and Aliyun OSS mirrors work the same way; pick whichever\nis fastest from your network.\n\n### Omarchy / AUR\n\nOn Omarchy, install the prebuilt AUR package:\n\n```bash\nomarchy pkg aur add codewhale-bin\ncodewhale --version\n```\n\n`codewhale-bin` packages the same checksum-pinned Linux release archives as the\nother binary install paths and provides both `codewhale` and `codew`. It does\nnot carry a separate Codewhale version; the existing `codewhale-tui`\ncompatibility command remains an alias to the same runtime. Package updates\narrive through `omarchy update`; the in-app updater leaves the pacman-owned\nbinary to Omarchy.\n\nThe AUR update follows the matching Codewhale tag and release assets, so it may\nappear after the GitHub release while its generated `PKGBUILD` and `.SRCINFO`\nare validated. Release-maintainer instructions live in\n[`packaging/aur/README.md`](../packaging/aur/README.md).\n\n---\n\n### Windows\n\n#### Windows Scoop\n\nThe `codewhale` package is listed in Scoop's main bucket:\n\n```powershell\nscoop update\nscoop install codewhale\ncodewhale --version\n```\n\nScoop manifests are maintained outside this repository's release workflow and\ncan lag GitHub/npm/Cargo releases. Use npm or manual GitHub release downloads\nwhen you need the newest version immediately.\n\n#### Windows winget\n\nWinGet installs **`HunterBown.CodeWhale`** as the portable x64 command\n`codewhale`. It downloads `codewhale-tui-windows-x64.exe` and declares a\ndependency on the Microsoft Visual C++ 2015+ x64 runtime.\n[Published 0.10.2 manifest](https://github.com/microsoft/winget-pkgs/tree/master/manifests/h/HunterBown/CodeWhale/0.10.2)\n\n```powershell\nwinget install HunterBown.CodeWhale\ncodewhale --version\n```\n\nUpdate with `winget upgrade --id HunterBown.CodeWhale`. Each\nversion goes through winget-pkgs review after the GitHub Release, so winget can\nlag GitHub/npm/Cargo; use npm or the GitHub Release asset when you need the\nnewest version immediately.\n\nKnown limits of the winget route:\n\n* **x64 only.** The published manifest has no ARM64 installer. On Windows ARM64,\n use `npm install -g codewhale` under native ARM64 Node.js, or download\n `codewhale-windows-arm64.zip` from GitHub Releases.\n* **`codewhale` only.** The short `codew` alias is not installed by winget; use\n `codewhale`.\n* The release generator prepares a matching local manifest. [WinGet packaging](../packaging/winget/README.md).\n\n#### Windows NSIS Installer\n\nFor the OS/terminal compatibility targets and current evidence, see\n[Windows support and release qualification](ENVIRONMENTS.md#windows-support-and-release-qualification).\n\nA standalone NSIS-based installer is available starting with v0.8.50 for\nWindows users who prefer a traditional double-click setup (no npm, no Scoop, no\nCargo required).\n\nThe NSIS installer currently contains the Windows x64 binaries. Windows ARM64\nusers should install through npm running under native ARM64 Node.js or download\n`codewhale-windows-arm64.zip` from the same release; both paths then use native\nARM64 binaries.\n\n**Download** `CodeWhaleSetup.exe` from the\n[Releases page](https://github.com/codewhale-hq/CodeWhale/releases/latest).\n\n**Install** by double-clicking the setup executable. The installer:\n\n- Installs `codewhale.exe` and `codew.exe` side-by-side (single binary, no `codewhale-tui.exe`) into\n `%LOCALAPPDATA%\\Programs\\CodeWhale\\bin`\n- Installs `codewhale.bat`, which prefers Windows Terminal (`wt.exe`) when it is on `PATH` and\n otherwise launches the exe directly\n- Creates a current-user Start Menu shortcut that opens that launcher, not the raw `.exe`\n- Adds the install directory to the **current user** `PATH`\n- Registers in Windows **Apps & Features** for easy uninstall\n\nUninstall removes the binaries, `codewhale.bat`, the Start Menu shortcut, and the user `PATH` entry.\n\n**Silent install** (for IT admins, SCCM, Intune):\n\n```powershell\nCodeWhaleSetup.exe /S\n```\n\nThe installer is per-user and does not request elevation. Run silent installs in\nthe target user's context, or use a deployment tool that can run the installer\nfor each user profile that needs Codewhale.\n\nThe release-built installer is currently unsigned and may trigger Windows\nSmartScreen. Verify the SHA-256 checksum from `codewhale-artifacts-sha256.txt`\nbefore deploying, and sign the installer in your internal deployment pipeline if\nyour environment requires signed application packages.\n\n**Build the installer yourself** (requires [NSIS](https://nsis.sourceforge.io)):\n\n```powershell\ncd scripts\\installer\n# Place codewhale.exe and codew.exe here (single binary, no codewhale-tui.exe), then:\nmakensis /DVERSION=<version> codewhale.nsi\n```\n\n**Manual fallback** — if the installer is blocked by group policy, see the\n[CLASSROOM_INSTALL.md](CLASSROOM_INSTALL.md) guide for step-by-step PowerShell\ncommands.\n\n> **Deploying to a classroom or lab?** See the full\n> [Classroom Install Checklist](CLASSROOM_INSTALL.md) for silent install,\n> API key provisioning, imaging notes, and troubleshooting.\n\n<a id=\"freebsd\"></a>\n\n### FreeBSD, cross-compiling, Windows source builds\n\n#### FreeBSD 14+ source-build workaround (#1097)\n\nFreeBSD has no prebuilt GitHub Release asset — `npm install -g codewhale` intentionally\nfails with `Unsupported platform: freebsd` and points to Cargo. Install from source:\n\n```bash\npkg install -y rust pkgconf git\ncargo install codewhale-cli --locked # installs `codewhale`\ncodewhale --version\ncodewhale doctor\n```\n\nThe `rquickjs` FreeBSD bindings are generated at build time via `bindgen` (see\n`1582ba965`/`5eb0385e8`). No separate `pkg install codewhale` port exists yet —\na native port is tracked as the follow-up to #1097 under `packaging/freebsd/`\n(contributions welcome). Validate with `cargo check --target x86_64-unknown-freebsd -p codewhale-cli --locked`\non the release branch; the 7×1 release matrix (Linux musl x64/arm64,\nAndroid arm64, macOS x64/arm64, Windows x64/arm64) stays 7 targets — FreeBSD is a\nsource-build target, not a prebuilt asset.\n\n#### Cross-compiling from x64 to ARM64 Linux\n\nThe release asset uses `aarch64-unknown-linux-musl` and is built on a native ARM\nrunner. If you want to build a GNU-linked ARM64 Linux binary on an x64 Linux\nhost (e.g. for the openEuler Linux subsystem on a HarmonyOS PC), use\n[`cross`](https://github.com/cross-rs/cross), which wraps the official Rust\ncross-targets in a Docker container:\n\n```bash\n# Once\nrustup target add aarch64-unknown-linux-gnu\ncargo install cross --locked\n\n# Per build\ncross build --release --target aarch64-unknown-linux-gnu -p codewhale-cli # single binary\n```\n\nThe resulting binary lands in\n`target/aarch64-unknown-linux-gnu/release/codewhale`. Copy it to the ARM64 host\n(e.g. via `scp`) and make it executable. This local GNU build is distinct from\nthe portable musl release asset; either executable can be copied under the\n`codew` convenience name.\n\nIf you don't have Docker available, install the cross-linker directly and let\nCargo do the work:\n\n```bash\nsudo apt-get install -y gcc-aarch64-linux-gnu\nrustup target add aarch64-unknown-linux-gnu\n\ncat >> ~/.cargo/config.toml <<'EOF'\n[target.aarch64-unknown-linux-gnu]\nlinker = \"aarch64-linux-gnu-gcc\"\nEOF\n\ncargo build --release --target aarch64-unknown-linux-gnu -p codewhale-cli # single binary\n```\n\nProducing `aarch64-unknown-linux-musl` while cross-compiling requires an\nappropriate musl cross-linker. The release workflow avoids that extra moving\npart by building and launching the musl binary on GitHub's native ARM runner.\n\n#### Windows build from source\n\nBuilding on Windows requires the **MSVC C toolchain** from\n[Visual Studio Build Tools](https://visualstudio.microsoft.com/downloads/#build-tools-for-visual-studio-2022)\n(the free workload-selectable installer, not the full IDE).\n\n**Prerequisites (Windows)**\n\n1. Install Visual Studio 2022 Build Tools — select the **\"Desktop development\n with C++\"** workload.\n2. Install [Rust](https://rustup.rs) 1.89+ (see the\n [China mirror instructions](#china--mirror-friendly-install) above if\n downloading from mainland China).\n3. Install [Git for Windows](https://git-scm.com/download/win) (provides `git`\n and the `git-bash` terminal).\n\n**Recommended terminals**: Windows Terminal, `git-bash`, or PowerShell.\n`cmd.exe` works but has a small buffer and limited PATH behavior.\n\n**Setting up the MSVC environment**\n\nVisual Studio Build Tools install `cl.exe` to a versioned directory but do\n**not** add it to `PATH` globally. You must set the environment manually or\nuse a Developer Command Prompt. The required variables are:\n\n```powershell\n# Adjust version numbers to match your installation\n$msvc = \"C:\\Program Files (x86)\\Microsoft Visual Studio\\2022\\BuildTools\\VC\\Tools\\MSVC\\14.44.35207\"\n$sdk = \"C:\\Program Files (x86)\\Windows Kits\\10\"\n$sdkv = \"10.0.26100.0\"\n\n$env:INCLUDE = \"$msvc\\include;$msvc\\atlmfc\\include;$sdk\\Include\\$sdkv\\ucrt;$sdk\\Include\\$sdkv\\um;$sdk\\Include\\$sdkv\\shared\"\n$env:LIB = \"$msvc\\lib\\x64;$msvc\\atlmfc\\lib\\x64;$sdk\\Lib\\$sdkv\\ucrt\\x64;$sdk\\Lib\\$sdkv\\um\\x64\"\n$env:LIBPATH = \"$msvc\\lib\\x64;$msvc\\atlmfc\\lib\\x64\"\n$env:CC = \"$msvc\\bin\\Hostx64\\x64\\cl.exe\"\n$env:CXX = \"$msvc\\bin\\Hostx64\\x64\\cl.exe\"\n$env:PATH = \"$msvc\\bin\\Hostx64\\x64;$env:PATH\"\n```\n\nAlternatively, open a **\"Developer Command Prompt for VS 2022\"** (available\nfrom the Start Menu after installing Build Tools), which runs `vcvars64.bat`\nto configure all of the above automatically. Then add `cargo` to `PATH` inside\nthat session and run `cargo build` from the project root.\n\n**Cargo registry mirror** — on Windows the mirror config goes to\n`%USERPROFILE%\\.cargo\\config.toml`. See [Step 2 above](#china--mirror-friendly-install).\n\n**Build**\n\n```bash\ngit clone https://github.com/codewhale-hq/CodeWhale.git\ncd CodeWhale\nset CARGO_HTTP_CHECK_REVOKE=false # may be needed behind some Chinese ISPs\ncargo build --release\n```\n\nThe Cargo-built binary appears at `target\\release\\codewhale.exe`. Release\npackaging separately exposes the same executable as `codew.exe`.\n\n> Prefer not to build? Install via npm, Cargo, GitHub Releases, or the CNB\n> mirror — see the sections above.\n\n### Older-release and regional troubleshooting\n\n#### `Unsupported architecture: arm64 on platform linux`\n\nYou're on a release earlier than v0.8.8 that doesn't publish Linux ARM64\nbinaries. Use the GitHub installer in a fresh directory as described above, or use\n`cargo install` per [Section 4](#5-cargo-and-building-from-source).\n\n#### `MISSING_COMPANION_BINARY` after upgrading an older install\n\nThe current single binary runs the TUI in-process and does not require a\ncompanion executable. This error identifies a stale pre-v0.9.5 dispatcher.\nUse the fresh-directory GitHub migration above, then verify the selected\n`codewhale` and `codew` paths. Do not download another separate runtime.\n\n#### `codewhale update` reports `no asset found for platform codewhale-linux-aarch64`\n\nOlder updaters used Rust architecture names that did not match the published\nasset names. Use the official installer in a fresh directory as described above,\nthen run the newly installed command by its full path.\n\n#### npm download is slow or times out from mainland China\n\nOn Linux x64 the npm wrapper already probes GitHub Releases and the CNB\nfirst-party checksum manifests in parallel and downloads binaries only from\nthe first source that validates. You do not need `CODEWHALE_USE_CNB_MIRROR=1`\nfor that automatic path.\n\nIf both first-party sources fail, set `CODEWHALE_RELEASE_BASE_URL` to a\nmirrored release-asset directory (rsproxy, TUNA, Tencent COS, Aliyun OSS),\nor skip npm entirely and use the Cargo mirror setup in\n[Section 4](#5-cargo-and-building-from-source). The legacy\n`DEEPSEEK_TUI_RELEASE_BASE_URL` name is still accepted. `CODEWHALE_USE_CNB_MIRROR=1`\nstill forces CNB only on Linux x64 / OpenHarmony x64.\n\n#### `codewhale update` is blocked by GitHub from mainland China\n\n`codewhale update` prefers GitHub Releases. On supported Linux x64 targets,\na failed GitHub manifest permits the matching CNB manifest and binary fallback.\nIf GitHub metadata is also unreachable, explicitly select a known published CNB\nversion (`CODEWHALE_USE_CNB_MIRROR=1 CODEWHALE_VERSION=X.Y.Z codewhale update`)\nor a binary mirror below. Existing newer builds are kept.\n\nBuilding from the CNB source mirror with Cargo is a secondary option. Cargo\ninstalls its own `codewhale` command:\n\nTo check the latest release without downloading or replacing binaries, run\n`codewhale update --check`.\n\n```bash\ncargo install --git https://cnb.cool/codewhale.net/codewhale --tag vX.Y.Z codewhale-cli --locked --force # single binary\n```\n\nIf you operate a binary asset mirror, `codewhale update` can use it directly:\n\n```bash\nCODEWHALE_RELEASE_BASE_URL=https://your-mirror.example.com/CodeWhale/vX.Y.Z/ \\\nCODEWHALE_VERSION=X.Y.Z \\\ncodewhale update\n```\n\nThe mirror directory must contain `codewhale-artifacts-sha256.txt` and the\nplatform binaries from the GitHub release. The legacy\n`DEEPSEEK_TUI_RELEASE_BASE_URL` mirror variable remains supported as an alias.\n\n`codewhale update` only talks HTTPS, and only to GitHub's release hosts, the CNB\nmirror, and the host of the `CODEWHALE_RELEASE_BASE_URL` you set; every\nredirect hop is held to the same rule, so a plain-`http://` mirror is refused.\nA private mirror therefore works as soon as its base URL is HTTPS. If that\nmirror redirects asset downloads to a separate download host (a CDN or an\nobject-store domain), name that host too:\n\n```bash\nCODEWHALE_UPDATE_ALLOWED_HOSTS=cdn.your-mirror.example.com,objects.example.net \\\nCODEWHALE_RELEASE_BASE_URL=https://your-mirror.example.com/CodeWhale/vX.Y.Z/ \\\nCODEWHALE_VERSION=X.Y.Z \\\ncodewhale update\n```\n\nThe error message for a refused host names it and this variable.\n\n### Windows and npm-download troubleshooting\n\n#### Windows: `TLS handshake eof` or `CRYPT_E_REVOCATION_OFFLINE` from `rustup-init`\n\nThe TLS handshake to `static.rust-lang.org` fails from behind the GFW or\ncertain Chinese ISPs. Set the rustup mirror environment variables **before**\nrunning the installer:\n\n```bash\n# git-bash / msys2\nexport RUSTUP_DIST_SERVER=https://mirrors.tuna.tsinghua.edu.cn/rustup\nexport RUSTUP_UPDATE_ROOT=https://mirrors.tuna.tsinghua.edu.cn/rustup/rustup\n./rustup-init.exe -y --default-toolchain stable\n```\n\nIf you see `CRYPT_E_REVOCATION_OFFLINE` from Cargo after Rust is installed,\nalso set `CARGO_HTTP_CHECK_REVOKE=false` during `cargo build`.\n\n#### Windows: MSVC compiler (`cl.exe`) not found during `cargo build`\n\nVisual Studio Build Tools do not add `cl.exe` to the global `PATH`. Either:\n\n1. Open **\"Developer Command Prompt for VS 2022\"** from the Start Menu, add\n `%USERPROFILE%\\.cargo\\bin` to `PATH` in that window, and run `cargo build`\n from there; or\n2. Set the MSVC environment variables manually — see the\n [Windows build from source](#windows-build-from-source) section for the\n PowerShell snippet.\n\nVerify the compiler is reachable: `cl.exe /?` should print help text.\n\n#### Windows: `拒绝访问 (os error 5)` when Cargo executes build scripts\n\nThird-party antivirus software (Huorong, 360, Kaspersky, etc.) may block\nCargo from executing freshly-compiled build-script binaries\n(e.g. `libsqlite3-sys`, `aws-lc-sys`, `instability`). The error is\npath-agnostic — moving `target-dir` does not help.\n\n**Symptoms**: `could not execute process ... build-script-build (never executed)`\n\n**Workarounds** (pick one):\n\n1. **Add the project's `target/` directory to your AV exclusions list.**\n2. **Close the antivirus software temporarily** during `cargo build`.\n3. **Use the GitHub Release installer/archive instead** — the release assets\n ship prebuilt binaries and skip the Cargo build entirely\n ([Section 6](#3-manual-download-from-github-releases)).\n4. **Use `cargo install codewhale-cli --locked`** from crates.io — this\n changes the binary path, which some AV tools treat differently.\n\nTo verify that the build-script binary itself is valid (not corrupted), locate\nit under `target/debug/build/<crate>/build-script-build` and run it manually:\n\n```bash\ntarget/debug/build/libsqlite3-sys-*/build-script-build\n# If this runs but panics with \"NotPresent\" (no C compiler), the binary is\n# fine — the AV is blocking Cargo's process-spawning path specifically.\n```\n\n#### npm binary download times out\n\nIf `codewhale` waits several seconds and prints `connect ETIMEDOUT` or\n`EAI_AGAIN` while fetching from `github.com`, the npm wrapper installed\nsuccessfully but the prebuilt binary download is blocked or unreliable on\nyour network. This download is separate from the npm registry package\ndownload. On Linux x64 the wrapper first races the small GitHub and CNB\nchecksum manifests and does not wait for a full GitHub binary to time out\nbefore using a valid CNB manifest.\n\nUse one of these paths:\n\n1. Set a proxy and retry:\n\n ```bash\n export HTTPS_PROXY=http://your-proxy:port\n codewhale\n ```\n\n2. Mirror the release assets internally and set `CODEWHALE_RELEASE_BASE_URL`:\n\n ```bash\n export CODEWHALE_RELEASE_BASE_URL=https://your-mirror.example.com/CodeWhale/\n codewhale\n ```\n\n The directory must contain `codewhale-artifacts-sha256.txt` and the platform\n binaries from the GitHub release.\n\n3. Install via Cargo, which builds locally and does not download GitHub release\n assets. See [Section 4](#5-cargo-and-building-from-source).\n\n4. Download both matching `codewhale` and `codew`\n binaries from the [Releases page](https://github.com/codewhale-hq/CodeWhale/releases),\n place them in a directory on `PATH`, and make them executable. See\n [Section 6](#3-manual-download-from-github-releases).\n"
|
||
}
|
||
]
|
||
},
|
||
{
|
||
"id": "guide",
|
||
"slug": "guide",
|
||
"sources": [
|
||
{
|
||
"path": "docs/GUIDE.md",
|
||
"sha256": "7e2ac75c89723e95ef6c2d4ebd00368e1c82a78193a3258ffa969ea8214cd5f6",
|
||
"content": "# Codewhale User Guide\n\nThis guide is for your first hour with Codewhale. It explains the main\nworkflow, the important safety controls, and where to go next when you need a\ncomplete reference.\n\nCodewhale has deeper reference documents for installation, configuration,\nproviders, modes, keybindings, tools, and operations. Use this page as a guided\nwalkthrough, then follow the \"Next\" links when you need every option.\n\n## 1. Welcome to Codewhale\n\nCodewhale is a terminal coding agent. You run it from a workspace, give it a\ntask, and it can use structured tools to inspect files, run commands, edit\ncode, and report back with evidence.\n\nThe important difference from a normal chat model is that Codewhale is built\naround a harness:\n\n- It keeps the active workspace and session visible.\n- It routes each turn through explicit modes and approval rules.\n- It shows tool calls in the transcript instead of hiding the work.\n- It can preserve sessions, fork conversations, and continue later.\n- It can run sub-agents for focused background work.\n\nYou can use Codewhale for small questions:\n\n```text\nExplain the authentication flow in this repository.\n```\n\nYou can also use it for multi-step work:\n\n```text\nFind the failing validation path, propose a fix, and wait for my approval\nbefore editing files.\n```\n\nFor a new repository, start conservatively. Ask Codewhale to explore and plan\nbefore asking it to change files. That gives you a reviewable path and makes it\neasier to catch wrong assumptions early.\n\nNext: [ARCHITECTURE.md](ARCHITECTURE.md) explains the internal harness and\nruntime model.\n\n## 2. First Launch\n\nFor a new macOS or Linux installation, use the official GitHub release.\nThe installer verifies the release checksums and provides the same runtime\nunder the `codewhale` and `codew` command names:\n\n```bash\ncurl -fsSL https://codewhale.net/install.sh | sh\n```\n\nWindows users should choose the matching installer or archive from\n[GitHub Releases](https://github.com/codewhale-hq/CodeWhale/releases/latest).\nFor an existing direct install, use `codewhale update --check`, then\n`codewhale update`. npm and Cargo remain secondary packaging routes; Cargo\nalso supports source builds where a compatible prebuilt is unavailable.\nFor occupied directories, package-managed installs, and PATH setup, follow\n[the installation and migration guide](INSTALL.md#recommended-official-github-releases).\nAndroid/Termux uses its own [preview archive or source-build path](INSTALL.md#android--termux-arm64).\n\nDocker is also available when you want an isolated runtime. The published\nimage is currently 0.10.0; to test current `main`, [build it from source](DOCKER.md#building-locally).\n\n```bash\ndocker volume create codewhale-home\ndocker run --rm -it \\\n -e DEEPSEEK_API_KEY=\"$DEEPSEEK_API_KEY\" \\\n -v codewhale-home:/home/codewhale/.codewhale \\\n -v \"$PWD:/workspace\" \\\n -w /workspace \\\n ghcr.io/hmbown/codewhale:latest\n```\n\nOnce the install directory is on PATH, launch Codewhale from the repository or\ndirectory you want it to work in:\n\n```bash\ncodewhale\n```\n\nFor the default GitHub installer destination, you can use\n`\"$HOME/.local/bin/codewhale\"` until that directory is on PATH.\n\nOn first launch, Codewhale opens the composer, even with no key or route.\nWith no key or route, workspace trust is asked once a provider is connected;\notherwise only when the folder requires a decision. A provider is asked for\nwhen you send your first message: the message stays in the box while you sign\nin or use your own key, and a local model is offered only when a local Ollama\nserver answers. The provider step also has an explicit offline route.\n\nEverything optional stays available after that. Use `/setup` for the\nprogressive setup and repair guide, `/settings` for the full typed editor, and\n`/constitution` when you want your own constitution on top of the bundled one.\nThe localized telemetry choice appears only after the workspace is ready and\ndoes not block the composer.\n\nDeepSeek is the default provider. If you want to configure its key before or\nafter the first launch, the most direct setup path is:\n\n```bash\ncodewhale auth set --provider deepseek\n```\n\nYou can also provide a key through the environment:\n\n```bash\nexport DEEPSEEK_API_KEY=\"your-key\"\ncodewhale\n```\n\nNew Codewhale config is stored under `~/.codewhale/config.toml`. Legacy\n`~/.deepseek/config.toml` files are still supported for users migrating from\nthe old name.\n\nUse `/constitution` to review or change standing guidance. After setup, run a\ndoctor check:\n\n```bash\ncodewhale doctor\n```\n\nUse the JSON form when you need a machine-readable report for an issue:\n\n```bash\ncodewhale doctor --json\n```\n\nBoth forms are offline by default. They report structural configuration and\nliteral unknown/not-probed credential states without loading workspace `.env`\ncredentials, opening secret/OAuth files, probing a keyring, contacting a\nprovider, or starting MCP servers. Use `--check-updates`, `--probe-api`,\n`--probe-local`, or `--probe-mcp` only when you intentionally want that live\nboundary. JSON remains offline and does not accept live flags.\n\nJSON reports credential `source` separately from literal `availability`.\nConfigured environment, external-auth, OAuth, consent, and secret-store sources\nremain `not_probed`; their declaration alone does not make Setup or fleet ready.\nOnly a structurally present literal config value, or a route where credentials\nare not required, certifies offline readiness. A legacy secret-store sentinel on\na route that cannot use the shared store is reported separately as\n`secret_store_unavailable`/`unavailable`, not as eligible or merely unknown.\n\nBoth `doctor` and `doctor --json` also include a session-recovery diagnostic\nthat compares legacy session filenames against the current store and reports\none of `isolated`, `no_legacy_sessions`, `migration_pending`,\n`migration_incomplete`, `migration_complete`, or `scan_failed`; it never reads\nsession contents. Use `migration_pending` or `migration_incomplete` as your\ncue to finish moving sessions from `~/.deepseek` to `~/.codewhale`, the same\nlegacy-path migration described above. Setting an explicit `CODEWHALE_HOME`\nsuppresses this ambient inspection.\n\nNext: [INSTALL.md](INSTALL.md) covers platform-specific install paths,\n[CONFIGURATION.md](CONFIGURATION.md) covers config resolution, and\n[PROVIDERS.md](PROVIDERS.md) covers provider IDs and credentials.\n\n## 3. Your First Task\n\nStart with a read-only task in a real workspace:\n\n```text\nMap the repository structure and tell me where the CLI entrypoint lives.\n```\n\nThen ask for a focused plan:\n\n```text\nI want to add a small validation for empty config values. Inspect the relevant\ncode and propose the smallest safe change before editing anything.\n```\n\nWhen you are ready for edits, be specific about the acceptance criteria:\n\n```text\nImplement the validation you proposed. Keep the change scoped to config\nparsing, add or update the narrowest test, and run the relevant check.\n```\n\nGood first prompts include four details:\n\n- The outcome you want.\n- The files, feature, or behavior you care about.\n- What is out of scope.\n- What verification should count as done.\n\nFor example:\n\n```text\nFix the broken provider error message in the config loader. Do not change the\nprovider registry. Add a regression test and run only the config crate tests.\n```\n\nIf you are not sure where the bug is, say that:\n\n```text\nInvestigate why `codewhale doctor` reports the wrong provider. Do not edit\nfiles yet. Return the likely cause, evidence, and a proposed patch plan.\n```\n\nCodewhale works best when you let investigation and implementation happen in\nseparate steps for unfamiliar code. For small, well-understood changes, a\nsingle implementation request is fine.\n\nNext: [MODES.md](MODES.md) explains when to use Plan, Work, and Operate.\n\n## 4. Understanding the Interface\n\nThe interactive TUI has a few stable regions:\n\n- Header: current session, active model, mode, and high-level status.\n- Transcript: the conversation, tool calls, command output summaries, and\n model responses.\n- Composer: where you type prompts, slash commands, and file mentions.\n- Workbar: the strip under the composer (or an optional side workbar) that\n holds the active goal, the to-do list, and sub-agents. Rows stay for the\n whole session — finished work reads as done rather than disappearing — and\n clicking a row (or pressing `Enter` on it) opens its detail.\n- Status and footer areas: live activity, queued follow-ups, and short command\n hints.\n\nWhen the model asks a question (`request_user_input`), a bottom sheet opens\nover the transcript rather than a centered overlay. The conversation stays\nvisible above it. Use `PageUp`/`PageDown`, `Home`/`End`, or modified `↑`/`↓`\n(`Ctrl`, `Alt`, or `Shift`) to review the transcript while the sheet stays open.\nThe mouse wheel scrolls the transcript above the sheet and the question content\nover the sheet itself. Moving the highlight or typing brings that content back\ninto view after wheel browsing. Use `↑`/`↓` to move, `Enter` to confirm, `←`/`h`\nto go back to a previous question, and `Esc` to cancel the whole request.\nEvery question offers an \"Other\" row for a custom response; that text stays on\nscreen while you type. Keys for the sheet are in [KEYBINDINGS.md](KEYBINDINGS.md).\n\nThe bottom chrome is configurable. Run `/statusline` to choose what is\nvisible, or set `[tui].status_items` in `config.toml`. Each key owns exactly\none thing on screen: `mode` is the posture bar's plan/act/operate chip, and\n`model`, `context_percent`, `cost`, `balance` (prepaid providers only:\nDeepSeek, DeepSeekCN, OpenRouter, SiliconFlow), `cache`, `tokens` and\n`ttft`, `output_rate`, `workspace` and `git_branch` are segments of the metrics line below it. Omit\n`status_items` to keep the built-in default; set it to `[]` to strip the\nmetrics line down to the help hint.\n\n`workspace` and `git_branch` are opt-in. The workspace chip shows the folder\nname; linked worktrees include its parent to distinguish repeated names. The\nbranch chip shows the current branch or a short detached HEAD SHA, with `(wt)`\nfor linked worktrees. Both keep the last 24 display columns when long. Git\nmetadata refreshes in the background on the existing 15-second cadence and\nwhen a refresh is requested; unavailable Git data removes the branch chip.\nThese identify the active session workspace. The full path remains in `/status`.\n\n`context_percent` is on by default and shows `ctx NN%` at every fullness —\n0.9.12 went silent below 50% and left most of a session with no context\nsignal at all. The reading keeps its warning colour from 80% up.\n\nThe keys `status`, `agents`, `reasoning_replay`, `prefix_stability`,\n`last_tool_elapsed` and `rate_limit` were retired in 0.9.13:\nthey drove nothing. Old configuration files still load — the retired keys are\nignored with a warning in the log.\n\n`status_items` composes the rows; two size presets decide how much of each\nrow paints. `[tui].posture_bar` and `[tui].metrics_line` each take `full`,\n`compact`, or `hidden`. The posture bar defaults to `full` so active controls\nstay visible; the metrics line defaults to `compact` to keep selected performance\nreadings while removing secondary counts and help. These are also settable at runtime with\n`/config posture_bar compact`. TOML values must be lowercase; `/config`\naccepts either case. `compact` is the row after its first shed\nrungs: the posture bar keeps its permission and mode chips — and the cap\nwarning, which is advice, not decoration — and drops the clocks, counts and\nhint; the metrics line keeps the route, the context reading, the cost and\nthe balance, plus selected TTFT and output rate when space allows, and drops\nsecondary counts and the help hint. `hidden` gives the\nrow back to the transcript. A small tmux pane can hide both rows without\ntouching what `/statusline` composes.\n\nBoth `ttft` and `output_rate` are on by default and work in full or compact\nrows. `/statusline` lets you toggle them separately; Space previews, Enter saves,\nand Esc restores your previous settings. Legacy `session_metrics` still enables\nboth readings. The pair shows: `ttft 1.5s` — the mean time to first streamed token — and `120 avg tok/s`,\nthe session's provider-reported output tokens divided by the measured request\nseconds for those same calls. The rate includes connection setup, time to first\ntoken and pauses within a response, and excludes tools and idle time between\ncalls. It measures effective request throughput, not decoder speed. Streaming\nand non-streaming calls follow the same rule; receipts without individual\nrequest timing are excluded from both tokens and time. While a request runs,\nthe last measured average stays visible. Both readings use the same\naccumulators `/status` prints in full. Missing evidence is omitted rather than\nestimated. On narrow rows the pair sheds before cost and context.\n\nEvery file read, command, and edit appears in the transcript as it happens.\n`/receipts` lists what the session did, one line per action; see\n[What Codewhale records](#what-codewhale-records). If a command fails, use the\nvisible failure output as part of your next instruction instead of starting\nover.\n\nThe composer accepts normal prompts and slash commands. Type `/` to discover\navailable commands. Use file mentions when you want the model to focus on a\nspecific file or directory instead of searching broadly.\n\nThe workbar is useful when a turn spans multiple steps. It keeps the goal,\nthe to-do list, and agent state visible while the transcript continues to\ngrow — including after the work settles, so you can still open what happened.\n\nKeyboard shortcuts vary by context, terminal, and platform. This guide avoids\nduplicating the full shortcut catalog so it does not drift from the TUI.\n\nNext: [KEYBINDINGS.md](KEYBINDINGS.md) is the complete shortcut reference.\n\n## 5. Modes\n\nCodewhale has three visible TUI modes:\n\n| Mode | Use it for | Default posture |\n| --- | --- | --- |\n| Plan | Exploration, design, and review before changes | Read-only investigation |\n| Work | Normal multi-step coding work | Tool use with approval gates |\n| Operate | Direct work plus parallel or background coordination | Tools follow the active posture; delegate when useful |\n\nSwitch modes from the TUI with the mode picker:\n\n```text\n/mode\n```\n\nOr switch directly:\n\n```text\n/mode plan\n/mode work\n/mode operate\n```\n\nPlan mode is the safest place to start in an unfamiliar repository. It is for\ninspection and decision-making, not file edits.\nWhen a Plan turn successfully finishes a nonempty response, Codewhale asks\nhow to continue, including when the plan is plain text. **Work (Ask)** and\n**Work (Auto-Review)** switch to Work with that permission and send the exact\napproved response as the next message. Existing To-dos from that turn are\npreserved; a prose-only plan adds one pending item without replacing unrelated\nwork. If Codewhale cannot prepare the work state, it stays in Plan without\nstarting the task. **Keep planning**, or `Esc`, stays in Plan. You can also\ntype feedback there to have the plan revised. The question appears only when\nthe composer is empty and no other view is open; `/mode work` always works.\n\nWork mode is the default for most contribution work. It lets Codewhale read,\nrun checks, and edit files while keeping risky actions behind approval gates.\n\nOperate keeps that direct tool surface and its approval, sandbox, shell,\nask-rule, and repository protections. Small or tightly coupled work stays\ndirect. Multi-step delegation uses a compact Workflow plan with dependencies,\nbounded scopes, and completion evidence passed between steps. Fleet configures\nand manages those same sub-agents and their roles. One bounded, independent\ntask can use a direct agent; continued work reuses it through `followup`.\n`codewhale dispatch \"<task>\"` runs work on a Codewhale cloud computer when\nyour account has access. Cloud availability and admission come from the account\nservice.\n\nLocal work needs no Codewhale account. `/login` opens account sign-in inside\nthe TUI, with a browser link and code, Copy, and Cancel. `/login status` shows\naccount status; `/connect` connects a model provider or local model. Signing in\npreserves your selected model and unsent message. An account connection is\nseparate from usable model access.\n\n`/logout`, `codewhale logout` and `codewhale account logout` sign out of\nCodewhale while keeping local model connections and conversations. To remove\nall saved provider credentials as well, use `codewhale logout --all`; that\noperation asks for confirmation (`--all --yes` confirms non-interactively).\n\nFor trusted workspaces where you intentionally want actions to proceed without\napproval prompts, select the Full Access permission posture with `Shift+Tab`.\nDo not use Full Access in a repository you do not trust.\n\nModes are separate from model routing. `Tab` cycles visible modes when the\ncomposer is idle, while `/model auto` controls model and thinking selection for\nturns.\n\nYou can also change approval behavior from `/config` by editing the approval\nmode. Use this only when you understand how it changes tool execution.\n\nNext: [MODES.md](MODES.md) has the full mode, approval, and trust-mode\nreference.\n\n## 6. Slash Commands\n\nSlash commands are typed into the composer. They are useful when you want to\nchange Codewhale state directly instead of asking the model in natural\nlanguage.\n\nCommon commands for first-time users:\n\n| Command | Use |\n| --- | --- |\n| `/mode` | Open the mode picker or switch with `/mode agent` |\n| `/model` | Select a model or use `/model auto` |\n| `/provider` | Pick the active API provider |\n| `/fleet` | Open the selected fleet's member roster |\n| `/fleet saved` | Pick or switch among named saved fleets |\n| `/goal` | Set a persistent objective the agent works toward across turns; bare `/goal` shows progress |\n| `/workflow` | Orchestrate the current work as a Workflow; `status`, `cancel`, `settings` answer without a model turn |\n| `/workflows` | Open the live Workflow run dashboard: every run this workspace's journal keeps, with phases, children, progress, and host-side cancel |\n| `/config` | Edit runtime and provider settings |\n| `/statusline` | Choose which footer status chips are visible |\n| `/color` | Give this chat's composer box its own color: bare `/color` cycles red, orange, amber, green, cyan, blue, purple and pink, `/color blue` picks one, `/color off` returns to the theme |\n| `/receipts` | List what this session did: files changed, commands run, web and MCP calls, agents, approvals and who gave them, failures |\n| `/compact` | Summarize long context to recover token budget |\n| `/copy` | Copy the last completed assistant response to the clipboard |\n| `/export` | Copy the conversation, or save it as Markdown, HTML or JSON; see [Exporting a conversation](#exporting-a-conversation) |\n| `/review` | Ask for a structured review workflow |\n| `/memory` | Inspect or manage memory when enabled |\n| `/mcp` | Configure or inspect MCP server integration |\n| `/plugin` | Review and manage disabled-by-default local plugin bundles |\n| `/rc` | Hand this exact session to the signed-in Codewhale web app |\n\nToolbox commands stay searchable when you type them directly: `/models`\nfetches live endpoint IDs, `/modeldb` opens the bundled model reference, and\n`/rlm` loads a file or block of text into a working context that stays\navailable for the rest of the session.\n\nUse `/provider` when you want to switch away from the default DeepSeek route.\nProvider IDs, environment variables, model defaults, and capability notes are\nkept in the provider registry document.\n\nSoft-auto multi-agent work: [AUTOMATIC_WORKFLOWS.md](AUTOMATIC_WORKFLOWS.md).\n\nPosting Codewhale PR reviews as a bot identity:\n[GITHUB_APP.md](GITHUB_APP.md).\n\nNext for durable multi-worker work: [FLEET_WORKFLOW_TUTORIAL.md](FLEET_WORKFLOW_TUTORIAL.md)\nwalks through fleet task specs, monitoring, and Workflow authoring.\n\nFleet is the public noun for the durable roster. `codewhale fleet …` is\nthe command and `/fleet` the slash command. The Fleet name is\nshared by what has to stay stable across versions: the durable ledger\n`.codewhale/fleet.jsonl`, saved rosters `fleets/<name>.toml`, the `[fleet]`\nconfig table, and the `codewhale workflow run --fleet` flag.\n\nUse `/model auto` when you want Codewhale to choose the model and thinking\nlevel per turn. When the DeepSeek routing model is available, Auto may select\nany runnable provider/model pair in the redacted inventory. That classification\nsends the latest request (capped at 4,000 characters) plus a bounded summary of\nup to six recent context rows (900 characters each) to\n`DeepSeek / deepseek-flash`. Credentials, endpoints, and provider error text\nare not included in the inventory. Without that router, Auto uses a local,\nprovider-aware heuristic and sends no routing request. If a classifier attempt\nfails validation or errors, Auto falls back to that heuristic while retaining\nthe attempted classifier data path in the turn receipt.\n\nThe `/model` picker states which data path is available and shows the last\nresolved route. `Ctrl+O` opens the reasoning detail for the selected or current\nturn; `Ctrl+Alt+O` (or `/turn inspect`) opens the whole-turn Turn Inspector,\nwhose model-route section records the concrete provider/model, strong/fast pair,\nselected tier, selection scope, route reason, and whether the classifier received\nrouting context. Use a\nfixed model when you need repeatable comparisons, a strict provider boundary,\nor no classification request.\n\nUse `/compact` when a session gets long and the model starts carrying too much\nhistory. Compaction trades raw transcript detail for a concise working summary.\n\n### Exporting a conversation\n\n`/export` copies a redacted transcript to the clipboard. `/export file` saves\nit instead, and the file format follows the extension:\n\n```text\n/export file auto-named Markdown under the Codewhale exports folder\n/export file notes.html a self-contained HTML page (light and dark, no scripts)\n/export file notes.json structured JSON with typed message blocks\n/export file notes.md --no-tools Markdown without tool calls and results\n/export file notes.md --force replace an existing file\n/export turn the latest turn as a handoff note\n```\n\n`--format md|html|json` overrides the extension, and `--format html` or\n`--format json` on its own writes an auto-named file. HTML and JSON are\nfile-only because a clipboard paste would lose their structure. Auto-named\nfiles are `codewhale-<title>-<timestamp>.<ext>` in `<codewhale home>/exports`.\n`~` is expanded, quotes keep a path with spaces together, and an existing file\nis never replaced without `--force`.\n\nEvery format omits the system prompt and internal reasoning, redacts\nsecret-like values and credential-bearing URLs, and fences tool output so it\ncannot forge a heading or a message. `/share` publishes the same HTML page as a\nsecret gist after a confirmation.\n\nThis guide intentionally does not list every command. The command surface\nchanges more often than the onboarding flow, and the TUI command palette is the\nsource of truth while you are inside a session.\n\nNext: [CONFIGURATION.md](CONFIGURATION.md) covers runtime settings and\n[MCP.md](MCP.md) covers Model Context Protocol integration.\n[PLUGIN_BUNDLES.md](PLUGIN_BUNDLES.md) covers the disabled-by-default bundle\ninventory, capability review, and namespaced Skill/MCP activation boundary.\n\n## 7. Working with Tools\n\nCodewhale tools are structured actions. Instead of only producing prose, the\nmodel can call tools to inspect and change the workspace.\n\nExamples of tool-backed work include:\n\n- Reading a file before explaining it.\n- Searching for call sites before proposing a refactor.\n- Running a focused test command.\n- Applying a small patch.\n- Opening a sub-agent for parallel investigation.\n\nTool use is governed by mode, approvals, and sandbox policy. The exact behavior\ndepends on the current mode and config, but the basic rule is simple: start in\nPlan for read-only exploration, use Work for normal changes, and reserve Full\nAccess for trusted automation.\n\nThe workspace boundary matters. Codewhale is expected to work in the directory\nyou launched it from or the workspace you configured. Be explicit when a task\nshould stay inside a repo:\n\n```text\nOnly inspect and edit files under this repository. Do not touch parent\ndirectories or global config.\n```\n\nWhen a command needs network, writes outside the workspace, or a risky shell\noperation, expect an approval prompt unless you have configured more permissive\nbehavior.\n\nGood tool instructions are concrete:\n\n```text\nRun the narrowest test that covers this parser change. If it fails, report the\nfailure and stop before broadening the test scope.\n```\n\nAvoid asking for broad cleanup during a focused fix. Smaller tool scopes make\nthe transcript easier to review and the final diff easier to merge.\n\nNext: [TOOL_SURFACE.md](TOOL_SURFACE.md) lists the tool surface and\n[SANDBOX.md](SANDBOX.md) explains sandbox behavior.\n\n### What Codewhale records\n\nCodewhale keeps these records on your machine, under `~/.codewhale/`:\n\n- **The session.** `sessions/<id>.json` holds the full conversation,\n including every tool call and its result text. App and `codewhale serve`\n threads keep each call as a turn item under `tasks/runtime/`, with its\n input, status, start and end time, and structured result.\n- **Approvals.** `sessions/<id>/approval_receipts.jsonl` records every\n approval Codewhale asked for, the decision, and who made it: you, a\n session rule, or the active posture. App threads also record each decision\n in their event log. A call that ran without asking (Full Access, an allow\n rule, a remembered grant) has no approval record; the posture each turn\n ran under is saved with the turn.\n- **Undo points.** `/undo` takes back your last request and its proven file\n changes. `/undo select` lets you choose the first request to undo along with\n all later requests, with visible checking, restoring and saving progress.\n Neither command changes your access mode. File reversal requires the\n request's own recorded tool executions, complete restore points and files\n that still match what those executions left. Missing or incomplete records,\n including older sessions or turns run with snapshots disabled, cause a\n refusal that preserves the files and conversation. `/undo force` cannot\n bypass these checks. A request with no file-writing work can rewind only its\n conversation. Esc Esc also rewinds the conversation only. `/restore` selects\n an explicit workspace snapshot and can overwrite later edits; it requires\n `/trust on` or Full Access.\n- **Security events.** `audit.log` records credential changes, hook\n environment key names, compaction passes, the terminal's approval\n routing, and Auto-Review verdicts. It is not a list of what a session did.\n\nTo see what a session did, run `/receipts`, or from a shell:\n\n```bash\ncodewhale receipts --last\ncodewhale receipts <session-id> --format json\n```\n\nA receipt says what it cannot show. A terminal session's receipt lists the\nfiles a command changed from each turn's workspace snapshots; a Runtime\nthread's lists only file tools. Terminal sessions do not save a passing\ncommand's exit code or how long each call took. A call Codewhale blocked\nbefore it started (Auto-Review, a policy, invalid input) is listed as\nblocked, with the reason, and is not counted as run. [RECEIPTS.md](RECEIPTS.md) has the full contract.\n\n## 8. Sub-agents and Parallel Work\n\nSub-agents are background child agents. The parent session gives a child a\nfocused task, receives an agent id, and can continue working while the child\nruns.\n\nThe main orchestration tool is:\n\n- `agent`: start a focused child with a task and role. The child runs in the\n background and returns a compact receipt plus transcript handle.\n\nYou normally do not need to call these tools directly. Ask for parallel work in\nplain language:\n\n```text\nOpen one read-only explorer for the config crate and another for the TUI\nprovider picker. Have both return file references and risks before we plan the\nfix.\n```\n\nUseful roles include:\n\n| Role | Good for |\n| --- | --- |\n| `general` | Multi-step tasks; the default when no role is specified |\n| `explore` | Read-only code mapping |\n| `plan` | Design and migration planning |\n| `review` | Bug-focused review of an existing change |\n| `implementer` | A tightly specified edit |\n| `verifier` | Running checks and reporting pass/fail evidence |\n\nSub-agents are most useful when work can be separated cleanly. Do not use them\nfor tiny edits, and do not ask multiple agents to write the same files at the\nsame time.\n\n### How long work stays coherent\n\nWork that spans many turns does not rely on an ever-growing chat transcript.\nThis is ordinary Agent behavior — there is nothing to turn on and no separate\nworkflow to learn:\n\n- A working context stays loaded for the session. Large source material and the\n durable transcript are held as data the agent can search and slice, and useful\n variables and imports survive across turns.\n- Workflow composes independent `task(...)` calls and parallel fan-out.\n- `agent` messages and follow-ups coordinate active children directly.\n- Goals retain the durable objective across the work.\n\n`/rlm <file-or-text>` points that working context at a specific file or block\nof text. The historic action-shaped `rlm` tool remains registered only so older\nsessions replay, and is deliberately not taught to new model turns.\n\nCodewhale can also keep a small project-local ledger at\n`.codewhale/harness/state.json`: evidence-backed prompt notes, reusable child\nbriefs, and skill-routing hints. Later turns receive it as untrusted\nsupplemental guidance, never as authority or executable instructions. Reading it\nis automatic; adding or removing an entry goes through the normal approval\nreceipt. It is separate from personal memory, and it must never hold secrets,\nscratch transcripts, or unverified claims.\n\nNext: [SUBAGENTS.md](SUBAGENTS.md) covers roles, lifecycle, concurrency, and\noutput contracts.\n\n## 9. Skills\n\nSkills are reusable instruction packs. A skill is usually a `SKILL.md` file\nthat teaches Codewhale how to perform a recurring workflow, use a tool family,\nor follow a project convention.\n\nUse skills when a task has a repeatable process:\n\n- Reviewing a specific kind of PR.\n- Working with a document or spreadsheet format.\n- Following a team release checklist.\n- Using a project-specific memory or wiki workflow.\n\nInside the TUI, `/skill <name>` activates a skill when one is available, and\nbare `/skills` opens Extensions → Skills (owned-only inventory, no network).\nUse `/skills manage` for the manager and\n`/skills <prefix>`, `/skills inspect`, `/skills --remote`, `/skills suggest <task>`,\nor `/skills sync` for the text/registry paths. Suggestions rank the remote\ncatalog but never install or activate anything. The command palette can also\nsurface skill entries alongside normal slash commands.\n\nGood skills are narrow. They should tell the model what workflow to follow,\nwhat evidence to collect, and what to avoid. They should not hide credentials\nor replace normal repository documentation.\n\nIf a repository has its own instructions, treat them as part of the active\nwork. Read the local guidance before editing, and keep any contribution within\nthe repository's conventions.\n\nNext: see [SKILLS.md](SKILLS.md) for the manager, ownership, and provenance\nrules; [CLAUDE_PLUGIN_COMPAT.md](CLAUDE_PLUGIN_COMPAT.md) for Claude Code\nskill/plugin compatibility; and [CONFIGURATION.md](CONFIGURATION.md) for config\npaths and project authority.\n\n## 10. Getting Help\n\nStart with doctor output:\n\n```bash\ncodewhale doctor\n```\n\nUse JSON when filing a detailed issue:\n\n```bash\ncodewhale doctor --json\n```\n\nFor authentication problems, use the structural source state to identify what\nis declared. Doctor deliberately does not inspect environment, secret-store,\nkeyring, or OAuth token values. When a live check is appropriate, opt in with\n`codewhale doctor --probe-api` (or `--probe-local` for a local endpoint).\n\nFor provider problems, confirm the active provider and model:\n\n```text\n/provider\n/model\n```\n\nFor long or confusing sessions, use `/compact` to reduce context pressure, or\nstart a fresh session in the same workspace and summarize what you need.\n\nWhen reporting an issue, include:\n\n- Codewhale version.\n- Install method.\n- Operating system and terminal.\n- Provider and model.\n- The exact command or prompt.\n- Relevant doctor output.\n- Whether the problem happens in a fresh workspace.\n\nDo not paste API keys, private source code, or secrets into a public issue.\n\nNext: [OPERATIONS_RUNBOOK.md](OPERATIONS_RUNBOOK.md) has operational triage and\nrecovery steps.\n\n## FAQ\n\n### Is Codewhale only for DeepSeek?\n\nDeepSeek is the default and first-class route, but Codewhale also supports\nother hosted and local OpenAI-compatible providers. Use `/provider` or\n`codewhale --provider <id>` to choose a provider. Keep the provider registry\nopen when configuring a non-default route.\n\n### Which mode should I use first?\n\nUse Plan for unfamiliar code, Work for normal implementation, and Full Access\nonly for trusted repositories where automatic execution is acceptable.\n\n### Why does Codewhale ask before running commands?\n\nApprovals are part of the safety model. Shell commands, paid tools, writes, and\nactions outside the expected workspace can have side effects. Approval prompts\nlet you keep control while still letting the model do useful work.\n\n### How do I run a Python file on macOS?\n\nOpen Terminal in the folder that contains the file and run:\n\n```bash\npython3 your_file.py\n```\n\nIf macOS says `python3` is missing, install Python from\n[python.org](https://www.python.org/downloads/macos/) or with Homebrew:\n\n```bash\nbrew install python\n```\n\nInside Codewhale, ask the agent to inspect the file and run it with\n`python3 your_file.py`. If the script needs packages, install them in a virtual\nenvironment first:\n\n```bash\npython3 -m venv .venv\nsource .venv/bin/activate\npython3 -m pip install -r requirements.txt\npython3 your_file.py\n```\n\n### Where is my config stored?\n\nNew Codewhale config uses `~/.codewhale/config.toml`. Legacy\n`~/.deepseek/config.toml` remains supported for compatibility. Project overlays\ncan also affect behavior when a workspace config exists.\n\n### How do I keep costs predictable?\n\nUse `/model auto` for routing, choose a fixed model when you need a strict\nprofile, and compact long sessions. For larger tasks, ask Codewhale to plan\nbefore implementing so you do not spend tokens on the wrong path.\n\n### How do I continue previous work?\n\nCodewhale saves sessions. Use the session picker or resume/continue CLI paths\ndocumented in the README and modes guide. For a risky experiment, fork the\nsession before changing direction.\n\nThe `/sessions` picker starts scoped to the current workspace so resumes stay\nattached to the project you opened. Press `a` in the picker to show sessions\nfrom every workspace, or run `codewhale sessions` to list all saved sessions\nwith last-updated timestamps before resuming a specific id.\n\nTo archive the durable record and its artifacts, run:\n\n```sh\ncodewhale sessions export <id-or-unique-prefix> --output session.tar.xz\n```\n\nThe archive contains `session.json`, a portable `container.json`, a manifest,\nand regular files under `artifacts/`. Use `--skip-artifacts` for the record\nonly, `--compression 0` through `9` to choose the xz preset (default `6`),\nand `--force` to replace an existing output. Store the archive outside the\nsession store. Symlinks are skipped; linked artifact roots, hard links,\nnonportable filenames, and trees exceeding 64 directory levels or 100,000\nentries fail the export without replacing the destination.\n\nUnlike the sanitized `/export`, these archives retain unredacted\nsession content, including system prompts, thinking, tool calls and results,\njournal branches, and approval receipts. Extract `session.json` and open it\nwith `/load` in the TUI; `/resume` imports the conversation only. Extracted\nartifacts remain separate files and are not installed into the artifact store\nby `/load`. Pause writes before archiving if every artifact must reflect the\nsame instant; growing files are bounded to their recorded size and shrinking\nfiles abort the export.\n\nTo continue the exact running session from the web app, type `/rc` or launch\nwith `codewhale rc`. Approve the one-time code in the system browser. While the\nlease is active, the browser owns new prompts and approvals and the terminal is\na readable safety surface. Once connected, the banner and a transcript note\nshow the live session link (`https://app.codewhale.net/session?run=…`);\n`/rc open` opens it in your browser and `/rc link` prints it. `/rc status`\nshows ownership, `/rc stop` returns it to the terminal, and interrupt remains\navailable. A dropped connection keeps local input locked until the last web\nlease expires so two controllers never race. Every folder you enroll from one\nterminal shares a single stable device id, so the web app lists one computer\nper machine rather than one per session.\n\nIf remote control fails, the reason remains in the current terminal transcript\nand TUI log after the error toast expires. The footer then shows `/rc status`;\nrun it to inspect the failure and whether retrying must wait for a prior lease.\n\n> Note (2026-09-14): the hosted web app at app.codewhale.net sunsets in phases\n> under the 2026-09-14 product-client decision; the native GPUI desktop app\n> (private `codehwhale-gpui` repo, phase map in `docs/TRANSITION.md`) is the\n> successor surface. `/rc` keeps working against the web app while it remains\n> live.\n\n### What should I do when the model gets confused?\n\nStop and restate the goal, constraints, and current evidence. If the transcript\nis long, use `/compact` or start a fresh session with a short handoff. If the\nproblem is operational, run `codewhale doctor` and inspect the reported config\nand provider state.\n\n### Should I put project rules in prompts or files?\n\nUse repository files for durable project rules and prompts for turn-specific\nintent. If a workflow repeats across projects, consider turning it into a\nskill.\n\n### Can Codewhale edit files outside the current repository?\n\nThat depends on workspace boundaries, sandbox settings, trust mode, and\napproval policy. For contribution work, keep instructions scoped to the current\nrepository unless you intentionally need something else.\n\n### Where should I go after this guide?\n\nRead the focused reference for the thing you are changing. For most users, the\nnext pages are install, configuration, providers, modes, keybindings, tools,\nand sub-agents.\n\nNext: [INSTALL.md](INSTALL.md), [CONFIGURATION.md](CONFIGURATION.md),\n[PROVIDERS.md](PROVIDERS.md), [MODES.md](MODES.md), and\n[TOOL_SURFACE.md](TOOL_SURFACE.md).\n\n## Live Terminal dock\n\nOpen `/workbar terminal` to watch the same workspace PTY sessions the model\nuses. Click the dock or press `Alt+W` to give it input focus. While focused,\ntyping, Enter, Tab and Ctrl+C reach the selected shell. Esc returns focus to\nthe composer; the output remains visible. Click a session or use `Alt+Down` /\n`Alt+Up` to switch. `Ctrl+N` starts your own fresh shell without replacing the\nmodel’s session. Mouse-wheel and PageUp/PageDown review the retained output.\nThe active permission posture and filesystem scope govern fresh shells and\nrefuse reuse of an unsandboxed shell after the scope narrows.\n\nThe dock shows the latest 64 KiB, reports omitted bytes, and never consumes the\nmodel’s output cursor. A reset or lost session requires explicit selection;\ninput cannot silently follow a reused session name. PTYs are currently supported\non macOS and Linux. This is an ANSI-styled output view: cursor-addressed programs,\nalternate screens and replay after process restart are outside its scope. Paste\nis direct terminal input and may execute commands containing newlines; control\ncharacters and pastes over 64 KiB are refused. While a process uses canonical\nline input, batches of 1 KiB or more are refused before any bytes are sent:\nthe terminal can discard oversized input. Transfer long content through a file.\n"
|
||
},
|
||
{
|
||
"path": "docs/KEYBINDINGS.md",
|
||
"sha256": "72a081c68ffba992e80c288141b1b82f5e5a767145f60cdabc71f2f92fba48e9",
|
||
"content": "# Keybindings\n\n> 阅读简体中文版:[zh_hans/KEYBINDINGS.md](zh_hans/KEYBINDINGS.md)\n\nThis is the source-of-truth catalog of every keyboard shortcut the TUI recognizes. Bindings are grouped by **context** — the focus or modal state they fire in. A binding listed under \"Composer\" only takes effect when the composer is focused; one under \"Transcript\" only when the transcript has focus; and so on.\n\nGlobal key chords are not yet user-configurable — tracked for a future release (#436, #437). Hotbar slot actions are configurable with `[[hotbar]]` and `/hotbar`; the Hotbar activation chord remains `Alt-1` through `Alt-8`.\n\n## Global (any context)\n\n| Chord | Action |\n|----------------------|---------------------------------------------------------------|\n| `/help`, `F1` or `Ctrl-/` | Toggle the help overlay. `/help` is what the chrome advertises: it is the one route every terminal delivers, where `F1` is eaten by tmux and some emulators and `Ctrl-/` encoding varies |\n| `F2` | Toggle the typed Settings editor |\n| `F3` | Open the provider/model picker (same as `/provider`) |\n| `Ctrl-K` | Open the command palette (slash-command finder) |\n| `Ctrl-C` | Cancel current turn / dismiss modal / clear a non-empty draft (recoverable — see `Ctrl-Z`) / arm-then-confirm quit |\n| `Ctrl-B` | Release the current shell wait so the turn can continue; foreground commands move into `/jobs`, and background tasks keep running. Use `/jobs` or `Bash` with `action: \"wait\"` to inspect output |\n| `Ctrl-D` | Quit (only when the composer is empty) |\n| `Tab` | When the composer is empty, cycle TUI mode: Plan → Work → Operate → Plan. Works while a turn is running: the change applies at the agent's next step in the same turn, and the status line says so until it does |\n| `Shift+Tab` | Cycle permission posture: Ask → Auto-Review → Full Access. Live regardless of composer contents or whether a turn is running (suppressed only while a modal other than Config is open). During a turn the change applies at the agent's next step; a call you already approved runs only if the new posture is equal or broader. Model, thinking level and provider stay locked until the turn ends |\n| `Ctrl-T` | Cycle reasoning effort for the active model. Walks the same ladder as `/model` and `/effort` (catalog or documented route dialect). Always-thinking models omit `off`; Grok 4.6 includes `xhigh`. |\n| `Ctrl-Shift-T` | Toggle live transcript overlay (sticky-tail auto-scroll) |\n| `Ctrl-R` | Open the resume-session picker |\n| `Ctrl-L` | Compact the conversation context (status line shows progress; no-op while a compaction is already running) |\n| `Ctrl-O` | Open the reasoning detail for the selected or current turn, regardless of composer contents |\n| `Ctrl-Alt-O` | Open the whole-turn Turn Inspector, regardless of composer contents |\n| `Alt-V` / `Option-V` (macOS) | Open the details pager for the selected tool/sub-agent card; with nothing selected, the newest visible error, else the first visible card with details, else the most recent one. Click a card first to keep Alt-V on it while newer cards arrive. Terminals that emit the legacy Option-V glyph are also handled |\n| `Ctrl-Shift-E` / `Cmd-Shift-E` | Toggle the file tree in the workbar |\n| `Ctrl-]` or `Ctrl-Tab` | Cycle the pet view while the pet main view is showing; otherwise cycle the work dock: TODO → Agents → Jobs → Background. `Ctrl-]` is portable; `Ctrl-Tab` requires the kitty keyboard protocol. Terminal.app uses it to switch terminal tabs |\n| `Ctrl-Shift-Tab` | Cycle the pet view or work dock backwards where the terminal sends the kitty keyboard chord. Terminal.app uses it to switch terminal tabs |\n| `Alt-G` / `Alt-Shift-G` | Scroll transcript to top / bottom when the composer is empty |\n| `Alt-1`-`Alt-8` | Dispatch Hotbar slots 1-8 when no modal or inline picker is open |\n| `Alt-!` / `Alt-@` / `Alt-#` / `Alt-$` | Select the workbar panel: Tasks / Agents / Context / Files |\n| `Ctrl-Alt-0` | Toggle the workbar off / back to the bottom placement |\n| `Alt-L` | Open the pager for the last message (composer empty) |\n| `Alt-P` / `Alt-A` / `Alt-Y` | Jump to Plan / Work, or request Full Access (`Alt-Y` is the legacy permission channel — Work + Full Access — not a separate mode; it honors a locked approval policy) |\n| `Ctrl-X` (Activity workbar) | Cancel all running background shell jobs |\n| `Esc` | Close topmost modal · cancel slash menu · dismiss toast. During a compact that is serving an in-flight turn, Esc stops the turn (the compact is collateral). |\n\n## Composer\n\nEditing the message you're about to send.\n\n| Chord | Action |\n|-----------------------------|---------------------------------------------------------|\n| `Enter` | Send when idle; queue while busy; with an empty composer, send the next queued follow-up now |\n| `Shift-Enter` / `Alt-Enter` / `Ctrl-J` | Insert a newline without sending (idle or busy) |\n| `Ctrl-Enter` / `Cmd-Enter` | Steer: send into the current turn, closing the in-flight model request at once (reasoning, text or tool call) and continuing the same turn with your message; send normally when idle (when the terminal reports it distinctly). Text already shown is kept; unfinished reasoning and partial tool calls are discarded, never run. A second `Enter` within 500 ms of queueing a message while busy steers too |\n| `Ctrl-U` | Clear the whole draft (recoverable — see `Ctrl-Z`) |\n| `Ctrl-Z` | Restore the cleared draft (only while the composer is empty) |\n| `Ctrl-C` | Clear the whole draft when no turn is running (recoverable — see `Ctrl-Z`) |\n| `Ctrl-W` / `Ctrl-Backspace` / `Alt-Backspace` | Delete previous word |\n| `Ctrl-A` / `Home` | Move to start of input / start of line (readline convention) |\n| `Ctrl-E` / `End` | Move to end of input / end of line |\n| `Ctrl-←` / `Alt-←` | Move backward one word |\n| `Ctrl-→` / `Alt-→` | Move forward one word |\n| `Shift-←` / `Shift-→` | Extend the selection one grapheme at a time |\n| `Ctrl-Shift-←/→` / `Alt-Shift-←/→` | Extend the selection one word at a time |\n| `Shift-Home` / `Shift-End` | Extend the selection to the start / end of the line |\n| `Ctrl-Shift-Home` / `Ctrl-Shift-End` | Extend the selection to the start / end of the draft |\n| `Ctrl-Shift-A` / `Cmd-A` | Select the whole draft (see note below) |\n| `Ctrl-Shift-U` | Run `/update install` from the keyboard: check for and install the latest Codewhale release without leaving the TUI. Managed installs (Homebrew/npm/cargo) keep their package-manager gate; when already current the updater's \"Already up to date.\" result is shown and nothing changes |\n| Mouse drag | Select composer text; click moves the cursor |\n| `Cmd-V` / `Ctrl-Shift-V` | Terminal-local paste (arrives as bracketed paste when supported) |\n| `Ctrl-V` | Direct graphical clipboard paste; rich composer content becomes Markdown |\n| `Ctrl-Y` | Yank (paste) from kill buffer |\n| `Ctrl-X` | Cut the selection. The text is deleted only when a native clipboard confirmed the copy; over SSH or tmux, where only the terminal clipboard (OSC 52) takes it and never confirms, the text is copied and kept |\n| `↑` / `↓` | Cycle composer history (also selects popup/attachment items) |\n| `Shift-↑` / `Shift-↓` | Browse conversation history |\n| `Ctrl-P` / `Ctrl-N` | Navigate slash-command menu entries; `Ctrl-P` opens the file picker when the menu is empty |\n| `Ctrl-G` / `Ctrl-S` | Stash current draft (`/stash pop` restores it); never sends or queues |\n| `Alt-R` | Search prompt history (Alt-R to exit) |\n| `Tab` | Slash-command / `@`-mention completion (popup-aware) |\n| `Ctrl-Shift-O` / `F4` | Open the composer draft in `$VISUAL` / `$EDITOR`; F4 works when the terminal cannot distinguish Ctrl-Shift-O from Ctrl-O |\n| `! command` | Run a shell command through normal approval, sandbox, and output surfaces |\n\nDirect composer paste preserves headings, lists, links, tables and code from\nHTML clipboard content. Plain text stays literal, and configuration fields\nalways paste literal text. SSH without a forwarded graphical display uses the\nterminal's text paste. Copying a whole answer preserves its original Markdown,\nwithout terminal wrapping or decorative rails; the destination app decides how\nto render it. Transcript drag selections copy the same way by default:\nMarkdown source, whole cells joined with blank lines (see the Transcript\nsection below), with `tui.selection_copy_markdown = false` restoring rendered\ntext. Empty or oversized HTML falls back to plain text.\n\nSet `composer_multiline_mode = true` for `Enter` to insert a newline and\n`Alt-Enter` to send. `Shift-Enter` also sends when the terminal reports it\ndistinctly; legacy terminals may report it as plain `Enter`. `Ctrl-J` always\ninserts a newline. Supported `Ctrl-Enter` / `Cmd-Enter` behavior stays unchanged.\nWith multiline mode off, `Alt-Enter` and supported `Shift-Enter` insert newlines.\n\n### Selection semantics\n\nTyping, pasting, `Backspace`, or `Delete` with an active selection replaces or\nremoves the selected text, like any GUI editor. Plain movement keys (arrows,\n`Home`/`End`, word motions) collapse the selection. When a selection covers the\nwhole draft, deleting or typing over it stashes the outgoing text the same way\n`Ctrl-U` does, so `Ctrl-Z` (on an empty composer) or `Alt-R` draft recovery can\nbring it back.\n\nCursor movement and deletion are grapheme-aware: one `←`/`→` step or one\n`Backspace` covers a full emoji ZWJ sequence, flag pair, or combining-mark\ncluster — never half of one. CJK text moves and deletes per character as\nexpected.\n\n**Why select-all is not `Ctrl-A`:** the composer follows the readline\nconvention, where `Ctrl-A` jumps to the start of the input (paired with\n`Ctrl-E`). Select-all is `Ctrl-Shift-A` on every platform (like\n`Ctrl-Shift-O` / `Ctrl-Shift-E`, it needs a terminal with an enhanced keyboard\nprotocol). On macOS terminals that forward the Command key (kitty, WezTerm,\niTerm2 with Command remapping), native `Cmd-A` also selects all; `Cmd-Shift-A`\nworks everywhere on macOS because Cmd normalizes to Ctrl.\n\n### Hotbar\n\nHotbar trigger semantics are intentionally `Alt-1` through `Alt-8` only. On macOS keyboards this is the Option/Alt key plus the number row. Bare `1`-`8` is normal text input in the composer and remains owned by pickers, onboarding, approval prompts, and modal views.\n\nFunction keys and `Cmd-1` through `Cmd-8` are not the primary Hotbar chords. Many terminals reserve those keys for tabs, windows, or OS shortcuts, and some never forward them to terminal apps. If a terminal is configured to send `Alt-1` for a custom shortcut, the Hotbar receives the same reliable chord.\n\nSince #3807 a missing `hotbar` key renders **no bar** — fresh configs show no Hotbar until you configure `[[hotbar]]` slots (an explicit `hotbar = []` also disables it). When configured, a bar looks like:\n\n| Slot | Chord | Default action | Label |\n|------|---------|--------------------|-----------|\n| 1 | `Alt-1` | `slash.workflow` | `wf` |\n| 2 | `Alt-2` | `slash.goal` | `goal` |\n| 3 | `Alt-3` | `slash.auto` | `auto` |\n| 4 | `Alt-4` | `mode.plan` | `plan` |\n| 5 | `Alt-5` | `mode.agent` | `agent` |\n| 6 | `Alt-6` | `mode.operate` | `operate` |\n| 7 | `Alt-7` | `palette.open` | `palette` |\n| 8 | `Alt-8` | `sidebar.toggle` | `side` |\n\n| Focus state | Hotbar behavior |\n|-------------|-----------------|\n| Composer empty, text, or whitespace | `Alt-1`-`Alt-8` dispatches a configured slot |\n| Workbar focused, hidden, or auto | `Alt-1`-`Alt-8` still dispatches a configured slot |\n| Slash menu or history search open | Blocked; the inline selector owns the key event |\n| Command palette, help, approval, file picker, session picker, Fleet setup, or any modal stack | Blocked; the modal owns the key event |\n| Onboarding | Blocked; onboarding owns numeric choices |\n\n### `@` mentions\n\nType `@<partial>` to open the file mention popup. `↑`/`↓` cycle the entries, `Tab` or `Enter` accepts. `Esc` hides the popup. As of v0.8.10 (#441), completions are re-ranked by mention frecency — files you mention often + recently float to the top.\n\nTwo mentions resolve to curated git context instead of a path (v0.9.2, #4067):\n\n| Mention | Inlines | Byte budget |\n|---------|---------|-------------|\n| `@git` | `git status --short --branch` for the workspace | 8 KB |\n| `@diff` | The working-tree diff, staged and unstaged (`git diff HEAD`) | 32 KB |\n\nBoth appear in the completion popup alongside paths, and both show up in the context inspector with their resolved size and, when the diff exceeds its budget, the truncation marker. When git is missing, the workspace is not a repository, or there is nothing to show, the turn carries an explicit `<git-unavailable>` note rather than silently contributing nothing. A path that merely starts with the token (`@diff.txt`, `@git/config`) stays a file mention.\n\n### `#` quick-add (memory)\n\nWhen `[memory] enabled = true`, typing `# foo` and pressing `Enter` appends `foo` as a timestamped bullet to your memory file *without* sending a turn. See `docs/MEMORY.md`.\n\n## Transcript (when transcript has focus)\n\n| Chord | Action |\n|----------------------|-----------------------------------------------------|\n| `↑` / `↓` | Scroll one line (bare arrows scroll when the composer is empty) |\n| `Alt-↑` / `Alt-↓` | Scroll transcript (works with a draft in the composer) |\n| `PgUp` / `PgDn` | Scroll one page |\n| `Alt-G` / `Alt-Shift-G` | Jump to top / bottom |\n| `Ctrl-Home` / `Ctrl-End` | Jump to top / bottom (also works from the composer) |\n| `Alt-[` / `Alt-]` | Jump between tool output blocks |\n| `Esc Esc` | Backtrack to a previous user message (`←`/`→` steps, `Enter` rewinds). Rewinds the conversation only; files are not changed; `/restore` rolls the folder back (needs `/trust on` or Full Access) |\n| `Esc` | Return focus to composer |\n| `Ctrl-Y` | With an empty composer, copy the focused transcript cell |\n| `Alt-V` | Open raw detail for the focused tool or message |\n| `Ctrl-O` | Open reasoning detail for the focused turn |\n| `Enter` | Open the focused transcript block fullscreen |\n| Mouse drag | Select transcript text in Codewhale |\n| `Ctrl-C` | Copy an active Codewhale selection |\n| `Cmd-click` (macOS) / `Ctrl-click` (Linux/Windows) | Open an OSC 8 link in a supporting terminal (terminal-owned) |\n\nBare printable keys are never transcript shortcuts. Under TUI-DOG-002 a\nprintable character always belongs to the composer — `j`, `k`, `g`, `G`, `y`,\n`Y`, `r`, `v`, and `l` type themselves in every focus state, including\ntranscript selection. Earlier revisions of this table documented them as\nnavigation; they were never wired, and the help catalog has a test\n(`transcript_navigation_catalog_does_not_advertise_bare_typing_keys`) that\nkeeps them out.\n\nReleasing a drag selection, or pressing `Ctrl-C` with one active, copies the\nintersected cells as Markdown source by default: each selected cell is\nserialized the same way `Ctrl-Y` and `/copy` serialize it, partial\nintersections round out to whole cells, cells join with blank lines, and the\ntoast names the copied cell count. Set `[tui] selection_copy_markdown = false`\nto copy the rendered text as displayed instead.\n\nFor terminal-native selection, hold `Shift` while dragging (terminal support\nvaries), then use the terminal's own copy command: usually `Cmd-C` on macOS or\n`Ctrl-Shift-C` on Linux/Windows. Those commands are handled by the local\nterminal and are intentionally separate from Codewhale's `Ctrl-C` selection\nbinding. Over SSH, Codewhale sends copy requests back through OSC 52, or via\ntmux's `load-buffer -w` path when running inside tmux.\n\n## Pointer (mouse)\n\nThe mouse is a first-class input. Every row here is wired in\n`crates/tui/src/tui/mouse_ui.rs`.\n\n| Gesture | Action |\n|----------------------|-----------------------------------------------------|\n| Wheel up / down | Scroll the transcript; over the composer, move the cursor a line at a time |\n| Click | Activate the row, chip, or tool block under the pointer |\n| Drag | Select transcript text, or drag the scrollbar |\n| Right click | Act on what is under the pointer: copy, paste, open details or a `file:line` in your editor, focus or stop an agent. Letters and digits shown in the menu run their row. A stop is confirmed in place: the first Enter or click arms it, and Enter or a click after the double-click window runs it. A letter only arms it, and the second click of a double-click is ignored |\n\nHover feedback follows the same rule: anything that responds to a click\nhighlights under the pointer. A row that highlights but does nothing, or acts\nwithout highlighting first, is a bug worth reporting.\n\n## Work bar (after `Alt-W` claims focus)\n\n| Chord | Action |\n|----------------------|-----------------------------------------------------|\n| `↑` / `↓` | Move selection |\n| `Home` / `End` | Jump to the first / last row |\n| `PageUp` / `PageDown`| Move selection a viewport at a time |\n| `Enter` | Open the selected row's world (work inspector / agent details); on an already-open row, close it |\n| `Esc` | Close the open detail, else return focus to the composer |\n| any printable key | Return focus to the composer (typing always wins) |\n\nMouse parity: clicking any work-bar row does what `Enter` does, in every\npanel and placement. `Alt-!`/`Alt-@`/`Alt-#`/`Alt-$` switch panels.\n\n## Slash-command palette (after `Ctrl-K` or typing `/`)\n\n| Chord | Action |\n|--------------------------------|-----------------------------------------------------|\n| `↑` / `↓` / `Ctrl+P` / `Ctrl+N`| Move selection |\n| `Enter` / `Tab` | Run / complete the highlighted command |\n| `Esc` | Dismiss palette |\n\n## Provider and model pickers\n\n| Chord | Action |\n|---|---|\n| `F7` | View the OrcaRouter partner offer in `/provider` or `/model`, keeping the current selection |\n\nClick the OrcaRouter recommendation for the same offer. Its pager includes the\nredemption URL and referral disclosure. On Apple keyboards, hold Fn if needed.\n\n## Session Picker (`Ctrl-R` or `/sessions`)\n\n| Chord | Action |\n|----------------------|-----------------------------------------------------|\n| `↑` / `↓` / `j` / `k`| Move selection in the session list |\n| `1`-`9` | Open the visible session history at that list slot |\n| `PgUp` / `PgDn` | Page the history pane |\n| `Enter` | Resume the selected session |\n| `/` | Search sessions |\n| `s` | Cycle sort order |\n| `a` | Toggle current-workspace scope vs all workspaces |\n| `e` | Archive / restore the selected session |\n| `x` | Show or hide archived sessions |\n| `d` | Delete selected session after confirmation |\n| `Esc` / `q` | Close the picker |\n\nArchive (`e`) is undestructive and needs no confirmation: the session stays on\ndisk and stays loadable, it just leaves the default list and stops being an\nauto-resume candidate. Press `e` again to bring it back. Delete (`d`) is the\ndestructive one and keeps its confirmation.\n\n## Question sheet (`request_user_input`)\n\nWhen the model asks a question, a **bottom-anchored sheet** opens over the\ntranscript. The conversation stays visible above it. The sheet grows with the\nquestion and options; if they still do not fit, it scrolls so the highlighted\noption and any typed custom response stay on screen. Resize is not required to\nreveal content.\n\n| Chord | Action |\n|-------|--------|\n| `↑` / `↓` / `k` / `j` | Move the highlight |\n| `1`–`9` | Quick-pick that option |\n| `Enter` | Confirm the highlight (or enter custom-response typing) |\n| `Space` | Toggle a multi-select option |\n| `←` / `h` | Previous question — change that answer. Esc is not back; it cancels the whole request. |\n| `Esc` | Cancel the whole question request |\n| `PageUp` / `PageDown` | Scroll the transcript while keeping the sheet open |\n| `Home` / `End` | Go to the start / latest part of the transcript |\n| `Alt+↑` / `Alt+↓` | Scroll the transcript three lines |\n| Mouse wheel above the sheet | Scroll the transcript; side work surfaces keep their own input |\n| Mouse wheel over the sheet | Browse the question content without changing the answer or highlight |\n\nWhile the question sheet is open, `Ctrl+↑` / `Ctrl+↓` and `Shift+↑` / `Shift+↓`\nalso scroll the transcript three lines. Plain arrows continue to move the option\nhighlight.\n\nAfter wheel browsing, option navigation or typing brings the highlighted option\nor custom-response text back into view. Transcript scroll keys work in both\noption selection and custom-response typing.\n\n**Custom response.** Every question includes an \"Other\" row. Enter it, type,\nthen `Enter` to submit or `Esc` to return to the options without sending.\n\n## Approval modal (when a tool requests approval)\n\n| Chord | Action |\n|----------------------|-----------------------------------------------------|\n| `y` / `Y` | Approve once |\n| `a` / `A` | Approve all (auto-approve subsequent calls) |\n| `n` / `N` / `Esc` | Deny |\n| `e` | Edit the approved input before running |\n\n## Onboarding (first-run flow)\n\n| Chord | Action |\n|----------------------|-----------------------------------------------------|\n| `Enter` | Advance to next step (Welcome → Language → API/trust gates → setup checkpoint) |\n| `Esc` | Step back one screen |\n| `1`–`9` | Pick a language (Language step) |\n| `0`–`9` | Pick a provider (Provider step; SGLang, vLLM, and Ollama are keyless by default) |\n| `y` / `Y` | Trust the workspace (Trust step) |\n| `n` / `N` | Skip the trust prompt |\n\n## v0.8.29 audit notes\n\n- **`Shift+Enter` / `Alt+Enter` newlines now work in VSCode on Windows (#1359).** crossterm's `PushKeyboardEnhancementFlags` command unconditionally returns `Unsupported` on Windows (`is_ansi_code_supported() == false`), so the Kitty keyboard protocol escape was never written to the terminal. Without it, VSCode's xterm.js stays in legacy mode where `Shift+Enter` is indistinguishable from plain `Enter`, causing the composer to send the message instead of inserting a newline. The fix writes the push/pop escapes (`\\x1b[>1u` / `\\x1b[<1u`) directly on Windows, bypassing crossterm's capability gate. VSCode integrated terminal and Windows Terminal ≥1.17 both honour the Kitty keyboard protocol; terminals that do not understand the sequences silently discard them.\n\n## v0.8.13 audit notes\n\n- **Ctrl-S is stash, not history search.** Fixed in this revision — `Alt-R` is history search.\n- **Phantom `Alt+Up` removed.** The \"Edit last queued message\" binding was listed in README but never existed in the key dispatch code.\n- **Bare Up/Down arrows scroll transcript when composer empty (v0.8.13).** Previously the `should_scroll_with_arrows` gate was hardcoded to false, meaning bare arrows always navigated composer history even when the composer was empty. Users in virtual terminals (Ghostty, Codex, Kitty-protocol) were especially affected because they couldn't use Cmd+Up / Alt+Up shortcuts.\n- **Configurable keymap (#436) and `tui.toml` (#437) remain deferred.** The `TuiPrefs` struct and loader exist in `settings.rs` but are not wired at startup. The named-binding registry that would let `~/.codewhale/tui.toml` override individual entries is still pending.\n- **No other broken bindings found.** Every other chord listed above resolves to a live handler in `crates/tui/src/tui/ui.rs` (key-event dispatch) or `crates/tui/src/tui/app.rs` (mode + state transitions).\n\n## Live Terminal dock\n\nIn `/workbar terminal`, click the dock or press `Alt+W` to focus it. Typing,\nEnter, Tab and Ctrl+C reach the selected shell. Esc returns to the composer.\n`Alt+Down` / `Alt+Up` switch sessions; `Ctrl+N` starts a fresh shell.\nMouse-wheel and PageUp/PageDown scroll output.\n\nSee the [Terminal dock guide](GUIDE.md#live-terminal-dock) for permission\nboundaries, paste behavior and supported platforms.\n"
|
||
}
|
||
]
|
||
},
|
||
{
|
||
"id": "auth",
|
||
"slug": "auth",
|
||
"sources": [
|
||
{
|
||
"path": "docs/CONFIGURATION.md",
|
||
"sha256": "618dd41d6edfb50b884d8634663b9d7ab75b1526a3f3e9121903631804e75e43",
|
||
"content": "# Configuration\n\ncodewhale reads configuration from a TOML file plus environment variables.\nAt process startup it may also load literal built-in-provider credentials from\na workspace-local `.env` file. Use the tracked `.env.example` as the template;\ncopy it to `.env`, then add only credential values.\n\nA workspace is not configuration authority. Codewhale therefore ignores\nconfig/profile/home paths, provider/model/base-URL routing, MCP/plugin state,\napproval/sandbox/shell posture, executable paths, runtime settings, and every\nother non-credential `.env` entry. Variable expansion is rejected so a\nrepository cannot substitute an ambient secret into a credential value. Use\n`config.toml`, CLI flags, or values exported by the launching shell for those\nexplicit control-plane settings. `.env` is read from a stable regular-file\nhandle, is capped at 1 MiB, and symbolic links, reparse points, and multiply\nlinked files are rejected.\n\n## Reading and checking configuration from the CLI\n\n`codewhale config get <key>` reads scalar keys, whole tables such as `tools`,\nand nested paths such as `tools.user_input_timeout_seconds`. Displayed tables\nand nested values apply the same recursive credential redaction as `config dump`.\n\n`config set` supports its named scalar keys and the provider, route, and\nnotification commands. It refuses a key that nothing reads and suggests the\nnearest real key (`config set calm_mod on` names `calm_mode`). A settings.toml\nkey such as `calm_mode` or `tool_collapse` is validated and written to the\nuser-global settings.toml, not config.toml; `config get` reads it back from\nthere even when an old config.toml copy (which nothing reads) is still present,\nand names that copy so you can `config unset` it. Settings keys have no project\nscope, so `--project` refuses them. A config.toml key is checked against the\ntype its reader expects and stored as a TOML boolean or number where the\nreader needs one (`yolo = true`, `max_subagents = 4`); `reasoning_effort`\naccepts the same aliases as `/effort` (#6563). Other dotted writes fail before\nmodifying the file and name the TOML table to edit. For example, set a tools\ntimeout in the file as:\n\n```toml\n[tools]\nuser_input_timeout_seconds = 0\n```\n\n`codewhale config doctor` checks credential presence and endpoint shape, and\nwarns about each config.toml root key that nothing reads: a settings.toml key\nleft in config.toml is named as misplaced, and any other unread key gets a\ndid-you-mean. Keys read by any runtime reader are not reported. The warnings do\nnot fail the check, and a clean result does not validate every runtime setting's\nvalue (#6083, #6563).\n\n## Usage and execution statistics\n\n`codewhale stats` is an alias of `codewhale metrics`; both use the same local,\nread-only backend. Use `--json` for the structured `stats` report. For example:\n\n```sh\ncodewhale stats --days 7 --models 10 --tools 20 --roles 10\ncodewhale metrics --project /path/to/workspace --json\n```\n\n`--days N` accepts a positive number of days and conflicts with `--since`.\n`--since 24h` and the existing duration syntax remain available. `--models N`,\n`--tools N` and `--roles N` select the top positive N rows by known tokens,\nidentified tool executions and recorded Fleet tasks respectively. These limits\nonly shorten the display; they do not remove stored receipts.\n\nThe report separates saved-document and Runtime views, frozen native-source\nmoney and token subtotals by actual model, retained tool-result history\nestimates and outcomes, completed compaction observations, and recorded Fleet\nroles. USD and CNY stay separate. A cumulative correction replaces its proven\npredecessor; copied Session/Runtime receipts and inherited Runtime fork rows\nare not added as new provider work. Conflicting source versions are shown\nseparately and excluded from the subtotal. Saved legacy display totals remain\nvisible as document projections without being charged again globally.\n\nCoverage is explicit. A missing quote, usage receipt, tool outcome, output\nshape or recovery link is unknown, not zero. Empty scalar usage from older\ntransport records cannot prove a reported zero. Token counts describe known\nreported data; tool history tokens are estimates of retained text, not bytes\nproven to have been replayed on a provider request. Latest prepared-request\ncomponents and source-linked compaction/role economics are unavailable where\nthey were not captured. The report does not infer provider invoices or prove\ncomplete historical coverage.\n\n`--project WORKSPACE` filters current saved/thread/Fleet views. Original source\nworkspace is not yet captured, so native money and model subtotals are\nunavailable for that filter; mutable current metadata is not used to assign a\ncharge. Older receipt projections without reliable workspace scope are also\nunavailable under this filter. Without it, those legacy receipt views remain\nseparate from native subtotals and may overlap one another.\n\nInspection uses the existing session-size bound (16 MiB per record/frame and\nfor each retained query index). Unreadable, linked, oversized or incomplete\nrecords create visible gaps; an exhausted native index refuses the query\nwithout evicting evidence. Use a narrower time window where possible. Reading\nstatistics does not create a store, repair history, refresh pricing, start a\nmodel call or retire accounting receipts. Existing state-root overrides remain\nthe read boundary.\n\n`/cache calls [count]` shows recent retained native source receipts in the TUI;\nit defaults to 10 and displays at most 50. A source correction updates its row.\nThe view keeps frozen USD/CNY estimates and known subtotals separate from missing\nusage or prices; absent or contradictory cache classes show unknown. The header\ncounts retained sources, not physical requests or complete historical calls.\nThe command does not issue a model request or remove accounting receipts.\n\nPublished prices are frozen for DeepSeek's root, `/v1`, `/beta` and Anthropic\nendpoint variants, and for other providers' default endpoints. Each quote stays\nbound to the exact endpoint, identity, model and dispatch time when saved or replayed.\nOperator endpoint overrides do not become published pricing authorities, and\nsubscription or token-plan routes remain outside money coverage. Signed cloud\nfacts keep their separate route-scoping rules. Recognition as an official\ncredential endpoint alone does not authorize sharing prices across regions or plans.\n\nMissing request outcomes, omitted provider usage, unverified legacy usage and\nusage corrections have distinct cost reasons. Known limitation: saved receipts\nwithout a frozen price are not repriced from today's catalog; their complete\ncost remains unknown even after updating the binary.\n\n## Constitution, project instructions, and repo authority\n\nCodewhale has several instruction surfaces. They are deliberately separate so a\npersonal constitution, repo policy, project instructions, and runtime security\ncontrols do not blur together.\n\n- **Bundled global Constitution** — the compiled base law in the binary. It is\n the default floor for every session.\n- **User-global constitution** — the normal guided setup output. Manage it with\n `/constitution` or `/setup`; Codewhale stores structured data at\n `$CODEWHALE_HOME/constitution.json` (default `~/.codewhale/constitution.json`)\n and renders it into a separate `<codewhale_user_constitution>` prose block.\n This can express preferences and stop conditions, but it does not change\n runtime approval policy, sandbox, shell, network, trust, or MCP permissions.\n- **Repo-local constitution** — optional project policy in\n `.codewhale/constitution.json`, described below.\n- **`AGENTS.md`** — cross-agent **project instructions** (prose). This is the\n canonical file for \"how should an agent work in this repo.\" Run `/init` to\n scaffold one. Instruction files written for other tools (`CLAUDE.md`,\n `.claude/instructions.md`, `.cursorrules`, `.github/copilot-instructions.md`\n and similar) are read only when you opt in with the top-level\n `project_instruction_imports` key, for example `[\"claude\"]`, `[\"cursor\"]` or\n `[\"all\"]` (see `config.example.toml`); Codewhale warns when it finds one it\n did not load.\n\n Codewhale reads one instruction file per directory, from the repository\n root down to the workspace, and puts your global instruction file (such as\n `~/.codewhale/AGENTS.md`) in front of them. The nearest scope reads last\n and wins: the workspace file overrides its parents, and project files\n override the global file. Global and project instructions are budgeted\n separately in estimated tokens (see [Instruction budget](#instruction-budget-instruction_budget-6526)\n below): a scope over its budget drops whole files from the broadest end\n first, and a single file larger than the remaining budget keeps its head\n with a marker that names the bytes and tokens dropped. A file keeps at most\n its first 100 KiB, and a file larger than `file_max_bytes` (8 MiB, lowerable\n only) is skipped with a notice. Every loss — trimmed, skipped, or refused —\n reaches the model and `/context report`.\n- **Memory and handoffs** — recalled state. Useful, but lower authority than\n constitutions and project instructions.\n\n### Managing the user-global constitution (`/setup` and `/constitution`)\n\nThe bundled **working agreement** is the safe default and no longer adds a\nrequired first-run screen. Customize it later through `/constitution` or the\nprogressive `/setup` guide. Provider/model readiness, workspace trust, and\nruntime posture stay separate from this guidance.\n\nOn the **Working agreement** (constitution) step:\n\n- **`1`–`6`** tune the guided draft. **`G`** previews it, and **`G`** again\n ratifies and saves a fresh structured `constitution.json`.\n- **`A`** (shown only when a provider is configured) asks your first configured\n model to draft the constitution. Drafting is **not** saving: the draft is\n rendered through the same preview and you still press **`G`** to ratify\n before anything persists.\n- **`K`** keeps your existing loaded constitution unchanged (shown only when a\n valid file is already present).\n- **`U`** (or `/constitution bundled`) records the bundled/default law.\n\n`/constitution` (alias `/law`) is the primary management surface once you are\nset up. Subcommands: `status` (the default), `preview`, `review`, `repo` (the\nrepo-local law block), `explain`, `base`, `suggestions`, `ratify <id>`,\n`migrate`, `rollback`, `edit`/`guided`, `repair`, `posture`, and `bundled`.\nManaging the constitution never changes runtime approval, sandbox,\nshell, network, trust, default mode, or MCP authority — those stay in runtime\nposture/config.\n\nEach repo can carry two distinct, complementary files:\n\n- **`AGENTS.md`** — ordinary project working instructions.\n- **`.codewhale/constitution.json`** — Codewhale-specific **repo authority /\n prioritization policy**: when local sources conflict, which should Codewhale\n trust first, and what to verify before claiming a task is done. `.codewhale/`\n lives inside the repo (like `.github/`). Example:\n\n ```json\n {\n \"schema_version\": 1,\n \"authority\": [\n \"current user request\",\n \"live code and tests\",\n \"GitHub issue/PR details\",\n \"AGENTS.md\",\n \"memory\",\n \"old handoffs\"\n ],\n \"protected_invariants\": [\n \"do not break old-session transcript replay\"\n ],\n \"branch_policy\": \"PRs target the integration branch, not main\",\n \"verification_policy\": {\n \"before_claiming_done\": [\"run focused tests\", \"read changed files back\"]\n },\n \"escalate_when\": [\n \"a destructive action was not explicitly authorized\"\n ]\n }\n ```\n\n All fields are optional. When present, the file is rendered into the system\n prompt as concise prose in a higher-authority block. Legacy `WHALE.md` files\n are ignored and reported as migration-only diagnostics.\n\n Each `protected_invariants` entry may be either a plain string (advisory\n prose, the historical shape) or an object carrying path globs, which is\n additionally **mechanically enforced** in the tool gate. See\n [Enforced repo-law invariants](#enforced-repo-law-invariants) below.\n\n This is the **repo-local law** layer. When guidance conflicts, the\n **Whose word wins** section of the effective base constitution owns the\n ordering; the bundled source is\n [`BASE_PROMPT`](../crates/engine/src/prompts/text.rs). Use `/constitution base`\n to inspect the effective base, including any opted-in expert override.\n The order in which these files are described or assembled is not an\n authority ranking. Runtime policy (permissions, sandbox and cost limits\n enforced in code) is separate from prompt guidance; editing a constitution\n does not grant permissions.\n\n> **`WHALE.md` is deprecated.** It overlapped confusingly with `AGENTS.md`.\n> Codewhale no longer reads `WHALE.md` as project or global context. If one is\n> present, setup/context diagnostics report it as ignored so you can migrate it.\n> Move ordinary instructions to `AGENTS.md` and Codewhale-specific authority\n> policy to `.codewhale/constitution.json`. Personal standing guidance belongs\n> in `/constitution` / `$CODEWHALE_HOME/constitution.json`. (The global\n> Codewhale Constitution shipped in the model prompt is a separate thing and is\n> unaffected.)\n\n### Enforced repo-law invariants\n\nBy default a `protected_invariants` entry is advisory prose: it is rendered into\nthe prompt as guidance the agent should honor, but nothing stops a write. An\nentry written as an **object with `paths`** is different — it compiles into a\nmechanical write hold that the engine's tool gate evaluates before the write\nruns. The law becomes mechanism, not just a request.\n\nAn enforced entry has this shape:\n\n```json\n{\n \"schema_version\": 1,\n \"protected_invariants\": [\n \"Keep DeepSeek support first-class.\",\n {\n \"text\": \"The wire format is frozen; protocol changes need a human.\",\n \"paths\": [\"crates/protocol/**\"],\n \"action\": \"block\"\n },\n {\n \"text\": \"Release notes need human review.\",\n \"paths\": [\"CHANGELOG.md\"],\n \"action\": \"ask\"\n }\n ]\n}\n```\n\n- `text` — required. The reason surfaced on the hold. An empty `text` is skipped.\n- `paths` — workspace-relative globs (globset syntax, e.g. `crates/protocol/**`,\n `**/secrets.toml`, `CHANGELOG.md`). An object with no usable `paths` stays\n advisory-only despite the object shape.\n- `action` — optional, defaults to `ask`. `ask` force-prompts in Ask and\n Auto-Review; in Full Access it denies the protected write without opening a\n modal. `block` **denies the write outright** in every posture.\n\nSemantics:\n\n- **Tighten-only.** The schema has no allow/widen shape, so law can only *add*\n holds — a crafted constitution can never grant authority or weaken a gate\n above it.\n- **Not bypassable by mode.** Like the built-in safety floor, an `ask` hold\n force-prompts in Ask and Auto-Review. Full Access never opens approval\n modals, so the same hold fails closed as a hard block; `block` always denies.\n Mode cannot turn a hold off.\n- **Repo-local only.** Only the repo's `.codewhale/constitution.json`\n participates. The user-global constitution stays advisory prose and never\n reaches this mechanism.\n- **Fails safe.** A missing file, parse error, or invalid glob degrades to\n fewer or zero rules — never a hold on unprotected paths and never a poisoned\n gate. Across matches the strongest action wins, so `block` outranks `ask`.\n- **Leaves a receipt.** Every hold emits a `tool.repo_law_decision` tool-audit\n event naming the invariant, the matched path, and the source file; the\n approval/denial reason names the invariant too.\n\n**Coverage is deliberately limited.** Holds are evaluated only for the write\ntools `write_file`, `edit_file`, `apply_patch`, and `fim_edit`, and only\nagainst the filesystem targets named in their inputs (`path`/`target`/\n`destination`/`file_path`, `changes[].path`, and unified-diff /\n`apply_patch`-envelope headers). A shell command that writes a protected path is **not** held by\nrepo law — those writes are still governed by the ordinary approval, sandbox,\nand shell-write gates, not by this mechanism.\n\n### Expert full base-prompt override (#3638)\n\nThe global Constitution (the base system prompt, normally compiled in from\n`crates/engine/src/prompts/text.rs` as `BASE_PROMPT`) can be replaced per-user\nwithout rebuilding. This is\nan expert escape hatch, not the normal `/constitution` guided setup output.\nBecause this is a prompt trust boundary, it takes **two deliberate steps** — a\nfile alone is not enough:\n\n1. Drop the replacement at `~/.codewhale/prompts/constitution.md` (under\n `$CODEWHALE_HOME` when set).\n2. Set the explicit opt-in flag `CODEWHALE_ALLOW_BASE_PROMPT_OVERRIDE=1`\n (`true`/`on`/`yes` also accepted).\n\nIf the file exists but the flag is unset, the override is **ignored** (with a\nlog line pointing to the flag) and the bundled Constitution stays in place.\nThis is intended for repurposing the TUI beyond software engineering — e.g.\nlong-form writing or document review — where the engineering-oriented base\nprompt is a poor fit. It is loaded once at startup; a **missing or empty file\nis a no-op**, so existing installs keep the bundled prompt.\n\nScope is deliberately narrow: only the byte-stable **base prompt segment** is\noverridable. Mode deltas, the approval policy, the tool taxonomy, Context\nManagement, and the Compaction Relay are still owned by Codewhale's runtime\nassembly, so an override **cannot remove safety-relevant guidance** (sandbox,\napprovals) — it only swaps the task/voice framing. To customize ordinary\npersonal behavior, prefer `/constitution`; to customize per-repo behavior,\nprefer `AGENTS.md` + `.codewhale/constitution.json` above.\n\n## Where It Looks\n\nDefault config path:\n\n- `~/.codewhale/config.toml`\n- Legacy fallback: `~/.deepseek/config.toml`\n\nOverrides:\n\n- CLI: `codewhale --config /path/to/config.toml`\n- Env: `CODEWHALE_CONFIG_PATH=/path/to/config.toml`\n- Legacy env alias: `DEEPSEEK_CONFIG_PATH=/path/to/config.toml`\n\nIf both are set, `--config` wins. Environment variable overrides are applied after the file is loaded.\n\n### TUI editability audit\n\nInside the TUI, run `/config audit` to see which documented keys can be changed\nfrom the current session, which ones can also be persisted, and which ones stay\nfile-only or restart-only. The audit includes current values for the high-impact\nruntime controls such as `approval_policy`, `allow_shell`,\n`stream_chunk_timeout_secs`, `base_url`, `mcp_config_path`, and the\n`[subagents]` concurrency/depth/timeout keys.\n\nUse the command's \"Command / reason\" column as the source of truth before\nediting by hand. For example, `/config approval_mode on-request --save` writes\ntop-level `approval_policy = \"on-request\"`, while provider base URLs are saved\nbut still require restarting the model client.\n\n### User workspace entries\n\nInteractive Agent sessions expose shell tools by default with approval gating\nunless you explicitly disable them. For a shell opt-in that should live in the\nuser's global config for noninteractive or durable-task profiles rather than in\nthe repository, add a workspace-scoped entry:\n\n```toml\n[workspace.'/absolute/path/to/project']\nallow_shell = true\n```\n\nThe entry applies only when the launched workspace path matches the table key.\nThe legacy `[projects.\"/absolute/path/to/project\"]` table is also accepted for\nthis user-owned override.\n\nIn interactive mode, the per-project overlay\n`<workspace>/.codewhale/config.toml` is applied after this user entry. A\nproject-level `allow_shell = false` can still tighten the session; project-level\n`allow_shell = true` is ignored.\n\n### Per-project overlay (#485)\n\nWhen the TUI starts in a workspace that contains a regular-file\n`<workspace>/.codewhale/config.toml`, the safe values declared in that file are\nmerged on top of the global config. Legacy\n`<workspace>/.deepseek/config.toml` files are still read when the Codewhale path\nis absent. Symlinked project config files are rejected. This lets a repo suggest\na model or tighten local safety posture without touching the user's\n`~/.codewhale/config.toml`. Pass `--no-project-config` to skip the overlay for\none launch.\n\nSupported keys in the project overlay (top-level fields only):\n\n| Key | Effect |\n|---|---|\n| `model` | override `default_text_model` |\n| `reasoning_effort` | force `\"high\"` / `\"max\"` for a complex repo |\n| `approval_policy` | only values that tighten the user's current permission posture |\n| `sandbox_mode` | only values that tighten the user's current sandbox posture |\n| `max_subagents` | clamp sub-agent concurrency for a constrained repo (clamped to 1..=128) |\n| `allow_shell` | `false` can disable shell access; `true` is ignored |\n\nThe overlay is intentionally narrow — it covers the fields a repo\nmaintainer is most likely to want to standardize across contributors.\nCredential, endpoint, provider-selection, MCP config, hooks, skills,\nretry, hotbar bindings, and `instructions = [...]` settings stay user-global.\nIf a repo-local config declares `api_key`, `base_url`, `providers`, `provider`,\n`mcp_config_path`, `notes_path`, `hotbar`, `allow_shell = true`, or `instructions`,\nCodewhale ignores that key and keeps the user's global setting.\n\nThe consolidated `codewhale` runtime uses `~/.codewhale/config.toml` for\nprovider and model settings. `codewhale auth set --provider deepseek` saves\nthe key in the local secret store (a private file under `~/.codewhale/secrets/`\nby default; the OS keychain is an explicit option), rather than writing it\ninto that config file. Legacy `~/.deepseek/config.toml` is migrated on first\nlaunch when needed. `codewhale --model deepseek-v4-flash` is forwarded to the\nTUI as `CODEWHALE_MODEL`. The dispatcher no longer writes `DEEPSEEK_*` twins;\na value you set yourself remains a legacy alias when its `CODEWHALE_*`\nequivalent is unset.\n\n### Account provider keys\n\n`/connect` offers Continue with Codewhale or Connect on this computer. The\naccount path signs in and lists usable model connections reported by the\naccount service. When none are ready, it offers retry or on-computer setup.\nSign-in alone does not supply model access or change the selected model.\nLocal provider keys and local models never need an account; use `/provider`\nor `codewhale auth set --provider <provider>` to manage them.\n\nFor an account that supports saved provider keys, these commands manage them:\n\n```bash\ncodewhale account keys list # configured status, never secret values\ncodewhale account keys remove deepseek # remove the account's saved key\n```\n\nLogin never uploads local keys, and removing an account key does not change\nlocal copies or revoke the key at the provider.\n\nThe account session is separate from provider keys. `/login` signs in inside\nthe TUI; `codewhale login` does so from the shell. `codewhale account status`\ninspects it. `/logout`, `codewhale logout` and `codewhale account logout` remove\nonly the account session for the selected `--profile`. The explicit\n`codewhale logout --all` action also clears saved model credentials.\nAccount sessions use the private `0600`\nCodewhale secrets file, scoped to the profile and account API origin; they do\nnot use the OS keychain. The older `codewhale cloud ...` spelling remains an\nalias for account commands.\n\n### Portable config bundles\n\n`codewhale config export --portable [--project] [--out FILE]` writes a\nportable, secret-free bundle of your configuration: sorted TOML with\ncredential and machine-specific keys (API keys, base URLs, socket paths)\ndropped, never a redacted placeholder in their place. Without `--out` the\nbundle goes to stdout. Typed tables, arrays, numbers, booleans, and datetimes\nremain typed. Machine-bound authority is deliberately non-portable: project\ntrust overlays, credential readers, auto-running hooks, executable LSP\ndefinitions, and local path bindings are omitted rather than copied to a new\nhost. So is trust posture (`yolo`, `allow_shell`, `approval_policy`,\n`sandbox_mode`, `sandbox_network_access`, and the sandbox read-denylist\npaths) and local endpoints or executables (`[lifecycle_outbox]`,\n`[extension_host]`, `[control_socket]`). URLs that carry a credential —\nuserinfo, a token-named query parameter, or a Slack/Discord/Teams webhook\npath — are treated as credentials.\n\n`codewhale config import <FILE|HTTPS_URL|-> [--dry-run] [--yes] [--project]`\napplies a bundle. The envelope is strict (`schema_version = 1`, kind\n`codewhale.portable-config`; unknown fields fail). Import prints a\ndeterministic plan — added / changed / skipped / conflicting / rejected —\nthen asks for consent unless `--yes` is given; headless use requires it.\nCredential-shaped entries are rejected by key name and by value shape;\nrejections name the field, never a value. Remote bundles come from HTTPS\nonly (loopback http excepted) with a 5 MiB cap. Application backs up the\ntarget document to `<config>.bundle-backup-<timestamp>-<random>`, rolls back on any\nfailure, and re-importing an applied bundle changes nothing.\n\nImport also rejects the non-portable authority classes omitted by export,\nincluding nested/camel/dotted credential keys and cookie headers. This keeps a\nhand-authored or remote bundle from reintroducing machine trust, local\ncredential access, or automatically executable commands that a local export\nwould refuse to carry. Structured tables are deep-merged: a portable model or\npreference update does not erase target-local provider credentials, endpoints,\nhooks, or executable definitions that were deliberately omitted from the\nbundle. Arrays and scalar values still replace the corresponding portable\nvalue.\n\nSections map to scope: `[project]` entries land only in the workspace\ndocument (`--project`, which must target an actual workspace config),\n`[global]` only in the user-global one; `preferences`, `profiles`, and\n`plugins` apply at either scope. A global bundle operation refuses a workspace\ndocument just as a project operation refuses the user-global document.\n\n### Credential read precedence (#5197)\n\nCredential reads are **folder-independent by default**: every layer below is\nuser-global or process-scoped, so a key saved in one repo resolves identically\nin every other repo. Repo-local config never carries credential material — the\nproject overlay above reads only its allowlisted keys and ignores `api_key`,\nand a credential write aimed at a workspace-scoped config is rescoped to the\nuser-global `~/.codewhale/config.toml` (#5045, #5193).\n\nFor the active provider, the runtime resolves the API key in this exact order\n(first match wins):\n\n1. **Route-specific auth contract.** Routes whose `auth_mode` disables API\n keys stop here with no credential. OAuth routes use their explicitly\n granted token: the official `openai-codex` plan route accepts only\n Codewhale-owned credentials from `codewhale auth chatgpt`, bound to its\n issued registration and verified account. Imported Codex CLI and process\n tokens cannot authorize this route;\n `[providers.xai] auth_mode = \"oauth\"` reads Codewhale's own xAI\n device-login store (or a consent-granted Grok CLI file).\n2. **Explicit CLI key.** `--api-key` forwarded with its source marker wins\n over every saved slot.\n3. **Config file `api_key`.** The `[providers.<name>] api_key` table slot for\n the active provider. `deepseek-CN` also reads `[providers.deepseek]`. An\n older top-level `api_key` is read as `[providers.deepseek] api_key` (see\n [Legacy top-level `base_url` and `api_key`](#legacy-top-level-base_url-and-api_key)).\n File-owned keys stay bound to their file-owned endpoint: when the\n environment replaces the route's base URL with a custom host, the saved\n key is not sent there.\n4. **`api_key_env` binding.** `[providers.<name>] api_key_env = \"VAR\"` reads\n the named environment variable. For custom providers an unset or empty\n binding is a loud error, not a silent fallback (#5104).\n5. **Secret store.** The durable per-provider slot written by\n `codewhale auth set` (file-backed under `~/.codewhale/secrets/` by\n default; the OS keyring only when explicitly selected). Skipped for named\n custom routes, self-hosted providers, custom endpoints other than an\n explicitly authenticated loopback, and routes whose `auth_mode` needs no\n key.\n6. **Ambient environment.** The provider's own variable\n (`DEEPSEEK_API_KEY`, `OPENROUTER_API_KEY`, `MOONSHOT_API_KEY`, …).\n Ambient keys are only ever sent to the provider's official endpoint and\n are skipped under the same conditions as the secret store.\n7. **Keyless fallback.** Self-hosted providers and loopback endpoints may run\n with no credential; every other route fails with provider-specific setup\n guidance.\n\nLegacy compatibility: `~/.deepseek/config.toml` is migrated into\n`~/.codewhale/config.toml` on first launch, `DEEPSEEK_*` environment\nvariables remain accepted aliases for the `CODEWHALE_*` forms, and\n`DEEPSEEK_SECRET_BACKEND` is the legacy alias for `CODEWHALE_SECRET_BACKEND`.\n\nRun `codewhale auth status` to inspect the active provider's config\nfile, OS keyring backend, environment variable, winning source, and last-four\nlabel without printing the key itself. The command only probes the active\nprovider's keyring entry.\n\nFor hosted, generic OpenAI-compatible, self-hosted, OpenAI Responses, or native\nAnthropic providers, set `provider = \"<id>\"` or pass\n`codewhale --provider <id>`. The canonical provider IDs are `deepseek`,\n`nvidia-nim`, `openai`, `atlascloud`, `wanjie-ark`, `volcengine`,\n`openrouter`, `orcarouter`, `xiaomi-mimo`, `novita`, `fireworks`,\n`siliconflow`, `arcee`, `siliconflow-CN`, `moonshot`, `sglang`, `vllm`,\n`ollama`, `ollama-cloud`, `huggingface`, `modelscope`, `together`, `qianfan`, `openai-codex`,\n`anthropic`, `openmodel`, `zai`, `stepfun`, `minimax`, `deepinfra`,\n`sakana`, `longcat`, `opencode-go`, `opencode-zen`, `meta`, `xai`,\n`mistral`, `telecomjs`, `modelstudio-token-plan`, `google`,\n`edenai`, `concentrate`, `codewhale`, and `custom` (a user-defined OpenAI-compatible endpoint via\n`[providers.<name>]`).\nFor the provider-by-provider registry, including wire protocol, auth variables,\ndefault base URLs, model IDs, and capability metadata, see\n[PROVIDERS.md](PROVIDERS.md).\nThe facade saves provider credentials in the local secret store and forwards\nthe resolved key, base URL, provider, and model to the TUI process. Use\n`codewhale auth set --provider nvidia-nim --api-key \"YOUR_NVIDIA_API_KEY\"` or\n`codewhale auth set --provider openai --api-key \"YOUR_OPENAI_COMPATIBLE_API_KEY\"` or\n`codewhale auth set --provider atlascloud --api-key \"YOUR_ATLASCLOUD_API_KEY\"` or\n`codewhale auth set --provider wanjie-ark --api-key \"YOUR_WANJIE_API_KEY\"` or\n`codewhale auth set --provider xiaomi-mimo --api-key \"YOUR_XIAOMI_KEY\"` or\n`codewhale auth set --provider fireworks --api-key \"YOUR_FIREWORKS_API_KEY\"` or\n`codewhale auth set --provider siliconflow --api-key \"YOUR_SILICONFLOW_API_KEY\"` or\n`codewhale auth set --provider arcee --api-key \"YOUR_ARCEE_API_KEY\"` or the\nmatching provider ID from [PROVIDERS.md](PROVIDERS.md) to save provider keys\nthrough the facade. The generic `openai` provider defaults\nto `https://api.openai.com/v1`, accepts `OPENAI_BASE_URL`, and defaults to\n`gpt-5.6`. A custom OpenAI-compatible gateway can still select its own model\nexplicitly. `atlascloud` defaults to\n`https://api.atlascloud.ai/v1`, accepts `ATLASCLOUD_BASE_URL`, and uses\n`deepseek-ai/deepseek-v4-flash` as its default model. `wanjie-ark` targets\nWanjie Ark's OpenAI-compatible endpoint at\n`https://maas-openapi.wanjiedata.com/api/v1`, defaults to `deepseek-reasoner`,\nand passes model IDs through unchanged because Wanjie model access is\naccount-scoped. SGLang, vLLM, and Ollama are\nself-hosted and can run without an API key by default. Ollama defaults to\n`http://localhost:11434/v1` and sends model tags such as `codewhale-coder:1.3b`\nor `qwen2.5-coder:7b` unchanged. Self-hosted providers and loopback custom\nURLs (`localhost`, `127.0.0.1`, `[::1]`, `0.0.0.0`) do not read the secret store\nunless API-key auth is explicitly requested; use an env var or config-file key\nwhen a local server does require bearer auth.\nOllama Cloud is the separate hosted `ollama-cloud` provider. It defaults to\n`https://ollama.com/v1` and `gpt-oss:120b`; save its key with\n`codewhale auth set --provider ollama-cloud`. Ambient auth reads\n`OLLAMA_CLOUD_API_KEY` first, then Ollama's official `OLLAMA_API_KEY`.\nSiliconFlow defaults to `https://api.siliconflow.com/v1`, accepts\n`SILICONFLOW_BASE_URL`, and uses `deepseek-ai/DeepSeek-V4-Pro` by default.\n`provider = \"siliconflow-CN\"` selects the China regional default\n`https://api.siliconflow.cn/v1` with the `[providers.siliconflow_cn]` table and\n`SILICONFLOW_API_KEY` credential slot.\nArcee AI defaults to `https://api.arcee.ai/api/v1`, accepts `ARCEE_BASE_URL`,\nand uses `trinity-large-thinking` by default for Codewhale agent work.\n`trinity-large-preview` is also listed as a direct Arcee API model; OpenRouter's\n`arcee-ai/trinity-large-thinking` remains the OpenRouter namespaced form, while\nthe direct Arcee provider uses the bare `trinity-large-thinking` ID. Direct\nArcee large-model API calls are tracked as 256K-context BF16 serving; Thinking\nis reasoning-capable, while Preview is not marked as a thinking model.\n\n### OpenRouter vendor pinning\n\nOpenRouter serves each model through several upstream vendors, and Codewhale\ncan pin requests to a vendor with `[providers.openrouter] vendor` (#6007):\n\n```toml\nprovider = \"openrouter\"\n[providers.openrouter]\nmodel = \"deepseek/deepseek-v4-pro\"\nvendor = \"deepinfra\" # copy the vendor slug from the model's OpenRouter page\n```\n\nThis sends `\"provider\": {\"order\": [\"deepinfra\"], \"allow_fallbacks\": false}`\non OpenRouter requests. A base slug can match multiple endpoint variants;\ncopy a full slug such as `deepinfra/turbo` to select one variant. An unavailable\npin fails at OpenRouter. Codewhale's separate `fallback_providers` setting can\nstill switch the whole route after a recoverable error.\n\nThe pin applies across OpenRouter models, including auxiliary requests on that\nroute. Set `vendor = \"\"` to clear it. Reload config or restart to apply edits;\nrequests already in flight keep their captured route. Other providers do not\ninherit the pin. `/preview-request` shows the primary request's routing fields.\n\nModel strings still pass through verbatim: `:floor` sorts by price, `:nitro`\nsorts by throughput, and `@preset/my-team-preset` references an account preset.\nSee OpenRouter's [provider routing](https://openrouter.ai/docs/guides/routing/provider-selection)\nand [presets](https://openrouter.ai/docs/guides/features/presets) documentation.\nCodewhale does not fetch per-vendor endpoint prices or availability; pinned\nusage reports a routing-dependent unknown cost instead of a catalog estimate.\n\n### Custom OpenAI-Compatible Gateways\n\nFor a single third-party service that implements the OpenAI Chat Completions\nAPI, the simplest setup is the built-in `openai` provider name pointed at the\ngateway:\n\n```toml\nprovider = \"openai\"\ndefault_text_model = \"your-model-id\"\n\n[providers.openai]\napi_key = \"YOUR_OPENAI_COMPATIBLE_API_KEY\"\nbase_url = \"https://your-gateway.example/v1\"\n```\n\nPut the endpoint under `[providers.openai]`, not the legacy top-level\n`base_url`, so the OpenAI-compatible provider receives it. `default_text_model`\nis the model ID sent to the gateway; `[providers.openai].model` can be used as\nthe OpenAI-provider-specific override.\n\nIf you keep several OpenAI-compatible gateways, or need a stable name for an\nAgentProfile provider pin, define a user-named custom provider table:\n\n```toml\nprovider = \"lm-studio\"\n\n[providers.lm-studio]\nkind = \"openai-compatible\"\nbase_url = \"http://127.0.0.1:1234/v1\"\napi_key = \"lm-studio\"\nmodel = \"qwen-2.5-7b\"\n```\n\nCustom provider names may be selected with `provider = \"<name>\"`,\n`--provider <name>`, or an AgentProfile `provider = \"<name>\"` when the matching\n`[providers.<name>]` table exists.\n\nA custom table sends Chat Completions requests by default. Add\n`wire = \"responses\"` to send OpenAI Responses requests to\n`<base_url>/responses` instead, or `wire = \"anthropic\"` for Anthropic Messages:\n\n```toml\n[providers.my-gateway]\nkind = \"openai-compatible\"\nbase_url = \"https://gateway.example/v1\"\napi_key_env = \"MY_GATEWAY_API_KEY\"\nmodel = \"gpt-5.6\"\nwire = \"responses\"\n```\n\nStepFun has a first-class provider entry, so keep Coding Plan credentials and\nbase URL scoped to `[providers.stepfun]`:\n\n```toml\nprovider = \"stepfun\"\n\n[providers.stepfun]\napi_key = \"YOUR_STEPFUN_API_KEY\"\nbase_url = \"https://api.stepfun.ai/step_plan/v1\"\nmodel = \"step-3.7-flash\"\n```\n\n`/provider` setup asks which StepFun billing route the key belongs to —\npay-as-you-go (`https://api.stepfun.ai/v1`) or a Step Plan subscription\n(`https://api.stepfun.ai/step_plan/v1`) — and validates the key against the\nendpoint you pick before saving it. The answer is written to\n`[providers.stepfun].base_url` and nowhere else. If that key already holds a\nbase URL Codewhale does not recognize as one of those two routes, the question\nis skipped and your value is left untouched.\n\nAlibaba Bailian / Model Studio DashScope Qwen routes use the same OpenAI\nprovider shape:\n\n```toml\nprovider = \"openai\"\n\n[providers.openai]\napi_key = \"YOUR_DASHSCOPE_API_KEY\"\nbase_url = \"https://dashscope-intl.aliyuncs.com/compatible-mode/v1\"\nmodel = \"qwen-plus\"\ncontext_window = 1000000\n```\n\nUse the regional DashScope `compatible-mode/v1` base URL that matches the\nregion of your API key. Codewhale keeps `qwen-plus` scoped to the `openai`\nprovider route and does not infer a different provider from the model prefix.\nThe same rule applies to all provider-prefixed model strings: a prefix such as\n`deepseek-ai/...` or `deepseek/...` is a provider-owned wire ID under the\nselected provider, not an automatic switch to the DeepSeek provider.\nSet `context_window` to the gateway/model's real total context window when it\ndiffers from Codewhale's static model metadata. See\n[Context length (context window)](#context-length-context-window) for the full\nresolution order and for how to check which value is in effect.\n\nIf the gateway accepts `POST /chat/completions` but rejects\n`/v1/chat/completions`, set a provider-local `path_suffix`:\n\n```toml\n[providers.openai]\nbase_url = \"https://your-gateway.example/v1\"\npath_suffix = \"/chat/completions\"\n```\n\nThe suffix applies only to chat-completion requests. Model listing and\nDeepSeek beta paths keep their built-in routing so a generic gateway override\ndoes not accidentally rewrite `/models` or `/beta/completions`.\n\nFor private gateways with broken or intercepted certificates, use\n`SSL_CERT_FILE` with a trusted CA bundle. The legacy provider-table key\n`insecure_skip_tls_verify = true` is still parsed so `codewhale doctor` can\nreport stale configs, but provider clients reject it instead of disabling TLS\ncertificate verification.\n\nLocal HTTP endpoints such as Ollama, SGLang, and vLLM are allowed by default\nwhen they use localhost or loopback addresses. For a non-local `http://`\ngateway, launch with `DEEPSEEK_ALLOW_INSECURE_HTTP=1` only on a trusted network:\n\n```bash\nDEEPSEEK_ALLOW_INSECURE_HTTP=1 codewhale\n```\n\nThird-party OpenAI-compatible gateways that need extra request headers can set\n`http_headers = { \"X-Model-Provider-Id\" = \"your-model-provider\" }` at the top\nlevel or under a provider table such as `[providers.deepseek]`. When configured,\ncodewhale sends those custom headers on model API requests. The equivalent\nenvironment override is `DEEPSEEK_HTTP_HEADERS`, using comma-separated\n`name=value` pairs such as\n`X-Model-Provider-Id=your-model-provider,X-Gateway-Route=dev`. `Authorization`\nand `Content-Type` are managed by the client and are not overridden by this\nsetting.\n\n### Vision Model\n\nCodewhale's chat provider and `image_analyze` tool are configured separately.\nThe main chat path remains the selected text/tool provider; image analysis runs\nthrough `[vision_model]` when the `vision_model` feature is enabled.\n\nXiaomi's current image-understanding docs include `mimo-v2.5` for image input.\nTo use MiMo for `image_analyze`, configure the vision model explicitly:\n\n```toml\n[features]\nvision_model = true\n\n[vision_model]\nmodel = \"mimo-v2.5\"\napi_key = \"YOUR_XIAOMI_KEY\"\nbase_url = \"https://api.xiaomimimo.com/v1\"\n```\n\nThe example above uses Xiaomi MiMo's pay-as-you-go OpenAI-compatible endpoint.\nIf you are using a Token Plan key (`tp-...`) for `[vision_model]`, you must set\n`base_url` explicitly because this generic OpenAI-compatible block does not\nauto-select MiMo endpoints. Use\n`https://token-plan-sgp.xiaomimimo.com/v1` for Singapore accounts,\n`https://token-plan-cn.xiaomimimo.com/v1` for China-region accounts, or\n`https://token-plan-ams.xiaomimimo.com/v1` for Europe/Amsterdam accounts.\n\n### Auto Model Routing (`[auto]`, `[auto.router]`)\n\nWith `model = \"auto\"`, each turn runs on your **declared default model** unless\nyou have opted into something else. Auto never guesses a cheaper or stronger\nmodel from how a request is worded; the old keyword-and-length heuristic was\nremoved (`auto_route_declared_fallback` in `crates/engine/src/model_routing.rs`).\nTwo optional layers change that:\n\n- an `[auto.router]` classifier, which you write down, that picks a model per\n turn; and\n- `[auto] cost_saving`, which prefers the active provider's fast sibling.\n\nWith neither set, Auto is local and free: the turn uses the default model and\nno classifier call is made.\n\n**There is no default classifier.** With `[auto.router]` unset, no classifier\ncall happens, whatever keys you hold. Holding a DeepSeek key used to elect\n`deepseek-v4-flash` automatically. That was removed because it spent tokens on\na route the user never chose and privileged one provider over the rest\n(`AutoRouterConfig` in `crates/tui/src/config.rs`). Electing a network\nclassifier is now something you write down.\n\nPoint the classifier at any configured provider with `[auto.router]`:\n\n```toml\n[auto.router]\nprovider = \"zai\"\nmodel = \"glm-5-turbo\"\nthinking = \"off\" # optional; defaults to off\ntimeout_secs = 4 # optional; default 4, 0 = default, capped at 300\n```\n\nA classifier call happens only when `[auto.router]` names both `provider` and\n`model` *and* that provider has a key:\n`router_available = router_configured && has_api_key_for(...)` in\n`ModelInventory::from_config` (`crates/engine/src/model_inventory.rs`). If either\ncondition fails, or the classifier call errors or times out, the local\nfallback decides: the default model, or the fast sibling under `cost_saving`.\nThe turn's route receipt (Turn Inspector, Ctrl+Alt+O or `/turn inspect`, \"Model\nroute + tokens/cost\") records which path was taken, and a\nrouter you configured that cannot run or fails (missing key, HTTP error,\ntimeout, invalid answer) is shown as `Auto router: failing — …` rather than\nsilently ignored.\n\n#### Set up model routing\n\n`/router` (also `/model router`) opens one Router setup view with these\npresets. Each writes only `[auto.router]`; none is ever chosen for you.\n\n| Preset | What it writes | Cost and privacy |\n| --- | --- | --- |\n| `/router jev` | Jev, TypeSafe's decision model, over OpenRouter (`typesafe/jev-1.13`) or TypeSafe direct, whichever key you have | About $0.00002 per turn ($0.042 per million input tokens, output free). Your latest request and up to six recent context lines go to OpenRouter → TypeSafe (or TypeSafe). |\n| `/router fast` | The active provider's runnable fast tier with thinking off | Your existing key; the classifier sees the same request text. |\n| `/router off` | Removes `[auto.router]` | No router call; Auto turns use the default model, or the fast tier while `[auto] cost_saving = true` (Off leaves that setting alone). |\n| `/router custom` | Nothing; prints the TOML to edit | — |\n\nChoosing Jev or Fast makes **one test call** with a fixed sample request and\nshows the tier it picked, the probabilities and confidence, the latency and the\nprovider-reported cost. `Enter` (or `/router save <preset>`) then saves through\nthe normal config writer; `Esc` discards it. TypeSafe paused new signups on\n2026-09-22, so OpenRouter is the default route for new users. A TypeSafe key is\nread from `TYPESAFE_API_KEY`, the `typesafe` secret-store entry, or\n`[providers.typesafe] api_key` / `api_key_env`.\n\n#### Decision routers (`kind = \"decision\"`)\n\nA decision router asks a non-generative decision model typed Choice questions\nfor the active provider's `fast` and `strong` tiers and thinking level, then\nreads calibrated probabilities. No prose is parsed.\n\n```toml\n[auto.router]\nkind = \"decision\" # default \"chat\"\nprovider = \"openrouter\" # or \"typesafe\"\nmodel = \"typesafe/jev-1.13\" # \"~typesafe/jev-latest\" also works; TypeSafe direct: \"jev-latest\"\ntimeout_secs = 2\nmin_confidence = 0.5 # default 0.5, clamped to 0..1\n```\n\n- The router is called only when the active provider has a runnable strong/fast\n pair; otherwise there is no call and no spend.\n- An answer below `min_confidence` takes the local fallback. Under\n `[auto] cost_saving`, a `strong` answer also needs a probability of at least\n 0.75, or the turn stays on the fast tier.\n- An unknown `kind`, or a decision `provider` other than `openrouter` /\n `typesafe`, leaves the router unconfigured and shown as failing.\n- `[auto.router] thinking` is ignored for decision routers. Both routes settle tokens through\n the originating session's routed-usage ledger. TypeSafe is a named Custom route\n with unknown billing; its price is never borrowed from the active chat provider.\n Provider-reported cost remains verbatim on the decision receipt.\n- OpenRouter uses `POST /api/alpha/decisions`; TypeSafe uses `POST /v1/systemone`.\n The shared client validates Choice, Noul and Score against the offered questions\n and bounds responses to 256 KiB. Malformed answers fail open with usage retained.\n\n#### Shadow Decision Gate (experimental, off by default)\n\nThe Superfast Decision Gate asks a decision model three typed questions about\nthe latest user message — does it need a tool, can it be answered from the\nconversation, and what is its intent — and logs a conservative recommendation.\nIt is shadow-only: it never changes routing, never skips or delays the model\ncall, and fails open on any error, timeout or malformed answer. It uses the same\nSystem One client as the decision router above; there is no separate HTTP\nclient. It is configured from the environment and reads it when a turn starts:\n\n```sh\nSUPERFAST_ENABLED=1 # off unless set\nSUPERFAST_PROVIDER=typesafe # or openrouter; required when enabled\nSUPERFAST_BASE_URL=http://localhost:8000/v1 # optional TypeSafe-route base, e.g. self-hosted\nSUPERFAST_MODEL=jev-latest # default jev-latest / ~typesafe/jev-latest\nSUPERFAST_TIMEOUT_MS=150 # 1..=10000, default 150\n```\n\n- Enabling the gate never picks an endpoint by itself: without\n `SUPERFAST_PROVIDER` nothing is sent and a warning is logged.\n- The key comes from the same place the decision router reads it. The TypeSafe\n route always authenticates, so a self-hosted server that ignores auth still\n needs a placeholder `TYPESAFE_API_KEY`.\n- Only the latest user message is sent, truncated to 4,000 characters and\n redacted of configured secrets. The log (target `superfast`) carries the\n route, failure class and latency, never prompt text.\n- The gate retains the originating turn's accounting owner and cancellation.\n Its tokens settle through the shared ledger; missing usage or a cancelled/timed\n out request after dispatch creates an explicit coverage gap. Unknown TypeSafe\n pricing is recorded as unpriced rather than free.\n- Provider-reported cost survives rejected answers and late responses in bounded\n receipts on the originating turn or session. These are diagnostic evidence;\n unknown pricing never becomes an authoritative dollar total. Incomplete token\n counters record a coverage gap while preserving the reported raw evidence.\n\nThe wire contracts are documented in [TypeSafe's OpenAPI schema](https://api.typesafe.ai/openapi.json)\nand [OpenRouter's Decisions examples](https://openrouter.ai/blog/insights/what-is-jev/).\n\nThe Decision Gate concept and reference implementation are by Andrea Bruno,\nreleased under CC BY 4.0:\n[harness-superfast](https://github.com/Andrea-Bruno/harness-superfast).\n\nTwo `[auto]` keys shape routing (`AutoConfig` in `crates/tui/src/config.rs`):\n\n```toml\n[auto]\ncost_saving = false # default false\ncross_provider = false # default false\n```\n\n- **`cost_saving`** (default `false`). Without a classifier, Auto pins the\n active provider's validated fast sibling instead of the default model. A\n provider with no runnable fast sibling stays on the default. With a\n classifier, the classifier is told to prefer the fast tier for routine or\n ambiguous work and to pick the strong tier only for clearly agentic,\n multi-step, architecture, security or debugging work. Cost-saving never\n switches provider just to save money.\n- **`cross_provider`** (default `false`). Auto stays on the provider the session\n is configured to use. The classifier is only shown that provider's models,\n and the fallback never leaves it. Setting `cross_provider = true` lets the\n classifier choose among every runnable provider. There is no interactive\n toggle for `cross_provider`; it has to be set in config. A decision router\n always chooses within the active provider.\n\nTo bootstrap MCP and skills directories at their resolved paths, run `codewhale setup`.\nTo only scaffold MCP, run `codewhale mcp init`.\n\nNote: `setup`, `doctor`, `mcp`, `features`, `sessions`, `resume`/`fork`, `exec`,\n`review`, and `eval` are all available from the installed `codewhale` command.\nThe consolidated dispatcher also provides `auth`, `config`, `model`, `thread`, `sandbox`,\n`app-server`, `mcp-server`, `completions`, `login`/`logout`, `account`,\n`metrics`, `update`, `lane`, `workflow`, and `web`. Plain prompts enter the\nin-process TUI runtime. Release installers expose the same bytes as `codew`.\n\n### Startup Update Checks\n\nBy default, the TUI starts a background check for the latest stable Codewhale\nrelease and shows a short toast only when a newer release is available and the\nofficial release assets are complete. The check never blocks startup, never\nblocks a turn, and fails silently when offline.\n\nDisable the startup check entirely for air-gapped, corporate-proxy, or managed\ndesktop environments:\n\n```toml\n[update]\ncheck_for_updates = false\n```\n\n#### Automatic updates\n\n`mode` decides what happens when a newer release is found (release-binary\ninstalls only; package-manager and desktop-app copies always just notify):\n\n```toml\n[update]\nmode = \"auto\" # auto (default): download in the background, apply at next start\n # notify: tell me only; off: no checks\n```\n\n`codewhale config set update.mode notify` writes it. `CODEWHALE_UPDATE_MODE`\noverrides it, and an organization policy file overrides both; see\n[INSTALL.md](INSTALL.md#automatic-updates) for the full flow, signing, the\npolicy file and `codewhale update --rollback`.\n\n#### Throttling\n\nThe answer is cached in `~/.codewhale/update-check.json` and reused for\n`check_interval_hours` (default `6`). Only the *network request* is throttled —\nthe notice still appears on every launch while an update is outstanding. Set `0`\nto check on every launch.\n\n```toml\n[update]\ncheck_interval_hours = 6\n```\n\nA failed check keeps the last answer (so the notice stays) and retries about an\nhour later rather than on every launch, which keeps offline and rate-limited\nmachines quiet.\n\n#### Automatic suppression\n\nChecks are skipped, without contacting the network, when any of these is set to\na non-falsey value:\n\n| Variable | Why |\n| --- | --- |\n| `CODEWHALE_NO_UPDATE_CHECK` | Explicit opt-out. |\n| `NO_UPDATE_NOTIFIER` | The cross-CLI convention, honored for compatibility. |\n| `CI`, `CONTINUOUS_INTEGRATION`, `GITHUB_ACTIONS`, `GITLAB_CI`, `BUILDKITE`, `CIRCLECI`, `JENKINS_URL`, `TEAMCITY_VERSION`, `TF_BUILD` | Automated build; nobody is at the terminal. |\n\nValues of `\"\"`, `0`, `false`, `no`, and `off` do not count as set, so a\n`CI=false` export does not disable checks for ordinary users.\n\n#### Which update command is offered\n\nCodewhale never installs anything on its own — it only tells you an update\nexists. The command it names depends on how the running binary was installed,\ndetected from its path:\n\n| Install | Command offered |\n| --- | --- |\n| GitHub release binary (including Termux) | `codewhale update` |\n| npm (`node_modules` on the path) | `npm install -g codewhale@latest` |\n| Homebrew (`Cellar` / `linuxbrew` prefix) | `brew upgrade codewhale` |\n| `cargo install` (`~/.cargo/bin`) | `cargo install codewhale-cli --locked --force` |\n\nFor package-managed installs the notice also warns against `codewhale update`:\nreplacing a binary Homebrew or npm owns leaves the manager describing a version\nthat is no longer on disk, and the next upgrade silently reverts you.\n\nOverride the detection with `CODEWHALE_INSTALL_METHOD=npm|homebrew|cargo|binary`\nif you relocated the binary somewhere the path heuristics cannot read.\n\nTo redirect the startup check, set `update_uri` to an internal endpoint that\nreturns GitHub-compatible latest-release JSON. Minimal mirror metadata with a\n`tag_name` field is accepted; if `assets` are present, Codewhale requires the\nsame uploaded asset set as the official release before showing the toast.\n\n```toml\n[update]\ncheck_for_updates = true\nupdate_uri = \"https://internal.mirror.example/codewhale/releases/latest\"\n```\n\nWhen `update_uri` is not set, startup checks honor release mirror environment\nvariables such as `CODEWHALE_RELEASE_BASE_URL` before falling back to the\nofficial GitHub API endpoint. If a configured `update_uri` cannot be fetched or\nparsed and a release mirror env var is set, the TUI falls back to that mirror\ninstead of failing startup.\n\n#### What's new after an update\n\nThe first time Codewhale starts after its version goes up, it says so once and\nlinks that version's release notes on codewhale.net:\n\n```text\nUpdated to 0.10.2. Got 12 features, 31 fixes and 8 other changes.\nRelease notes: https://codewhale.net/en/changelog/0.10.2\n```\n\nThe counts come from the changelog compiled into the binary. Entries under\nAdded or New count as features; Fixed, Security and Latest fixes count as\nfixes; Contributors, Thanks, Community and Known limits or issues are not\ncounted; every other section counts as other changes. The link uses your UI\nlanguage where the site has it (`zh` for Chinese) and English otherwise.\n\nThe TUI shows the notice on the startup card and moves it into the transcript\nwhen the card leaves (at once when the session opens from `--resume`,\n`--continue` or a prompt), adding `/change <version>` for reading the notes in\nplace. The `doctor`, `models`,\n`sessions`, `receipts`, `setup`, `init`, `install`, `features`,\n`integrations`, `mcp`, `config`, `model`, `metrics`, `providers`, `account`,\n`login`, `logout` and `update` commands print it to stderr instead, when\nstdout and stderr are both terminals and no JSON output is requested. `exec`,\nservers, JSON output and non-terminal runs never show it; the next start that\ncan show it does.\n\nNothing is shown on a fresh install or after a downgrade. The version that\nlast started is recorded in `~/.codewhale/last-launch.json`. The notice reads\nonly local files, so it is independent of `check_for_updates`. To turn it off:\n\n```toml\n[update]\nshow_whats_new = false\n```\n\n## Workshop output budgets\n\nBy default an oversized tool result uses bounded spillover: a result\nunder the byte threshold stays inline, a larger one gets a head/tail\npreview plus a session artifact the model can read back. There is no\nsynthesis sub-agent and no per-call `raw = true` escape.\n\n`[workshop] large_output_threshold_tokens` and\n`[workshop.per_tool_thresholds]` (exact model-visible tool names such as\n`bash`) only take effect when the process opts in to adaptive evidence\nrouting with `CODEWHALE_ADAPTIVE_OUTPUT_ROUTING=1`; they set where a result\nstops being inline and becomes handle-only evidence.\n\nTwo optional byte ceilings (#5367) apply either way. They raise the\nmodel-visible floor and never lower it:\n\n- `read_result_max_bytes` — cap for a single `read` / `read_file`\n result. Absent keeps the compile-time defaults (100000 bytes for\n `read`, which has no line cap; 16KiB / 500 lines for `read_file`).\n For `read` this is the middle of a three-layer budget: the model's\n own per-call `max_bytes` (hard maximum 500000) raises the budget for\n one call, this setting raises the floor for the whole process, and\n either way 2MiB is the absolute ceiling. Highest wins; neither layer\n can lower a budget the other granted.\n- `tool_result_max_bytes` — cap for a generic tool result after\n spillover. Absent keeps the 12K-character compact floor (48K on\n windows ≥500K tokens). Hard cap is 2MiB.\n\n## Context length (context window)\n\nAlso called context size, context limit, max context, or window. This is the\ntotal token window Codewhale budgets against, and it drives the header/footer\ncontext percent, the auto-compaction trigger, context-pressure checks, and the\nrequest output cap. If Codewhale compacts at 128K on a model you know serves a\n1M window, this is the setting to change (#5134).\n\n**See what is in effect, and where the value came from.** Every one of these\nprints the resolved window *and* its source:\n\n- `/status` — a `Context window:` row with the percent and token counts, and a\n `Window source:` row naming the provenance and the exact key that overrides\n it.\n- `/config` → Provider — `Context window` (your override, or `(not set)`) and\n `Effective context window` (`1048576 tokens · configured`). Typing\n `context length` in the `/config` filter jumps straight to them.\n- `/context report` — `Window: 1048576 tokens (12.4% used, ...; source: configured)`.\n- `/context json` — machine-readable `context_window_tokens` and\n `context_window_source`.\n\n**Change it** with the provider-table key `context_window`:\n\n```toml\n[providers.moonshot]\ncontext_window = 1048576\n```\n\nor from the CLI:\n\n```bash\ncodewhale config set providers.moonshot.context_window 1048576\ncodewhale config unset providers.moonshot.context_window # back to automatic\n```\n\nUse the table for the provider you are actually on (`providers.openai`,\n`providers.deepseek`, `providers.moonshot`, …); `/status` names it for you. The\nvalue is a positive token count for the route's *total* window.\n\nWhen one gateway fronts models with heterogeneous windows, scope the override\nto an exact wire model id with `[providers.<name>.model_context_windows]`:\n\n```toml\n[providers.command_code]\ncontext_window = 204800\n\n[providers.command_code.model_context_windows]\n\"MiniMaxAI/MiniMax-M2.5\" = 204800\n\"google/gemini-3.1-flash-lite\" = 1000000\n```\n\nKeys are the exact wire model ids the route sends (dotted and `org/model`\nspellings both work as TOML keys when quoted); each value must be a positive\ntoken count. A matching entry beats the provider-level `context_window` for\nthat model only — every other model on the provider still resolves against\n`context_window` and the rungs below. From the CLI:\n\n```bash\ncodewhale config set 'providers.command_code.model_context_windows.\"MiniMaxAI/MiniMax-M2.5\"' 204800\ncodewhale config unset 'providers.command_code.model_context_windows.\"MiniMaxAI/MiniMax-M2.5\"'\n```\n\n### How the effective window is resolved\n\nFirst match wins, and the source label each surface prints is exactly this\nrung:\n\n1. `configured (per-model)` — a `[providers.<name>.model_context_windows]`\n entry keyed by the route's exact wire model id. A hard override for that\n model only; it never rewrites another model's window.\n2. `configured` — `[providers.<name>] context_window` in `config.toml`. A hard\n override for every model on the provider: nothing below it can raise or\n lower the result. Read-time aliases:\n `contextWindow`, `context_window_tokens`, `contextWindowTokens`,\n `context_length`, `contextLength`.\n3. `provider-reported` — route-scoped 1M metadata a provider actually reported\n for the Kimi Code `k3` route, when it was observed within the last 24 hours.\n4. `static Kimi Code safe floor` — 262,144 tokens for Kimi Code memberships,\n because 1M access is plan-gated (Allegretto and above).\n5. `catalog` — the bundled route catalog (hand-curated offerings first, then\n the bundled Models.dev rows). The official `openai-codex` roster supplies\n account-specific model choices, not context-window metadata. Refresh it\n with `codewhale models --update --provider openai-codex`; Codewhale does\n not read `$CODEX_HOME` for this route.\n6. `model-name hint` — an `_Nk` suffix parsed from the model name itself\n (`qwen3-32b-256k` → 256,000), vendor-agnostic. A naming convention the\n serving engine may not honor is not a fact about the route, so this rung\n sits *below* the catalog: any catalog row for the same id beats it (#5441).\n7. `fallback` — the static per-provider capability table: 200,000 for\n Anthropic-wire routes, 128,000 for `openai-codex`, 8,192 for Ollama,\n otherwise Codewhale's static per-model metadata, and finally 128,000 when\n the model is unknown.\n\n### What \"(unverified)\" means\n\nThe `model-name hint` and `fallback` rungs still drive real budgets — the\ncompaction trigger, the context meter, and the output reservation all use the\nnumber — but they are guesses, not capabilities anyone checked. Every surface\nthat renders one of these windows appends `(unverified)` to its source label\n(the status line, the context-pressure message, `/status`, `/config`, and the\nmodel picker chip), so a window you did not configure and no provider reported\ncan never read as a verified limit (#5239, #5441). The `context_window` and\n`model_context_windows` provider-table keys above are the fix: a configured\nwindow is a hard override and renders as `configured` (or\n`configured (per-model)`) with no marker.\n\nOutput ceilings follow the same rule (#5440): an Anthropic-family model the\ncatalog does not describe keeps the 64K Messages floor as its clamp, labeled\n`unverified` (or an \"assumed floor\") instead of `documented`. The official\nChatGPT plan preview does not accept `max_output_tokens`; Codewhale sends no\noutput-token cap on that route. A local request budget is not an upstream\nlimit or a guarantee about the completed response length.\n\nThere is no environment variable for the context window; the provider-table\n`context_window` and per-model `model_context_windows` keys are the user\nknobs. They are the right ones to set when a gateway or self-hosted runtime\nserves a window Codewhale's catalog does not model — per-model when only some\nof a provider's routes differ, provider-wide when they all do. Codewhale will\nnot invent a window it cannot justify — it falls back to a conservative value,\nlabels it `fallback`, and marks it `(unverified)` at every surface that shows\nit.\n\n### Adjacent knobs\n\n- `auto_compact_threshold_percent` (settings.toml; also accepted as\n `auto_compact_threshold`; `10`–`100`, default `80`): the share of the full\n route context window at which auto-compaction fires, clamped so it can never\n cross the spendable input ceiling after output reservation and headroom.\n Editable from `/config`. Raising the window without touching this raises the\n absolute compaction point along with it.\n- `auto_compact` (settings.toml, on/off): turns automatic compaction off\n entirely; `/compact` and Ctrl+L stay available.\n- `[compaction] summary_instructions` and\n `[compaction] retained_user_message_tokens` (config.toml): standing\n summarizer instructions and the verbatim user-message retention budget. See\n the `compaction.*` entry in the config-key reference below.\n- `CODEWHALE_MAX_OUTPUT_TOKENS` (environment variable; legacy alias\n `DEEPSEEK_MAX_OUTPUT_TOKENS`): overrides the requested output cap. Without an\n override, Codewhale starts at the safe `65536` request cap and intersects it\n with any smaller documented model or route ceiling; a catalog `max_output`\n such as DeepSeek V4's 384K remains a capability ceiling, not the amount every\n response requests. Explicit overrides are preserved within the resolved\n route context window and any route output ceiling, and preflight/emergency\n budgeting reserves the same effective value that can reach the wire. A\n separately documented route input ceiling also clamps preflight and\n compaction even when the total context window is larger. A blank canonical\n variable falls through to a nonblank legacy value; a nonblank invalid or\n zero canonical value is authoritative and falls back to the safe automatic\n default instead of activating a stale legacy setting. There is no\n `max_output_tokens` key in `config.toml`.\n\nBefore compaction replaces conversation history, Codewhale durably saves the\noriginal messages to the session's `artifacts/context-transfer-<id>.json` and,\nwhen a model summary is produced, its handoff to the matching `.md` file.\nThese use the existing session artifact store and persistence redaction.\nA failed write aborts compaction without replacing context. Pruning-only passes\nsave the original messages without making an extra model call. The model can\nread them back through `context_archive_search` (ASCII case-insensitive\nsubstring search returning short snippets and an archive id) and\n`context_archive_get` (paged read of one message by id); the handoff note names\nboth so the model knows the archive exists. Pressure metadata\nshows estimated input tokens and the configured trigger; it is an estimate, not\nan exact promise about a provider's remaining context.\n\nCompaction history is available through `codewhale metrics` (or `--json`) and\n`audit.log` in the Codewhale home. Completed passes record their trigger,\nsummary/pruning path, message and estimated-token counts, effective threshold,\nand summarizer token usage. Automatic refusals are recorded once per turn with\ntheir reason. These are local diagnostics, not provider invoice totals; earlier\nartifacts are not retroactively counted. A text-mode `exec` that attempts\ncompaction saves its owning session at turn completion so its recovery artifacts\nremain discoverable. A killed process may leave artifacts without that final\nsession snapshot; the audit writer reports I/O failures instead of inventing data.\n\nSee [Settings File](#settings-file-persistent-ui-preferences) for the\ncompaction settings and [Token Quantities and\nDrivers](#token-quantities-and-drivers) for what each displayed token number\nactually measures.\n\n## Profiles\n\nYou can define multiple profiles in the same file:\n\n```toml\ndefault_text_model = \"deepseek-flash\"\n\n[providers.deepseek]\napi_key = \"PERSONAL_KEY\"\n\n[profiles.work.providers.deepseek]\napi_key = \"WORK_KEY\"\nbase_url = \"https://api.deepseek.com/beta\"\n\n[profiles.nvidia-nim]\nprovider = \"nvidia-nim\"\ndefault_text_model = \"deepseek-ai/deepseek-v4-pro\"\n\n[profiles.nvidia-nim.providers.nvidia_nim]\napi_key = \"NVIDIA_KEY\"\nbase_url = \"https://integrate.api.nvidia.com/v1\"\n\n[profiles.fireworks]\nprovider = \"fireworks\"\ndefault_text_model = \"accounts/fireworks/models/deepseek-v4-pro\"\n\n[profiles.siliconflow]\nprovider = \"siliconflow\"\ndefault_text_model = \"deepseek-ai/DeepSeek-V4-Pro\"\n\n[profiles.siliconflow.providers.siliconflow]\nbase_url = \"https://api.siliconflow.com/v1\"\n\n[profiles.openai-compatible]\nprovider = \"openai\"\n\n[profiles.openai-compatible.providers.openai]\nbase_url = \"https://openai-compatible.example/v4\"\nmodel = \"glm-5\"\n\n[profiles.atlascloud]\nprovider = \"atlascloud\"\n\n[profiles.atlascloud.providers.atlascloud]\nbase_url = \"https://api.atlascloud.ai/v1\"\nmodel = \"deepseek-ai/deepseek-v4-flash\"\n\n[profiles.sglang]\nprovider = \"sglang\"\n\n[profiles.sglang.providers.sglang]\nbase_url = \"http://localhost:30000/v1\"\nmodel = \"deepseek-ai/DeepSeek-V4-Pro\"\n\n[profiles.vllm]\nprovider = \"vllm\"\n\n[profiles.vllm.providers.vllm]\nbase_url = \"http://localhost:8000/v1\"\nmodel = \"deepseek-ai/DeepSeek-V4-Pro\"\n\n[profiles.ollama]\nprovider = \"ollama\"\n\n[profiles.ollama.providers.ollama]\nbase_url = \"http://localhost:11434/v1\"\nmodel = \"qwen2.5-coder:7b\" # an installed chat-capable tag\n\n[profiles.ollama-cloud]\nprovider = \"ollama-cloud\"\n\n[profiles.ollama-cloud.providers.ollama_cloud]\nbase_url = \"https://ollama.com/v1\"\nmodel = \"gpt-oss:120b\"\n```\n\nSelect a profile with:\n\n- CLI: `codewhale --profile work`\n- Env: `DEEPSEEK_PROFILE=work`\n\nIf a profile is selected but missing, codewhale exits with an error listing available profiles.\n\nFor local Ollama, omit `providers.ollama.model` to select a tag whose native\ncatalog confirms completion support. `/provider ollama`, exec, and Runtime API\ndefault-model admission refresh that catalog; embedding-only catalogs have no\nautomatic chat default. An explicit model keeps its exact tag.\n\nA loaded model's `/api/ps` context observation is usable for 60 seconds.\n`/api/show` reports the trained maximum, which does not establish the served\nwindow. An unloaded model or an expired observation uses the unverified 8192-token\nfallback; explicit context-window configuration still takes precedence.\n\n## Legacy top-level `base_url` and `api_key`\n\nOlder releases kept DeepSeek's endpoint and key at the top of `config.toml`,\nand each reader decided for itself which other routes inherited them. They\nnow live in provider tables only. An older file keeps working unchanged:\nevery load reads the top-level keys as if they were already in their table,\nby one rule, per file and per `[profiles.<name>]`:\n\n1. `provider = \"custom\"` with no `[providers.custom]` table: the endpoint, key\n and a copy of the model become `[providers.custom]`.\n2. An endpoint on another vendor's official host (for example\n `integrate.api.nvidia.com`, `xiaomimimo.com`, `openrouter.ai`, or the\n ChatGPT Codex endpoint) belongs to that vendor's table, so a DeepSeek route\n never sends requests there.\n3. Anything else belongs to `[providers.deepseek]`, which DeepSeek-CN also\n reads for its endpoint and key.\n\nThe key goes with DeepSeek (or the literal custom route). It follows the\nendpoint to another vendor only when the same table sets `provider` to that\nvendor, the host is that vendor's own, and the vendor's table has no key. When\nno `provider` is set, a NIM host still selects `nvidia-nim` and\n`api.deepseeki.com` still selects `deepseek-cn`. A `[vision_model]` without a\nkey of its own keeps using the top-level key. A profile's top-level\n`base_url` now overrides the base file's `[providers.deepseek] base_url`\n(before, the base table silently won).\n\nWhen a top-level value and its table disagree, the table's `base_url` and the\ntop-level `api_key` are used, which is what the runtime sent before.\n\nLoading never rewrites the file. The next save Codewhale makes (for example\n`codewhale config set`, `/config ... --save`, or `auth set`) moves the keys,\nkeeps comments, writes a one-time credential-free copy of the old file to\n`config.toml.pre-migrate.bak`, and prints one line saying what moved. A\ndisagreeing pair is never resolved in the file on its own; both values stay\nuntil you choose, and `codewhale config doctor` / `codewhale doctor` report\nit (sources only, never values). `codewhale config get|set|unset base_url`\n(and `api_key`) address the active provider's table.\n\nTo tidy the file yourself:\n\n```bash\ncodewhale config migrate --dry-run # show what would move\ncodewhale config migrate # move it (backup first)\ncodewhale config migrate --prefer top-level # resolve a conflict: keep the top-level value\ncodewhale config migrate --prefer table # resolve a conflict: keep the table value\n```\n\n## Environment Variables\n\nMost runtime environment variables override config values. API-key variables are\nfallbacks after saved config and keyring credentials.\n\nThe three user-facing slots — provider, model, base URL — expose `CODEWHALE_*`\naliases. When both forms are set the `CODEWHALE_*` value wins; the\n`DEEPSEEK_*` form is kept for older shells:\n\n- `CODEWHALE_PROVIDER` (preferred) / `DEEPSEEK_PROVIDER` (legacy alias) —\n `deepseek|deepseek-anthropic|nvidia-nim|openai|atlascloud|wanjie-ark|volcengine|openrouter|xiaomi-mimo|novita|fireworks|siliconflow|arcee|siliconflow-CN|moonshot|sglang|vllm|ollama|ollama-cloud|huggingface|modelscope|together|qianfan|openai-codex|anthropic|openmodel|zai|stepfun|minimax|deepinfra|mistral`\n- `CODEWHALE_MODEL` (preferred) / `DEEPSEEK_MODEL` (legacy alias) — default model for the active provider\n- `CODEWHALE_BASE_URL` (preferred) / `DEEPSEEK_BASE_URL` (legacy alias) — base URL for the active provider\n\n`CODEWHALE_BASE_URL` applies to the **active** route only. A request pinned to\nanother provider — a subagent or fleet child, a routed tool, the per-turn\nauto-router, a picker preview — resolves its endpoint from that provider's own\n`[providers.<table>]`, then its provider-scoped variable (`MOONSHOT_BASE_URL`,\n`OPENAI_BASE_URL`, …), then that provider's default. It never inherits the\nactive session's host, and a custom route with no configured `base_url` fails\nclosed on a loopback placeholder rather than borrowing another provider's\nendpoint. The environment writes the value into the active identity's own\ntable: DeepSeek-CN falls back to `[providers.deepseek]` for its endpoint and\nkey, but never to a DeepSeek value the environment addressed to DeepSeek alone.\nA managed-config overlay that supplies or reselects the effective\nroute's endpoint takes the generic override away from every route.\n\nRemaining variables:\n\n- `CODEWHALE_LOCAL_PROXY_FALLBACK` (`0`) — keeps reqwest's build-time proxy snapshot; the loopback fallback is on by default. Read once when the first client is built.\n- `DEEPSEEK_API_KEY`\n- `DEEPSEEK_ANTHROPIC_BASE_URL`\n- `DEEPSEEK_HTTP_HEADERS` (custom model request headers, comma-separated `name=value` pairs)\n- `DEEPSEEK_DEFAULT_TEXT_MODEL` (extra legacy alias of `DEEPSEEK_MODEL`)\n- `DEEPSEEK_STREAM_IDLE_TIMEOUT_SECS` (stream idle timeout in seconds; default `900`, clamped to `1..=3600`)\n- `DEEPSEEK_STREAM_OPEN_TIMEOUT_SECS` (connection setup + response-header wait in seconds; default `45`, clamped to `5..=300`; distinct from the per-chunk idle timeout; `stream.open_timeout_secs` (legacy `tui.stream_open_timeout_secs` fallback) wins when positive)\n- `CODEWHALE_CACHE_MAXIMAL` (`1`/`true`/`on`/`yes`) — cache-maximal context mode (#528). When on, the Repo Working Set block materializes the **full current contents** of the top active files into the system prompt each turn (deterministic order, byte-bounded), instead of only listing their paths. The block stays byte-stable while those files are unchanged so DeepSeek's KV prefix cache keeps hitting; editing a file cache-misses from its block onward. Off by default (path list only). Byte caps default to 24 KB per file / 96 KB total.\n- `NVIDIA_API_KEY` or `NVIDIA_NIM_API_KEY` (when provider is `nvidia-nim`)\n- `NVIDIA_NIM_BASE_URL`, `NIM_BASE_URL`, or `NVIDIA_BASE_URL`\n- `NVIDIA_NIM_MODEL`\n- `OPENAI_API_KEY`\n- `OPENAI_BASE_URL`\n- `OPENAI_MODEL`\n- `ATLASCLOUD_API_KEY`\n- `ATLASCLOUD_BASE_URL`\n- `ATLASCLOUD_MODEL`\n- `WANJIE_ARK_API_KEY`, `WANJIE_API_KEY`, or `WANJIE_MAAS_API_KEY`\n- `WANJIE_ARK_BASE_URL`, `WANJIE_BASE_URL`, or `WANJIE_MAAS_BASE_URL`\n- `WANJIE_ARK_MODEL`, `WANJIE_MODEL`, or `WANJIE_MAAS_MODEL`\n- `VOLCENGINE_API_KEY`, `VOLCENGINE_ARK_API_KEY`, or `ARK_API_KEY`\n- `VOLCENGINE_BASE_URL`, `VOLCENGINE_ARK_BASE_URL`, or `ARK_BASE_URL`\n- `VOLCENGINE_MODEL` or `VOLCENGINE_ARK_MODEL`\n- `OPENROUTER_API_KEY`\n- `OPENROUTER_BASE_URL`\n- `OPENROUTER_MODEL`\n- `XIAOMI_MIMO_TOKEN_PLAN_API_KEY`, `MIMO_TOKEN_PLAN_API_KEY`, `XIAOMI_MIMO_API_KEY`, `XIAOMI_API_KEY`, or `MIMO_API_KEY`\n- `XIAOMI_MIMO_BASE_URL` or `MIMO_BASE_URL`\n- `XIAOMI_MIMO_MODEL` or `MIMO_MODEL`\n- `XIAOMI_MIMO_MODE` or `MIMO_MODE` (`token-plan-sgp`, `token-plan-cn`,\n `token-plan-ams`, or `pay-as-you-go`)\n- `NOVITA_API_KEY`\n- `NOVITA_BASE_URL`\n- `NOVITA_MODEL`\n- `FIREWORKS_API_KEY`\n- `FIREWORKS_BASE_URL`\n- `FIREWORKS_MODEL`\n- `HUGGINGFACE_API_KEY` or `HF_TOKEN` (`HF_TOKEN` is a fallback alias accepted when provider is `huggingface`)\n- `MODELSCOPE_API_KEY`\n- `HUGGINGFACE_BASE_URL` or `HF_BASE_URL`\n- `HUGGINGFACE_MODEL` or `HF_MODEL`\n- `SILICONFLOW_API_KEY`\n- `SILICONFLOW_BASE_URL`\n- `SILICONFLOW_MODEL`\n- `ARCEE_API_KEY`\n- `ARCEE_BASE_URL`\n- `ARCEE_MODEL`\n- `TOGETHER_API_KEY`\n- `TOGETHER_BASE_URL`\n- `TOGETHER_MODEL`\n- `QIANFAN_API_KEY` or `BAIDU_QIANFAN_API_KEY`\n- `QIANFAN_BASE_URL` or `BAIDU_QIANFAN_BASE_URL`\n- `QIANFAN_MODEL` or `BAIDU_QIANFAN_MODEL`\n- `OPENAI_CODEX_ACCESS_TOKEN` or `CODEX_ACCESS_TOKEN` (legacy process tokens; ignored by the official ChatGPT plan route)\n- `OPENAI_CODEX_BASE_URL` or `CODEX_BASE_URL`\n- `OPENAI_CODEX_MODEL` or `CODEX_MODEL`\n- `OPENAI_CODEX_ACCOUNT_ID` or `CODEX_ACCOUNT_ID` (legacy identity hints; ignored by the official ChatGPT plan route)\n- `CODEWHALE_CHATGPT_NEW_ACCOUNT=1` explicitly replaces the single selected ChatGPT registration after a validated browser sign-in\n- `ANTHROPIC_API_KEY`\n- `ANTHROPIC_BASE_URL`\n- `ANTHROPIC_MODEL`\n- `ZAI_API_KEY` or `Z_AI_API_KEY`\n- `ZAI_BASE_URL` or `Z_AI_BASE_URL`\n- `ZAI_MODEL` or `Z_AI_MODEL`\n- `STEPFUN_API_KEY` or `STEP_API_KEY`\n- `STEPFUN_BASE_URL` or `STEP_BASE_URL`\n- `STEPFUN_MODEL` or `STEP_MODEL`\n- `MINIMAX_API_KEY`\n- `MINIMAX_BASE_URL`\n- `MINIMAX_MODEL`\n- `DEEPINFRA_API_KEY` or `DEEPINFRA_TOKEN`\n- `DEEPINFRA_BASE_URL`\n- `DEEPINFRA_MODEL`\n- `MISTRAL_API_KEY`\n- `MISTRAL_BASE_URL`\n- `MISTRAL_MODEL`\n- `MOONSHOT_API_KEY` or `KIMI_API_KEY`\n- `MOONSHOT_BASE_URL` or `KIMI_BASE_URL`\n- `MOONSHOT_MODEL`, `KIMI_MODEL_NAME`, or `KIMI_MODEL`\n- `SGLANG_BASE_URL`\n- `SGLANG_MODEL`\n- `SGLANG_API_KEY` (optional; many localhost SGLang servers do not require auth)\n- `VLLM_BASE_URL`\n- `VLLM_MODEL`\n- `VLLM_API_KEY` (optional; many localhost vLLM servers do not require auth)\n- `OLLAMA_BASE_URL`\n- `OLLAMA_MODEL`\n- `OLLAMA_API_KEY` (optional; many localhost Ollama servers do not require auth)\n- `OLLAMA_CLOUD_BASE_URL`\n- `OLLAMA_CLOUD_MODEL`\n- `OLLAMA_CLOUD_API_KEY` (preferred Cloud key; `OLLAMA_API_KEY` is the official fallback)\nFor every product-level `CODEWHALE_*` variable below, the matching legacy\n`DEEPSEEK_*` name is still read as a compatibility fallback; when both are set,\nthe `CODEWHALE_*` value wins.\n\n- `CODEWHALE_TIMING=1` emits local JSON phase durations on stderr for startup, model preparation, tool planning, approval waits and postprocessing. Nested intervals overlap. The records contain only phase, process ID, end time and elapsed microseconds; stream-json stdout is unchanged.\n- `CODEWHALE_LOG_LEVEL` or `RUST_LOG` (`info`/`debug`/`trace` enables lightweight verbose logs)\n- `CODEWHALE_SKILLS_DIR`\n- `CODEWHALE_MCP_CONFIG`\n- `CODEWHALE_NOTES_PATH`\n- `CODEWHALE_MEMORY` (`1|on|true|yes|y|enabled` turns user memory on)\n- `CODEWHALE_MEMORY_PATH`\n- `CODEWHALE_TELEMETRY` / `DEEPSEEK_TELEMETRY` (legacy alias) — anonymous usage\n counting is on by default in the current 0.9.12 source, with a disclosure\n naming Codewhale and PostHog and an easy durable opt-out. Prior explicit\n declines remain off. Accepts `0|1|true|false|yes|no|on|off|enabled|\n disabled`. An explicit \"off\" is a **floor**: it beats `--telemetry true` and\n `telemetry = true` in config, and a value this list cannot read also resolves\n to off, because a typo in a kill switch must never resolve to \"on\". See\n [`TELEMETRY.md`](TELEMETRY.md).\n- `CODEWHALE_TELEMETRY_ENDPOINT` / `DEEPSEEK_TELEMETRY_ENDPOINT` (legacy alias)\n — `https://`, or plain `http://` only for loopback. Overrides the config file.\n Unset selects the shipped default,\n `https://telemetry.codewhale.net/v1/telemetry`; setting it to the **empty\n string** routes batches to a local dry-run file and contacts nobody. Either\n way it only decides where a session sends — it cannot override an opt-out.\n- `CODEWHALE_ALLOW_SHELL` (`1`/`true` enables)\n- `CODEWHALE_APPROVAL_POLICY` (`on-request|untrusted|never`)\n- `CODEWHALE_SANDBOX_MODE` (`read-only|workspace-write|danger-full-access|external-sandbox`)\n- `CODEWHALE_NO_NEW_PRIVS` (`0`/`false`/`no`/`off`/`disabled` opts out) — Linux only. The\n TUI process sets the kernel's irreversible no-new-privileges flag at startup\n as defense-in-depth, which blocks `sudo`/`su`/setuid helpers for Codewhale's\n whole process tree. The flag is already skipped when the startup sandbox\n mode resolves to `danger-full-access` (#5723), so this variable is the\n explicit override in the remaining cases: set it to a falsey value before\n launching if you administer through Codewhale as a wheel-group user under a\n narrower posture and need escalation to work (#5413), or set a truthy value\n to force the flag on even under `danger-full-access`. Unset, the posture\n decides; the other startup hardening (no ptrace, no core dumps) always\n stays on.\n- `CODEWHALE_MANAGED_CONFIG_PATH`\n- `CODEWHALE_REQUIREMENTS_PATH`\n- `CODEWHALE_MAX_SUBAGENTS` (clamped to `1..=128`)\n- `CODEWHALE_TASKS_DIR` (runtime task queue/artifact storage, default\n `~/.codewhale/tasks`, with legacy `~/.deepseek/tasks` fallback when only the\n legacy directory exists)\n- `CODEWHALE_RUNTIME_DIR` (override the runtime thread store root). Interactive\n sessions default to `$CODEWHALE_HOME/sessions/<session-id>/runtime` so each\n Codewhale process owns its own store (#5630). The store is single-owner: a\n second process on the **same** root fails at startup. Set this variable to\n share one store across processes, or when the runtime API server should use a\n stable non-session path. Unset, the API/server path remains\n `$CODEWHALE_HOME/tasks/runtime`. Legacy alias: `DEEPSEEK_RUNTIME_DIR`.\n- `CODEWHALE_ALLOW_INSECURE_HTTP` (`1`/`true` allows non-local `http://` base URLs; default is reject)\n- `CODEWHALE_FORCE_HTTP1` (`1|true|yes|on` pins the HTTP client to HTTP/1.1, disabling HTTP/2; useful on Windows or behind proxies that mishandle long-lived H2 streams)\n- `CODEWHALE_HOME` (override the base data directory; defaults to `~/.codewhale`).\n If you previously exported `DEEPSEEK_HOME`, rename it to `CODEWHALE_HOME`;\n the old env var is not used for new Codewhale state paths.\n- `CODEWHALE_RELEASE_BASE_URL` (release asset mirror used by `codewhale update`\n and by TUI startup update checks when `[update].update_uri` is not set, or as\n a fallback when that configured URI cannot be fetched)\n- `CODEWHALE_AUTOMATIONS_DIR` (override the automations storage directory; uses\n `~/.codewhale/automations` by default, with legacy `~/.deepseek/automations`\n fallback when only the legacy directory exists)\n- `NO_ANIMATIONS` (`1|true|yes|on` forces `low_motion = true` and\n `fancy_animations = false` at startup, regardless of the saved\n settings; see [`docs/ACCESSIBILITY.md`](./ACCESSIBILITY.md)).\n- `SSL_CERT_FILE` — corporate-proxy / TLS-inspecting MITM users\n point this at a PEM bundle (or single DER cert) and the cert(s)\n get added alongside the platform's system trust store. Failures\n log a warning and continue — the existing system roots still\n apply.\n\n### Instruction sources (`instructions = [...]`, #454)\n\nAdd a list of additional system-prompt sources that get\nconcatenated, in declared order, alongside the auto-loaded\n`AGENTS.md`:\n\n```toml\ninstructions = [\n \"./AGENTS.md\",\n \"~/.codewhale/global.md\",\n \"~/team/agents-shared.md\",\n]\n```\n\nRules:\n\n- Paths run through `expand_path` so `~` and env vars work.\n- These entries are **global-scope** instruction authority: they are charged\n to the global budget together with the global `AGENTS.md`/`instructions.md`\n fallbacks and `~/.claude/CLAUDE.md`.\n- A file keeps at most its first 100 KiB. A file larger than\n `[instruction_budget] file_max_bytes` (8 MiB by default) is skipped with a\n notice. Missing and empty files are skipped with a notice so a stale entry\n doesn't fail the launch.\n- Only user-owned config, profiles, and managed config may set this array.\n Project config (`<workspace>/.codewhale/config.toml`, or legacy\n `<workspace>/.deepseek/config.toml`) ignores `instructions` so a cloned repo\n cannot choose arbitrary local files to place into the prompt.\n\n### Instruction budget (`[instruction_budget]`, #6526)\n\nStanding instruction files are assembled into two independent scopes and\nbudgeted in **estimated tokens** — the same conservative estimator `/context`\nuses, never a `bytes/4` shortcut, which underestimates Cyrillic by 30–50%.\n\n- **global** — `~/.codewhale/AGENTS.md` (or the `.agents`/`.deepseek`\n fallbacks), the global `instructions.md` fallbacks, `~/.claude/CLAUDE.md`\n as a last resort when `claude_fallback` is on, and the\n `instructions = [...]` array.\n- **project** — the repository-root → workspace `AGENTS.md` chain, co-read\n foreign files, and the `.codewhale/rules`/`.claude/rules` block.\n\nEach scope is capped at `min(absolute_tokens, percent × usable_window)`. The\nusable window is the route window minus the output reservation the request\nwill actually carry and a safety reserve of 10% of the window (minimum 2,048\ntokens). A route whose context window is unknown has no usable window: the\npercentage ceilings and the guard are reported as `unknown` and only the\nabsolute caps apply.\n\n```toml\n[instruction_budget]\nglobal_budget_tokens = 100000\nproject_budget_tokens = 100000\nglobal_budget_percent = 25\nproject_budget_percent = 25\nstatic_prefix_guard_percent = 80\nfile_max_bytes = 8388608\npolicy = \"trim\"\nclaude_fallback = false\n```\n\nWith a 64K (65,536-token) output reservation:\n\n| window | usable | per scope | both scopes | guard 80% |\n| --- | --- | --- | --- | --- |\n| 128,000 | 49,664 | 12,416 | 24,832 | 39,731 |\n| 1,000,000 | 834,464 | 100,000 (absolute cap) | 200,000 | 667,571 |\n\nBehaviour:\n\n- **Nothing is trimmed while a source is within its scope budget.**\n- A scope over budget drops whole files from the broadest end first — the\n root-most ancestor goes first, the workspace file last — and a single file\n that alone exceeds the remaining budget keeps its **head** and drops its\n tail with a marker carrying bytes and estimated tokens.\n- Every instruction, rules, and configured file retains at most its first\n 100 KiB of text, ending in a marker that names the bytes kept and dropped.\n- Rules are charged to the project scope after the instruction chain and keep\n a head-first trim.\n- `file_max_bytes` is a hard ceiling that can only be lowered from its 8 MiB\n default: a larger file is **skipped**, never trimmed, and the read is\n bounded on the open file, so a FIFO or a growing file cannot exhaust memory.\n The same applies to a whitespace-only file and to files beyond the\n 50-per-directory rules cap.\n- `policy = \"trim\"` (default) trims and reports. `policy = \"refuse\"` stops the\n request before dispatch when a scope budget, the rules budget, or the\n 100 KiB retention would trim, or when the request prefix exceeds the guard;\n skips stay skips, because one bad file must not make the project unopenable.\n- `static_prefix_guard_percent` caps the request prefix: every system block\n plus the tool schemas the request carries. When it trips, the two scopes are\n narrowed largest first until the prefix fits, only the installed instruction\n block is replaced, and the change is recorded as a declared prefix change.\n If the prefix still exceeds the guard afterwards, the residual excess is\n reported rather than looped on. The guard must leave at least 10 points\n above the two scope percents. Prompts owned by a host (child agents,\n isolated chat, RLM) are measured and can be refused, but are never trimmed.\n- `claude_fallback = true` (opt in; default `false`): a directory with no\n `AGENTS.md` (or codewhale `instructions.md`) uses its `CLAUDE.md`. A skipped\n `AGENTS.md` counts as absent; a file already loaded from a wider scope (a\n symlink) is a reported duplicate, not a fallback trigger. Other tools'\n instruction files are never imported unless you opt in here or through\n `project_instruction_imports`.\n- Every loss — trimmed, skipped, refused — reaches the model as one\n runtime-provenance message whenever the list changes, and appears per source\n in `/context report`. `codewhale doctor`'s report is a preview built from the\n current files and config; it does not claim a request passed the tool-schema\n guard.\n- The table is **user config only**, like `instructions`: project config is\n ignored and a cloned repository cannot shrink the global budget or force\n `refuse`. An invalid table (unknown key, zero budget, percent outside\n 1–100, guard without room left, `file_max_bytes` outside 1–8388608) fails\n the launch.\n\n### Hooks\n\nHooks are a **TUI runtime feature**. They fire from the interactive TUI and\nthe engine turn loop it drives; `codewhale exec`, the CLI subcommands, the\napp-server / ACP surfaces, and the `workflow` tool do not fire them.\n\n[`docs/HOOKS.md`](HOOKS.md) is the authoritative reference for all eleven hook\nevents — their firing points, environment variables, stdin payloads, timeout\nand background semantics, and which three of them can steer Codewhale. The\nsections below cover the configuration surface and the steering contracts in\nmore depth.\n\nTwo contract points worth reading there before writing a hook:\n\n- `background = true` means **submitted and never awaited**. The hook still\n gets the documented stdin payload and the same timeout, but it has no exit\n code and cannot steer.\n- A condition that references context its event never carries (an `exit_code`\n condition outside `tool_call_after` / `on_error`, a `mode` condition on\n `shell_env`, a tool condition on a non-tool event) is **rejected at load**,\n logged, and shown in `/hooks list`. It does not silently never match.\n Rejection is per entry, so a broken hook never drops another one that merely\n shares its `name` or is likewise unnamed.\n\n### `/hooks` listing\n\nRun `/hooks` (or `/hooks list`) inside the TUI to see every\nconfigured lifecycle hook grouped by event, including each\nhook's name, command preview, effective timeout, and condition. When\n`[hooks].default_timeout_secs` is set it replaces every per-hook\n`timeout_secs`, and the listing shows that effective value and names the\noverride rather than echoing the per-hook number. A\n`default_timeout_secs = 0` is rejected at load — it would expire every hook\nin the config immediately — so the override is ignored, per-hook\n`timeout_secs` applies, the listing shows that per-hook value with no\noverride provenance, and the rejection appears under `configuration\nproblems`. The\n`[hooks].enabled` flag's state is shown at the top so it's\nobvious when hooks are globally suppressed, and any entry rejected\nat load is listed under `configuration problems` with the reason.\nHooks are configured under `[[hooks.hooks]]` entries — see\n[`docs/HOOKS.md`](HOOKS.md) for the full schema.\n\n### Mutable `message_submit` hooks\n\n`message_submit` hooks run before a submitted message is added to\nhistory or sent to the model. Unlike observer-only lifecycle hooks,\nnon-background `message_submit` hooks can replace or block the\nsubmitted text.\n\n```toml\n[[hooks.hooks]]\nevent = \"message_submit\"\ncommand = \"~/.codewhale/hooks/inject-context.sh\"\ntimeout_secs = 2\ncontinue_on_error = true\n```\n\nThe hook receives JSON on stdin:\n\n```json\n{\n \"event\": \"message_submit\",\n \"text\": \"original user text\",\n \"text_bytes\": 18,\n \"text_original_bytes\": 18,\n \"text_truncated\": false,\n \"session_id\": \"sess_12345678\",\n \"workspace\": \"/path/to/workspace\",\n \"mode\": \"agent\",\n \"model\": \"deepseek-chat\",\n \"total_tokens\": 1234\n}\n```\n\nThe entire serialized document is capped at 32 KiB. Codewhale retains the\nlargest UTF-8-safe `text` prefix that fits after JSON escaping and bounded\nmetadata, and the three `text_*` fields make truncation explicit. Immediate\nmessages, restored queue entries, merged steers, and prior-hook replacements\nall cross this same serialization boundary.\n\nIf the hook exits `0` and prints JSON with a non-empty string `text` field,\nthat value replaces the submitted text:\n\n```json\n{ \"text\": \"replacement user text\" }\n```\n\nExit `0` with empty stdout, or stdout JSON without `text`, leaves\nthe current text unchanged. A JSON `text` field must not be empty;\n`{\"text\":\"\"}` is treated as invalid stdout and ignored. Exit `2`\nblocks the submission before the turn starts; a structured `reason` field can\nprovide the bounded, redacted status message shown in the TUI. Raw stdout,\nstderr, and process-error text are not copied into denial receipts.\nOther non-zero exits follow the hook's `continue_on_error` setting.\nTimeouts and spawn failures are also surfaced as transient TUI status\nmessages when `continue_on_error = true` lets submission continue.\n\nMultiple `message_submit` hooks run in config order, and each hook\nreceives the text produced by the previous hook. Hooks marked\n`background = true` are observer-only and cannot transform or block\nthe message — they still receive the same stdin payload and the same\nenvironment, they are simply never awaited. Existing environment\nvariables remain available.\n`shell_env` hooks keep their existing `KEY=VALUE` stdout contract;\nJSON stdout contracts exist for `message_submit` (above) and\n`tool_call_before` (below).\n\n### `tool_call_before` decision hooks\n\n`tool_call_before` hooks run before each tool call executes. In\naddition to the legacy hard deny (exit code `2`, which always wins\nregardless of stdout), a foreground hook may print a JSON decision on\nstdout with exit code `0`:\n\n```json\n{\n \"decision\": \"allow\" | \"deny\" | \"ask\",\n \"reason\": \"human-readable explanation (used for deny)\",\n \"updatedInput\": { \"command\": \"ls -la\" },\n \"additionalContext\": \"text appended to the tool result for the model\"\n}\n```\n\nAll fields are optional. Empty stdout, non-JSON stdout, and JSON\nwithout a `decision` field behave exactly as before (allow). An\nunrecognized `decision` string logs a fixed warning without echoing the\nuntrusted value and is treated as allow.\n\n- `deny` blocks the tool; the model receives a permission-denied tool\n result containing `reason`.\n- `ask` forces the interactive approval prompt in Ask and Auto-Review even for\n tools that would otherwise auto-run. Full Access does not open tool-approval\n prompts, so hook `ask` does not downgrade that posture.\n- `updatedInput` must be a JSON object; it replaces the tool input\n before execution. When several hooks supply it, the last hook wins.\n- `additionalContext` is appended to the tool result sent back to the\n model as `[hook context] ...`. Multiple hooks' contexts are\n concatenated.\n\nWhen multiple hooks match, precedence is deny > ask > allow. Hooks\nmarked `background = true` cannot steer tool calls — they are\nsubmitted and never awaited, so they have no verdict to contribute.\n\nA foreground hook that produced no verdict at all — it hit its timeout, the\nprocess could not be started, or a strict process exited non-zero without an\nexplicit JSON decision — is not treated as permission. If\n*that* hook is configured with `continue_on_error = false`, the outcome\ndenies the tool call and the denial names the hook and a bounded reason.\nStrictness is read off the hooks that actually matched this call, so a\nstrict gate scoped to another tool cannot deny it, and a lenient hook's\ntimeout does not deny merely because a strict hook exists elsewhere in\nconfig. Under the default `continue_on_error = true` the outcome is\nlogged and the call proceeds.\n\n`reason` and `additionalContext` are capped (2 000 characters per field,\n8 000 for the concatenated context of one call) and stripped of control\ncharacters before they reach the TUI or the model.\n\nExample deny hook:\n\n```toml\n[[hooks.hooks]]\nevent = \"tool_call_before\"\ncommand = '''echo '{\"decision\":\"deny\",\"reason\":\"blocked by project policy\"}' '''\ncondition = { type = \"tool_name\", name = \"exec_shell\" }\n```\n\nExample ask hook (force approval for every MCP tool):\n\n```toml\n[[hooks.hooks]]\nevent = \"tool_call_before\"\ncommand = '''echo '{\"decision\":\"ask\"}' '''\ncondition = { type = \"tool_name\", name = \"mcp__*\" }\n```\n\nExample input rewrite:\n\n```toml\n[[hooks.hooks]]\nevent = \"tool_call_before\"\ncommand = \"~/.codewhale/hooks/clamp-shell-timeout.sh\"\ncondition = { type = \"tool_name\", name = \"exec_shell\" }\n```\n\nwhere the script reads the hook context, then prints\n`{\"updatedInput\": {...}}` with the adjusted arguments.\n\n`tool_name` conditions support `*` globs: `mcp__*` matches every MCP\ntool (e.g. `mcp__github__create_issue`) but not built-ins like\n`read_file`; exact names keep matching exactly. Other regex\nmetacharacters in the pattern are matched literally.\n\n### Project-local hooks\n\nRepositories can ship policy in `<workspace>/.codewhale/hooks.toml`,\nusing the same shape as the `[hooks]` table (top-level fields plus\n`[[hooks]]` entries). Project hooks are executable shell\nconfiguration, so Codewhale only loads them after the workspace has\nbeen trusted in user-owned config through the trust prompt or a\n`[projects.\"<workspace>\"] trust_level = \"trusted\"` entry. Session\n`/trust on` mode does not enable repo-supplied hooks by itself, and\nrepo-local legacy markers such as `.deepseek/trusted` do not enable\nproject hooks. Once trusted, project hooks are appended after global\nhooks from `config.toml`, so they run last and, for `updatedInput`,\nwin ties. A malformed trusted project file logs a warning and startup\nfalls back to global hooks only.\n\n```toml\n# .codewhale/hooks.toml\n[[hooks]]\nevent = \"tool_call_before\"\ncommand = '''echo '{\"decision\":\"deny\",\"reason\":\"no shell in this repo\"}' '''\ncondition = { type = \"tool_name\", name = \"exec_shell\" }\n```\n\n### Turn-end observer hooks\n\n`turn_end` hooks observe the end of each model turn after post-turn\nstate, usage totals, cost accounting, notifications, receipts, and\nqueue recovery have been updated. They receive JSON on stdin and are\nobserver-only: stdout is ignored, failures are logged as warnings, and\nthe hook cannot block user input, mutate the transcript, or change the\nnext queued follow-up.\n\nObserver-only UI events share one 32-entry queue and two persistent workers;\nthe terminal loop uses non-blocking submission and does not create a thread per\nevent. A full queue or unavailable dispatcher drops that observer event and is\nkept as an event-specific error toast, independent of ordinary agent/turn\nstatus text.\n\n```toml\n[[hooks.hooks]]\nevent = \"turn_end\"\ncommand = \"~/.codewhale/hooks/turn-audit.sh\"\ntimeout_secs = 2\ncontinue_on_error = true\n```\n\nThe payload includes common hook metadata plus post-turn accounting:\n\n```json\n{\n \"event\": \"turn_end\",\n \"session_id\": \"sess_12345678\",\n \"workspace\": \"/path/to/workspace\",\n \"mode\": \"agent\",\n \"created_at\": \"2026-07-12T10:30:00+00:00\",\n \"model_backed\": true,\n \"provider\": \"deepseek\",\n \"model\": \"deepseek-chat\",\n \"billing_surface\": null,\n \"turn_id\": \"turn_12345678\",\n \"status\": \"completed\",\n \"error\": null,\n \"duration_ms\": 1834,\n \"usage\": {\n \"input_tokens\": 1200,\n \"output_tokens\": 180,\n \"prompt_cache_hit_tokens\": 900,\n \"prompt_cache_miss_tokens\": 300,\n \"prompt_cache_write_tokens\": 0,\n \"reasoning_tokens\": null,\n \"reasoning_replay_tokens\": null\n },\n \"totals\": {\n \"session_tokens\": 1380,\n \"conversation_tokens\": 1380,\n \"input_tokens\": 1200,\n \"output_tokens\": 180\n },\n \"tool_count\": 2,\n \"queued_message_count\": 1,\n \"stop_hook_active\": false\n}\n```\n\n`created_at` anchors time-window pricing; `provider` and `model` identify the\neffective route used for model-backed turns. `billing_surface` is an optional,\nnon-secret classification derived from the endpoint that actually served the\nturn. Recognized StepFun routes emit `stepfun-payg` or `stepfun-plan`; the raw\nbase URL is never written to hook or runtime records. Runtime `TurnRecord`\nexports call the same field `effective_billing_surface`, which `scorecard`\naccepts as an alias. This keeps subscription quota separate from token-priced\nusage. Unrecognized and custom endpoints remain `null` and unpriced.\n\nShell-only lifecycle completions set `model_backed` to `false` and may report a\n`null` provider; offline scorecards exclude those records from model token and\ncost totals. Completion-only shell, manual-compaction, and purge events that do\nnot have a matching `TurnStarted` retain the observer notification with a\nsynthetic `lifecycle_<uuid>` turn id and the time the completion was observed.\n\nFor `interrupted` or `failed` turns, `status` reflects that terminal\nstate and `error` carries the engine error string when one is available.\n`stop_hook_active` is reserved for future re-entry protection and is\ncurrently always `false`.\n\n### Sub-agent lifecycle hooks\n\n`subagent_spawn` and `subagent_complete` hooks observe sub-agent lifecycle\nevents. They receive bounded JSON metadata on stdin and are observer-only:\nhook failures are logged as warnings and do not block sub-agent scheduling,\nchange prompts, or change results. For these observer events,\n`continue_on_error` has no effect: later matching hooks still run even when an\nearlier hook exits non-zero.\n\n```toml\n[[hooks.hooks]]\nevent = \"subagent_complete\"\ncommand = \"~/.codewhale/hooks/subagent-audit.sh\"\ntimeout_secs = 2\ncontinue_on_error = true\n```\n\n`subagent_spawn` receives:\n\n```json\n{\n \"event\": \"subagent_spawn\",\n \"agent_id\": \"agent_12345678\",\n \"session_id\": \"sess_12345678\",\n \"workspace\": \"/path/to/workspace\",\n \"mode\": \"agent\",\n \"model\": \"deepseek-chat\",\n \"total_tokens\": 1234,\n \"prompt_preview\": \"bounded prompt preview\",\n \"prompt_truncated\": false\n}\n```\n\n`subagent_complete` receives the same common fields plus terminal metadata:\n\n```json\n{\n \"event\": \"subagent_complete\",\n \"agent_id\": \"agent_12345678\",\n \"session_id\": \"sess_12345678\",\n \"workspace\": \"/path/to/workspace\",\n \"mode\": \"agent\",\n \"model\": \"deepseek-chat\",\n \"total_tokens\": 1234,\n \"status\": \"completed\",\n \"result_preview\": \"bounded result preview\",\n \"result_truncated\": false\n}\n```\n\nPreviews are capped before delivery so lifecycle hooks do not receive full\nsub-agent prompts, transcripts, or unbounded results. Use the transcript handle\nreturned by `agent` when full sub-agent details are needed.\n\n### Running-turn input\n\nComposer shortcuts keep the same role throughout a session:\n\n- **Enter** sends when idle and queues a next-turn follow-up while busy. The\n behavior does not change before versus after the provider's first token.\n- With an empty composer and queued follow-ups visible, **Enter** sends the\n oldest queued follow-up into the active turn now.\n- **Ctrl+Enter** (or **Cmd+Enter** when the terminal forwards it) explicitly\n steers the active turn. It sends normally when idle.\n- By default, **Shift+Enter**, **Alt+Enter**, and **Ctrl+J** insert a newline.\n- Set `composer_multiline_mode = true` to make **Enter** insert a newline and\n **Alt+Enter** send instead. **Shift+Enter** also sends when the terminal\n reports it distinctly; legacy terminals may report it as plain **Enter**.\n **Ctrl+J** still inserts a newline, and supported **Ctrl+Enter** /\n **Cmd+Enter** behavior is unchanged.\n- **Ctrl+G** and **Ctrl+S** only stash drafts; they never send or steer.\n\n### Composer stash (`/stash`, Ctrl+G / Ctrl+S)\n\nPress **Ctrl+G** in the composer to park the current draft to\n`~/.codewhale/composer_stash.jsonl`. `/stash list` shows parked\ndrafts with one-line previews and timestamps; `/stash pop`\nrestores the most recently parked draft (LIFO); `/stash clear`\nwipes the file. Capped at 200 entries; multiline drafts round-trip intact.\n**Ctrl+S** remains an alias in terminals that forward it; Cursor and VS Code\nreserve Ctrl+S for Save, so Ctrl+G is the portable default.\n\n## Settings File (Persistent UI Preferences)\n\ncodewhale also stores user preferences in:\n\n- `~/.codewhale/settings.toml` on new installs\n- `~/.deepseek/settings.toml` or the legacy platform config-dir\n `deepseek/settings.toml` when an existing settings file is present\n\nNotable settings include `auto_compact`, which uses a model-aware default-on\npolicy for known context windows up to the 1M-token V4 class. Automatic\ncompaction runs before the active model limit and carries the compacted summary\nforward into the next request. The trigger defaults to\n`auto_compact_threshold_percent = 80`. Users who prefer manual continuity can\npersist `auto_compact = false`; manual `/compact` / Ctrl+L remains available.\nYou can inspect or update these from the TUI with `/settings` and `/config`\n(interactive editor).\n\nCommon settings keys:\n\n- `theme` (`system`, `terminal`, `underwater`, `underwater-retro`,\n `shoreline`, `shoreline-light`, `dark`, `light`, `grayscale`,\n `catppuccin-mocha`, `tokyo-night`, `dracula`, `gruvbox-dark`, `claude`,\n `matrix`, `solarized-light`, `uwu`; default `underwater`): `underwater` is\n the dark navy fresh-install default, `shoreline` is its warm charcoal\n alternative and `shoreline-light` the light variant, `system` follows terminal\n background detection, `dark`/`light` use the Codewhale Whale pair,\n `terminal` inherits the host terminal, `grayscale` is the low-opinion\n black/white theme, and the named community presets apply across the TUI.\n Aliases such as `whale`, `mono`, `black-white`, `tokyonight`, and `gruvbox`\n are accepted. In Whale, cobalt blue owns action/focus, seafoam owns live\n work, Signal Gold owns human decisions and the whale, coral owns warnings,\n rose owns danger, violet owns Operate, and green remains completed/verified.\n Text labels, markers, and motion policy carry the same states when color is\n unavailable; color is never the only cue.\n User-authored overlays live only at `~/.codewhale/themes/<name>.json` (or\n `$CODEWHALE_HOME/themes/<name>.json`) and are selected with\n `/theme custom:<name>`. The filename is a bounded slug, symlinks and files\n over 64 KiB are refused, colors must be `#RRGGBB`, and unknown fields fail\n validation. `/theme schema` prints the embedded JSON Schema and `/theme path`\n shows the exact directory. An overlay names one compiled `base` theme and\n changes only listed semantic colors; it cannot include or read another file.\n Open `/theme` to browse valid overlays, preview them live, and keep the\n active `custom:<name>` selector when the picker is opened without moving.\n- `auto_compact` (on/off, model-aware default on for known context windows\n unless explicitly configured)\n- `auto_compact_threshold_percent` (10-100, default `80`): pre-send\n auto-compaction threshold used only when `auto_compact` is enabled.\n- `paste_burst_detection` (on/off, default on): fallback rapid-key paste\n detection for terminals that do not emit bracketed-paste events. This is\n independent of terminal bracketed-paste mode.\n- `work_surface_placement` (`bottom`, `top`, `left`, `right`, or `off`;\n default `bottom`): places the workbar — Tasks / To-do / Workers — under the\n composer (the default bottom workbar), above the transcript, in a side\n workbar, or hides it entirely (`off`). Side choices fall back to the top\n layout on narrow terminals without changing the saved preference. Set it\n live with `/config work_surface_placement right --save` (or `left` / `top` /\n `bottom` / `off`).\n- `rail_panel` (`tasks`, `agents`, `background`, `files`, `notepad`,\n `context`, `git`, `price`; default `tasks`, alias key `rail`): which panel\n the workbar shows. Panel selection is orthogonal to placement. `tasks` is\n the full live work list (to-dos, then sub-agents); `agents` narrows to the\n sub-agent rows; `background` lists background shells and automations;\n `files` lists touched files; `notepad` shows the workspace notes; `context`\n is a read-only session-facts list; `git` shows branch status; `price`\n shows cost. In every panel except `context`, rows are selectable and\n clickable and open their detail surface. `Alt+!`/`Alt+@`/`Alt+#`/`Alt+$`\n switch panels live.\n- `work_surface_top_height` (2–16) and `work_surface_side_width` (26–80):\n ceilings for the top strip's height and the side workbar's width. Both are\n normally persisted by dragging the divider rather than edited by hand; the\n strip still auto-fits its content below the ceiling.\n- `focus_texture` (`off`, `scrim`, or `grain`; default `off`): focus-context\n texture for modal views. `scrim` dims the already-rendered background\n outside the focused modal toward the theme surface; `grain` sprinkles\n sparse dots over blank cells there. The texture is static (no time\n component, so it is unaffected by `low_motion`), never writes over a cell\n that carries text, and preserves the 4.5:1 body-text contrast floor\n wherever both colors are resolvable. It is skipped entirely on frames\n below the ambient-life minimum size and when the focused modal already\n covers 90% or more of the frame. Set it live with\n `/config focus_texture scrim --save`.\n- `mention_menu_limit` (integer, default `128`): maximum number of\n `@`-mention popup candidates retained before the composer renders the\n visible window. The visible rows still depend on terminal height.\n- `mention_walk_depth` (integer, default `10`): maximum workspace depth for\n `@`-mention completion walks. Set to `0` for unlimited depth in deeply\n nested workspaces; keep the default in very large repos unless needed.\n- `mention_menu_behavior` (`fuzzy`, `browser`; default `fuzzy`): controls how\n `@`-mention completions are populated. `fuzzy` searches the workspace and\n applies mention frecency. `browser` lists only the immediate children of the\n currently typed directory segment in deterministic alphabetical order.\n- `show_thinking` (on/off)\n- `thinking_default_expanded` (on/off, default off): renders thinking blocks\n expanded initially when `show_thinking` is enabled. Space still toggles the\n selected block, and it decides only the blocks you have not touched: a block\n you expanded or collapsed yourself keeps that state if you change this\n setting later. This is useful in SSH/tmux environments where the Space\n binding may be intercepted.\n- `thinking_preview_lines` (integer, default `2`): how many body rows a\n **collapsed** completed thought still shows. `0` is header-only; `10` is\n the older dump. Live streaming preview is unchanged. Expand a block with\n Space, or set `thinking_default_expanded` to open every block.\n- `help_expand_groups` (on/off, default off): start Help/shortcuts with every\n group expanded. Default folds the long tail (Grok-style); type-to-filter\n still unfolds matches.\n- `pin_last_prompt` (on/off, default on): pin the last user prompt at the top\n of the transcript viewport after it scrolls off.\n- `show_tool_details` (on/off)\n- `inline_diffs` (`full`, `summary`, or `off`; default `full`): controls the\n inline presentation of successful structured File mutations. `full` shows a\n bounded red/green diff and semantic statistics, `summary` keeps only the\n statistics, and `off` keeps the calm changed-file outcome. All three retain\n the exact applied change in the selected File receipt's Alt/Option+V detail.\n Failure and cancellation never render a successful diff. Save the choice\n with `/config inline_diffs <mode> --save`.\n- `locale` (`auto`, `en`, `ja`, `zh-Hans`, `zh-Hant`, `pt-BR`, `es-419`, `vi`,\n `ko`; default `auto`): UI chrome locale. `auto` checks `LC_ALL`,\n `LC_MESSAGES`, then `LANG`; unsupported locale selections resolve to English.\n Every shipped pack holds full `en.json` parity, so no string falls back\n to English. The runtime also exposes the resolved locale in the system\n prompt as the fallback natural language for V4 reasoning and replies when the\n latest user message is ambiguous. Clear user language still takes priority;\n Chinese turns should produce Chinese `reasoning_content` and Chinese final\n replies even when the resolved locale is English.\n- `background_color` (`#RRGGBB`, `RRGGBB`, or `default`): optional main TUI\n background color applied to the root, header, transcript, and footer\n surfaces while preserving panel contrast.\n- `cost_currency` (`usd`, `cny`; default `usd`): currency used by the footer,\n context panel, `/cost`, `/tokens`, and long-turn notification summaries. The\n aliases `rmb` and `yuan` normalize to `cny`.\n- `default_mode` (`agent`, `plan`, or `operate`; legacy values are accepted for migration but are not live mode vocabulary)\n- `launch_screen` (legacy, migration-only): this historical `on`/`off` value\n is still accepted when reading an existing settings file, but it no longer\n changes behavior and is omitted from new saves. A fresh interactive launch\n always opens Tideline Startup; only an explicit resume or an explicit\n initial prompt enters the live session directly.\n- `sidebar_focus` (legacy, migration-only): the classic right sidebar this key\n configured was removed in the 0.9.4 rail unification. The key is still read\n once so old settings carry forward, then folds into the live keys:\n `pinned`/`work`/`plan`/`todos` become `rail_panel = \"pinned\"`,\n `agents`/`subagents` become `rail_panel = \"agents\"`, `context`/`session`\n become `rail_panel = \"context\"`, `tasks`/`auto` (the old default) become the\n `tasks` panel, `sessions` enables `sessions_rail`, and `hidden` turns the\n workbar off via `work_surface_placement = \"off\"`. An explicit `rail_panel`\n in the file always wins over the migrated value. Configure the workbar with\n `rail_panel` and `work_surface_placement`, not this key.\n- `sessions_rail` (`on`/`off`; default `off`): show the persistent Sessions\n list in the workbar. Rows list this workspace's recent\n non-archived sessions, newest first, with the active one marked; activating a\n row opens the session picker preselected on it (`/sessions open <id>`), so\n resume keeps its single implementation. Rows are projected from cached\n session metadata — the list never reads a transcript per frame, and never\n contacts a provider.\n- `session_auto_resume` (`on`/`off`; default `off`): reattach to this\n workspace's most recent session when Codewhale starts. Off by default so\n plain `codewhale` keeps starting fresh. `--resume`, `--continue`, and\n `--fresh` always take precedence. When it is on, startup still refuses to\n resume a session that is archived, fails to load, or is recorded against a\n different workspace; each of those falls back to a fresh transcript and says\n which session was skipped and why. It applies to the interactive launch only\n — `codewhale \"<prompt>\"` and `codewhale exec` are never silently prefixed\n with a prior conversation.\n- `max_input_history` (number of submitted input history entries; cleared\n drafts are also kept locally for composer history search). Note the spelling:\n the serde field on disk is `max_input_history`\n (`crates/tui/src/settings.rs:426`, default 100). `max_history` is the key\n name accepted by `/config set` and `settings.set()` (`settings.rs:1388`), not\n a settings.toml key — writing `max_history` into the file is silently\n ignored.\n- `default_model` (model name override)\n\n`/task digest` (alias `/tasks digest`) renders the canonical Work Graph\noperations and four-state To-do list as plain text, running work first. It\nreads the same snapshots as the styled Work surface and owns no parallel\nprogress state.\n\nPlan and Work are the everyday visible modes in the UI; Operate is an explicit\npreview entry while its Workflow control surface is still being built. Switch\nbetween them with `/mode`. For compatibility, older settings files with\n`default_mode = \"normal\"` still load as `agent`.\n\nLocalization scope is tracked in [LOCALIZATION.md](LOCALIZATION.md). The v0.7.6\ncore pack covers high-visibility TUI chrome only; provider/tool schemas,\npersonality prompts, and full documentation remain English unless explicitly\ntranslated later.\n\nReadability semantics:\n\n- Selection uses a unified style across transcript, composer menus, and modals.\n- Footer hints use a dedicated semantic role (`FOOTER_HINT`) so hint text stays readable across themes.\n\n### Token Quantities and Drivers\n\nDeepSeek V4 prefix caching makes token labels matter. These quantities are kept\nseparate:\n\n| Quantity | Meaning | Allowed to drive |\n|---|---|---|\n| Active request input estimate | Conservative estimate of the next request's live system prompt and transcript payload. | Header/footer context percent, auto-compaction trigger, opt-in Flash seam trigger, and emergency overflow preflight. |\n| Reserved response headroom | The effective request cap plus `1024` safety tokens. Normal no-override requests start at `65536`; a smaller route/provider ceiling narrows that value, and an explicit output override raises it only within the resolved route window and output ceiling. The identical cap reaches the wire and drives preflight, except on a route that states an output limit above half its known context window with no explicit output override: there compaction aims to keep a quarter of the window free (at most `65536`, and never more than the request cap), preflight refuses a request only when fewer than `128` output tokens would fit, and every request's `max_tokens` is what the window has left after that request's estimated input and the `1024` safety tokens, never above the request cap. Reasoning effort does not add a second hidden reservation. A separately published route input ceiling independently clamps the spendable input budget. | Emergency overflow budget checks only. |\n| Cumulative API usage | Provider-reported input plus output tokens summed across completed API calls; multi-tool turns may count the same stable prefix more than once. | Session usage and approximate cost telemetry only. |\n| Prompt cache hit/miss | Provider cache telemetry for the most recent call when available. | Cache-hit display and cost estimation only; never compaction or seam triggers. |\n| Context percent | Active request input estimate divided by the model context window. | Display only; it mirrors the active-input basis used by context safeguards. |\n| Cost estimate | Approximate spend from provider usage and configured DeepSeek rates. | Display only. |\n\nFor known context-window models, including 1M-class V4 models, replacement\ncompaction is enabled by default unless the user explicitly configures\n`auto_compact = false`. It fires at the active model's compaction threshold and\nreplaces old history with recent user context followed by one ordinary\ncheckpoint message. The standing system prompt remains unchanged. Unknown model\nids remain opt-in.\n\n### Command Migration Notes\n\nIf you are upgrading from older releases:\n\n- Old: `/codewhale`\n New: `/links` (aliases: `/dashboard`, `/api`)\n- Old: `/set model deepseek-reasoner`\n New: `/config` and edit the `model` row to `deepseek-v4-pro` or `deepseek-flash`\n- Old: visible `Normal` mode or `default_mode = \"normal\"`\n New: use `Agent` / `default_mode = \"agent\"`; legacy `normal` still maps to `agent`\n- Old: discover `/set` in slash UX/help\n New: use `/config` for editing and `/settings` for read-only inspection\n\n## Key Reference\n\n### Kimi Code membership model IDs\n\nThe exact `https://api.kimi.com/coding/v1` endpoint accepts `k3`, `k3-256k`,\n`kimi-for-coding`, and `kimi-for-coding-highspeed`. Use `k3-256k` for a fixed\n262,144-token K3 window; use bare `k3` with `context_window = 1048576` only\nwhen the membership plan includes the 1M entitlement. Both K3 ids use the same\nreasoning contract, and all four membership ids omit generic sampling fields.\n\n### Core keys (used by the TUI/engine)\n\n- `provider` (string, optional): `deepseek` (default), `deepseek-anthropic`, `nvidia-nim`, `openai`, `atlascloud`, `wanjie-ark`, `volcengine`, `openrouter`, `xiaomi-mimo`, `novita`, `fireworks`, `siliconflow`, `arcee`, `siliconflow-CN`, `moonshot`, `sglang`, `vllm`, `ollama`, `ollama-cloud`, `huggingface`, `modelscope`, `together`, `qianfan`, `openai-codex`, `anthropic`, `openmodel`, `zai`, `stepfun`, `minimax`, `deepinfra`, `sakana`, `longcat`, `opencode-go`, `meta`, `mistral`, `telecomjs`, `xai`, `orcarouter`, `modelstudio-token-plan`, `google`, `edenai`, or `custom`. Legacy `deepseek-cn` configs are still accepted as an alias for `deepseek`; DeepSeek uses the same official host [`https://api.deepseek.com`](https://api-docs.deepseek.com/) worldwide. `deepseek-anthropic` targets DeepSeek's Anthropic Messages-compatible endpoint at `https://api.deepseek.com/anthropic` using `DEEPSEEK_API_KEY`; `nvidia-nim` targets NVIDIA's NIM-hosted DeepSeek endpoints through `https://integrate.api.nvidia.com/v1`; `openai` targets a generic OpenAI-compatible endpoint, defaulting to `https://api.openai.com/v1`; `atlascloud` targets AtlasCloud's OpenAI-compatible endpoint at `https://api.atlascloud.ai/v1`; `wanjie-ark` targets Wanjie Ark's OpenAI-compatible endpoint at `https://maas-openapi.wanjiedata.com/api/v1`; `volcengine` targets Volcengine Ark's OpenAI-compatible coding endpoint at `https://ark.cn-beijing.volces.com/api/coding/v3`; `openrouter` targets `https://openrouter.ai/api/v1`; `xiaomi-mimo` targets Xiaomi MiMo's OpenAI-compatible endpoint, using `https://token-plan-sgp.xiaomimimo.com/v1` by default for Token Plan keys (`tp-...`) and `https://api.xiaomimimo.com/v1` for pay-as-you-go keys. For Token Plan accounts outside the Singapore default, set `base_url` explicitly or use `mode = \"token-plan-cn\"` for China and `mode = \"token-plan-ams\"` for Europe/Amsterdam; `novita` targets `https://api.novita.ai/openai/v1`; `fireworks` targets `https://api.fireworks.ai/inference/v1`; `siliconflow` targets SiliconFlow, defaulting to `https://api.siliconflow.com/v1`; `arcee` targets Arcee AI's OpenAI-compatible endpoint at `https://api.arcee.ai/api/v1`; `siliconflow-CN` targets the SiliconFlow China regional endpoint through `[providers.siliconflow_cn]`; `moonshot` targets Moonshot/Kimi, defaulting to `https://api.moonshot.ai/v1`; `sglang` targets a self-hosted OpenAI-compatible endpoint, defaulting to `http://localhost:30000/v1`; `vllm` targets a self-hosted vLLM OpenAI-compatible endpoint, defaulting to `http://localhost:8000/v1`; `ollama` targets Ollama's OpenAI-compatible endpoint, defaulting to `http://localhost:11434/v1`; `huggingface` targets Hugging Face Inference Providers at `https://router.huggingface.co/v1`; `modelscope` targets ModelScope's OpenAI-compatible inference API at `https://api-inference.modelscope.cn/v1`; `together` targets Together AI at `https://api.together.xyz/v1`; `qianfan` targets Baidu Qianfan at `https://api.baiduqianfan.ai/v1`; `openai-codex` targets official ChatGPT plan inference with Codewhale-owned OAuth at `https://api.openai.com/v1`; `anthropic` targets Claude's native Messages API; `openmodel` targets OpenModel's Anthropic-compatible Messages API at `https://api.openmodel.ai`; `zai` targets Z.ai at `https://api.z.ai/api/coding/paas/v4`; `stepfun` targets StepFun at `https://api.stepfun.ai/v1`; `minimax` targets MiniMax at `https://api.minimax.io/v1`; `deepinfra` targets DeepInfra at `https://api.deepinfra.com/v1/openai`; `sakana` targets Sakana AI Fugu at `https://api.sakana.ai/v1`; `longcat` targets Meituan LongCat at `https://api.longcat.chat/openai/v1`; `opencode-go` targets the subscription-backed OpenCode Go model-aware route (Chat Completions, Responses, or Messages according to the documented model) at `https://opencode.ai/zen/go/v1`; `meta` targets Meta Model API; `mistral` targets Mistral AI's OpenAI-compatible endpoint at `https://api.mistral.ai/v1`; `telecomjs` targets TelecomJS TokenHub at `https://aigw.telecomjs.com/v1`; and `xai` targets xAI's API-key or OAuth route.\n- `opencode-zen` (string provider value): selects the model-aware OpenCode Zen gateway through `[providers.opencode_zen]`. The default base URL is `https://opencode.ai/zen/v1`, the default model is `gpt-5.6`, and credentials come from `api_key`, `OPENCODE_ZEN_API_KEY`, or fallback `OPENCODE_API_KEY`—never ChatGPT/Codex OAuth. `OPENCODE_ZEN_BASE_URL` and `OPENCODE_ZEN_MODEL` are accepted. The selected model's wire comes from the curated Zen snapshot, then from the AI SDK package its Models.dev `opencode` row names (refresh with `codewhale models --update`): GPT, Grok, and Muse Spark use Responses; Claude and most Qwen rows use Anthropic Messages; DeepSeek, MiniMax, GLM, Kimi, `qwen3.8-max`, and the free rows use Chat Completions. Gemini, Models.dev rows marked `deprecated`, and models no loaded catalog lists fail closed because Codewhale has no proven supported wire contract for them. See the exact current model groups in [`PROVIDERS.md`](PROVIDERS.md#opencode-zen-protocol-catalog).\n- `minimax-anthropic` (string provider value): selects MiniMax's Anthropic-compatible Messages route through `[providers.minimax_anthropic]`. The default Base URL is `https://api.minimax.io/anthropic`; set `https://api.minimaxi.com/anthropic` for China. Keep the `/anthropic` suffix because Codewhale appends `/v1/messages`. The route uses `MINIMAX_API_KEY` and defaults to `MiniMax-M3`; `MiniMax-M2.7` is also registered. Official M3 input modalities are text, image, and video, with adaptive or disabled thinking. M2.7 is text-only and always keeps thinking enabled.\n- `api_key` (string, required for hosted providers): must be non-empty for DeepSeek/hosted providers (or set the provider API key env var). Self-hosted SGLang, vLLM, and local `ollama` can omit it. `ollama-cloud` requires a key saved for that provider or supplied by `OLLAMA_CLOUD_API_KEY`, then `OLLAMA_API_KEY`.\n- `auth_mode` (string, optional provider-table key): selects a provider-specific authentication contract. Kimi Code membership uses `auth_mode = \"api_key\"` (or omit the field), a key created in the [Kimi Code console](https://www.kimi.com/code/console), `base_url = \"https://api.kimi.com/coding/v1\"`, and bare `model = \"k3\"` for K3. Codewhale gives that route a safe 262,144-token baseline; set `context_window = 1048576` only when the Kimi Code plan includes 1M access (Allegretto and above). `k3[1m]` is a Claude Code-only convention, not an API model ID, and Codewhale rejects it instead of silently changing the wire model or assuming an entitlement. `model = \"kimi-for-coding\"` remains the valid K2.7 compatibility route available to all Kimi Code members. Legacy `auth_mode = \"kimi_oauth\"` fails closed with API-key guidance and never probes, reads, refreshes, or rewrites `kimi_cli`/`kimi_code_cli` credential files. First-class OAuth requires Codewhale's own vendor-registered client identity and remains tracked in #4417.\n- `base_url` (string, optional, `[providers.<name>]` key; see [Legacy top-level `base_url` and `api_key`](#legacy-top-level-base_url-and-api_key) for the older top-level spelling): defaults to `https://api.deepseek.com/beta` for DeepSeek's OpenAI-compatible Chat Completions API, including legacy `provider = \"deepseek-cn\"` configs. Other defaults are `https://api.deepseek.com/anthropic` for `deepseek-anthropic`, `https://integrate.api.nvidia.com/v1` for `nvidia-nim`, `https://api.openai.com/v1` for `openai`, `https://api.atlascloud.ai/v1` for `atlascloud`, `https://maas-openapi.wanjiedata.com/api/v1` for `wanjie-ark`, `https://ark.cn-beijing.volces.com/api/coding/v3` for `volcengine`, `https://openrouter.ai/api/v1` for `openrouter`, `https://token-plan-sgp.xiaomimimo.com/v1` for `xiaomi-mimo` when the API key starts with `tp-...` and `https://api.xiaomimimo.com/v1` otherwise, `https://api.novita.ai/openai/v1` for `novita`, `https://api.fireworks.ai/inference/v1` for `fireworks`, `https://api.siliconflow.com/v1` for `siliconflow`, `https://api.siliconflow.cn/v1` for `siliconflow-CN`, `https://api.arcee.ai/api/v1` for `arcee`, `https://api.moonshot.ai/v1` for `moonshot`, `https://api.minimax.io/v1` for `minimax`, `https://api.openmodel.ai` for `openmodel`, `https://api.z.ai/api/coding/paas/v4` for `zai`, `https://api.stepfun.ai/v1` for `stepfun`, `https://api.deepinfra.com/v1/openai` for `deepinfra`, `https://api.sakana.ai/v1` for `sakana`, `https://router.huggingface.co/v1` for `huggingface`, `https://api-inference.modelscope.cn/v1` for `modelscope`, `https://api.together.xyz/v1` for `together`, `https://api.baiduqianfan.ai/v1` for `qianfan`, `https://api.openai.com/v1` for `openai-codex`, `https://api.anthropic.com` for `anthropic`, `https://api.mistral.ai/v1` for `mistral`, `http://localhost:30000/v1` for `sglang`, `http://localhost:8000/v1` for `vllm`, `http://localhost:11434/v1` for `ollama`, and `https://ollama.com/v1` for `ollama-cloud`. Set `base_url = \"https://token-plan-cn.xiaomimimo.com/v1\"` for China-region Xiaomi MiMo Token Plan accounts or `base_url = \"https://token-plan-ams.xiaomimimo.com/v1\"` for Europe/Amsterdam accounts. Mistral-specific reasoning fields and polymorphic replay are enabled only on the documented first-party HTTPS `/v1` hosts; a custom Mistral base URL keeps generic Chat semantics. Set `https://api.deepseek.com` or `https://api.deepseek.com/v1` explicitly to opt out of DeepSeek beta features.\n- `ollama-cloud` route: select `provider = \"ollama-cloud\"`, configure `[providers.ollama_cloud]` when overriding the default `https://ollama.com/v1` / `gpt-oss:120b` tuple, and save a key from [Ollama account settings](https://ollama.com/settings/keys) with `codewhale auth set --provider ollama-cloud`. Ambient precedence is `OLLAMA_CLOUD_API_KEY`, then `OLLAMA_API_KEY`; arbitrary Ollama model IDs pass through unchanged.\n- Legacy Ollama Cloud migration: a released `provider = \"ollama\"` config whose normalized `[providers.ollama].base_url` is exactly `https://ollama.com/v1` is upgraded to the `ollama-cloud` runtime identity in memory. Only that exact tuple may read its old `ollama` provider table and secret slot. The config and secrets are never rewritten, and neighboring paths, HTTP downgrades, lookalike hosts, or an explicit `ollama-cloud` selection never consume the fallback.\n- `telecomjs` base URL and catalog: `[providers.telecomjs]` defaults to `https://aigw.telecomjs.com/v1`; `TELECOMJS_BASE_URL` overrides it. With `TELECOMJS_API_KEY`, `/models` refreshes a key-scoped catalog without mixing rows into another provider.\n- `edenai` gateway: select `provider = \"edenai\"`; `[providers.edenai]` defaults to `https://api.edenai.run/v3` and `deepseek/deepseek-v4-pro`. `EDENAI_API_KEY`, `EDENAI_BASE_URL`, and `EDENAI_MODEL` are accepted. Use `EDENAI_BASE_URL = \"https://api.eu.edenai.run/v3\"` for Eden AI's documented EU endpoint; the default `deepseek/deepseek-v4-pro` is only listed on the global catalog, so pair the EU endpoint with an EU-listed model such as `qwen/deepseek-v4-pro` via `EDENAI_MODEL` or `model`. The provider refreshes Eden AI's `/models` catalog, but leaves model-specific reasoning controls untouched because the gateway spans multiple model families.\n- `codewhale` (Codewhale API): select `provider = \"codewhale\"`; `[providers.codewhale]` defaults to `https://api.codewhale.net/v1` and `deepseek/deepseek-v4-pro`. The credential is a Codewhale account API key (`cwc_key_…`) with the `models:infer` scope, read from `CODEWHALE_API_KEY` or the `codewhale` secret-store slot; `codewhale account api-keys create --name <name> --use` mints one and saves it locally. `CODEWHALE_API_BASE` overrides the origin and must be HTTPS except on loopback. The model catalog is the account's own authenticated `GET /v1/models`: ids are `provider/model` and each row states its wire (`chat-completions` → `/v1/chat/completions`, `anthropic-messages` → `/v1/messages`, `responses` → `/v1/responses`). Connect the underlying provider keys with `codewhale account keys set <provider>`.\n- `concentrate` gateway: select `provider = \"concentrate\"`; `[providers.concentrate]` defaults to `https://api.concentrate.ai/v1` and `deepseek-v4-pro` over the OpenAI Responses wire. `CONCENTRATE_API_KEY`, `CONCENTRATE_BASE_URL`, and `CONCENTRATE_MODEL` are accepted. Model ids pass through verbatim (`gpt-5.6-sol`, `openai/gpt-5.6-sol`, or `concentrate/auto` for the gateway router). BYOK only; see [PROVIDERS.md](PROVIDERS.md#concentrate-notes).\n- `mistral` model and reasoning contract: `[providers.mistral]` defaults to `mistral-code-latest`; `MISTRAL_MODEL` overrides it and the generic `CODEWHALE_MODEL` override wins when both are set. The current picker also lists `mistral-medium-latest`, `mistral-small-latest`, and `mistral-large-latest`. On exact first-party HTTPS `/v1` routes, Medium and Small accept only `reasoning_effort = \"none\" | \"high\"` and replay polymorphic thinking blocks. Deprecated native Magistral IDs may still be configured explicitly, remain always-reasoning, and never receive the adjustable effort field.\n- `context_window` (integer, optional provider-table key): override the total context window for the active `[providers.<name>]` route when an OpenAI-compatible gateway, hosted model alias, or self-hosted runtime has a different limit than Codewhale's static model table. For example, `[providers.openai] context_window = 1000000` lets an OpenAI-compatible DashScope/Qwen route budget against a 1M-token window instead of the conservative fallback. For Kimi Code K3, keep `model = \"k3\"` and set `[providers.moonshot] context_window = 1048576` only when the membership plan includes 1M access; otherwise omit it to retain the 262,144-token safe baseline. The value must be greater than 0 and affects prompt context notes, compaction thresholds, context-pressure checks, and request output caps. Full resolution order, and how to see which rung produced the current window: [Context length (context window)](#context-length-context-window).\n- `path_suffix` (string, optional provider-table key): override the chat-completions path for OpenAI-compatible gateways that do not serve `/v1/chat/completions`. For example, `[providers.openai] path_suffix = \"/chat/completions\"` sends chat requests to the unversioned base URL plus `/chat/completions`; `models` and `beta/*` requests keep their normal routing.\n- `reasoning_stream_style` (string, optional provider-table key): override how streaming reasoning is separated from answer text for the active provider route. Use `separate_field` for `reasoning_content` / `reasoning` deltas, `inline_tags` for gateways that stream `<think>...</think>` inside `delta.content`, or `none` to render incoming content exactly as answer text. When unset, every Chat Completions route uses `separate_field` (Mistral's first-party route uses its typed thinking blocks); set `none` only for a gateway that streams its answer inside `reasoning_content`.\n- `[providers.<name>.auth]` (table, optional): provider-scoped auth source metadata. `source = \"command\"` stores a command argv plus optional `timeout_ms`; `source = \"secret\"` stores a `secret_id`. This slice lets provider readiness, `/provider`, and doctor JSON report the auth source class without exposing command argv output or secret values; executing commands and resolving external secret material is handled by the follow-up resolver work.\n- `insecure_skip_tls_verify` (bool, optional provider-table key): legacy compatibility key, disabled by default. When true on the active provider table, provider clients reject the configuration instead of skipping TLS certificate verification. Use `SSL_CERT_FILE` for corporate or private CA bundles; `codewhale doctor` reports stale uses of this setting.\n- `default_text_model` (string, optional): defaults to `deepseek-flash` for DeepSeek and `deepseek-anthropic`, `gpt-5.6` for OpenAI, `grok-4.6` for xAI, `deepseek-ai/deepseek-v4-pro` for NVIDIA NIM, `deepseek-ai/deepseek-v4-flash` for AtlasCloud, `deepseek-reasoner` for Wanjie Ark, `DeepSeek-V4-Pro` for Volcengine Ark, `deepseek/deepseek-v4-pro` for OpenRouter and Novita, `mimo-v2.6-pro` for Xiaomi MiMo, `accounts/fireworks/models/deepseek-v4-pro` for Fireworks, `deepseek-ai/DeepSeek-V4-Pro` for SiliconFlow and DeepInfra, `trinity-large-thinking` for Arcee AI, `kimi-k2.7-code` for Moonshot, `MiniMax-M3` for MiniMax, `GLM-5.3` for Z.ai, `step-3.7-flash` for StepFun, `ernie-4.0-turbo-8k` for Qianfan, `fugu` for Sakana AI, `deepseek-ai/DeepSeek-V4-Pro` for SGLang/vLLM, no fixed id for local Ollama (Codewhale adopts a chat-capable tag from the live local catalog — coder or tool-capable tags first, then the largest context — and never an embedding or reranker model), and `gpt-oss:120b` for Ollama Cloud. Hugging Face and Together AI both default to `deepseek-ai/DeepSeek-V4-Pro`; `openai-codex` defaults to `gpt-6.1-sol`; `anthropic` defaults to `claude-sonnet-5-5`; `openmodel` defaults to `deepseek-v4-flash`. Current public DeepSeek IDs include `deepseek-v4-pro` and `deepseek-flash` (V4.1 Flash, shipped as the unversioned id), both with 1M context windows, 384K max output, and thinking mode enabled by default. DeepSeek's live pricing/model page now labels the Pro backend `DeepSeek-V4-Pro-0813`; the callable API ID remains `deepseek-v4-pro`, so Codewhale does not send the backend label or the Claude Code-specific `deepseek-v4-pro[1m]` selector. DeepSeek retired `deepseek-chat` and `deepseek-reasoner` on July 24, 2026; direct first-party routes migrate both to `deepseek-flash`, with omitted reasoning settings preserving their former non-thinking (`off`) and thinking (`high`) intent. DeepSeek has also retired the `deepseek-v4-flash` and `deepseek-v4-flash-vision-exp` models: it still accepts both names, serves them with V4.1 Flash, and bills them at the `deepseek-flash` price, which Codewhale applies to turns from 2026-09-10 04:00 UTC. The `flash`, `deepseek-v4flash`, `flash-vision`, and `deepseek-v4flashvisionexp` shorthands select `deepseek-flash`. Explicit `reasoning_effort` wins, and provider-owned ids on Wanjie Ark, aggregators, self-hosted runtimes, and custom endpoints are not globally rewritten. SiliconFlow retains its own mapping: `deepseek-reasoner` and `deepseek-r1` select its Pro model while `deepseek-chat` and `deepseek-v3` select Flash. Provider-specific mappings translate `deepseek-v4-pro` / `deepseek-v4-flash` to each provider's model ID where supported. OpenRouter also recognizes recent large IDs such as `arcee-ai/trinity-large-thinking`, `minimax/minimax-m3`, `minimax/minimax-m2.7`, `xiaomi/mimo-v2.5-pro`, `qwen/qwen3.6-flash`, `qwen/qwen3.6-35b-a3b`, `qwen/qwen3.6-max-preview`, `qwen/qwen3.6-27b`, `qwen/qwen3.6-plus`, `qwen/qwen3.7-max`, `google/gemma-4-31b-it`, `moonshotai/kimi-k2.7-code`, `moonshotai/kimi-k2.6`, `nvidia/nemotron-3-nano-omni-30b-a3b-reasoning:free`, and `nvidia/nemotron-3-ultra-550b-a55b`; direct Arcee uses bare IDs such as `trinity-large-thinking` and `trinity-large-preview`; direct Moonshot recognizes `kimi-k3`, `kimi-k2.7-code`, and `kimi-k2.6`. The exact Kimi Code endpoint recognizes bare `k3` for K3 and `kimi-for-coding` for K2.7; those membership IDs are distinct from the direct Moonshot IDs and are never rewritten across routes. Direct MiniMax recognizes `MiniMax-M3` and the documented M2.x chat model IDs; direct Z.ai recognizes `GLM-5.3` (the default), `GLM-5.2`, `GLM-5.1`, and `GLM-5-Turbo`, and OpenRouter recognizes the matching `z-ai/glm-5.1`, `z-ai/glm-5.2`, `z-ai/glm-5.3`, and `z-ai/glm-5-turbo` IDs — `GLM-5.3` has been live on the Z.ai Coding Plan since 2026-08-13; it inherits its catalog metadata from `GLM-5.2` until Z.ai publishes distinct 5.3 numbers and carries no price, and an explicit `GLM-5.2` selection keeps its own id; direct Sakana recognizes `fugu` and `fugu-ultra-20260615`; direct Xiaomi MiMo recognizes chat IDs `mimo-v2.6-pro`, `mimo-v2.6-flash`, and `mimo-v2.5-pro-ultraspeed` (the retired `mimo-v2.5-pro` and `mimo-v2.5` are sent as `mimo-v2.6-pro` and `mimo-v2.6-flash`), while TTS IDs are selected through `codewhale speech` / `tts`. Generic `openai`, `atlascloud`, `wanjie-ark`, `xiaomi-mimo`, `arcee`, `moonshot`, `minimax`, `openmodel`, `zai`, `stepfun`, `qianfan`, `sakana`, local Ollama, and Ollama Cloud model IDs are passed through unchanged after known aliases are normalized. OpenRouter and SiliconFlow provider configs with a custom `base_url` also preserve explicit model values, which lets OpenAI-compatible gateways accept bare model IDs. Use `/models` or `codewhale models` to discover live IDs from your configured endpoint. `CODEWHALE_MODEL` overrides this for a single process; `DEEPSEEK_MODEL` is the legacy alias.\n- TelecomJS uses `deepseek-v4-pro` only as a conservative pre-refresh fallback. Once its key-scoped `/models` catalog is available, the picker uses those live rows; Codewhale omits unsupported reasoning request fields on this route.\n- `reasoning_effort` (string, optional): `auto`, `off`, `minimal`, `low`, `medium`, `high`, `xhigh`, `ultra` (`ultracode` is an alias of `ultra`), or `max`; defaults to the configured UI tier. DeepSeek Platform receives top-level `thinking` / `reasoning_effort` fields. Ollama Cloud's OpenAI-compatible Chat Completions route preserves its documented `none` / `low` / `medium` / `high` / `max` ladder (`off` is sent as `none`; `xhigh` and `ultracode` normalize to `max`). Direct xAI `grok-4.7` and `grok-4.6` on exact `https://api.x.ai/v1` receive top-level `reasoning_effort = \"low\" | \"medium\" | \"high\" | \"xhigh\"` (the ladder comes from the bundled catalog row; `grok-4.5` maps `xhigh` to `high`, and rows without a documented effort such as `grok-4.3` get no field); Grok reasoning cannot be disabled, so `off` normalizes to `high`, `max`/`ultracode` to `xhigh`, and `auto` leaves the field omitted so xAI's documented default `high` applies. A custom xAI-compatible `base_url` does not inherit that dialect. Direct Moonshot `kimi-k3` on exact `https://api.moonshot.ai/v1` is always-thinking and receives only top-level `reasoning_effort = \"low\" | \"high\" | \"max\"`; `off` normalizes to `low`, and `medium` to `high`. Kimi Code membership `k3` on exact `https://api.kimi.com/coding/v1` instead receives nested `thinking.effort`, and its `off` setting also normalizes to enabled `low`. Normal dispatched `auto` uses Codewhale's auto-reasoning selector and sends a concrete route-normalized tier; only an omitted reasoning setting leaves the provider default in control. Neighboring gateways and model/endpoint combinations retain the generic Moonshot contract. OpenAI Codex normalizes stale `off` to `low` and sends `max` / `ultracode` as Responses `xhigh`. Z.ai receives documented `thinking` controls and treats enabled thinking as the GLM coding high/max lane. NVIDIA NIM receives equivalent settings through `chat_template_kwargs`.\n- `verbosity` (string, optional): `normal` or `concise`. `normal` keeps the\n default conversational prompt. `concise` appends a prompt discipline block\n for direct, low-chatter output; CLI noninteractive commands (`exec` and\n `eval`) default to `concise` unless config/env/CLI overrides it.\n Override per process with `CODEWHALE_VERBOSITY` or the legacy\n `DEEPSEEK_VERBOSITY` alias.\n- `telemetry` (bool, optional): anonymous usage counting, **`true` by default\n in the current 0.9.12 source**. Notice version `5` names Codewhale and PostHog\n and describes the opt-out policy; no acceptance is invented for a default\n user. Existing explicit declines remain off. An explicit `false` here is the\n durable *opt-out*: it deletes the random install id, truncates buffered and\n dry-run events, and leaves a tombstone reasserted while the setting is false.\n It is a floor: `--telemetry true` and `CODEWHALE_TELEMETRY=1` lose to it.\n Use `/settings` or `codewhale config set telemetry true` to turn counting\n back on explicitly for new sessions through the existing privacy transition. `CODEWHALE_TELEMETRY`\n (legacy alias `DEEPSEEK_TELEMETRY`) and `--telemetry false` provide a run-scoped\n kill switch that stops collection and delivery without erasing the owner's\n state. A repo-local `.codewhale/config.toml` cannot set this preference.\n `codewhale config telemetry` shows the disclosure; `codewhale config get\n telemetry` reports preference and privacy status. Full schema and opt-out\n behavior: [`TELEMETRY.md`](TELEMETRY.md).\n- `telemetry_endpoint` (string, optional): where batches are POSTed. Leaving it\n unset selects the shipped default,\n **`https://telemetry.codewhale.net/v1/telemetry`** — the first-party ingest\n service described in [`TELEMETRY.md`](TELEMETRY.md), whose source is in\n `telemetry-ingest/`. This key decides only *where* a permitted session sends;\n it cannot override an opt-out. Setting it\n to the **empty string** is how you stay enabled and contact nobody: each batch\n is then written to `$CODEWHALE_HOME/telemetry/dryrun.jsonl` and no HTTP client\n is constructed at all, so you can read exactly what would have been sent. Any\n other value replaces the default outright. `https://` is required; plain\n `http://` is accepted only for loopback hosts, and there is no environment\n variable that overrides that refusal. A rejected endpoint turns telemetry off\n for the run rather than falling back to plaintext or to the default. Override\n per process with `CODEWHALE_TELEMETRY_ENDPOINT` (legacy alias\n `DEEPSEEK_TELEMETRY_ENDPOINT`), where an empty value means the same \"contact\n nobody\". A repo-local `.codewhale/config.toml` cannot set it.\n- `allow_shell` (bool, optional): in interactive TUI Agent sessions, omitting\n this keeps shell tools available with approval prompts; setting it to `false`\n hides shell tools. Headless, durable-task, and other noninteractive profiles\n keep the conservative omitted-field default and require `allow_shell = true`\n to expose shell. Plan mode always hides shell; Full Access enables shell and\n auto-approval.\n- `approval_policy` (string, optional): `on-request`, `untrusted`, or `never`. Runtime `approval_mode` editing in `/config` also accepts `on-request` and `untrusted` aliases.\n- `[approval] default_selection` (string, optional): which option an approval\n card highlights when it first appears — `deny` (default) or `allow_once`.\n `deny` means a reflexive Enter on a card you have not read refuses the call.\n The highlighted row is tagged `(Enter)`; press `y` to allow once.\n Set `allow_once` to restore the pre-v0.9.6 Enter-to-approve muscle memory\n (#5293). It moves the highlight only: which calls are prompted for is still\n `approval_policy` plus the rules in `permissions.toml`.\n\n ```toml\n [approval]\n default_selection = \"allow_once\"\n ```\n- `[approval] timeout_seconds` (integer, optional): bound how long an\n approval may wait — the TUI card and the Runtime API approvals the desktop\n app and web use alike. When the window elapses the call is refused\n (fail-closed) and recorded as a timeout, not as the operator's denial.\n Omitted or `0` waits indefinitely, which is the default everywhere: no\n approval is ever denied on your behalf unless you set this. Values above\n 24h clamp with a warning (#6101).\n\n ```toml\n [approval]\n timeout_seconds = 300\n ```\n- `sandbox_mode` (string, optional): `read-only`, `workspace-write`, `danger-full-access`, `external-sandbox`.\n The value can only narrow the sandbox the permission mode picks (Full\n Access: no wrapper; Ask and Auto-Review: `workspace-write`; Plan:\n `read-only`), so `danger-full-access` and `external-sandbox` take effect\n only in Full Access (`yolo`).\n Platform support is not identical. macOS uses Seatbelt when its runtime\n probe succeeds. Linux uses bubblewrap by default whenever `/usr/bin/bwrap`\n is installed and a probe proves it can confine a child; `prefer_bwrap =\n false` opts out and reports no OS command sandbox. Windows does not currently advertise an OS sandbox; its planned helper contract starts\n with process-tree containment only and must not be described as read-only\n filesystem isolation, workspace-write enforcement, network blocking,\n registry isolation, or AppContainer isolation until those are implemented.\n- The cross-layer relationship between mode admission, hooks, registered tool\n requirements, typed rules, auto-review, repo law, human approval, and the\n execution sandbox is defined in\n [Authorization order](AUTHORIZATION_ORDER.md).\n- **Read deny-list.** Every sandbox posture — `read-only` included — grants\n read access to the whole filesystem; the postures differ in what they may\n *write* and whether they may reach the network. The read deny-list narrows\n that, in two scopes. Codewhale's own file tools (`read`, `grep`,\n `file_search`, `read_media`) refuse every listed path. A shell command\n refuses only the *key material and other-principal* entries — SSH and GPG\n keys, `/etc/shadow`, `/etc/sudoers`, system and login keychains, browser\n profiles, other agents' credential stores, Codewhale's own secret stores,\n runtime daemon sockets, and anything in `sandbox_denied_read_paths`. A\n tool's own auth config is refused to the model but stays readable to the\n tool: `gh` cannot start without `~/.config/gh/hosts.yml`, so denying it to\n the process would disable the CLI without keeping its token out of reach.\n - `sandbox_read_denylist_defaults` (bool, default `true`): apply the built-in\n credential-store set — `~/.ssh`, `~/.gnupg`, cloud credential directories\n (`~/.aws`, `~/.config/gcloud`, `~/.azure`, `~/.kube`, …), `~/.netrc`,\n `~/.npmrc`, `~/.git-credentials`, macOS keychains, browser profiles,\n Codewhale's own secret stores, and `.env` files (but not `.env.example`\n and friends). Ordinary source, `Cargo.toml`, `~/.gitconfig`, `~/.cargo`,\n and `~/.npm` stay readable so builds and tests still work. Set `false` to\n restore the pre-0.9.12 full-disk-read behavior.\n - `sandbox_denied_read_paths` (list of paths): additional denied subpaths.\n `~` expands. These can never be exempted.\n - `sandbox_read_denylist_exempt` (list of paths): subtract a path from the\n *built-in defaults* when a project genuinely needs it. Deny wins over\n allow: this never reopens anything in `sandbox_denied_read_paths`.\n\n Exemption granularity is **whole-rule**, not per-file. An exempt path\n removes a built-in rule only when the rule's own path is at or below it,\n so exempting `~/.ssh/config` does nothing: the `~/.ssh` rule still denies\n it, because `~/.ssh` does not lie within `~/.ssh/config`. To reopen that\n one file you must exempt `~/.ssh` itself — which also reopens the private\n keys next to it. That is the documented tradeoff: there is no shipped way\n to narrow a built-in rule to \"everything except one file\"; copy what you\n need out of the denied tree instead. The one name-shaped rule, `.env`\n files, is exempted by *name* rather than by path: any exempt entry whose\n file name is exactly `.env` — bare `.env`, `~/.env`,\n `some/project/.env` — disables the entire `.env` filename rule, i.e.\n every `.env` and `.env.<name>` on disk rather than one project's.\n (`.env.example` and friends are never denied, so they need no exemption.)\n\n Enforced at two points, with different reach. Codewhale's own in-process\n tools, which the OS sandbox never wraps, refuse every rule — `read_file` /\n `read` / `read_media` for contents, `list_dir` / `file_search` for\n *enumeration* (listing a denied directory, or searching one, is refused just\n as Seatbelt blocks its readdir; a name search rooted above a denied tree\n skips entries inside it). Sandboxed shell commands (Seatbelt last-match-wins\n `deny file-read*` rules; bubblewrap masks each path) refuse only the\n key-material and other-principal rules, so an authenticated CLI can still\n read its own config. A refused read is always an explicit error, never an\n empty result, and the error names the path as the caller spelled it rather\n than a symlink target's real location.\n\n **This is defense-in-depth, not a security boundary.** It does not stop a\n hardlink to a denied file, a secret already copied into the workspace, an\n indirect read (`ssh-agent`, `security find-generic-password`, `aws sts …`),\n reads under `danger-full-access` shell commands, reads by MCP servers or\n other unwrapped child processes, or exfiltration of anything that *was*\n read. Keep least-privilege credentials and short-lived tokens doing the real\n work.\n- `permissions.toml` (sibling file, optional): typed permission rule records\n loaded next to `config.toml`, for example `~/.codewhale/permissions.toml`.\n This active user file is the only permission-rule source today; project\n config overlays do not load a project-local `permissions.toml`. A rule's\n optional `workspace` field is its repository scope, not a second source.\n Manually authored `[[rules]]` entries accept `tool`, optional `command` or\n `path`, optional absolute `workspace`, optional `command_exact = true`, and\n optional `action = \"deny\" | \"ask\" | \"allow\"`; omitted `action` defaults to\n `\"ask\"`. `workspace` limits a rule to that repository, while\n `command_exact = true` changes a command rule from the historical\n arity-aware prefix match to a complete-command match. `deny` blocks matching\n invocations before mode-based\n approval handling, `allow` skips approval for matching invocations, and\n `ask` forces approval only in modes that can prompt. Outside the TUI\n auto-approve path, a matching `ask` rule under `approval_policy = \"never\"`\n is rejected because no prompt can be shown. In Full Access / auto-approval sessions,\n `ask` rules do not downgrade the session into prompting or blocking; explicit\n `deny` rules still block according to the current execution-policy logic.\n\n In a supported approval card, press `S` to allow the request once and append\n exact `action = \"ask\"` rules to this file. For eligible safe requests, choose\n **Always allow this exact rule in this repo** (shortcut `P`) to append an\n `action = \"allow\"` rule with the current absolute `workspace` scope.\n Remembered shell grants set `command_exact = true`, so later commands with\n extra arguments do not inherit the grant. File and patch grants retain the\n exact workspace-relative paths produced by the existing validation path.\n Supported saves are intentionally narrow:\n `exec_shell` stores the exact approved command string; `write_file` and\n `edit_file` store the exact workspace-relative file path; `apply_patch`\n stores one exact workspace-relative `path` rule per validated touched file\n from apply-patch preflight. Existing exec command matching remains\n arity-aware for manually authored prefix rules; approval-card allow grants\n use complete-command matching. File paths are normalized to the same\n workspace-relative form used by runtime matching.\n\n `read_file` rules can still be authored manually when you want future reads\n of a specific path to ask, allow, or deny, but the approval UI does not save\n `read_file` rules. Commands classified as requiring approval or dangerous,\n critical approval cards, and repo-law prompts cannot save allow grants and\n continue to require review.\n\n `/permissions` (or `/permissions list`) is the narrow rule-management\n surface. It lists each numbered rule with the active user-file source, its\n exact effective matcher (tool-wide, command prefix, exact command, or exact\n normalized path), global or repository scope, and whether that scope\n applies in the current workspace. `/config ask-rules` remains a compatibility\n entry to the same list.\n\n Deletion is review-gated: `/permissions remove <number>` only previews the\n selected rule and prints a confirmation command. That command carries an\n opaque token bound to the exact file bytes and rule index; if another writer\n changes `permissions.toml`, confirmation fails instead of deleting a rule\n that moved into the old position. Confirmed removal and approval-card appends\n share the adjacent `permissions.toml.lock`, preserve unrelated TOML comments\n and formatting, and atomically replace the file. The running TUI reloads the\n user ruleset without clearing session-only approvals.\n\n This editor intentionally does not create or rewrite rules, persist deny\n choices from approval cards, expand globs, or create broad\n directory/recursive rules. Author those supported exact/prefix records\n manually when needed.\n- `[[hotbar]]` (array of tables, optional): user-owned 1-8 slot bindings for\n the TUI hotbar. Each entry has `slot`, `action`, and optional `label`.\n The Hotbar is hidden by default (#3807): omitting `hotbar` and setting\n `hotbar = []` both mean no hotbar. `/hotbar on` writes the eight default\n slots (`slash.workflow`, `slash.goal`, `slash.auto`, `mode.plan`,\n `mode.agent`, `mode.operate`, `palette.open`, `sidebar.toggle`) as explicit\n `[[hotbar]]` tables. When one or more `[[hotbar]]` tables are present, that\n list is the whole bar; missing slots stay empty. Invalid slots outside `1..=8` are skipped with a warning, duplicate\n slots use the later entry, and unknown action IDs are kept so the UI can show\n a disabled/unknown cell instead of silently deleting user config. Trusted\n user config, profiles, and managed config replace the whole list; project\n overlays cannot change hotbar bindings. Setup or wizard flows that persist\n hotbar bindings write this same schema to the resolved `~/.codewhale/config.toml`\n path, preserving legacy `~/.deepseek/config.toml` only when that fallback file\n is already the active config.\n\n ```toml\n [[hotbar]]\n slot = 1\n action = \"mode.plan\"\n label = \"Plan\"\n\n [[hotbar]]\n slot = 2\n action = \"session.compact\"\n ```\n- `[auto_review]` (table, optional): tool-call review policy — a deterministic floor plus a model guardian tier.\n This layer sits on top of the existing permission posture; it can hold or block a\n tool call, but it is not an auto-push, auto-merge, or hosted review service.\n Block rules are checked first, then the built-in safety floor, then allow\n rules. In Ask, a safety hold opens approval that no session grant or allow\n rule can answer; in Auto-Review or a non-interactive `never` posture it\n fails closed as a hard block. Full Access skips the built-in floor. The\n safety floor covers publish-like actions, destructive background/headless\n actions, and `curl`/`wget` uploads whose request body carries a credential\n file or a secret-named variable, or reads stdin fed from one of those or\n from `env`/`printenv`, even if an allow rule matches; a shell command,\n shell input or verifier over 64 KiB or 4,096 words whose text contains\n `curl` or `wget` anywhere, or one nested too deeply to read, is held too.\n Outside Full\n Access it also refuses, in every posture, a\n shell command, shell input or verifier containing a hidden Unicode character\n (bidirectional controls, zero-width and other invisible characters, tag\n characters, stray variation selectors, and terminal control characters\n except ASCII control keys in shell input), naming the code point and its\n position.\n\n ```toml\n [[auto_review.allow]]\n id = \"read-only-inspection\"\n action_kind = \"read\"\n reason = \"Read-only inspection is safe to run automatically.\"\n\n [[auto_review.block]]\n id = \"no-release-publish\"\n action_kind = \"publish\"\n reason = \"Release and publish actions require maintainer review.\"\n ```\n\n Rule matchers are exact `tool` and/or `action_kind`. At least one matcher is\n required. `action_kind` accepts the six decision-relevant kinds `read`,\n `write`, `shell`, `external`, `publish`, and `destructive`. Invalid names\n fail config validation instead of silently broadening into another policy\n class. In block rules, the old names remain conservative compatibility\n aliases: `network`, `git`, `mcp_action`, `browser`, and `unknown` map to\n `external`; `secret` maps to `destructive`; and `mcp_read` maps to `read`.\n Retired narrow kinds in allow rules fail validation rather than widening to\n a broader class. The retired `text_contains` matcher likewise fails\n validation instead of silently broadening an old intent-dependent rule.\n Fallback holds in interactive Auto-Review escalate to one stateless guardian\n request. The request contains the exact held call and deterministic\n observations as separate JSON fields. Conversation history, skill\n instructions, attached file contents, and other expanded model context are\n excluded. The guardian does not infer user intent or compute an authorization\n score. It exposes no tools and returns a risk level, allow/deny, and a\n rationale. High or critical risk cannot auto-run even if the model says\n allow. An oversized exact call is denied rather than truncated. Exactly one\n reviewer request is made; incomplete or malformed output, timeout,\n cancellation, provider failure, or an empty rationale all fail closed. The\n deterministic floor is never model-reviewed, and headless adapters use the\n deterministic-only tier. The pinned Codex, Kimi, and DeepSeek source\n boundaries are linked from [Permission Posture](MODES.md#permission-posture).\n Reviewer outcomes emit `tool.auto_review` audit events with\n `gate = \"guardian\"`.\n\n Auto-review decisions emit `tool.auto_review` audit events with\n `gate = \"deterministic\"` when tool\n audit logging is enabled. Future PreToolUse/PostToolUse hooks can add\n observer input around this layer, but the configured auto-review policy is\n evaluated before a tool call is allowed to proceed.\n- `managed_config_path` (string, optional): managed config file loaded after user/env config.\n- `requirements_path` (string, optional): requirements file used to enforce allowed approval/sandbox values.\n- `max_subagents` (int, optional): defaults to `64` and is clamped to `1..=128`.\n- `subagents.*` (optional compatibility table): manual per-role model pins\n for direct and Workflow `agent` starts. An explicit saved profile wins,\n then a manual role pin, then a unique saved role pin. Conflicting tool\n `model` or `model_strength` choices are refused before admission. Unpinned\n roles allow task model/strength choices before inherited defaults.\n `[subagents.roles.<role>] model = \"provider/model\"` folds into the existing override\n map and wins over `[subagents.models]`, then the convenience keys. Structured\n canonical role keys win over legacy aliases. Only this structured syntax\n separates the explicit provider from the model suffix; unknown providers\n fail before admission. Bare structured model ids inherit the active provider.\n Legacy scalar/map values preserve namespaced provider-owned ids unchanged.\n Supported convenience keys are\n `default_model`, `worker_model`, `scout_model`, `planner_model`,\n `reviewer_model`, `custom_model`, `max_concurrent`, `max_admitted`,\n `launch_concurrency`, `token_budget`, `api_timeout_secs`, and\n `heartbeat_timeout_secs`. The v0.9.x keys `explorer_model`, `awaiter_model`,\n and `review_model` remain accepted as aliases. The `[subagents]\n max_concurrent` value overrides\n top-level `max_subagents` and is also clamped to `1..=128`. `[subagents]\n max_admitted` (aliases: `max_total`, `admission_limit`) is the bounded total\n of queued plus running sub-agents; it defaults to `1024`\n (`MAX_SUBAGENT_ADMISSION`, `crates/tui/src/config/subagent_limits.rs:21`,\n applied at `config.rs:6400`) so high-fanout turns can queue and drain while\n runtime launch pressure remains bounded, and is clamped to\n `max_concurrent..=1024`. `[subagents]\n launch_concurrency` sets how many direct children start at once before the\n rest queue for a launch slot; it defaults to the resolved `max_subagents` cap\n and is clamped to `1..=max_subagents` (the deprecated\n `interactive_max_launch` key is accepted as an alias, with the new key\n winning when both are set). `[subagents] token_budget` is an optional\n aggregate token ceiling for each root `agent` run and its descendants; unset\n or `0` preserves unlimited legacy behavior. `[subagents] api_timeout_secs`\n controls the per-step API timeout for sub-agent model calls and is clamped to\n `1..=3600`, with `0` or unset preserving the 600 second default. A timeout\n after possible dispatch interrupts with a preserved checkpoint and unknown\n outcome. Only a transient connection timeout proven to precede dispatch uses\n exponential backoff, up to 5 retries.\n `[subagents] heartbeat_timeout_secs` controls stale running agent cleanup,\n defaults to `300`, and is clamped to `30..=3600` while staying above the\n resolved API timeout. `[subagents.providers.<provider>]` accepts the same\n fanout, depth, budget, and timeout knobs (`enabled`, `max_concurrent`,\n `max_admitted`, `launch_concurrency`, `max_depth`, `token_budget`,\n `api_timeout_secs`, `heartbeat_timeout_secs`) and inherits the global\n `[subagents]` value for any key you omit. Provider keys accept canonical\n names such as `deepseek`, `zai`, `openrouter`, `anthropic`, plus convenience\n aliases such as `glm` for Z.ai and `deepseek_api` for direct DeepSeek:\n\n ```toml\n [subagents]\n max_concurrent = 20\n launch_concurrency = 20\n max_admitted = 200\n max_depth = 6\n\n [subagents.providers.deepseek]\n max_concurrent = 20\n launch_concurrency = 20\n max_admitted = 200\n\n [subagents.providers.glm]\n max_concurrent = 4\n launch_concurrency = 3\n max_admitted = 12\n max_depth = 2\n\n [subagents.providers.openrouter]\n max_concurrent = 5\n launch_concurrency = 3\n max_admitted = 20\n ```\n\n `/config subagents status` prints both global values and the active\n provider's resolved profile so rate-limit tuning is visible in the TUI.\n `[subagents.models]` accepts lower-case Fleet role keys such as `worker`,\n `scout`, `planner`, `reviewer`, `builder`, and `verifier`; legacy type keys\n remain accepted during v0.9.x. Values are validated\n against the active provider at spawn time; direct DeepSeek requires DeepSeek\n IDs, while OpenAI-compatible/custom provider routes pass explicit model IDs\n through to that provider. To route a child to a different provider than the\n parent session, save a Fleet/AgentProfile with explicit `provider` and\n `model` fields (including user-named custom providers such as `lm-studio`)\n and call `agent(profile: \"...\")`; see [SUBAGENTS.md](SUBAGENTS.md).\n- `skills_dir` (string, optional): defaults to `~/.codewhale/skills` (each skill is\n a directory containing `SKILL.md`). After workspace trust, project skills\n take precedence, with `.codewhale/skills` ahead of compatible roots such as\n `.agents/skills`. Flat `./skills` requires explicit opt-in below. The runtime\n also discovers global agentskills.io-compatible `~/.agents/skills` and the\n broader Claude-ecosystem\n `~/.claude/skills`. With the default bundle policy and no `skills_dir` override, first launch installs versioned bundled skills for common\n workflows including skill creation, delegation, MCP/plugin scaffolding,\n documents, presentations, spreadsheets and PDFs. Feishu/Lark is outside the\n default pack. Only\n Codewhale-owned roots (`<workspace>/.codewhale/skills` and\n `~/.codewhale/skills`) are writable install/import targets; compatible harness\n roots stay read-only. Bare `/skills` opens Extensions → Skills (owned-only,\n zero network); `/skills manage` opens the manager. See [SKILLS.md](SKILLS.md)\n for the manager, audit statuses, provenance markers, and mutation rules, and\n [CLAUDE_PLUGIN_COMPAT.md](CLAUDE_PLUGIN_COMPAT.md) for the supported boundary\n between portable `SKILL.md` bundles and Claude Code plugin runtimes.\n- `[skills.bundled].install` (`\"auto\"` default, `\"all\"`, `\"none\"`): controls bundle writes independently of discovery. `auto` keeps deletion-as-opt-out and disables installation when `skills_dir` is set. `all` repairs enabled missing skills into `$CODEWHALE_HOME/skills` (normally `~/.codewhale/skills`) even with a discovery override. `none` writes nothing and leaves the marker untouched. Modified and managed bodies are preserved in every mode.\n- `[skills.bundled].selected` / `disabled` / `explicit_only` (arrays of names, default `[]`): selection limits installation and discovery; empty selects the whole bundle. Disabled wins and reveals lower-root copies. Explicit-only retains user invocation while removing the shipped copy from the model index and model loading. Aliases share canonical policy. These settings affect recognized global bundle copies only and never delete files. Profile fields merge separately; `[]` clears an inherited list. `doctor` warns about unknown/ignored entries and `/skills inspect` reports per-path reasons. See [bundled skills](SKILLS.md#bundled-skills).\n- `[skills].listing_max_description_chars` (integer, default `120`, range `1..=40000`): ambient-index description cap, shared by parent and child prompts and `/skills inspect`.\n- `[skills].listing_budget_fraction` (finite number, default `0.002`, greater than `0` and at most `1`): share of the route window for that same index, estimated at four characters per token and clamped to `2400..=40000` characters. Explicit profile fields override inherited values independently; `/context` uses the same resolution. Full skill loading is unaffected.\n- `[skills].scan_codewhale_only` (bool, default `false`): when `true`, session\n skill discovery ignores cross-tool roots such as `.claude/skills`,\n `.opencode/skills`, `.cursor/skills`, and `~/.agents/skills`. Codewhale still\n scans `<workspace>/.codewhale/skills`, `~/.codewhale/skills`, and any explicit\n `skills_dir` override. The Skills Manager can still toggle a local compatible\n audit scan independently of this runtime knob — see [SKILLS.md](SKILLS.md).\n- `[skills].flat_workspace_root` (bool, default `false`): opt in to the flat\n `<workspace>/skills` compatibility root after workspace trust. Without this\n opt-in it is an audit candidate only; an explicit `skills_dir` remains an\n alternative. `scan_codewhale_only = true` excludes the flat compatibility\n root regardless of this flag, unless it is the explicit `skills_dir`.\n- `[skills].registry_url` / `[skills].max_install_size_bytes` (optional): used by\n `/skills --remote`, `/skills suggest <task>`, `/skills sync`, and `/skill\n install|update`. The default manager open path does not contact the registry.\n- `[verifier].enabled` (bool, default `false`): enables automatic\n claim-of-done verifier preview once that runtime trigger is active. The\n manual `run_verifiers` tool is still available when this is false.\n- `mcp_config_path` (string, optional): defaults to `~/.codewhale/mcp.json`, with\n legacy `~/.deepseek/mcp.json` fallback when the Codewhale path is absent.\n Custom paths must be absolute; a relative value falls back to the user-global\n path so changing the launch directory cannot silently change the MCP pool.\n It is visible in `/config` and can be changed from the TUI. The new path is\n used immediately by `/mcp`; run `/mcp reload` to rebuild the model-visible\n MCP tool pool without restarting the TUI. A headless runtime still needs a\n restart because its `ConfigReload` request does not refresh MCP connections.\n- `notes_path` (string, optional): defaults to `~/.codewhale/notes.txt`, with\n legacy `~/.deepseek/notes.txt` fallback when the Codewhale path is absent, and\n is used by the model-visible `note` tool.\n- `[memory].enabled` (bool, optional): defaults to `false`. When `true`,\n the TUI loads the user memory file into a `<user_memory>` prompt block,\n enables `# foo` quick-capture in the composer, surfaces the `/memory`\n slash command, and registers the `remember` tool. The same toggle is\n available via `DEEPSEEK_MEMORY=on`.\n- `memory_path` (string, optional): anchors the native memory store. The\n configured filename is **not** the file that is written. Under the Native\n backend (the only backend) the store is re-rooted to\n `<parent-of-memory_path>/memory/global/MEMORY.md` — so the default\n `~/.codewhale/memory.md` yields `~/.codewhale/memory/global/MEMORY.md`\n (plus workspace-scoped files and a rebuildable SQLite FTS5 index). See\n [`MEMORY.md`](MEMORY.md) for the full feature surface (`# foo` composer\n prefix, `/memory` slash command, `remember` tool, opt-in toggle).\n- `snapshots.*` (optional): side-git workspace snapshots for file rollback:\n - `[snapshots].enabled` (bool, default `true`)\n - `[snapshots].max_age_days` (int, default `7`)\n - snapshots live under\n `~/.codewhale/snapshots/<project_hash>/<worktree_hash>/.git`, with legacy\n `~/.deepseek/snapshots/...` fallback when only the legacy state exists, and\n never use the workspace's own `.git` directory\n- `context.*` (optional):\n - `[context].project_pack` (bool, default `false`): include a deterministic\n project context pack (a large pretty-printed directory listing) in the\n stable prompt prefix (#4781). Useful for weak tool-calling models; the\n model can rebuild the same information with one `File` call.\n - The removed seam-manager keys (`enabled`, `verbatim_window_turns`,\n `l1_threshold`, `l2_threshold`, `l3_threshold`, `seam_model`) are\n ignored if an older config still carries them; they no longer load.\n- `compaction.*` (optional, config.toml): how a compaction pass behaves once\n it fires. `auto_compact` / `auto_compact_threshold_percent` (settings.toml)\n still decide *when* it fires. Both keys are absent by default, and absent\n means the built-in behaviour, unchanged:\n - `[compaction].summary_instructions` (string, default empty): standing\n operator instructions appended to the summarizer prompt as a clearly\n delimited \"Additional instructions from the operator\" section on **every**\n pass, manual and automatic — the effort-free counterpart to a one-off\n `/compact <focus>`, which still composes after this text. Useful for\n \"always list exact file paths and line numbers\", \"always restate open\n decisions and their trade-offs\", \"always write a TL;DR first\". Truncated\n at 4 000 characters with a warning naming the key; whitespace-only reads\n as unset. The summarizer still runs with no system prompt and no tools —\n this suffix is the only operator-authored input it sees.\n - `[compaction].retained_user_message_tokens` (int, default `20000`, clamped\n to `2000`–`200000`; also accepted as `retained_user_message_max_tokens`):\n token budget for the recent plain user messages kept **verbatim** in the\n replacement history. Raising it keeps more of the user's own earlier\n messages instead of only whatever the lossy summary captured; the\n last-round survival contract still applies on top. The `/compact` receipt\n names the effective budget and whether operator instructions were applied,\n so you can tell the knob took effect.\n- `retry.*` (optional): HTTP retry settings. The following backoff fields govern\n ordinary transport retries; HTTP 429 uses the independent policy below:\n - `[retry].enabled` (bool, default `true`)\n - `[retry].max_retries` (int, default `3`): ordinary transport retries\n - `[retry].initial_delay` (float seconds, default `1.0`)\n - `[retry].max_delay` (float seconds, default `60.0`)\n - `[retry].exponential_base` (float, default `2.0`)\n - `[retry].jitter` (bool, default `true`): randomize ordinary transport backoff\n - `[retry].jitter_factor` (float, default `0.1` = ±10%; clamps to `0.0..=1.0`)\n - `[retry].respect_retry_after` (bool, default `true`): wait for a server\n `Retry-After` preference retained for legacy retry configuration; it does not\n disable HTTP 429 server minima\n\n HTTP 429 recovery has its own validated `[retry.rate_limit]` fields:\n\n - `max_attempts` (integer, default `20`, range `1..=1000`): total attempts,\n including the first\n - `recovery_budget_secs` (integer seconds, default `600`, range `1..=86400`)\n - `initial_delay_secs` (integer seconds, default `5`)\n - `max_delay_secs` (integer seconds, default `60`), with\n `1 <= initial_delay_secs <= max_delay_secs <= 86400`\n\n This policy always honors server minima and uses equal jitter, independently\n of the legacy `initial_delay`, `max_delay`, `exponential_base`, `jitter`,\n `jitter_factor` and `respect_retry_after` fields. `max_retries = 0` does not\n disable 429 recovery; set `retry.rate_limit.max_attempts = 1` to disable only\n automatic 429 retries, or `retry.enabled = false` to disable this HTTP retry\n loop. A safe pre-dispatch HTTP/1.1 fallback inside a stream-opening operation\n remains possible. The one-hour limit on actual provider pauses and automatic sleeps\n still applies. See [HTTP rate-limit retries](rate-limit-retries.md) for the\n budget, server-minimum and active-time boundaries.\n\n `[retry]` schedules HTTP-request retries inside the client. Each physical\n streaming request gets its own `stream.open_timeout_secs` response-header\n wait. Provider backoff and honored `Retry-After` waits happen between\n attempts, outside that timer, so the configured retry count remains\n reachable. Inference retries require an ordinary rate limit or a proven\n transient connection failure before dispatch. Only that connection failure\n can switch once to HTTP/1.1; a header stall cannot. Both inference clients\n disable redirects and reqwest protocol retries. Explicit plan-window\n exhaustion is a terminal quota error. Server failures, uncertain timeouts\n and interrupted streams preserve unknown coverage and stop without\n automatically reissuing inference. Read-only catalog GETs retain bounded\n retries at their actual method owner. The `[stream]` keys below bound safe\n pre-dispatch recovery and stream consumption; legacy `tui.stream_max_*`\n keys remain configuration fallbacks.\n- `[notifications]`: notification delivery, attention, categories and audio share one\n policy. `quiet = true`, `method = \"off\"`, `condition = \"never\"` and disabled\n categories suppress both the banner and Codewhale's selected sound.\n- `notifications.method`: `auto` (default), `osc9`, `kitty`, `ghostty`, `bel`, `off`.\n- `notifications.condition`: `unfocused` (default), `always`, `never`. When absent,\n the legacy `tui.notification_condition` remains the fallback. `always` also\n bypasses the duration threshold; `unfocused` requires two seconds away.\n- `notifications.threshold_secs`: nonnegative integer, default `30`.\n- `notifications.include_summary`: boolean, default `false`.\n- `notifications.sound`: optional `off`, `whale`, `bell`, `beep`, `file`.\n A selected value controls audio across enabled categories. Absent keeps legacy\n `completion_sound` and `event_sound` choices; `off` overrides both.\n- `notifications.sound_file`: custom local WAV path for `sound = \"file\"` or legacy\n `completion_sound = \"file\"`.\n- `notifications.subagent_completion`: `always`, `final-only` (default), `off`.\n Covers all background work that finishes: sub-agents, background shells and\n durable tasks. `final-only` sends one notice naming everything that finished\n once no agent, workflow or durable task is still running; a running shell\n (a dev server, a watcher) never holds it back. `always` sends one per item.\n- `notifications.quiet`: boolean, default `false`.\n- `notifications.events`: six boolean categories, all enabled by default; see below.\n- `notifications.completion_sound`: legacy completion cue, default `off`, with the\n same values as `sound`. Used only when `sound` is absent.\n- `notifications.event_sound`: legacy `enabled` (default `false`), `events`\n (default `[\"turn-complete\", \"approval-needed\"]`), and `quiet` (default `false`).\n `min_interval_ms` (default `2000`) applies to each category's audio in both modes.\n- `tui.alternate_screen` (string, optional, default `auto`): which screen an interactive session starts on. `auto` and `always` start on the TUI-owned alternate screen; `never` starts in inline mode — a ratatui viewport the full height of the terminal with no alternate screen, so the shell's scrollback survives the session and stays scrollable after exit. `/fullscreen` and `/inline` switch it in-process; a switch that the terminal refuses rolls back and says why. Inline mode paints the whole transcript inside its viewport — nothing is written into the host scrollback while the session runs.\n- `tui.mouse_capture` (bool, optional, default `true` on non-Windows terminals and on Windows Terminal/ConEmu/Cmder when the alternate screen is active; `false` on legacy Windows console and inside JetBrains JediTerm — PyCharm/IDEA/CLion/etc. — where mouse-event escapes leak into the input stream as garbled text, see #878 / #898): enable internal mouse scrolling, transcript selection, right-click context actions, and transcript scrollbar dragging. TUI-owned drag selection copies the intersected cells, removes visual wrap-column line breaks from paragraphs, and keeps selection scoped to the transcript pane; the payload is Markdown source by default, see `tui.selection_copy_markdown` below. Set this to `false` or run with `--no-mouse-capture` for raw terminal selection; set it to `true` or run with `--mouse-capture` to opt in anywhere it's defaulted off. On raw terminal selection, especially on legacy Windows console or when mouse capture is disabled, selection may cross the right workbar and include visual wraps because the terminal, not the TUI, owns the selection.\n On Linux, finishing a transcript or composer selection quietly copies text to\n PRIMARY, leaving the regular clipboard unchanged. Middle-click inside the\n composer pastes PRIMARY at the pointer without submitting it. This uses native\n X11 or Wayland data control; compositors must support PRIMARY selection. Over\n SSH without a forwarded graphical display, use your terminal's selection/paste\n gestures or `--no-mouse-capture`. Explicit Copy still uses the regular clipboard.\n\n- `tui.selection_copy_markdown` (bool, optional, default `true`): copy TUI-owned\n transcript selections (drag release, context-menu Copy, and `Cmd+C`/`Ctrl+C`\n on an active selection) as Markdown source instead of rendered text. Every\n intersected cell serializes through the same canonical projection `Ctrl-Y`\n and `/copy` use — user and assistant cells keep their authored Markdown,\n other cells keep their full transcript form — partial intersections round out\n to whole cells, cells join with blank lines, and a toast names the copied\n cell count. Set `false` to copy the rendered text as displayed. Composer\n selections and the Linux PRIMARY auto-copy are unchanged; PRIMARY always\n carries rendered text.\n\n- `tui.stream_chunk_timeout_secs` (int, optional, default `900`): per-SSE-chunk idle timeout for streamed model responses. Slow local or compatible servers can raise this with `/config stream_chunk_timeout_secs <seconds>` (add `--save` to write canonical `stream.chunk_timeout_secs`); `0` maps to the default and explicit values must be `1..=3600`. The legacy `DEEPSEEK_STREAM_IDLE_TIMEOUT_SECS` env var is still honored when this key is omitted.\n- `tui.osc8_links` (bool, optional, default on for macOS/Linux, off for Windows): emit OSC 8 escape sequences around URLs in transcript output so supporting terminals (iTerm2, Terminal.app 13+, Ghostty, Kitty, WezTerm, Alacritty, recent gnome-terminal/konsole) can open them with the terminal's link gesture—usually Cmd-click on macOS and Ctrl-click on Linux/Windows. Terminals without OSC 8 support render the plain label and ignore the escape. The escapes are emitted out-of-band (not inside buffer cells), so column corruption is not a concern; set `false` only for terminals that misrender the OSC 8 terminator itself. Windows legacy consoles default off; opt in with `true`.\n- `tui.max_model_steps` (int, optional, default uncapped): optional model-step ceiling for one ordinary turn. Omission or `0` leaves model steps uncapped; explicit positive values are preserved (`4294967295` is the compatibility value for no limit). Headless `exec` and Fleet workers also have no implicit model-step ceiling; `exec --max-turns N` and positive worker budgets still apply. At exhaustion the turn ends `Failed` with `Maximum model steps reached before completion (limit: N)` after one bounded final-report response when needed. Cumulative wall-clock and per-stream limits remain independent. Active interactive goal turns use `goal.max_steps` instead (also uncapped by default); see the Goal loop section below.\n- `tui.turn_wall_clock_secs` (int, optional, default: no limit): cumulative per-turn wall-clock budget in seconds, measured across every model step of one turn (not per request). Time blocked on a human approval is excluded. Omitted or `0` means no limit; positive values are preserved without an added runtime ceiling. When exhausted the turn stops before authorizing another billable request with a message naming the limit and the key to raise.\n- `tui.stream_max_resumes` (int, optional, default `3`): turn-level recovery after a proven transient connection failure before request dispatch. `0` disables this additional recovery; values clamp to `0..=10`. Missing response headers, empty or partial streams, sleep and network drops never authorize inference replay.\n- `tui.stream_max_transparent_retries` (int, optional, default `2`): retained for configuration compatibility, clamped to `0..=10`. Interrupted streams no longer trigger transparent inference reissues, including before visible content.\n- `tui.stream_max_errors` (int, optional, default `5`): retained for configuration compatibility; `0` selects the default and positive values clamp to `1..=50`. Core ends an uncertain stream on its first read error or error frame regardless of this value.\n- `tui.stream_open_timeout_secs` (int, optional, default `45`): wait for a streaming request's response headers (connection setup included). A header stall ends with an unknown outcome and does not replay over HTTP/1.1. Omitted or `0` falls back to `CODEWHALE_STREAM_OPEN_TIMEOUT_SECS`, then the default; values clamp to `5..=300`.\n- `tui.connect_timeout_secs` (int, optional, default `30`): TCP/TLS connect timeout for the model HTTP client. Omitted or `0` uses the default; values clamp to `1..=300`.\n- `tui.force_http1` (bool, optional, default `false`): pin the model HTTP client to HTTP/1.1, for provider edges or proxies that mishandle long-lived HTTP/2 streams. `CODEWHALE_FORCE_HTTP1=1` does the same; either one pins.\n- `tui.stream_max_content_mb` (int, optional, default `10`) and `tui.stream_max_duration_secs` (int, optional, default `1800`): per-step caps on one stream's accumulated content and wall-clock duration. `0` selects the default; values clamp to `1..=512` MB and `10..=86400` seconds.\n- `transcript.prose_measure` (positive integer, optional, default absent = full width): wrap cap, in columns, for prose cells — user messages, assistant answers, and reasoning/thinking blocks — in the live transcript (#5436). Absent (or `0`) spends the full content width, consistent with tool/status cells and the #5322 wide-frame decision; the former 105-column prose rail is gone. Set a positive whole number (e.g. `prose_measure = 120` under `[transcript]`) to restore a bounded reading measure on ultrawide terminals. Narrow terminals always keep their content width — the cap clamps from above only. Tool, diff, and status cells never inherit this cap. Invalid values (negative or non-integer) are rejected at startup with a `transcript.prose_measure` config error. Resolved once per render pass, so the main transcript cache and the full-screen overlay always agree on the effective width.\n- `hooks` (optional): lifecycle hooks configuration (see `config.example.toml`).\n- `features.*` (optional): feature flag overrides (see below).\n\n### Stream and transport settings\n\n`[stream]` is the canonical table for model-stream policy and its HTTP clients.\n`codewhale config dump` and `codewhale config get stream` show effective values\nfrom the runtime resolver, including environment fallbacks and clamps; this does\nnot persist defaults. Set or unset individual fields with, for example,\n`codewhale config set stream.open_timeout_secs 120` and\n`codewhale config unset stream.open_timeout_secs`. Per-run\n`--set stream.open_timeout_secs=120` uses the same validation.\n\n| `[stream]` key | Default | Accepted behavior | Legacy `[tui]` fallback |\n| --- | --- | --- | --- |\n| `open_timeout_secs` | 45 | Positive values clamp to 5–300; 0 or omission falls through to the environment/default | `stream_open_timeout_secs` |\n| `chunk_timeout_secs` | 900 | 0 selects default; positive values clamp to 1–3600 | `stream_chunk_timeout_secs` |\n| `max_resumes` | 3 | Proven transient pre-dispatch recovery only; 0 disables, maximum 10 | `stream_max_resumes` |\n| `max_transparent_retries` | 2 | Compatibility value; interrupted streams never reissue inference | `stream_max_transparent_retries` |\n| `max_stream_errors` | 5 | Compatibility value; Core ends on the first uncertain stream error | `stream_max_errors` |\n| `max_duration_secs` | 1800 | Per-stream wall clock; 0 selects default, positive values clamp to 10–86400 | `stream_max_duration_secs` |\n| `max_content_mb` | 10 | Per-stream content; 0 selects default, positive values clamp to 1–512 MiB | `stream_max_content_mb` |\n| `connect_timeout_secs` | 30 | TCP/TLS setup; 0 selects default, positive values clamp to 1–300 | `connect_timeout_secs` |\n| `force_http1` | false | Boolean; a truthy environment pin always enables HTTP/1.1 | `force_http1` |\n| `tcp_keepalive_secs` | 30 | Idle time before TCP keepalive probes; 0 disables, positive values clamp to 1–3600 | none |\n| `http2_keep_alive_interval_secs` | 15 | PING interval on active HTTP/2 connections; 0 disables, positive values clamp to 1–3600 | none |\n| `http2_keep_alive_timeout_secs` | 20 | PING acknowledgement deadline; 0 selects default, positive values clamp to 1–3600 | none |\n\nFor each field, an explicit canonical value wins over the legacy `[tui]` field,\nincluding zero or false; an absent canonical field preserves the legacy value.\nThe existing environment precedence is unchanged: a positive configured header\nwait wins over `CODEWHALE_STREAM_OPEN_TIMEOUT_SECS` (then its `DEEPSEEK_` alias);\nzero falls through to those variables. An omitted chunk timeout uses\n`CODEWHALE_STREAM_IDLE_TIMEOUT_SECS` (then its `DEEPSEEK_` alias), whereas an\nexplicit zero uses 900. `CODEWHALE_FORCE_HTTP1` (legacy `DEEPSEEK_FORCE_HTTP1`)\nis ORed with the selected config flag, so a false config flag cannot defeat a\ntruthy environment pin. Profile overlays merge canonical fields independently.\n\nHTTP transport values apply to all newly constructed model, catalog and HTTP/1\nfallback clients. They do not rebuild an active client, govern MCP/other network\nservices, or send HTTP/2 PINGs on idle pooled connections. HTTP/2 knobs have no\neffect when HTTP/1.1 is pinned. The operating system controls TCP probe details;\nthese settings do not disable certificate validation. `[retry]` still owns the\nHTTP-request backoff schedule independently of stream-level budgets.\n\n### Workspace notes\n\n`/note` manages a simple notes file in the current workspace at\n`.codewhale/notes.md` (legacy `.deepseek/notes.md` is the fallback path when\nno `.codewhale/notes.md` exists yet).\nExisting `/note <text>` usage still appends a note.\nThe management forms are:\n\n| Command | Action |\n|---|---|\n| `/note <text>` | Append a note (legacy shorthand) |\n| `/note add <text>` | Append a note explicitly |\n| `/note list` | List notes with temporary 1-based numbers |\n| `/note show <n>` | Show the full note at number `n` |\n| `/note edit <n> <text>` | Replace note `n` with new text |\n| `/note remove <n>` | Delete note `n`; `rm` and `delete` are aliases |\n| `/note clear` | Empty the workspace notes file |\n| `/note path` | Show the resolved workspace notes path |\n\nThe numbers shown by `/note list` are not stored in the file; they are derived\nfrom the current order each time notes are read. This keeps the file format\ncompatible with the existing `---`-separated notes.\n\n### User memory\n\nUser memory is split across one top-level path setting and one opt-in\ntoggle table:\n\n```toml\n# Anchors the store only — actual writes go to\n# ~/.codewhale/memory/global/MEMORY.md (see MEMORY.md).\nmemory_path = \"~/.codewhale/memory.md\"\n\n[memory]\nenabled = true\n```\n\nNotes:\n\n- `memory_path` stays at the top level beside `notes_path` and\n `skills_dir`; it is not nested under `[memory]`.\n- The configured path is an **anchor**: its parent directory gains\n `memory/global/MEMORY.md`, workspace-scoped files, and `index.db`.\n Pointing `memory_path` at the native layout path itself would double-nest\n (`…/memory/global/memory/global/MEMORY.md`); keep the legacy-style\n anchor filename.\n- `DEEPSEEK_MEMORY_PATH` overrides the anchor path from the environment.\n- `DEEPSEEK_MEMORY=on` (also `1`, `true`, `yes`, `y`, or `enabled`)\n flips the feature on without editing `config.toml`.\n- The feature is inert when disabled: no file is injected, `# foo`\n falls through to normal message submission, and the model does not\n see the `remember` tool.\n- See [`MEMORY.md`](MEMORY.md) for examples and the full `/memory`\n command surface.\n\n### Goal loop (`[goal]`)\n\nOperate-mode goals run to their completion gate with no default token, time, or\ncontinuation ceiling (#5052). Token/time budgets, when explicitly supplied,\nare telemetry only and do not stop a goal. Users who want a circuit breaker can\nopt into one:\n\n```toml\n[goal]\n# Optional safety backstop on automatic goal continuation passes.\n# Default: 0 (unlimited). Set a positive value to opt into a ceiling.\nmax_continuations = 100\n\n# Optional cancellable quiet period between successful turns. This is useful\n# for coordinator goals that should poll on a cadence instead of keeping one\n# provider turn open. Default: 0 (continue immediately).\ncontinuation_delay_seconds = 300\n\n# Optional per-turn model-step ceiling while a goal is active (#6512).\n# Default: uncapped (0 or absent). Positive values are preserved.\n# This bounds each provider turn, never the number of continuation passes.\nmax_steps = 0\n\n# Optional independent judges (#6013). Default: false (the model's own\n# update_goal call is final). When true, one adversarial critique of the claim\n# runs before `update_goal` completes (complete_judge) or blocks (blocked_judge)\n# the goal. Each judged call is billed as auxiliary usage.\ncomplete_judge = false\nblocked_judge = false\n```\n\nThe effective delay is capped at 86,400 seconds (24 hours); use an automation\nfor schedules that are less frequent than once per day.\n\nWhen an explicit backstop fires, the goal pauses with a status message naming\n`[goal] max_continuations` and a warning is logged; resume the goal after\ninspecting progress, or raise/disable the backstop.\n\n`[goal] max_steps` governs one engine turn at a time. Like ordinary interactive\nturns, omitted or `0` leaves model steps uncapped. Explicit per-invocation\nceilings — `exec --max-turns N`, child-worker caps — always win over it. At exhaustion\nit gets one bounded final report and the turn classifies as budget-exhausted. An\nunfinished goal then pauses with the BudgetLimit reason instead of re-arming\nanother goal turn — resume it explicitly after reviewing the report. Wall-clock\nand stream protections are separate and still apply.\n\nWith `[goal] complete_judge = true`, a refuted completion claim keeps the goal\nactive and records the judge's findings as critical gaps. A missing,\nunparseable, uncertain or failed verdict never completes the goal; it records\none fixed gap, so a persistent judge outage ends in the existing no-progress\npause rather than a silent pass. With `[goal] blocked_judge = true`, a refuted\nblocker keeps the goal active, and an unverifiable one hands the goal back as a\nyield that your next message resumes. The judge reads only the objective, the\nevidence and the verification receipt, not the transcript or the diff. The\noperator `POST /v1/threads/{id}/goal/complete|block` routes and `/goal`\ncommands stay ungated.\n\nThe delay starts only after a successful turn while an explicitly created goal\nis still active. `/goal pause`, `/goal done`, `/goal blocked`, `/goal clear`,\nEsc, or Ctrl+C cancels a pending continuation before another provider request\nstarts. Failed turns and policy/route failures never schedule another turn.\nOnly the numeric cadence is stored in config; no prompt, credential, or secret\nis persisted for the loop.\n\n### Reasoning-only recovery (`[reasoning_only]`)\n\nWhen a model finishes with only reasoning and no answer or tool call, Codewhale\ncan request an answer again. A clean stop permits two retries by default:\nthe first keeps the request unchanged, and later retries carry a request-scoped\nnudge. The default nudge is `Continue: give your answer, or make the next tool call.`\n\n```toml\n[reasoning_only]\nmax_reprompts = 2\non_output_limit = false\n```\n\nSet `on_output_limit = true` to also retry `length`/`max_tokens` stops that\nproduced no answer or tool call, including providers that did not stream their\nhidden reasoning. This option is off by default. Output-limit retries carry the\nnudge immediately and share the same `max_reprompts` allowance. Set\n`max_reprompts = 0` to disable both reasoning-only and output-limit recovery;\ntransport and empty-stop recovery have their own existing limits.\n\nThis applies to ordinary interactive, exec and Runtime API engines and Fleet\nworkers. Workers use the captured parent configuration. RLM keeps its separate\ncompletion/FINAL/REPL stop protocol and does not use this option.\n\nEvery retry is a fresh request and may repeat the model's reasoning. It does not\ncarry hidden reasoning forward, raise output limits or bypass route, context,\nturn, cancellation or tool-admission controls. Each request can incur provider\ncost. Existing output-limit overrides still apply, and request preparation may\nreduce or refuse an allowance when input grows. Restart existing engines after\nchanging these settings.\n\nThe nudge appears in one outbound request without joining conversation history\nor later resumed model context. Retry status retains its stable protocol prefix\nand localizes its human text. Its receipt is visible in the transcript; Fleet\nalso stores an owner-bound prepared-request receipt in its existing private\nartifact before dispatch. A prepared receipt does not claim a completed model\ncall. Existing version-1 artifacts remain readable.\n\nTo customize or disable the nudge:\n\n```toml\n[reasoning_only]\nreprompt_message = \"Give your answer, or make the next tool call.\"\n```\n\nAn empty `reprompt_message = \"\"` disables the nudge, including on output-limit\nretries; the retry still counts against `max_reprompts` and uses the unchanged\nrequest. Configuration with recovery enabled does not guarantee the model will\ncomplete a long proof within its allowance.\n\n### Notifications\n\nNotification controls are available in the existing `/config` settings view,\nfrom terminal commands, and through the CLI. All write the same `config.toml`\nkeys. Terminal changes apply immediately; add `--save` to keep them. CLI writes\napply when the next process loads its configuration.\n\n```sh\ncodewhale config set notifications.sound whale\ncodewhale config set notifications.events.approval-needed false\ncodewhale config set notifications.quiet true\ncodewhale config get notifications\ncodewhale config unset notifications.quiet\n```\n\n```text\n/config notifications sound whale --save\n/config notifications condition unfocused --save\n/config notifications quiet true\n/config notifications status\n```\n\nNested CLI edits preserve TOML types, unrelated keys and comments. Unset removes\nonly the selected leaf. `notifications.sound legacy` in the TUI, or CLI unset of\n`notifications.sound`, restores previous sound choices. Invalid values are\nrejected before file or session changes. In an active TUI profile, saved edits\nupdate its notification table when it owns one; otherwise they update the\ninherited root table. The settings detail keeps saved and current values distinct.\n\n```toml\n[notifications]\nmethod = \"auto\" # auto | osc9 | kitty | ghostty | bel | off\ncondition = \"unfocused\" # unfocused | always | never\nthreshold_secs = 30\ninclude_summary = false\nsound = \"whale\" # optional; sound is opt-in, not enabled by default\nquiet = false\n\n[notifications.events]\nturn-complete = true\nsubagent-terminal = true\napproval-needed = true\ninput-needed = true\nelevation-needed = true\nmodel-notify = true\n```\n\n`quiet = true` mutes every category without changing saved choices. `method =\n\"off\"` also stops both banner and selected audio. Disabling a category stops its\nsound. Attention and duration gates apply before any sink runs. Title animation\ncompletion is silent; only the authorized notification event can request audio.\nSuccessful turn completion is a notification category; failed/cancelled turns do\nnot create a success notification.\n\n`auto` chooses a recognized terminal protocol or the existing macOS native\nfallback; unknown terminals remain unsupported and never invent a bell. `bel`\nis an audio-only transport: one selected cue is dispatched, without a second\ntransport bell. With `sound = \"off\"`, that transport is silent. `osc9`, `kitty`\nand `ghostty` use their terminal notification protocols; tmux passthrough is\npreserved. Terminal/OS notification preferences still govern display, attribution\nand any sound the host itself adds.\n\nBy default the terminal must stay unfocused for two seconds. `condition =\n\"always\"` allows foreground notifications and bypasses the duration threshold;\n`\"never\"` suppresses all delivery. The canonical condition takes precedence over\nlegacy `[tui].notification_condition`.\n\nThe bundled `whale` is a 1.55-second original whale-inspired cue, with no\nthird-party recording. It remains an opt-in candidate pending listening approval.\nWAV playback uses a background worker: macOS `/usr/bin/afplay`, Linux `aplay`\nfrom [ALSA utilities](https://github.com/alsa-project/alsa-utils), or Windows\n`PlaySoundW`. A missing player/file or unsupported platform has no fallback bell.\nOnly one WAV plays at a time. A worker-start receipt is a dispatch attempt, not\nproof of audible playback or OS acceptance. The existing macOS `osascript`\nbanner retains Script Editor attribution; this Core change does not provide a\nbranded native Apps banner.\n\n#### Previous sound settings\n\nWhen `notifications.sound` is absent, completion uses a non-off\n`completion_sound` selection, and other events use the existing event allow-list.\nThese are compatibility inputs to the same audio decision, not separate playback\npaths. When the global sound is selected, it takes precedence over the previous\ncompletion/event choices. The default remains silent unless a legacy sound or\nexplicit `bel` transport was already selected.\n\n```toml\n[notifications.event_sound]\nenabled = false\nevents = [\"turn-complete\", \"approval-needed\"]\nmin_interval_ms = 2000\nquiet = false\n```\n\nLegacy event cues use one bell for completion, subagent completion, input and\nmodel notices; approval/elevation cues use two. The per-category repeat interval\nsurvives settings refreshes. Old unknown event names are ignored on load; new\nCLI/TUI edits require names from the six categories above.\n\nLocal approval/input/elevation prompts and error receipts remain available when\nexternal notifications are muted. Action prompts use the selected UI language\nand retire when that request settles. Repeated live notices keep their first\nexpiry; a later routine update does not hide an unresolved warning at completion.\nOptional plugin suggestion toasts and contextual tips share one session guidance\nbudget. `/config contextual_tips off` hides those toasts while preserving\nrequired notices and explicit plugin review requests.\n\n#### What a notification can contain\n\nA desktop notification is a glance surface: on macOS it can appear on the\nlock screen, and on every platform it is visible to anyone near the machine.\nCodewhale therefore builds notifications from a typed payload with a fixed\nper-event disclosure policy rather than from whatever text was on hand:\n\n| Event | Shown | Never shown |\n|---|---|---|\n| Turn complete | status line (+ elapsed/cost when `include_summary`), preview of the assistant's reply | — |\n| Sub-agent finished | status line, agent id, preview of the child's summary line | — |\n| Approval needed | the tool name | the tool description, the command, the arguments |\n| Input needed | \"Answer the question in the terminal to continue\" | the question |\n| Sandbox elevation needed | the tool name and the denial reason | the command |\n| `notify` tool | model-supplied title and body | — |\n\nEvery field is capped (80 characters for the status line, 120 for the\nidentifier, 200 for the preview), stripped of control bytes and escape\nsequences, and passed through a redactor that replaces credential-shaped\nstrings with `[redacted]`, reduces absolute local paths to `…/basename`,\nand replaces raw tool JSON with `[details hidden]`. The redactor is\ndeliberately over-eager: an unbroken 40-character run has no word\nstructure, so it is redacted even when it is not a secret.\n\n#### macOS: why the banner says \"Script Editor\"\n\nOn macOS terminals that provide no notification escape of their own —\nApple Terminal, the VS Code and JetBrains embedded terminals, plain tmux\nwithout `LC_TERMINAL` — `method = \"auto\"` falls back to `osascript`'s\n`display notification`. That command posts on behalf of the *bundled*\nhost process, and `/usr/bin/osascript` is unbundled, so macOS attributes\nthe banner to `com.apple.ScriptEditor2`. That attribution supplies the\nScript Editor icon and owns the System Settings → Notifications entry\n(alert style, previews, Do Not Disturb). `display notification` has no\nicon parameter, so this cannot be fixed from the notification code; it\nneeds Codewhale to ship a real `.app` bundle. Tracked in\n[#4834](https://github.com/codewhale-hq/CodeWhale/issues/4834). In the meantime,\niTerm2, WezTerm, Ghostty, and kitty are matched first and use their own\nnotification protocols, and `method = \"osc9\"` / `\"bel\"` / `\"off\"` opt out\nof the `osascript` path explicitly.\n\n## Automations in the terminal\n\nOpen `/automation`, then choose **New automation** (`n`) or **Edit automation**\n(`e`). The form edits the name, multiline prompt, schedule, model, workspace,\nand enabled status. `Tab` moves between fields; `Enter` inserts a newline in\nthe prompt. Schedule presets include daily, weekly, hourly, once, and a custom\nRRULE. Time fields accept `HH:MM`; their arrow controls change the time by\n15 minutes. Weekly day buttons and model search support mouse and keyboard.\n\nThe next-run preview uses the scheduler's local time zone, shown with its UTC\noffset. An enabled automation can run after **Save** (`Ctrl+S`); a paused one\nhas no scheduled next run. **Cancel** (`Esc`) discards the draft. Editing keeps\nexisting permission settings, custom schedules, and additional workspace\nentries unless the corresponding supported field is explicitly changed.\n\nSchedules are evaluated in the machine's local time zone against the wall\nclock. A wall time that does not exist on a spring-forward day is skipped and\nan ambiguous fall-back time fires once. Occurrences missed while Codewhale was\nclosed, asleep, or still running the previous occurrence are coalesced: the\nnext start runs one catch-up occurrence and then continues from the next\nfuture slot, never replaying every missed slot. An occurrence never starts\nwhile an earlier run of the same automation is still queued or running. Each\nrun is recorded durably with its status, timing and error; a run that needs a\ntool approval has no operator to ask and fails once the approval wait expires.\n\nChoosing a concrete model pins both the model and its exact configured\nprovider, including named custom routes. Later changes to the active provider\ndo not move that automation's pin. The default-model choice and legacy\ndefinitions without a provider pin keep the runtime's existing default\nbehavior. New automation records use schema v2 and tasks use v3 so older\nruntimes reject records whose provider pins they cannot preserve.\n\n## Lifecycle Outbox (`[lifecycle_outbox]`)\n\nThe lifecycle outbox is an opt-in, machine-readable stream of session,\nturn, and sub-agent lifecycle events. With a path configured, Codewhale\nappends one JSON line per event to that file — for interactive TUI\nsessions *and* headless `codewhale exec` runs — so a supervisor\n(terminal multiplexer wrapper, automation harness, alerting setup) can\nreact to what happened without scraping the screen or installing per-hook\nshell commands. Unset or empty `path` = the feature is **off** and\nbehavior is unchanged.\n\n```toml\n[lifecycle_outbox]\npath = \"~/.codewhale/notifications/outbox.jsonl\" # unset/empty = OFF\nwebhook_url = \"\" # optional; POSTs events as JSON when set\nwebhook_token = \"\" # optional bearer token for webhook_url\n```\n\n### Events emitted\n\n| Event | Kind | Fired at |\n|---|---|---|\n| `turn_start` | `turn.started` | a new turn begins (TUI TurnStarted; `exec` at message dispatch) |\n| `turn_end` | `turn.completed` / `turn.failed` / `turn.interrupted` | turn completion, kind projected from the turn status |\n| `turn_stalled` | `turn.stalled` | the stall watchdog recovers a wedged turn |\n| `subagent_spawn` | `subagent.spawned` | a sub-agent is spawned |\n| `subagent_complete` | `subagent.completed` | a sub-agent reaches a terminal state |\n| `session_start` | `session.started` | interactive session start |\n| `session_end` | `session.ended` | interactive session end |\n\n### File contract\n\nEach line is a `RuntimeEventEnvelope`:\n\n```json\n{\"schema_version\": 1, \"seq\": 3, \"event\": \"turn_start\", \"kind\": \"turn.started\",\n \"thread_id\": \"…\", \"turn_id\": \"…\", \"item_id\": null, \"timestamp\": \"…\",\n \"created_at\": \"…\", \"payload\": {…}}\n```\n\n- `seq` is monotonic per outbox file and recovers from the last written\n line when a new process opens the file.\n- Lines are written one complete JSON line per append, serialized by an\n internal writer task and flushed before the next event; concurrent\n sessions writing the same path do not interleave bytes mid-line, but\n separate processes each continue from their own recovered `seq`, so seqs\n can repeat across processes sharing one file — prefer one file per\n process for strict uniqueness.\n- Parent directories are created lazily on the first event.\n- Payloads are constructed from bounded, pre-redacted fields only — never\n raw tool arguments, environment, or full transcript text. Free-form\n fields (error messages, previews) are capped at the notification limits\n (80 headline / 120 detail / 200 preview characters) and stripped of\n control bytes.\n\n### Webhook delivery\n\nWith `webhook_url` set, every event is additionally POSTed as\n`{\"at\": \"<ISO 8601 timestamp>\", \"event\": {…}}` with\n`Authorization: Bearer <webhook_token>` when a token is configured.\nDelivery is best-effort: failures are logged and dropped, never retried\ninto the agent loop, and a failing webhook never blocks the local file\nappend.\n\n## Control Socket (`[control_socket]`)\n\nPer-session control surface for supervised operation: with the feature\nenabled, the interactive TUI binds one unix domain socket per *running*\nsession at `<sessions-dir>/<session-id>/control.sock` (mode `0600`;\n`<sessions-dir>` is the same directory the session store uses, typically\n`~/.codewhale/sessions`). The socket is removed with the session's\nartifact directory, and a stale socket left by a crashed process is taken\nover by the next launch. Unset or `enabled = false` = the feature is\n**off** (the default) and behavior is unchanged. Unix-only; on other\nplatforms the key parses but no socket is bound.\n\n```toml\n[control_socket]\nenabled = false # default: OFF\n```\n\nThe socket speaks newline-framed JSON-RPC, one request per connection:\nwrite one request line, read one response line, close.\n\n```json\n{\"id\":\"1\",\"method\":\"message\",\"params\":{\"text\":\"hello\"}}\n{\"id\":\"2\",\"method\":\"interrupt\",\"params\":{}}\n{\"id\":\"3\",\"method\":\"status\",\"params\":{}}\n```\n\n- `message` — delivers `text` as a structured user message through the\n ordinary composer dispatch path; dispatched immediately when idle,\n queued when a turn is in flight (the response's `delivery` field says\n which).\n- `interrupt` — the Esc-shaped cancel of the active turn; `cancelled`\n reports whether active work was in flight.\n- `status` — answers `turn_state` (`idle` / `in_progress` / `waiting`) and\n `goal` (`objective`, `status`, `paused`).\n\nSuccess responses echo the request id with a `type`-tagged result;\nfailures carry `error.code` (`invalid_request`, `command_error`,\n`timeout`, `server_unavailable`). Requests are bounded at 1 MiB per line\nand a handler that does not answer within 5 s is reported as `timeout`.\n\n## Tool Catalog\n\nCodewhale loads a small core native tool catalog by default and leaves less\ncommon native tools discoverable through ToolSearch. To keep specific native\ntools loaded on every request, add them to `[tools].always_load`:\n\n```toml\n[tools]\nalways_load = [\"Git\", \"notify\"]\n```\n\n### Script tools and overrides\n\nScripts in `~/.codewhale/tools/` (or `[tools].plugin_dir`) that start with a\n`# name:` header become model-visible tools, and `/plugin tools` lists them.\nThe script reads the tool's JSON input on stdin and writes a JSON\n`ToolResult` (`{\"content\": \"...\", \"success\": true}`) on stdout.\n\n```sh\n#!/usr/bin/env sh\n# name: word_count\n# description: Count words in the given text\n# schema: {\"type\":\"object\",\"properties\":{\"text\":{\"type\":\"string\"}}}\n# approval: required\n```\n\n`# approval:` takes `suggest` (the default) or `required`; either way the\ntool follows the session's approval setting. A script cannot approve itself:\n`approval: auto` is no longer supported, so such a script gets the default,\nand the runtime log (`~/.codewhale/logs/`) and `/plugin tools` name it.\n\nA script cannot replace a built-in tool either. A script whose `# name:` is\nalready registered is not loaded. `[tools.overrides]` may disable a built-in,\nor add a script or command tool under a name of its own:\n\n```toml\n[tools.overrides]\n\"Web\" = { type = \"disabled\" } # turn a built-in off\n\"audited_shell\" = { type = \"script\", path = \"audit-shell.sh\" } # a new tool\n\"Bash\" = { type = \"script\", path = \"audit-shell.sh\" } # refused: Bash is built in\n```\n\nA `script` or `command` override keyed by a built-in is refused, and the\nbuilt-in stays active. A status line names the key once per session, and the\nruntime log records it. To route a\nbuilt-in through your own wrapper, disable the built-in and register the\nwrapper under a new name. An override keyed by a drop-in script's name still\nreplaces that script. Relative `path` values resolve against the plugin\ndirectory.\n\n### `request_user_input` limits\n\n`request_user_input` asks the user a short batch of multiple-choice questions.\nBoth ceilings are configurable (#5949): raise `user_input_max_questions` when a\nresearch or planning workflow legitimately needs more clarifications, lower it\nwhen interactive triage should stay terse.\n\n```toml\n[tools]\nuser_input_max_questions = 6 # default 6, clamped to 1..=10\nuser_input_max_options = 4 # default 4, clamped to 2..=10\n```\n\nThe effective values are applied in three places at once: the tool's JSON\nschema (`minItems` / `maxItems`), its model-visible description, and the\npayload validator. A rejected payload names the key to raise, so the model can\neither resize the batch or tell the user which setting to change.\n\n### User-input wait timeout\n\nQuestions from `request_user_input` wait until answered or canceled by default\n(#6003). An omitted setting or `0` leaves the wait unbounded; a positive value\ncancels the question when that many seconds pass, capped at 86,400 (24 hours).\nHeadless `exec` runs have no\nresponder, so `request_user_input` is withheld there by default:\nthe model reports the tool absent and finishes instead of stalling.\n\n```toml\n[tools]\nuser_input_timeout_seconds = 300 # opt into 5 minutes; omitted or 0 waits indefinitely; maximum 86400\n```\n\nThis key governs question waits only. Approvals have their own clock,\n`[approval] timeout_seconds`, which waits indefinitely unless you set it.\nWall-clock and stream protections elsewhere are unaffected.\n\n## Feature Flags\n\nFeature flags live under the `[features]` table and are merged across profiles.\nDefaults are enabled for built-in tooling, so you only need to set entries you\nwant to force on or off.\n\n```toml\n[features]\nshell_tool = true\nsubagents = true\nweb_search = true # enables deferred Web; the flag name is retained for config compatibility\napply_patch = true\nmcp = true\nexec_policy = true\ncode_mode = true # execute_tools composes MCP/plugin/native calls; false defers it behind tool_search\nverify_tool = true # agent-callable `verify` self-critique; false removes it from the tool catalog\nvision_model = false # true routes image analysis to the [vision_model] model (see above)\nextension_host = false # experimental: run reviewed plugins' native code (see EXTENSIONS.md)\n```\n\n`code_mode` is on by default: `execute_tools` is advertised from the first\nrequest and nested calls go through the same permission gate as direct calls\n(see [Tool surface](TOOL_SURFACE.md#code-mode-execute_tools)). Set\n`code_mode = false` to defer it behind `tool_search` again.\n\n`extension_host` is experimental and off by default. Turning it on lets reviewed\nplugins run their `native` TypeScript/JavaScript tool and slash-command code in\na separate Node (or, opt-in, Bun) process; toggling it in either direction\nchanges the plugin activation policy, so every plugin is reviewed again after a\nrestart. Its `[extension_host]` table (`runtime`, `node`, `bun`) and what the\nhost's sandbox does on each platform are in\n[Writing an extension tool](EXTENSIONS.md).\n\n`[extension_host] mcp_backend` (user config only) chooses the MCP client\nindependently of the `extension_host` flag: `\"host\"` (default) uses the official\nMCP SDK behind the Rust broker; `\"rust\"` keeps the previous Rust client for one\nrelease. A host refusal never falls back to Rust. See\n[MCP connection lifecycle](MCP.md#connection-lifecycle).\n\nEvery flag has a row in [`docs/features.toml`](features.toml), the feature\nregistry; a test fails when a flag and its row disagree.\n\nYou can also override features for a single run:\n\n- `codewhale --enable web_search`\n- `codewhale --disable subagents`\n\nUse `codewhale features list` to inspect known flags and their effective state.\nThe native `/config` view also includes a read-only **Experimental** section\nfor experimental feature flags. It shows each flag's effective enabled/disabled\nstate and whether that state comes from the default or a configured override.\nChange feature flags in `[features]` or with `--enable` / `--disable`; the\n`/config` section is an audit surface, not a stability promise. Goal and\nWorkflow preview rows may appear there as reserved entries until those workflows\ngraduate behind real gated flags.\n\n## Web Search Provider\n\n`web_search` uses keyless Firecrawl by default. Runtime failure or an exhausted\nkeyless quota degrades visibly through DuckDuckGo and Bing. China deployments\ncan explicitly select Baidu, Metaso, Volcengine, or a trusted SearXNG endpoint;\nCodewhale does not guess geography from locale or model provider.\n\nConfigured API providers are attempted first. Runtime failure or an empty\nresult visibly degrades through DuckDuckGo and then Bing; the structured search\nreceipt records every hop. Missing configuration and network-policy denials\nfail closed without sending the query to another provider.\n\n**Provider-native search.** On routes whose provider offers its own web-search\ntool (OpenAI, xAI, Anthropic, DeepSeek, Kimi and others), that search can run\nahead of the configured provider. It is a separate model call on the active\nroute. `[search] native` decides the order:\n\n- unset (default): native search leads only when no search provider is\n configured; a provider chosen in `[search] provider`,\n `CODEWHALE_SEARCH_PROVIDER`, or a Tavily key wins;\n- `native = true`: native search leads even when a provider is pinned;\n- `native = false`: native search is never used.\n\nNative citations are link-checked before they are returned. Each cited URL is\nrequested through the same guarded fetch as `Web` fetch (SSRF, DNS and redirect\nchecks, `[network]` policy): a `HEAD`, then a one-byte ranged `GET` when `HEAD`\nis refused, at most four at a time and five seconds each. The request uses the\naddress as it will appear in `results`, without userinfo, fragment or\ncredential-like query parameters; an address whose query string repeats the\nsearch query is not requested and is removed. Only sources that\nanswer with a 2xx status stay in `results`; the receipt lists each removed one\nas `citation_unverified` with its reason. When no citation passes, or when the\nquery asks for `recency` (no native search tool can enforce a time window), the\nsearch falls through to the configured provider. A citation whose host the\n`[network]` policy denies is removed. A citation whose host the policy would\nask you about is not fetched: it stays in `results` and the receipt records it\nas `citation_unchecked` until you allow the host.\n\nThe provider's prose answer is returned whole in a separate\n`unverified_synthesis` field: it is the provider model's summary, and neither\nits claims nor its links are checked. Oversized tool output spills to a session\nartifact the model can page back.\n\n**Recency and locale.** `recency` and `locale` are forwarded where the\nbackend's API takes them, and the search receipt reports each as honored or\nignored:\n\n| Backend | Recency | Locale |\n| --- | --- | --- |\n| Firecrawl | `tbs=qdr:d/w/m/y` | `country` from the region (`de-DE` → `DE`); a bare language is ignored |\n| Tavily | `time_range` | not sent (Tavily takes country names) |\n| SearXNG | `time_range` | `language`, as given |\n| Serply | not sent (undocumented) | `hl` language, `gl` country |\n| DeepSeek | not sent | not sent (`localization` takes an IP address) |\n| Provider-native | declined; the search falls through to the configured provider | not sent |\n\nRecency is rounded up to the backend's nearest window (day, week, month,\nyear), so `recency = 10` searches the last month. Other backends ignore both\nknobs and say so in the receipt.\n\nFor a private/internal search service that serves DuckDuckGo-compatible HTML,\nkeep `provider = \"duckduckgo\"` and set `base_url`; Codewhale appends the `q`\nquery parameter to that endpoint and applies network policy to its host.\nCustom endpoints do not fall back to public Bing. `CODEWHALE_SEARCH_BASE_URL`\ncan override this per process; `DEEPSEEK_SEARCH_BASE_URL` remains accepted as\nthe legacy alias.\n\n**SearXNG** ([docs](https://docs.searxng.org/dev/search_api.html)) uses the\nconfigured instance's JSON API. Set `provider = \"searxng\"` and\n`base_url = \"https://your-searxng.example\"`; Codewhale calls\n`/search?q=...&format=json`. Codewhale does not use a public SearXNG instance\nby default because public instances often disable JSON output or rate-limit API\ntraffic.\n\nSelf-host it as a separate process (Docker is fine); Codewhale never bundles or\nmanages the search engine itself:\n\n- Enable JSON on the instance (`settings.yml`, `search.formats` must include\n `json`) and restart it. An HTML-only instance answers the API with HTTP 403;\n Codewhale reports that as a JSON/API-access problem on the SearXNG hop rather\n than silently returning no results.\n- Bind it to loopback, or to a host and port your network policy allows. The\n instance keeps its own engine list, limiter, and limits.\n- Point Codewhale at it with `[search] provider = \"searxng\"` and `base_url`\n (required; either the root URL or the `/search` endpoint). No instance ships\n as a default, and none is discovered automatically.\n- `codewhale doctor --probe-search` sends a transport-only `HEAD` to that\n origin — no `q=`, no credentials, no redirects, no audit receipt — so a green\n probe proves reachability and network-policy admission, not that JSON is on.\n\nConfirm the JSON API itself before assuming a Codewhale bug:\n\n```sh\ncurl -sS \"$BASE/search?q=codewhale&format=json\" | jq '.results[0] | {title,url,score}'\n```\n\nCodewhale ranks the returned rows by `score`, highest first, and applies\n`max_results` to that ranking; rows an instance reports without a usable score\nkeep their original relative order.\n\n**Metaso** ([metaso.cn](https://metaso.cn)) requires a user-supplied key. Set\n`METASO_API_KEY` or `[search] api_key`; Codewhale does not ship a shared key.\n\n**Firecrawl** ([docs](https://docs.firecrawl.dev/sdks/cli)) searches Firecrawl\nCloud without a key using its bounded per-IP daily quota. Set\n`FIRECRAWL_API_KEY` or `[search] api_key` for authenticated limits. Codewhale\nsends no `Authorization` header in keyless mode.\n\n**Baidu** uses Baidu AI Search at\n`https://qianfan.baidubce.com/v2/ai_search/web_search`. Set\n`BAIDU_SEARCH_API_KEY` or `[search] api_key`. This is a search-tool backend\nonly; it does not add a Baidu model provider.\n\n**Sofya** ([sofya.co](https://sofya.co)) returns full extracted page content\nrather than snippets. Set `[search] api_key` to your `ay_live_...` key, or the\n`SOFYA_API_KEY` env var. This is a search-tool backend only; it does not add a\nSofya model provider.\n\n**Serply** ([serply.io](https://serply.io)) returns Google organic results with\ntitle, URL, and snippet. Set `[search] api_key` to your Serply key, or the\n`SERPLY_API_KEY` env var. This is a search-tool backend only; it does not add a\nSerply model provider.\n\n**DeepSeek** ([docs](https://api-docs.deepseek.com/guides/search)) uses\n`POST https://api.deepseek.com/v1/tools/search`. Set `provider = \"deepseek\"`\n(`deep-seek` and `deepseek-search` are also accepted) and supply `[search] api_key`\nor the `DEEPSEEK_API_KEY` env var, in that order. The saved DeepSeek model-provider\nkey is not reused by this search backend. Queries are truncated to 128 Unicode\ncharacters. The backend clamps counts to 1–50; the shared search tool currently\ncaps requests at 10 results. Query operators such as `site:`, `lang:`,\n`after:`, and `before:` are passed through; the `recency` and `locale` knobs are\nignored. Results include title, URL, and snippet, falling back to summary when a\nsnippet is empty.\n\n**Tavily** ([tavily.com](https://tavily.com)) is selected automatically when a\nTavily key is present and no provider is pinned: `TAVILY_API_KEY` set, or\n`[search] api_key` / `CODEWHALE_SEARCH_API_KEY` in the `tvly-` family. Doctor\nreports that as `source: tavily key`. Autodetect is runtime-only — Codewhale\nnever writes `[search] provider` for it, and `TAVILY_API_KEY` is never merged\ninto `[search] api_key`. An explicit `[search] provider` or\n`CODEWHALE_SEARCH_PROVIDER` always wins, so `provider = \"firecrawl\"` keeps\nFirecrawl even with a Tavily key in the environment. Pinned `tavily` accepts\nany non-empty `[search] api_key` and is configured by that key or\n`TAVILY_API_KEY`; with both empty it fails closed.\n\n```toml\n[search]\nprovider = \"firecrawl\" # also duckduckgo | bing | tavily | bocha | metaso | searxng | baidu | volcengine | sofya | serply | deepseek\n# base_url = \"https://search.example/\" # optional with provider = \"duckduckgo\"; required with \"searxng\"\n# api_key = \"YOUR_KEY\" # optional for firecrawl; required by the other API providers\n# native = false # provider-native search: unset = only when no provider is configured\n```\n\n## Local Media Attachments\n\nUse `@path/to/file` in the composer to add local text file or directory context\nto the next message. Use `/attach <path>` for local image/video media paths, or\n`Ctrl+V` to attach an image from a local clipboard or an explicitly forwarded\nX11/Wayland clipboard. SSH terminal paste without a forwarded graphical display\nis text-only; use the local terminal's paste command (`Cmd+V` on macOS or\n`Ctrl+Shift+V` on Linux/Windows), and use `/attach <path>` for remote image\nfiles. OpenSSH loopback X11 displays are detected automatically. For an\nexplicitly forwarded Wayland or non-loopback X11 display, set\n`CODEWHALE_SSH_CLIPBOARD=graphical`; set it to `terminal` to force terminal\ntransfer instead of an ambient remote display. DeepSeek's public Chat\nCompletions API currently accepts text message\ncontent, so media attachments are sent as explicit local path references instead\nof native image/video payloads.\nAttachment rows appear above the composer before submit; move to the start of\nthe composer, press `↑` to select an attachment row, then press `Backspace` or\n`Delete` to remove it without editing the sample text by hand.\n\n## Managed Configuration and Requirements\n\ncodewhale supports a policy layering model:\n\n1. user config + profile + env overrides\n2. managed config (if present)\n3. requirements validation (if present)\n\nBy default on Unix:\n- managed config: `/etc/deepseek/managed_config.toml`\n- requirements: `/etc/deepseek/requirements.toml`\n\nRequirements file shape:\n\n```toml\nallowed_approval_policies = [\"on-request\", \"untrusted\", \"never\"]\nallowed_sandbox_modes = [\"read-only\", \"workspace-write\"]\n```\n\nIf configured values violate requirements, startup fails with a descriptive error.\n\n## Notes On `codewhale doctor`\n\n`codewhale doctor` follows the same config resolution rules as the rest of the\nTUI. That means `--config`, `CODEWHALE_CONFIG_PATH`, and the legacy\n`DEEPSEEK_CONFIG_PATH` are respected, and MCP/skills\nchecks use the resolved `mcp_config_path` / `skills_dir` (including env overrides).\n\nTo bootstrap missing MCP/skills paths, run `codewhale setup --all`. You can\nalso run `codewhale setup --skills --local` to create the owned project\n`.codewhale/skills` directory. Project skills load after workspace trust.\n\nBoth plain `codewhale doctor` and `codewhale doctor --json` are structural and\noffline by default. They do not check the release service, hosted provider\nAPIs, local provider endpoints, or MCP processes, and they do not load a\nworkspace credential `.env`. Use `--check-updates`,\n`--probe-api`, `--probe-local`, or `--probe-mcp` to opt into the corresponding\nlive boundary; `--probe-local` may start a desktop-managed service such as\nOllama. Only the explicit API/local probe paths may load workspace credential\n`.env` values. Live flags conflict with `--json`, so machine-readable doctor\noutput is always offline. Top-level keys include `version`, `paths`, `secret_backend`,\n`config_path`, `config_present`, `workspace`, `api_key.source`,\n`api_key.availability`, `base_url`,\n`default_text_model`, `mcp`, `skills`, `tools`, `plugins`, `sandbox`,\n`platform`, `api_connectivity`, and `capability`. CI consumers should rely on\n`api_key.source` (`config_declared`/`env_declared`/`external_auth_declared`/\n`secret_store_unprobed`/`secret_store_unavailable`/`oauth_unprobed`/\n`external_consent`/`none`/`local_runtime`/`unknown`) and\n`api_key.availability`\n(`present`/`not_required`/`not_probed`/`unavailable`/`unknown`) rather than parsing the\nhuman-readable `doctor` text. Source is declaration metadata, not proof that a\ncredential exists or works. Only a non-empty, non-sentinel literal config value\nis structurally `present`; no-auth and local routes are `not_required`. Environment,\nexternal-auth, OAuth, consent, and secret-store declarations remain `not_probed`\nand cannot make structural Setup or Fleet readiness true. A secret-store sentinel\non a named/custom endpoint that is prohibited from using the shared store is\n`secret_store_unavailable`/`unavailable`, while `unknown` remains reserved for\nthe absence of a supported structural conclusion. Exact and whitespace-wrapped\nlegacy sentinels are never treated as literal credentials. The structural\nloader still honors safe environment routing/model/policy fields, but it never\nmaterializes environment HTTP headers, sandbox API keys, or search API keys;\nonly an explicit API/local probe switches to the normal credential-loading\npath. An opted-in update check also emits only typed generic failures: untrusted\nrelease metadata and transport errors are not echoed.\n\nIf configuration loading or validation fails, `doctor --json` returns nonzero\nand prints a bounded JSON error envelope with\n`status = \"error\"` and `error.kind = \"config_validation\"`. It does not emit a\nnormal route or capability report—or the underlying possibly sensitive error—\nfor an invalid configuration.\n\nMCP entries are configuration diagnostics unless an explicit MCP command is\nrun. `mcp.probe_scope` is `configuration`, `mcp.live_health_checked` is false,\nand each server separates `checks.configuration` / `checks.command` from\n`checks.process_reachable`, `checks.protocol_initialized`, and\n`checks.backend_tool_health`. The latter three remain `not_checked` in doctor\noutput. Run `codewhale mcp validate` to explicitly start enabled servers and\nverify protocol initialization/discovery; backend health still requires an\nappropriate explicit tool call. Doctor reports only safe structural MCP fields:\nURL userinfo/path/query/fragment and raw command arguments, environment values,\nheader values, and token material are omitted. Provider URLs follow the same\nrule and expose only `scheme://host[:explicit-port]`.\n\nThe `capability` key contains per-provider capability info derived from\nstatic knowledge (release docs, API guides) rather than live API probes.\nTop-level sub-keys: `resolved_provider`, `resolved_model`, `context_window`,\n`max_output`, `thinking_supported`, `cache_telemetry_supported`,\nand `request_payload_mode`.\n\nUse `capability.context_window` and `capability.max_output` for model-limit\nchecks in CI scripts; do not treat `capability.max_output` as the per-turn\nrequest budget. Use `capability.thinking_supported` to decide whether to\nconfigure reasoning effort.\n\n## Setup status, clean, and extension dirs\n\n`codewhale setup` accepts a few flags beyond the existing `--mcp`,\n`--skills`, `--local`, `--all`, and `--force`:\n\n- `--status` — print a compact one-screen status (api key, base URL, model,\n MCP/skills/tools/plugins counts, sandbox, `.env` presence). Read-only and\n network-free; safe to run in CI. If `.env` is missing and `.env.example` is\n present in the workspace, the status output points at `cp .env.example .env`.\n- `--tools` — scaffold `~/.codewhale/tools/` with a `README.md` describing the\n self-describing frontmatter convention (`# name:` / `# description:` /\n `# usage:`) and an `example.sh` that follows it. The directory is\n intentionally not auto-loaded; wire individual scripts into the agent via\n MCP, hooks, or skills.\n- `--plugins` — scaffold `~/.codewhale/plugins/` with a `README.md` and an\n `example/plugin.toml` plus a namespaced example Skill. Bundles are discovered\n read-only, untrusted, and disabled; review them through `/plugin` before\n enabling. v0.9.1 activates only declared Skills and MCP servers. See\n [PLUGIN_BUNDLES.md](PLUGIN_BUNDLES.md).\n- `--all` now scaffolds MCP + skills + tools + plugins together.\n- `--clean` — list `~/.codewhale/sessions/checkpoints/latest.json` and\n `offline_queue.json` if they exist. Legacy\n `~/.deepseek/sessions/checkpoints/` files are not scanned automatically; set\n `CODEWHALE_HOME=~/.deepseek` for a one-off legacy cleanup. Pass `--force` to\n actually remove matched files. This never touches real session history or the\n task queue.\n\n`--status` and `--clean` are mutually exclusive with the scaffold flags.\n\n## Why the engine strips XML/`[TOOL_CALL]` text\n\ncodewhale sends and receives tool calls only over the API tool channel\n(structured `tool_use` / `tool_call` items). The streaming loop in\n`crates/engine/src/core/engine.rs` recognizes a fixed set of fake-wrapper start\nmarkers — `[TOOL_CALL]`, `<codewhale:tool_call`, `<tool_call`, `<invoke `,\n`<function_calls>` — and scrubs them from visible assistant text without ever\nturning them into structured tool calls. When a wrapper is stripped, the loop\nemits one compact `status` notice per turn so the user can see why their\nvisible text shrank. Treat any change that re-enables text-based tool\nexecution as a regression; the protocol-recovery tests in\n`crates/tui/tests/integration/protocol_recovery.rs` lock the contract.\n\n## Model-bound redaction (`[redaction] model_bound`)\n\nCodewhale masks credential-looking values in tool output **before it is sent\nto an upstream model** — the \"model boundary\". A file read by a tool can\ncontain a configured API key, a bare provider token, or a credential-shaped\nopaque string, and the model must not see those bytes. This backstop is\nseparate from the display/export scrubbers: it decides what the model itself\ncan quote back, and it is deliberately conservative (`CredentialShaped`\npolicy, see `crates/config/src/persistence.rs`), so ordinary code and config\nstay byte-exact while keys, JWTs, bearer tokens, PEM blocks, and long opaque\nruns are masked.\n\nTurning that masking **off** is a security decision, so it is not a plain\nboolean:\n\n```toml\n[redaction]\nmodel_bound = \"disabled\" # \"enabled\" (default) | \"disabled\"\n```\n\nSetting `\"disabled\"` only records a *request*. It takes effect only when all\nof these are true:\n\n1. You restart the interactive TUI.\n2. The startup gate appears and you press `1`/`Y` on its first stage\n (\"confirm and disable\"). This only advances to a second, final-confirmation\n stage - the gate repeats the red warning and asks \"are you really sure?\".\n3. On that second stage you press `1`/`Y` again. The gate is rendered with the\n same explicit-key discipline as workspace trust - `Enter` never confirms by\n reflex, and `2`/`U` on the second stage steps back.\n4. Only that second confirmation persists a receipt to\n `~/.codewhale/redaction-state.json` (next to `config.toml`) and rebuilds\n the engine with masking off for the rest of this launch and future ones.\n\nThe receipt is bound to the config it was made against and is valid only\nwhile that config still requests `\"disabled\"`. Setting `model_bound` back\nto `\"enabled\"` - or rewriting `config.toml` in any way after the\nconfirmation - invalidates it, so requesting `\"disabled\"` again later\nasks for a fresh confirmation on the next launch. The receipt checks both the\nconfig contents and modification time; missing, unreadable, or malformed config\nand older receipts without this binding keep masking enabled. Legacy-home\ninstalls store the receipt beside their resolved config file.\n\nUntil a confirmation exists, the effective mode is always `\"enabled\"`:\n\n- Choosing `2`/`U` (\"keep masking on\") leaves the config field untouched, so\n the next launch asks again. Edit the field back to `\"enabled\"` to stop being\n asked.\n- Non-interactive entry points (`codewhale exec`, hooks, automations, headless\n agents) never confirm anything and never apply an unconfirmed request.\n- Routing/classification summaries and durable goal-state text keep their own\n always-on redaction regardless of this switch; the opt-out exists so the\n model can quote file bytes for exact edits, not to relax stored state.\n\nThe config value itself is forgiving: `true`/`false`, `\"on\"`/`\"off\"`, and\n`\"enabled\"`/`\"disabled\"` (any casing) all parse, with `false`/`\"off\"` meaning\n`\"disabled\"`.\n\nA confirmed opt-out still sends your configured API keys to the provider you\nare already talking to. Only use it when the model must read and edit files\nthat contain real credentials.\n\n### Stored sessions\n\nThe masking runs once, when tool output enters the transcript, so saved\nsession files (`~/.codewhale/sessions/*.json` and their checkpoints) and\nRuntime API thread items (their text and tool metadata) do not store a\ncredential-shaped value from tool output. With the confirmed opt-out above,\ntool output is stored as the model saw it.\n\nNot yet masked: large outputs spilled to `~/.codewhale/tool_outputs`, shell\ncompletion evidence artifacts, and tool-call *inputs* (for example a\n`curl -H \"Authorization: Bearer …\"` command line). `doctor` and\n`scrub-secrets` do not check those either.\n\nSessions saved by builds before this change may still hold credentials in\ntheir tool output; nothing rewrites them automatically. `codewhale doctor`\nreports them under **Stored Sessions** (it checks the newest 50 files), and\nthis command finds and masks them across every saved session:\n\n```sh\ncodewhale sessions scrub-secrets # report only\ncodewhale sessions scrub-secrets --apply # rewrite the affected files\n```\n\n`--apply` rewrites each file under the same per-session lock a save takes,\nso it never loses a concurrent save. A session that is still open can write\nits in-memory copy back on its next save, so close open sessions first, and\nrotate any credential that was exposed — masking a stored copy cannot un-leak it.\n"
|
||
},
|
||
{
|
||
"path": "docs/CODEWHALE_AGENT.md",
|
||
"sha256": "78c7a413de36e1e91fd6e5e077b8e80312bc6df00a166133d55d61676235bb56",
|
||
"content": "# Codewhale Agent — one identity, many surfaces\n\n\"Codewhale Agent\" is one product identity with many transport surfaces.\nThis page is the identity map: what each surface is, where each lives, and\nthe rules that keep them one product instead of several.\n\n## The one identity\n\n| Surface | Identity object | Where it lives | State |\n|---|---|---|---|\n| GitHub PR review | GitHub App bot `codewhale-agent[bot]` | GitHub org settings; repo variable `CODEWHALE_APP_ID` + secret `CODEWHALE_APP_PRIVATE_KEY` | `codewhale review --pr N --post` ships with the `codewhale-review.yml` workflow; it no-ops green until the App is configured (founder-gated), then posts as the App instead of the workflow token. Posting is always opt-in via `--post`. |\n| Commit co-author | `.github/AUTHOR_MAP` | repo | existing; harvested co-author credit lands in the contributor graph |\n| Chat channels | per-user bot registration bound to a Codewhale membership | control-plane `services/control-plane/src/integrations/chat/` BotGateway and adapters; contracts in `packages/contracts` | Source includes Telegram and Slack bot pairing and commands. Default read-only command allowlist; opt-in write verbs; approve/deny keyboards route as permission decisions. Availability depends on the deployed gateway and configured bot credentials. |\n\nSlack bot commands control Codewhale; they do not give an agent tools to search\nor read a Slack workspace. The Slack bot requires a user-managed Slack app,\ncredentials and webhook setup. Hosted Slack delivery and a real authenticated\nSlack task have not been qualified, so this source implementation is not a\nready-to-use Slack integration claim. External Slack tools require a separately\nconfigured and authenticated MCP server or API integration.\n\n## Rules that make it one identity\n\n- Every surface is I/O around the single `Engine::run_turn`\n (`crates/engine/src/core/engine/turn_loop.rs`). No surface gets its own\n engine or turn loop.\n- Every surface authenticates to the same Codewhale membership\n (`codewhale login` account session). Membership gates cloud agents;\n provider brands stay internal and invisible.\n- Approvals from any surface route back as permission decisions into the\n same engine. A bare message never executes a gated action.\n- Every command and approval is audited, on every channel.\n- All surfaces are FREE. Rate limits are anti-abuse only. There is no\n per-channel fee, message metering, team gating, or trial timer — and a\n billing hook on a channel feature is forbidden by the money model.\n\n## Naming\n\n- Product: Codewhale. Agent surface: \"Codewhale Agent\".\n- Bot handle wherever it can be chosen: `codewhale-agent`.\n- The agent speaks as \"Codewhale cloud agent\" — never as a provider brand.\n\n## Channel contract\n\nPer-channel command allowlists (default read-only: `status`, `jobs`,\n`receipts`, `help`; opt-in: `new task`, `approve`, `deny`), inline\napprove/deny keyboards routed as permission decisions, `assistant_changes`\nreply mode (message only when something changed), per-channel message\nformat adapters (including Telegram markdown and Slack blocks in source),\nquiet hours + digest, automation output routable\nto any paired channel, one pairing code bound inside the authenticated\napp, unbind revokes instantly.\n\nWeChat note: personal WeChat is supported only through Tencent's official\nClawBot (iLink) channel, never through unofficial account automation. The\nopen-source `integrations/weixin-bridge` uses it with a local\n`codewhale serve --http`, and it stops when Tencent's risk control objects.\nOfficial Account (公众号/服务号) APIs are not used.\n\n## Pointer\n\nChannel contracts live in `packages/contracts` (CWC repo); the gateway and\nadapters live in `services/control-plane/src` (CWC repo). The GitHub\nsurface lives in `.github/workflows/codewhale-review.yml` here.\n"
|
||
}
|
||
]
|
||
},
|
||
{
|
||
"id": "providers",
|
||
"slug": "providers",
|
||
"sources": [
|
||
{
|
||
"path": "docs/PROVIDERS.md",
|
||
"sha256": "92c7a86cd39b6d35a1e561e869df9c9d2ac2d735bcb12ccd750cae1e83545fbc",
|
||
"content": "# Provider Registry\n\n> 阅读简体中文版:[zh_hans/PROVIDERS.md](zh_hans/PROVIDERS.md)\n\nSpeech recognition, synthesis, translation and video have separate execution\npaths. See [media models](MEDIA_MODELS.md) for current support and reviewed\nupstream candidates; a chat catalogue row alone does not add a media backend.\n\nThis registry describes provider behavior that is wired into the current\nCodewhale codebase. It is intentionally conservative: shipped entries are\nlimited to provider IDs, config keys, auth paths, base URLs, model resolution,\nand capability metadata that the code already knows about.\n\nThe `/provider` and `/model` pickers recommend OrcaRouter as a partner option.\nClick the recommendation or press F7 to view the Codewhale × OrcaRouter offer:\na **30% top-up match for DeepSeek V4.1 Flash**. The pager includes the\n[redemption link](https://www.orcarouter.ai/redeem/1a6250545fdc4e0aaccb0a4470159e67)\nand can copy its text. Codewhale earns a referral fee from OrcaRouter.\nCurrent eligibility and terms are owned by the linked offer page. Viewing it\npreserves the current provider, model and credentials; redemption happens on\nOrcaRouter's site.\n\nDeepSeek remains the default provider, but every entry in `ProviderKind::ALL`\nis a first-class selectable provider route. `ALL` is the catalog/picker\nsurface — one identity per vendor. Dual-wire dialect kinds (`*Anthropic`, e.g.\n`deepseek-anthropic`) and the Model Studio plan variants stay on the enum for\nserde and `provider_for_kind` but are deliberately **not** catalog rows:\na plan is `mode`/`base_url` and a dialect is `wire = openai|anthropic` on the\nprimary provider config (`crates/config/src/provider_kind.rs:221-226`). Hosted\nroutes, generic OpenAI-compatible endpoints, the OpenAI Codex/ChatGPT route,\nnative Anthropic, and local runtimes all run the same terminal harness against\nthe selected provider/model/base URL.\n\nA host reached over plain Chat Completions is an ordinary named provider,\nnot a `ProviderKind`: enum variants are reserved for distinct *wires*\n(Anthropic Messages, Codex Responses, Google thought signatures). Any such\nhost is a `[providers.<name>]` table with a base URL, a model, and a key env\n(`docs/CONFIGURATION.md`); `/provider` and `/setup` keep a \"paste a Base URL\nand a key\" path for exactly this. Where available, offerings come from live\n`GET /v1/models` plus the Codewhale catalog (#5350, #6289).\n\nBundled compatible hosts. These ship as descriptor rows in\n`crates/config/assets/provider_descriptors.json` (compiled in by\n`crates/config/src/descriptors.rs`): each row says how to reach the host — wire,\nbase URL, key env, aliases — while model IDs come from configured models, the\nCodewhale catalog, and a live `GET /v1/models` when supported. The example model\nis only a bootstrap hint. Verify\nagainst the vendor's own docs before trusting any value here:\n\n| Host | Base URL | Example models | API key env |\n| --- | --- | --- | --- |\n| Thinking Machines Tinker (beta) | `https://tinker.thinkingmachines.dev/services/tinker-prod/oai/api/v1` | `thinkingmachines/Inkling-2-Small:peft:262144:sampling-nvfp4` | `TINKER_API_KEY` |\n| SenseNova | `https://token.sensenova.cn/v1` | `deepseek-v4-flash` | `SENSENOVA_API_KEY` |\n| Baseten | `https://inference.baseten.co/v1` | `deepseek-ai/DeepSeek-V4-Pro` | `BASETEN_API_KEY` |\n| Groq | `https://api.groq.com/openai/v1` | `llama-3.3-70b-versatile` | `GROQ_API_KEY` |\n| Cerebras | `https://api.cerebras.ai/v1` | `llama-3.3-70b` | `CEREBRAS_API_KEY` |\n| Command Code | `https://api.commandcode.ai/provider/v1` | `deepseek/deepseek-v4-flash` | `COMMAND_CODE_API_KEY` |\n| Alibaba Model Studio (DashScope) | `https://dashscope-intl.aliyuncs.com/compatible-mode/v1` | `qwen3.8-flash` | `DASHSCOPE_API_KEY` |\n| AICraft | `https://aicraftapi.com/v1` | `claude-4.6-sonnet`; DeepSeek / Claude / Gemini / Qwen / GLM / MiniMax / Doubao families | `AICRAFT_API_KEY` |\n| Tsubasa | `https://api.tsubasa.sh/v1` | `tsubasa-pro`, `tsubasa-fast` (32,768-token context) | `TSUBASA_API_KEY` |\n| Cheaper Inference | `https://api.cheaperinference.com/v1` | `gpt-5.4-mini`, `claude-sonnet-5`, `gemini-3.1-pro` | `CHEAPER_INFERENCE_API_KEY` |\n| Yolo-Auto | `https://yolo-auto.com/v1` | `qwen3.8-flash`; `yolo` / `yolo-small` | `YOLO_AUTO_API_KEY` |\n| Standard Compute | `https://api.stdcmpt.com/v1` | `standardcompute` (router); `deepseek/deepseek-v4-pro`, `anthropic/claude-sonnet-5.5` | `STANDARDCOMPUTE_API_KEY` |\n| Runcrate | `https://api.runcrate.ai/v1` | `deepseek-ai/DeepSeek-V4.1-Flash`, `zai-org/GLM-5.3`, `openai/gpt-oss-120b` | `RUNCRATE_API_KEY` |\n| Opper | `https://api.opper.ai/v3/compat` | `claude-sonnet-4-6`, `gpt-5.4-mini` (pool names); `anthropic/claude-sonnet-4-6` | `OPPER_API_KEY` |\n| Tencent TokenHub | `https://tokenhub-intl.tencentcloudmaas.com/v1` | `hy4` | `TENCENT_API_KEY` |\n\nTinker's OpenAI-compatible API and serverless inference are beta. Select\nTinker in `/provider` or configure the named route below. Its documented\nserverless IDs also include `thinkingmachines/Inkling-Small:peft:262144:sampling-nvfp4`\nand `thinkingmachines/Inkling:peft:262144:sampling-nvfp4`; enter one as the\nmodel to use it. The Tinker API also accepts your own sampler checkpoint paths.\n\n```toml\nprovider = \"tinker\"\n\n[providers.tinker]\nkind = \"openai-compatible\"\nbase_url = \"https://tinker.thinkingmachines.dev/services/tinker-prod/oai/api/v1\"\napi_key_env = \"TINKER_API_KEY\"\nmodel = \"thinkingmachines/Inkling-2-Small:peft:262144:sampling-nvfp4\"\n```\n\nCodewhale does not discover a Tinker model roster or verify the key through a\nmodel-list endpoint. The configured model appears in the picker; other IDs\nmust be entered explicitly. Cost remains unknown in Codewhale for this route.\nTinker documents its default reasoning effort as 0.9; this generic route does\nnot currently send an effort setting. Codewhale has not yet qualified a live\nTinker conversation, tool call, or image input. See the vendor's\n[API documentation](https://tinker-docs.thinkingmachines.ai/tinker/compatible-apis/openai/)\nand [model list](https://tinker-docs.thinkingmachines.ai/tinker/models/models_and_pricing/).\n\nAICraft's roster spans DeepSeek, Anthropic Claude, Google Gemini, Qwen, GLM,\nMiniMax and Doubao ids on its OpenAI-compatible endpoint. The authority is\n`GET https://aicraftapi.com/v1/models` with your key — pick a model from that\nlist, not from this table.\nTsubasa implements only `GET /v1/models` and Chat Completions. Its two public\nmodel ids share a 32,768-token context, smaller than the 128,000 tokens Codewhale assumes\nfor an unknown model, so set it on the route after saving:\n`codewhale config set providers.tsubasa.context_window 32768`.\n\nCheaper Inference is an OpenAI-compatible gateway with one key for models from\nseveral labs. Model ids are bare, such as `gpt-5.4-mini` or `claude-sonnet-5`.\nPricing varies by model and route; consult the provider’s current catalog.\nThe authority is `GET https://api.cheaperinference.com/v1/models` with your key.\nDocs: <https://cheaperinference.com/docs>.\n\nStandard Compute offers flat monthly plans with a compute budget. By default,\nrequests return HTTP 402 when it is used; eligible plans can enable Pay as you go\nto continue with additional prepaid charges. See its\n[budget policy](https://docs.standardcompute.com/budgets-and-limits).\nThe `standardcompute` model id routes each request across models and providers;\nvendor-prefixed ids such as `deepseek/deepseek-v4-pro` pin one model. The\nauthority is `GET https://api.stdcmpt.com/v1/models`.\nDocs: <https://docs.standardcompute.com/quickstart>.\n\nRuncrate is an OpenAI-compatible gateway with one key for models from several\nlabs. Model ids are case-sensitive `org/Name` ids such as\n`deepseek-ai/DeepSeek-V4.1-Flash`. The authority is\n`GET https://api.runcrate.ai/v1/models`. On a configured Runcrate route,\n`codewhale models --update --provider runcrate` refreshes this catalog\nanonymously. Inference requires `RUNCRATE_API_KEY`. A successful public listing\nleaves the key unverified. [Runcrate quickstart](https://www.runcrate.ai/docs/getting-started/quickstart).\n\nOpper is an EU-hosted, OpenAI-compatible gateway with 700+ models from 50+\nproviders behind one key. Bare pool names such as `claude-sonnet-4-6` let Opper\npick the route per request; a `provider/model` id such as\n`anthropic/claude-sonnet-4-6` pins one route. Token rates are the model\nproviders' rates with no markup. The authority is\n`GET https://api.opper.ai/v3/compat/models` with your key.\nDocs: <https://docs.opper.ai/build/gateway/drop-in-sdks>.\n\nTencent TokenHub is available as `/provider tencent` (aliases `tokenhub`,\n`tencent-tokenhub`, and `hunyuan`) on the existing Chat Completions transport.\nIts stable HY4 model is `hy4`; OpenRouter uses the distinct `tencent/hy4` wire\nID. Neither rewrites an explicitly selected `hy4-preview` into the stable model.\nTencent documents text/image input, tool calling and reasoning, with a 1M\ncontext, 960K maximum input and 64K maximum output. Its authenticated\n`GET /v1/models` determines account availability. If that response omits limits,\nthe compatible route retains conservative defaults; set\n`providers.tencent.model_context_windows.hy4` to `1000000` to use the documented\nwindow without changing the limits of other models on this gateway.\nDirect cost remains unknown without a scoped user price declaration: Tencent\nrates are not borrowed from OpenRouter. No paid endpoint call was used to\nqualify this descriptor.\n[Tencent HY4 guide](https://www.tencentcloud.com/document/product/1300/80695).\n\nOpenRouter's stable `tencent/hy4` row comes from its public catalog: 1,048,576\ncontext tokens, 65,536 maximum output, text/image input, tools, structured output,\nand reasoning efforts `none`, `low`, and `high`. `/model hy4` resolves to that\nexact ID only on OpenRouter; existing HY3 aliases retain their old selections.\nIts prices come from OpenRouter's own scoped catalog, not Tencent's direct API.\n\nOpenCode Zen and OpenCode Go are first-class provider routes, configured like\nany other provider below; they are not part of this table. In `/provider`,\ntype to filter the list (letters not bound to a row action); `Ctrl+T` probes the\nselected row's `/models` and records reachability only (a 2xx is not\nmodel-ready).\n\nSources to keep in sync:\n\n- `crates/config/assets/provider_descriptors.json` - built-in and compatible-host\n labels, defaults, aliases, key-env lists, config/secret slots, and credential\n guidance. `crates/config/build.rs` generates immutable typed views and the\n existing constant projections from this one data owner.\n- `crates/config/src/provider_kind.rs` and `src/lib.rs` - legacy identity serde,\n config schema, environment precedence and Rust route/auth behavior.\n- `crates/tui/src/config.rs` - captures and verifies exact configured provider\n identities and keeps provider-specific credential and route policy in Rust.\n- `crates/config/assets/catalog_corrections.json` `reviewed` - intrinsic facts,\n scoped selector aliases, completion references and pure transport metadata.\n The existing seed renderer embeds this reviewed supplement in Models.dev.\n- `crates/engine/src/provider_lake.rs` and `route_runtime.rs` - resolve current\n model facts for both CLI model commands, the picker and Runtime API. Public\n catalog facts, provider/account rosters and explicit configuration retain\n their separate ownership. `crates/agent/src/lib.rs` keeps the legacy selector\n compatibility projection; it does not decide the CLI's current route.\n- `config.example.toml` and `docs/CONFIGURATION.md` - user-facing config\n examples and environment variable reference.\n- `scripts/check-provider-registry.py` - drift check for canonical provider\n IDs, live TUI provider IDs, TOML table names, static registry rows, and\n documented defaults.\n\n## Captured Provider Identity\n\nPresentation names, labels, aliases, and historical wire tags come from\n`provider_descriptors.json`. `ProviderKind` remains the Rust-owned intrinsic\ncredential, protocol, and region distinction. A configured route captures both\nits kind and exact table key; a custom table named `openai` stays custom and\ndoes not acquire OpenAI credentials or protocol rules from its name. Case is\nsignificant for custom keys.\n\nDeepSeek China has three preserved representations: `deepseek_c_n` in released\nTUI serde wrappers, `deepseek-cn` as its captured route ID, and `deepseek_cn` as\nits config leaf. Its intrinsic kind is DeepSeek, while its table and endpoint\nstay distinct.\n\nAn old custom route without an additive provider ID can resume only with a\nverified root `base_url` migration into the active `providers.custom` table.\nThat private receipt is bound to the parsed table generation. A table-only,\nprofile-only, conflicting, or later replaced table cannot establish the\nmissing identity; an explicit empty ID is refused. Writes reuse the existing\nlocked config mutation and undo comparison, obtain the fresh migration receipt\nunder that lock, and verify the captured table before changing a leaf.\n\nIn-flight requests retain their captured identity, endpoint, and credential\ngeneration. Health lookup cannot treat an opaque credential reference as ready\nmerely because an earlier request used the same authentication class.\n\n## Provider Selection\n\nWith no saved model, no `default_text_model`, and no `CODEWHALE_MODEL` or\nprovider-specific model variable, a fresh install runs `deepseek-flash` on the\nDeepSeek provider. Precedence is the active provider's configured default, then\nits catalog default — so an explicit `default_text_model` is not silently\noverridden by whichever model the shipped catalog lists first.\n\n### DeepSeek V4.1 Flash\n\n`deepseek-flash` is DeepSeek's id for its newest Flash model. On 2026-10-17\nDeepSeek moved it to DeepSeek-V4.1-Flash-1017 with the same API and price.\nCodewhale 0.10.1 and later use `deepseek-flash` by default, so they run the\nnew model without an update:\n\n1. Create an API key at <https://platform.deepseek.com/api_keys>.\n2. Run `codewhale`, open `/provider` (or press `F3`), choose DeepSeek and\n paste the key. Setting `DEEPSEEK_API_KEY` works too.\n3. Give it a task. To choose the model explicitly, run\n `codewhale --provider deepseek --model deepseek-flash`, or type\n `/model deepseek-flash` in a session.\n\n`deepseek-v4-pro` is DeepSeek's larger model. The retired ids\n`deepseek-v4-flash` and `deepseek-v4-flash-vision-exp` still answer: DeepSeek\nserves them with the same Flash model. On this route `web_search` asks\nDeepSeek's own search first.\n\nRefresh model catalogs without installing a new Codewhale release:\n\n```sh\ncodewhale models --update\ncodewhale models --update --provider openai\ncodewhale models --provider openai --json\n```\n\n`models --update` (also `--refresh`) updates the shared Models.dev metadata\nand calls each configured provider’s `/models` endpoint. Authenticated\nendpoints use that provider’s credentials. Bundled public catalog endpoints\ncan be refreshed anonymously. `--provider ID` restricts the refresh to that exact\nprovider, including named custom endpoints. It makes no inference requests\nand never changes the saved provider or model. A command-line API key is\nconfined to the active provider; other routes are reported as skipped for\nthat invocation.\n\nPlain `models` lists the active provider's saved catalog without provider requests.\nFor ChatGPT, it validates the selected registration locally before reading\nthat account's saved roster. Successful refreshes are saved under Codewhale's\ncatalog directory and used by the model/provider pickers, except account\nlistings (the Codewhale account, Baseten and plugin account catalogs), which\nstay in the memory of the process that read them; `exec`, `review` and the\nworkflow tool therefore read the account listing once before their first\nrequest, and continue with built-in limits if it cannot be read. Cache files\nare scoped to provider identity and endpoint; a failed refresh preserves prior\nrows. Text output reports source, last successful fetch time (Unix seconds),\nand freshness. `--update --json` adds per-source receipts and aggregate counts;\npartial failures return a nonzero exit code after writing those receipts.\nOrdinary `models --json` keeps its model-array format. Bundled/configured\nfallbacks are not proof that an account can use every listed model.\n\n### Sign in with ChatGPT\n\nThe `openai-codex` provider ID now uses OpenAI's official open-source\n[Sign in with ChatGPT flow](https://developers.openai.com/siwc/token-sharing-open-source).\nSign in and refresh the selected account's catalog:\n\n```sh\ncodewhale auth chatgpt\ncodewhale models --update --provider openai-codex\ncodewhale --provider openai-codex\n```\n\nSign-in opens your system browser, uses a loopback callback and PKCE, validates\nthe returned identity, and stores the issued registration and renewable tokens\nin Codewhale's protected credential storage. Permission to use your ChatGPT\nplan is separate from identity sign-in. A declined or missing plan grant stops\ninference; choose another provider explicitly if you want another billing path.\n`codewhale auth chatgpt-revoke` signs out of the Codewhale-owned session.\nThis experimental implementation retains one active registration. A picker\nfor multiple saved ChatGPT accounts is not implemented yet.\n\nIf the browser succeeds but token verification fails, the error identifies the\nfailed check: signature, issuer, audience, expiry, not-before time, or a\nmissing required claim. For an expired or not-yet-valid token, synchronize\nyour device date and time and start a new sign-in. Other verification failures\nare not evidence of a plan limitation. Report the error text and Codewhale\nversion; never share tokens, callback codes, credential files or decoded claims.\nA failed verification does not activate the new registration.\n\nModel discovery uses your granted bearer token at\n`GET https://api.openai.com/v1/models`; inference uses the public\n`POST https://api.openai.com/v1/responses` endpoint. The model picker keeps\nOpenAI's display labels and order and lists only entries with `visibility: list`.\nThe catalog contains no credentials and is bound to the verified issuer,\nissued client ID, and account subject. Another account or workspace never\ninherits those model choices. Missing, invalid, or older-than-one-day rosters\nare reported explicitly and provide no account entitlement evidence. Run the\nrefresh command after signing in. Neither model discovery nor ordinary listing\nstarts an inference request. Imported Codex CLI tokens and legacy process-token\nvariables cannot authorize this official plan route.\n\nEligible requests consume your ChatGPT plan or credits. ChatGPT Plus's\nfive-hour allowance is shared across apps; each app receives no separate\nallowance. The documented five-hour limit does not apply to Pro. App-specific\nlimits can also apply. Review limits and access in\n[ChatGPT usage settings](https://chatgpt.com/settings/usage). Codewhale does\nnot silently switch to an API key or another provider when a limit is reached.\n\nThis is an OpenAI preview for open-source/local apps. It does not grant access\nto ChatGPT conversation history. Requests stream with `store: false` and\n`stream: true`, while Codewhale retains its own session history and tools.\nOpenAI-hosted image generation, file search, Code Interpreter, native computer\nuse, hosted MCP/connectors, and Responses `tool_search` are unavailable on this\nroute; Codewhale's own tools use supported function/custom tool calls.\nThe ChatGPT preview requests one function call at a time.\nSee [preview limitations](https://developers.openai.com/siwc/token-sharing-open-source/preview-limitations).\nPaid or remotely hosted applications require the\n[commercial partner interest process](https://openai.com/form/sign-in-with-chatgpt-interest/);\nthis local integration is not commercial approval.\n\nThe canonical provider IDs are the entries of `ProviderKind::ALL`\n(`crates/config/src/provider_kind.rs`), in that order:\n\n`deepseek`, `nvidia-nim`, `openai`, `atlascloud`, `wanjie-ark`, `volcengine`,\n`openrouter`, `orcarouter`, `xiaomi-mimo`, `novita`, `fireworks`, `siliconflow`, `arcee`,\n`siliconflow-CN`, `moonshot`, `sglang`, `vllm`, `ollama`, `ollama-cloud`, `huggingface`,\n`together`, `qianfan`, `openai-codex`, `anthropic`, `openmodel`, `zai`,\n`stepfun`, `minimax`, `deepinfra`, `sakana`, `longcat`, `opencode-go`,\n`opencode-zen`, `meta`, `xai`, `mistral`, `telecomjs`, `modelstudio-token-plan`, `modelscope`,\n`google`, `edenai`, `zenmux`, `csdn`, `concentrate`, `codewhale`, and `custom`.\n\n`deepseek-anthropic` is *not* on this list — it is a wire dialect of\n`deepseek`, reached with `wire = \"anthropic\"`, not a separate route to select.\n\nUse any of these surfaces to select a provider:\n\n- CLI: `codewhale --provider <id>`\n- TUI: `/provider <id>` or the provider picker\n- Env: `CODEWHALE_PROVIDER=<id>`; `DEEPSEEK_PROVIDER=<id>` is the legacy alias\n- Config: `provider = \"<id>\"`\n\n`deepseek-cn`, `deepseek_china`, `deepseekcn`, and `deepseek-china` are accepted\nas legacy aliases for `deepseek`. They do not select a different official host;\nDeepSeek uses the same official API host worldwide.\n\n`deepseek_anthropic`, `deepseek-claude`, and `deepseek_claude` select\n`deepseek-anthropic`, the opt-in DeepSeek route that speaks the Anthropic\nMessages API at `https://api.deepseek.com/anthropic`. It keeps the normal\nDeepSeek API key path but uses `x-api-key` plus `anthropic-version: 2023-06-01`\ninstead of Bearer auth. If the key already lives in official DeepSeek Harness\n(`dsh`) at `$DSH_HOME/.credentials.yaml`, grant read-only access with\n`codewhale auth external-consent --provider deepseek --mode read-only`.\nCodewhale never writes that file and only reads `DEEPSEEK_API_KEY`.\n\n`huggingface`, `hugging-face`, `hugging_face`, and `hf` all select the\nHugging Face Inference Providers route. This is the OpenAI-compatible router\npath for chat/inference, not Hub browsing, model-card inspection, uploads, or\nartifact export.\n\n`telecomjs`, `telecom-js`, `telecom_js`, `telecomjs-cn`, and `tokenhub` all\nselect the TelecomJS TokenHub route. Its authenticated `/models` catalog is\nkey-scoped and remains isolated from every other provider's live snapshot.\n\nFresh shared config writes to `~/.codewhale/config.toml`. Existing\n`~/.deepseek/config.toml` files are still read for compatibility.\n\n### Legacy Antigravity tombstone\n\nAntigravity is not a Codewhale provider and cannot be selected or run. Existing\nlegacy Antigravity provider state is recognized only as a non-runnable migration\ntombstone. Run `codewhale auth clear --provider antigravity` to forget only\nCodewhale-owned legacy configuration and consent metadata. This does not sign\nout of, revoke, read, or otherwise alter any official Google or Antigravity\nsession. For Gemini, select the supported `google` provider and supply\n`GEMINI_API_KEY`.\n\n### Wire Protocol Compatibility\n\nProvider selection is explicit. A model string prefix such as\n`deepseek-ai/...`, `deepseek/...`, `qwen/...`, or `arcee-ai/...` is a\nprovider-owned wire ID or catalog namespace hint under the selected provider.\nIt is not a provider switch and must not be treated as proof that the route is\nDeepSeek, OpenRouter, or any other provider.\n\nSet the route with `provider = \"<id>\"`, `CODEWHALE_PROVIDER=<id>`, or\n`codewhale --provider <id>`. Set the request model with `CODEWHALE_MODEL`, a\nprovider-specific model env var, top-level `default_text_model`, or\n`[providers.<table>].model`. Set the endpoint with `CODEWHALE_BASE_URL`, a\nprovider-specific base URL env var, or `[providers.<table>].base_url`. Set auth\nwith `codewhale auth set --provider <id>`, `[providers.<table>].api_key`, or\nthe listed provider env vars.\n\n| Provider ID | TOML table | Wire protocol | Auth env vars |\n| --- | --- | --- | --- |\n| `deepseek` | `[providers.deepseek]` | Model-aware: Responses default (`deepseek-flash`); Chat Completions (`deepseek-v4-pro`) | `DEEPSEEK_API_KEY` |\n| `deepseek-anthropic` | `[providers.deepseek_anthropic]` | Anthropic Messages | `DEEPSEEK_API_KEY` |\n| `nvidia-nim` | `[providers.nvidia_nim]` | OpenAI Chat Completions | `NVIDIA_API_KEY`, `NVIDIA_NIM_API_KEY` |\n| `openai` | `[providers.openai]` | OpenAI Chat Completions | `OPENAI_API_KEY` |\n| `atlascloud` | `[providers.atlascloud]` | OpenAI Chat Completions | `ATLASCLOUD_API_KEY` |\n| `wanjie-ark` | `[providers.wanjie_ark]` | OpenAI Chat Completions | `WANJIE_ARK_API_KEY`, `WANJIE_API_KEY`, `WANJIE_MAAS_API_KEY` |\n| `volcengine` | `[providers.volcengine]` | OpenAI Chat Completions | `VOLCENGINE_API_KEY`, `VOLCENGINE_ARK_API_KEY`, `ARK_API_KEY` |\n| `openrouter` | `[providers.openrouter]` | OpenAI Chat Completions | `OPENROUTER_API_KEY` |\n| `xiaomi-mimo` | `[providers.xiaomi_mimo]` | OpenAI Chat Completions | `XIAOMI_MIMO_TOKEN_PLAN_API_KEY`, `MIMO_TOKEN_PLAN_API_KEY`, `XIAOMI_MIMO_API_KEY`, `XIAOMI_API_KEY`, `MIMO_API_KEY` |\n| `novita` | `[providers.novita]` | OpenAI Chat Completions | `NOVITA_API_KEY` |\n| `fireworks` | `[providers.fireworks]` | OpenAI Chat Completions | `FIREWORKS_API_KEY` |\n| `siliconflow` | `[providers.siliconflow]` | OpenAI Chat Completions | `SILICONFLOW_API_KEY` |\n| `arcee` | `[providers.arcee]` | OpenAI Chat Completions | `ARCEE_API_KEY` |\n| `siliconflow-CN` | `[providers.siliconflow_cn]` | OpenAI Chat Completions | `SILICONFLOW_API_KEY` |\n| `moonshot` | `[providers.moonshot]` | OpenAI Chat Completions | `MOONSHOT_API_KEY`, `KIMI_API_KEY` |\n| `sglang` | `[providers.sglang]` | OpenAI Chat Completions | `SGLANG_API_KEY` |\n| `vllm` | `[providers.vllm]` | OpenAI Chat Completions | `VLLM_API_KEY` |\n| `ollama` | `[providers.ollama]` | Local OpenAI-compatible Chat Completions | `OLLAMA_API_KEY` (optional; only for an authenticated local route) |\n| `ollama-cloud` | `[providers.ollama_cloud]` | Hosted OpenAI-compatible Chat Completions | `OLLAMA_CLOUD_API_KEY`, `OLLAMA_API_KEY` |\n| `huggingface` | `[providers.huggingface]` | OpenAI Chat Completions | `HUGGINGFACE_API_KEY`, `HF_TOKEN` |\n| `modelscope` | `[providers.modelscope]` | OpenAI Chat Completions | `MODELSCOPE_API_KEY` |\n| `together` | `[providers.together]` | OpenAI Chat Completions | `TOGETHER_API_KEY` |\n| `qianfan` | `[providers.qianfan]` | OpenAI Chat Completions | `QIANFAN_API_KEY`, `BAIDU_QIANFAN_API_KEY` |\n| `openai-codex` | `[providers.openai_codex]` | OpenAI Responses | Official Sign in with ChatGPT (`codewhale auth chatgpt`) with a validated Codewhale-owned plan grant |\n| `anthropic` | `[providers.anthropic]` | Anthropic Messages | `ANTHROPIC_API_KEY` or `codewhale auth claude` |\n| `openmodel` | `[providers.openmodel]` | Anthropic Messages | `OPENMODEL_API_KEY` |\n| `zai` | `[providers.zai]` | OpenAI Chat Completions | `ZAI_API_KEY`, `Z_AI_API_KEY` |\n| `stepfun` | `[providers.stepfun]` | OpenAI Chat Completions | `STEPFUN_API_KEY`, `STEP_API_KEY` |\n| `minimax` | `[providers.minimax]` | OpenAI Chat Completions | `MINIMAX_API_KEY` |\n| `deepinfra` | `[providers.deepinfra]` | OpenAI Chat Completions | `DEEPINFRA_API_KEY`, `DEEPINFRA_TOKEN` |\n| `sakana` | `[providers.sakana]` | OpenAI Chat Completions | `FUGU_API_KEY`, `SAKANA_API_KEY` |\n| `longcat` | `[providers.longcat]` | OpenAI Chat Completions | `LONGCAT_API_KEY` |\n| `opencode-go` | `[providers.opencode_go]` | OpenAI Chat Completions | `OPENCODE_GO_API_KEY` |\n| `opencode-zen` | `[providers.opencode_zen]` | Model-aware: OpenAI Responses, Anthropic Messages, or OpenAI Chat Completions | `OPENCODE_ZEN_API_KEY`, `OPENCODE_API_KEY` |\n| `meta` | `[providers.meta]` | OpenAI Chat Completions | `META_MODEL_API_KEY`, `MODEL_API_KEY` |\n| `telecomjs` | `[providers.telecomjs]` | OpenAI Chat Completions | `TELECOMJS_API_KEY` |\n| `xai` | `[providers.xai]` | OpenAI Chat Completions | `XAI_API_KEY` |\n| `mistral` | `[providers.mistral]` | OpenAI Chat Completions | `MISTRAL_API_KEY` |\n| `google` | `[providers.google]` | OpenAI Chat Completions (official Gemini OpenAI-compat route; captures and replays thought signatures on tool calls) | `GOOGLE_API_KEY`, `GEMINI_API_KEY` |\n| `edenai` | `[providers.edenai]` | OpenAI Chat Completions | `EDENAI_API_KEY` |\n| `zenmux` | `[providers.zenmux]` | OpenAI Chat Completions | `ZENMUX_API_KEY` |\n| `csdn` | `[providers.csdn]` | OpenAI Chat Completions | `CSDN_API_KEY` |\n| `concentrate` | `[providers.concentrate]` | OpenAI Responses (`/v1/responses`) | `CONCENTRATE_API_KEY` |\n| `codewhale` | `[providers.codewhale]` | Model-aware: OpenAI Chat Completions (`/v1/chat/completions`) or Anthropic Messages (`/v1/messages`), chosen per model by the account catalog | `CODEWHALE_API_KEY` |\n| `modelstudio-token-plan` | `[providers.modelstudio_token_plan]` | OpenAI Chat Completions | `MODELSTUDIO_API_KEY`, `DASHSCOPE_API_KEY` |\n| `modelstudio-token-plan-anthropic` | `[providers.modelstudio_token_plan_anthropic]` | Anthropic Messages | `MODELSTUDIO_API_KEY`, `DASHSCOPE_API_KEY` |\n| `modelstudio-coding-plan` | `[providers.modelstudio_coding_plan]` | OpenAI Chat Completions | `MODELSTUDIO_API_KEY`, `DASHSCOPE_API_KEY` |\n| `modelstudio-coding-plan-anthropic` | `[providers.modelstudio_coding_plan_anthropic]` | Anthropic Messages | `MODELSTUDIO_API_KEY`, `DASHSCOPE_API_KEY` |\n\nDefault base URLs and models for each route are listed in the shipped provider\ntable below. The wire protocol values above are derived from\n`crates/config/src/provider.rs`: `ChatCompletions` is the default,\n`openai-codex` overrides to `Responses`; `deepseek-anthropic`, `anthropic`, and\n`openmodel` override to `AnthropicMessages`; `opencode-zen` resolves the\nprotocol from the selected model's curated offering; and `deepseek` is\nmodel-aware — the shipped default `deepseek-flash` (and legacy\n`deepseek-v4-flash`) rides the Responses endpoint while `deepseek-v4-pro`\nstays on Chat Completions.\n\n## Auth And Env Rules\n\nFor hosted providers, `codewhale auth set --provider <id>` saves an API key for\nthat provider. API-key environment variables are fallback inputs after saved\nconfig and keyring credentials; an explicit process-level `--api-key` still\nwins for that launch.\n\nFor base URL and model selection, prefer:\n\n- `CODEWHALE_BASE_URL` / `CODEWHALE_MODEL` for the active provider.\n- Provider-specific base URL/model env vars when listed below.\n- `DEEPSEEK_BASE_URL`, `DEEPSEEK_MODEL`, and `DEEPSEEK_DEFAULT_TEXT_MODEL` as\n legacy aliases.\n\nNon-local `http://` base URLs are rejected unless\n`DEEPSEEK_ALLOW_INSECURE_HTTP=1` is set. Loopback HTTP URLs are allowed for\nself-hosted runtimes.\n\n## Custom DeepSeek-Compatible Endpoints\n\nMost custom DeepSeek-compatible deployments can use an existing provider ID.\nDo not create `[providers.deepseek_custom]`; the provider table names are fixed.\nInstead, choose the closest shipped route and override its endpoint/model:\n\n- DeepSeek-compatible hosted API: keep `provider = \"deepseek\"` and set\n `[providers.deepseek].base_url` plus `[providers.deepseek].model`, or launch\n with `DEEPSEEK_BASE_URL` and `DEEPSEEK_MODEL`.\n- Generic OpenAI-compatible gateway: use `provider = \"openai\"` with\n `[providers.openai].base_url` plus `[providers.openai].model`, or launch with\n `OPENAI_BASE_URL` and `OPENAI_MODEL`.\n- Multiple named OpenAI-compatible gateways, or local routes you want to pin\n from an AgentProfile, can use a custom table such as\n `[providers.lm-studio] kind = \"openai-compatible\"` and select it with\n `provider = \"lm-studio\"` or a profile `provider = \"lm-studio\"`. Such a table\n speaks Chat Completions unless it sets `wire = \"responses\"` (OpenAI\n Responses at `<base_url>/responses`) or `wire = \"anthropic\"`.\n- Local OpenAI-compatible runtimes: use `provider = \"vllm\"`, `\"sglang\"`, or\n `\"ollama\"` with the matching provider-specific base URL/model values.\n\nExample user config for a DeepSeek-compatible host:\n\n```toml\nprovider = \"deepseek\"\n\n[providers.deepseek]\napi_key = \"YOUR_API_KEY\"\nbase_url = \"https://your-provider.example/v1\"\nmodel = \"deepseek-ai/DeepSeek-V4-Pro\"\n```\n\nExample user config for a generic gateway:\n\n```toml\nprovider = \"openai\"\n\n[providers.openai]\napi_key = \"YOUR_GATEWAY_API_KEY\"\nbase_url = \"https://gateway.example/v1\"\nmodel = \"your-deepseek-compatible-model\"\n```\n\nAlibaba Cloud Model Studio (Bailian / DashScope) is a first-class provider as\nof v0.9.4 with two plan profiles: Token Plan (Personal / Team) and Coding Plan.\nBoth plans expose an OpenAI-compatible Chat Completions endpoint and an\nAnthropic-compatible Messages endpoint.\n\n**Token Plan** (Personal and Team share the same AP-Southeast endpoint):\n\n```toml\nprovider = \"modelstudio-token-plan\"\n\n[providers.modelstudio_token_plan]\napi_key = \"YOUR_MODELSTUDIO_API_KEY\"\n# base_url defaults to https://token-plan.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1\nmodel = \"qwen3.8-max\" # or qwen3.7-plus | qwen3.7-max |\n # qwen3.6-flash | deepseek-v4-pro | deepseek-v4-flash-0731 |\n # glm-5.2\n```\n\n**Coding Plan** (separate international endpoint):\n\n```toml\nprovider = \"modelstudio-coding-plan\"\n\n[providers.modelstudio_coding_plan]\napi_key = \"YOUR_MODELSTUDIO_API_KEY\"\n# base_url defaults to https://coding-intl.dashscope.aliyuncs.com/v1\nmodel = \"qwen3.8-max\"\n```\n\n**Anthropic-compatible dialect** — both plans also expose a native Anthropic\nMessages path. Select it with the `-anthropic` provider suffix:\n\n```toml\nprovider = \"modelstudio-token-plan-anthropic\"\n\n[providers.modelstudio_token_plan_anthropic]\napi_key = \"YOUR_MODELSTUDIO_API_KEY\"\n# base_url defaults to https://token-plan.ap-southeast-1.maas.aliyuncs.com/apps/anthropic\nmodel = \"qwen3.8-max\"\n```\n\nCreate or copy a Model Studio API key from the\n[Bailian console](https://bailian.console.aliyun.com/). The API key is shared\nacross all four provider IDs above; only the base URL and wire protocol differ.\n\n**Thinking / reasoning.** Reasoning surfaces in the TUI's Thinking view on both\ndialects, per Model Studio's\n[deep-thinking docs](https://www.alibabacloud.com/help/en/model-studio/deep-thinking).\n\nOn the OpenAI-compatible routes the top-level controls are **route- and\nmodel-specific**, and Codewhale fails closed: they are sent only when the\nconfigured `base_url` is an official Alibaba Chat Completions host\n(`*.maas.aliyuncs.com/compatible-mode/v1`, including workspace-scoped hosts, or\n`coding-intl.dashscope.aliyuncs.com/v1`). A custom `base_url` on the same\nprovider ID gets `thinking`, `enable_thinking`, `preserve_thinking`, and\n`reasoning_effort` stripped, so an arbitrary OpenAI-compatible gateway is never\nhanded Alibaba's dialect. On a verified host:\n\n- **Hybrid models** (`qwen3.7-*`, `qwen3.6-*`, `deepseek-v4*`, `glm-*`,\n `kimi-k2.6*`) get `enable_thinking`: `false` for `off`, `true` otherwise.\n- **Thinking-only models** — `qwen3.8-max` (catalogued `thinking: always_on`)\n and `kimi-k2.7-code` — get **no** enable/disable switch at all. Sending one is at\n best ignored.\n- `preserve_thinking` is sent for the models documented to accept it\n (`qwen3.7-max`/`-plus`, `qwen3.6-max-preview`/`-plus`/`-flash`, `kimi-k2.6*`,\n `kimi-k2.7-code`), so the next turn keeps the assistant's trace.\n- `reasoning_effort` is sent only for the two families with a documented ladder\n — `deepseek-v4*` and `glm-5`/`5.1`/`5.2` — mapped to `high` or `max`.\n\nReasoning streams back as `delta.reasoning_content`. It is replayed to the\nprovider on later turns only for the `preserve_thinking` models above and the\nthinking-only models; `deepseek-v3.1`, `deepseek-v3.2`, and `glm-*` history\nstays stripped pending live confirmation that DashScope accepts\n`reasoning_content` in input messages. (`deepseek-v4*` replays regardless — the\nDeepSeek thinking-mode contract requires it on every provider.)\n\nOn the Anthropic-compatible routes, thinking uses the documented\n`{\"type\":\"enabled\",\"budget_tokens\":N}` / `{\"type\":\"disabled\"}` shapes from the\n[Anthropic-compatible Messages API](https://www.alibabacloud.com/help/en/model-studio/anthropic-api-messages),\nwith `budget_tokens` derived from the effort level.\n\nDeepSeek (`deepseek-v4-pro`, `deepseek-v4-flash-0731`) and GLM (`glm-5.2`)\nmodels served by Model Studio are provider-scoped and do not collide with the\nfirst-party DeepSeek or Zhipu/Z.ai routes. Model Studio publishes no `glm-5.3`\nentry, so Codewhale does not offer one on this route.\nPay-as-you-go workspace-id templating is not yet in the built-in provider; use\na custom provider entry for that plan until a follow-up adds it.\n\nPrivate gateways with broken or intercepted certificates should use\n`SSL_CERT_FILE` with a trusted CA bundle. The legacy\n`insecure_skip_tls_verify = true` key is still parsed so `codewhale doctor` can\nreport stale configs, but provider clients reject it instead of skipping TLS\ncertificate verification.\n\nKeep `provider`, `api_key`, and `base_url` in user config or process\nenvironment. Project-local config overlays intentionally cannot set those keys,\nso a repository cannot silently redirect prompts or credentials to another\nendpoint.\n\n## Local Models (DS4, Ollama, vLLM, SGLang)\n\nSelf-hosted OpenAI-compatible runtimes are first-class routes and are keyless\nby default — set an API key only when your server requires one. Start your\nrunner, then point Codewhale at it with `--provider` / `/provider` or a config\ntable.\n\n| Runner | Default base URL | Default model | Base URL override |\n| --- | --- | --- | --- |\n| `ollama` | `http://localhost:11434/v1` | live tag from `GET /v1/models` (pre-refresh: `unknown`) | `OLLAMA_BASE_URL` |\n| `vllm` | `http://localhost:8000/v1` | `deepseek-ai/DeepSeek-V4-Pro` | `VLLM_BASE_URL` |\n| `sglang` | `http://localhost:30000/v1` | `deepseek-ai/DeepSeek-V4-Pro` | `SGLANG_BASE_URL` |\n\n### DS4 (DwarfStar)\n\n[DS4](https://github.com/antirez/ds4/tree/84cc882352757baf628a1776badf7cc54d584e28)\nserves DeepSeek V4 Flash and Pro locally\nthrough an OpenAI-compatible API. Start DS4, then open Codewhale's prefilled,\nkeyless setup form:\n\n```bash\n./ds4-server --ctx 100000 --kv-disk-dir /tmp/ds4-kv --kv-disk-space-mb 8192\ncodewhale\n# In Codewhale: /setup provider ds4\n```\n\nReview the prefilled route and press Enter to save it. The preset budgets a\n100,000-token context to match that starter command and defaults to the Flash\ncompatibility alias. Check the local route explicitly with\n`codewhale doctor --probe-local`.\n\nDS4 loads the actual GGUF when the server starts. Its `deepseek-v4-flash` and\n`deepseek-v4-pro` API ids are compatibility aliases; changing `/model` does\nnot swap the resident model. To run Pro, download the supported Pro weights\nand start `ds4-server -m <pro.gguf> ...` as described by DS4. Update\n`context_window` whenever the server's `--ctx` value changes.\n\nThe equivalent config is:\n\n```toml\nprovider = \"ds4\"\n\n[providers.ds4]\nkind = \"openai-compatible\"\nbase_url = \"http://127.0.0.1:8000/v1\"\nmodel = \"deepseek-v4-flash\"\nauth_mode = \"none\"\ncontext_window = 100000\n```\n\nCodewhale reuses its existing OpenAI-compatible transport and DeepSeek\nreasoning/tool-call shaping for DS4. It does not invent an API key, confuse an\nAPI alias with the loaded GGUF, or silently switch to a hosted DeepSeek route.\nThe pinned DS4 [agent-client contract](https://github.com/antirez/ds4/blob/84cc882352757baf628a1776badf7cc54d584e28/README.md#agent-client-usage)\ndocuments Chat Completions at `/v1`, DeepSeek thinking replay, streamed usage,\n`max_tokens`, and no strict-tool mode; Codewhale follows those exact route\nfacts instead of inheriting unsupported capabilities from a generic gateway.\nThe primary sources for model-facing behavior are DeepSeek's official\n[thinking-mode](https://api-docs.deepseek.com/guides/thinking_mode),\n[tool-call](https://api-docs.deepseek.com/guides/tool_calls), and\n[Chat Completion](https://api-docs.deepseek.com/api/create-chat-completion)\ncontracts. The pinned\n[DeepSeek Harness adapter](https://github.com/deepseek-ai/deepseek-harness/blob/47f943859bef60e4160492346772ded9b24f765a/packages/llm/llm-deepseek/README.md)\nis only a secondary implementation cross-check; it is not the API contract.\n\n### Ollama\n\n```bash\nollama serve # if not already running\nollama pull <model> # e.g. deepseek-v4-flash, or any tag you prefer\ncodewhale --provider ollama --model <model>\n```\n\nProvider-hinted model names are sent as-is, so `--model qwen3:8b` works with\nany tag Ollama has pulled.\n\n### Ollama Cloud\n\nOllama Cloud is a separate hosted provider. It uses the authenticated\nOpenAI-compatible `/v1/chat/completions` route and defaults to `gpt-oss:120b`:\n\n```toml\nprovider = \"ollama-cloud\"\n\n[providers.ollama_cloud]\nbase_url = \"https://ollama.com/v1\"\nmodel = \"gpt-oss:120b\"\n```\n\nCreate a key in [Ollama account settings](https://ollama.com/settings/keys),\nthen run `codewhale auth set --provider ollama-cloud`. For ambient auth,\n`OLLAMA_CLOUD_API_KEY` wins over Ollama's official `OLLAMA_API_KEY`.\n`OLLAMA_CLOUD_BASE_URL` and `OLLAMA_CLOUD_MODEL` override the Cloud defaults;\narbitrary provider-owned model IDs pass through unchanged. Local `ollama`\nremains a separate, keyless-by-default provider.\n\nCompatibility is read-only and in memory: a released config that selected\n`provider = \"ollama\"` with the exact normalized\n`[providers.ollama] base_url = \"https://ollama.com/v1\"` tuple is treated as\n`ollama-cloud` at runtime. Only that exact tuple may fall back to the legacy\n`ollama` secret slot. Codewhale does not rewrite the config, copy or delete a\nsecret, migrate neighboring paths, or make an explicit `ollama-cloud` route\nconsume the legacy slot.\n\n### vLLM\n\n```bash\nvllm serve <model> --port 8000\n# or: python -m vllm.entrypoints.openai.api_server --model <model> --port 8000\ncodewhale --provider vllm --model <model>\n```\n\nvLLM's OpenAI-compatible server listens on port 8000 by default, matching\nCodewhale's `VLLM_BASE_URL`.\n\n### SGLang\n\n```bash\npython -m sglang.launch_server --model-path <model> --port 30000\ncodewhale --provider sglang --model <model>\n```\n\nSGLang's default port 30000 matches Codewhale's `SGLANG_BASE_URL`.\n\n### Pinning a local route in config\n\n```toml\nprovider = \"ollama\" # or \"vllm\" / \"sglang\"\n\n[providers.ollama]\nmodel = \"qwen3:8b\" # default: a tag the local daemon lists\n# base_url defaults to http://localhost:11434/v1\n```\n\nLocal models that print tool-call JSON without the wire markers: see\n[When a Local Model Prints Tool JSON](#when-a-local-model-prints-tool-json).\n\n## Credential Links\n\nProvider setup surfaces use the same typed credential metadata as onboarding,\n`/provider`, `/links`, setup receipts, and doctor output. A missing URL is\nintentional: local, OAuth-only, and user-defined routes show their supported\nconfiguration path instead of guessing a vendor page.\n\n| Provider ID | Credential or console link |\n| --- | --- |\n| `deepseek`, `deepseek-anthropic` | [DeepSeek API keys](https://platform.deepseek.com/api_keys) |\n| `nvidia-nim` | [NVIDIA NIM API keys](https://build.nvidia.com/settings/api-keys) |\n| `openai` | [OpenAI API keys](https://platform.openai.com/api-keys) |\n| `atlascloud` | [Atlas Cloud API keys](https://atlascloud.ai/docs/en/api-keys) |\n| `wanjie-ark` | [Wanjie MaaS APIKEY docs](https://docs.wanjiedata.com/maas/maas-openapi-v1.html) |\n| `volcengine` | [Volcengine Ark API keys](https://console.volcengine.com/ark/apiKey) |\n| `openrouter` | [OpenRouter keys](https://openrouter.ai/settings/keys) |\n| `xiaomi-mimo` | [Xiaomi MiMo Token Plan](https://platform.xiaomimimo.com/token-plan) |\n| `novita` | [Novita key management](https://novita.ai/en/settings/key-management) |\n| `fireworks` | [Fireworks API keys](https://fireworks.ai/api-keys) |\n| `siliconflow` | [SiliconFlow global API keys](https://cloud.siliconflow.com/account/ak) |\n| `siliconflow-CN` | [SiliconFlow China API keys](https://cloud.siliconflow.cn/account/ak) |\n| `arcee` | [Arcee API key guide](https://docs.arcee.ai/other/create-your-first-api-key) |\n| `moonshot` | [Kimi API platform keys](https://platform.kimi.ai/console/api-keys) or [Kimi Code membership console](https://www.kimi.com/code/console) |\n| `zai` | [Z.ai model API](https://z.ai/model-api) |\n| `stepfun` | [StepFun Open Platform](https://platform.stepfun.ai/) |\n| `minimax`, `minimax-anthropic` | [MiniMax interface keys](https://platform.minimax.io/user-center/basic-information/interface-key) |\n| `huggingface` | [Hugging Face tokens](https://huggingface.co/settings/tokens) |\n| `modelscope` | [ModelScope API Keys](https://modelscope.cn/my/settings/token) |\n| `deepinfra` | [DeepInfra API keys](https://deepinfra.com/dash/api_keys) |\n| `together` | [Together API keys](https://api.together.ai/settings/api-keys) |\n| `qianfan` | [Baidu Cloud access keys](https://console.bce.baidu.com/iam/#/iam/accesslist) |\n| `anthropic` | [Anthropic API keys](https://console.anthropic.com/settings/keys) |\n| `openmodel` | [OpenModel console](https://console.openmodel.ai/) ([authentication guide](https://docs.openmodel.ai/en/docs/getting-started/authentication)) |\n| `openai-codex` | Official Sign in with ChatGPT via `codewhale auth chatgpt` (eligible ChatGPT plan or credits, Codewhale-owned tokens). The `openai` API-key route has separate billing. Legacy imported Codex credentials do not authorize this route. |\n| `sglang`, `vllm` | Local OpenAI-compatible endpoints are keyless by default; configure a key only when the server requires one. |\n| `ollama` | Local Ollama is keyless by default; configure a key only when the local server requires one. |\n| `ollama-cloud` | Create an [Ollama API key](https://ollama.com/settings/keys), save it with `codewhale auth set --provider ollama-cloud`, or set `OLLAMA_CLOUD_API_KEY` / `OLLAMA_API_KEY` in that precedence order. |\n| `sakana` | [Sakana AI API keys](https://console.sakana.ai/api-keys) ([get started](https://console.sakana.ai/get-started)) |\n| `longcat` | [Meituan LongCat platform](https://longcat.chat/platform) |\n| `opencode-go` | [OpenCode Go](https://opencode.ai/docs/go/) |\n| `opencode-zen` | [OpenCode Zen](https://opencode.ai/docs/zen/) |\n| `meta` | [Meta Model API](https://developer.meta.com/ai/) |\n| `telecomjs` | [TelecomJS TokenHub](https://aigw.telecomjs.com/) |\n| `xai` | [xAI Console](https://console.x.ai/) for an API key, Codewhale-owned device login, or explicitly consented read-only Grok CLI credentials. |\n| `mistral` | [Mistral Console (la Plateforme)](https://console.mistral.ai/api-keys) |\n| `google` | [Google AI Studio](https://aistudio.google.com/apikey) — Codewhale uses the official Gemini OpenAI-compatible endpoint and never reads Google OAuth files. |\n| `edenai` | [Eden AI API keys](https://app.edenai.run/settings/api-keys) |\n| `zenmux` | [ZenMux API keys](https://zenmux.ai/platform/pay-as-you-go) |\n| `csdn` | [CSDN 星图 console](https://ai.csdn.net/workbench/api-key) — choose the Coding Plan key type for the `glm_for_coding` plan route; a general key bills metered. Docs: [Coding Plan](https://ai.csdn.net/coding-plan). |\n| `concentrate` | [Concentrate dashboard](https://concentrate.ai/) → API Keys → Create API Key (a Universal API key); docs: [API introduction](https://concentrate.ai/docs/api-reference/introduction). BYOK only — the key stays in the local secret store. |\n| `modelstudio-token-plan`, `modelstudio-token-plan-anthropic`, `modelstudio-coding-plan`, `modelstudio-coding-plan-anthropic` | [Alibaba Cloud Model Studio (Bailian console)](https://bailian.console.aliyun.com/) — create or copy a Model Studio API key. |\n| `codewhale` | [Codewhale account settings](https://app.codewhale.net/settings?section=api) — create an API key with the `models:infer` scope, or run `codewhale account api-keys create --name <name> --use`. |\n| `custom` | Set the named provider's `base_url` and `api_key_env` or `api_key`; no canonical vendor credential page exists. |\n\nFor Kimi, the official [quickstart](https://platform.kimi.ai/docs/overview)\ndirects users to sign in, open **API Keys**, create and copy a key, and keep it\nsecret. Codewhale links straight to that console and accepts the copied key.\nIt never probes or impersonates `kimi_cli`/`kimi_code_cli`; first-class Kimi\nOAuth remains blocked on a vendor-registered Codewhale identity.\n\n### Subscription sign-in accounts (ChatGPT, xAI)\n\nCodewhale keeps one sign-in of its own per subscription provider. It is\nseparate from the browser, the ChatGPT or Grok apps, and the Codex or Grok\nCLIs, so those can be signed in to a different account. A successful login\nreplaces the previous owned grant; there is no picker for multiple saved\nChatGPT accounts.\n\n- **See which account is signed in.** `codewhale auth status --provider\n openai-codex` (or `--provider xai`) and `codewhale auth list` show an account\n label when the ID token contains an email. ChatGPT plan and workspace\n details appear only when those claims are present; a workspace label uses\n the first characters of its account ID. The label is display metadata, not\n proof of plan entitlement. A valid sign-in may have no email label. Status\n reads local credential state without contacting the issuer: an expired\n access token with a stored refresh token remains structurally usable, while\n missing or invalid storage, or an expired token without a refresh token,\n is reported unusable. The provider picker shows the label of the sign-in\n the route would use (for xAI this includes a consented Grok CLI import).\n Successful login reports the account when known and whether it replaced\n an earlier sign-in, in the TUI's selected language. No token is printed.\n- **Reauthorize ChatGPT.** Run `/auth chatgpt` inside Codewhale to update the\n running session, or `codewhale auth chatgpt` from a shell and restart open\n Codewhale sessions. Ordinary login reuses the selected account/workspace's\n issued client ID and checks that the returned account is the same. The\n browser opens with `prompt=login`; `CODEWHALE_CHATGPT_OAUTH_NO_PROMPT=1`\n omits that parameter if the issuer refuses it.\n- **Replace the ChatGPT account or workspace.** From a shell, run\n `CODEWHALE_CHATGPT_NEW_ACCOUNT=1 codewhale auth chatgpt`, then refresh its\n model catalog and restart open Codewhale sessions. If the browser holds the\n wrong account, sign out there or open the printed URL in a private window.\n Only a fully validated new grant replaces the active owned sign-in.\n- **Switch xAI accounts.** Run `/auth xai-device` inside Codewhale, or\n `codewhale auth xai-device` from a shell and restart open sessions. The\n device page approves with whichever account the browser holds; sign out\n there or use a private window to approve another account. The login names\n the account it replaced, or says it is the same account as before.\n- **Usage limits.** ChatGPT plan requests can fail with\n `subscription_sharing_usage_limit_exceeded` or\n `subscription_sharing_usage_unavailable`. These errors, and subscription\n quota errors from other providers, name the account that made the request\n when its label is known and offer recovery guidance. Codewhale does not\n retry these terminal errors or silently choose another billing route. An\n xAI route set to OAuth that fell back to an API key gets no sign-in\n guidance, since no sign-in was used.\n- **ChatGPT credential authority.** The official ChatGPT plan route uses\n only its selected, verified Codewhale-owned grant.\n `OPENAI_CODEX_ACCESS_TOKEN`, `CODEX_ACCESS_TOKEN`, and consented Codex CLI\n tokens cannot override or authorize this route.\n\n### External CLI credential consent\n\nCredential files owned by another CLI are disabled by default. Without an\nexplicit grant, provider discovery, setup, routing, `auth status`, and doctor\ndo not stat, read, refresh, contact an identity provider for, or rewrite Codex,\nGrok, Kimi, or future external credential files.\n\nCodewhale currently supports exact-path, provider-scoped **read-only** grants\nfor the Codex CLI and Grok CLI. Codex grants remain available for legacy\ncredential inspection only; they cannot authorize the official ChatGPT plan\nroute. Use `codewhale auth chatgpt` for that route.\n\n```bash\n# Legacy Codex credential inspection; does not authorize ChatGPT plan requests.\ncodex login\ncodewhale auth external-consent --provider openai-codex --mode read-only\n\ngrok login\ncodewhale auth external-consent --provider xai --mode read-only\n\ncodewhale auth status --provider openai-codex\ncodewhale auth external-revoke --provider openai-codex\n```\n\nPass `--path /absolute/path/to/auth.json` when the external CLI uses a custom\nlocation. Consent persists the provider, external owner, exact absolute path,\nand consent schema version. Later environment-variable changes do not redirect\nthat authority to a different file. Read-only grants never refresh, contact an\nidentity/discovery service, or rewrite the external file. Supported routes such\nas xAI may use the external token after explicit selection. The official ChatGPT\nplan route ignores imported Codex tokens and requires its own verified grant.\nAn expired external token fails with login guidance. Doctor reports structural\nconsent/config state without\nopening credential files and is always non-mutating.\n\n`managed` is reserved for a future provider-specific preservation adapter.\nv0.9.1 rejects it before file or network I/O because no reviewed adapter can\nyet preserve every unknown external schema field safely. Codewhale-started xAI\ndevice login instead atomically activates a Codewhale-owned generation named\n`$CODEWHALE_HOME/credentials/xai-auth-<generation>.json`, stores only that\nvalidated basename in config, and revokes any Grok-file grant. Superseded\ngenerations are cleaned only after the new config pointer commits.\nKimi remains API-key-only; external consent for Kimi is rejected.\n\nThe official DeepSeek Harness (`dsh`) is a third read-only credential owner:\n`codewhale auth external-consent --provider deepseek --mode read-only` grants\nexact-path read access to `DEEPSEEK_API_KEY` in `$DSH_HOME/.credentials.yaml`\n(or `~/.dsh/.credentials.yaml`), which Codewhale never writes, refreshes, or\nloads into the process environment. This is separate from the DSH *harness*\nintegration (`codewhale integrations dsh …`, see\n[INTEGRATIONS_DSH.md](INTEGRATIONS_DSH.md)), which never touches credentials\nin either direction: it pins Codewhale's route identity into a `--patch`\noverlay and lets DSH resolve its own keys.\n\n## Shipped Providers\n\n| Provider ID | TOML table | Auth env | Base URL env and default | Default or static models | Notes |\n| --- | --- | --- | --- | --- | --- |\n| `deepseek` | `[providers.deepseek]` | `DEEPSEEK_API_KEY` | `CODEWHALE_BASE_URL` / `DEEPSEEK_BASE_URL`; default `https://api.deepseek.com/beta` | `deepseek-flash` (shipped default; V4.1 Flash, unversioned id), `deepseek-v4-pro`, `deepseek-v4-flash`, experimental `deepseek-v4-flash-vision-exp`; compatibility aliases `deepseek-chat`, `deepseek-reasoner` | First-class default. The live Pro backend is labeled `DeepSeek-V4-Pro-0813`; the callable API ID remains `deepseek-v4-pro`. Beta URL enables strict tool mode, chat prefix completion, and FIM completion. The documented V4 routes can use provider-native web search through a separate bounded Responses request; compatible custom endpoints do not inherit that capability. Set `https://api.deepseek.com` or `/v1` explicitly to opt out of beta-only features. The shipped default `deepseek-flash` speaks the Responses API (DeepSeek's documented path for Codex-style integration, since the 2026-07-31 Flash production update); the Chat-only controls (the `thinking` toggle and strict-tool `/beta` routing) apply to Chat Completions routes, and `deepseek-v4-pro` stays on Chat Completions until its announced Responses rollout. Reasoning effort follows DeepSeek's documented requested-to-actual mapping: `minimal`/`low` land on `low`, `medium`/`xhigh` on `high`, and `max`/`ultra` on `max`; `off` disables thinking (`thinking: {\"type\":\"disabled\"}` on Chat, `reasoning.effort: \"none\"` on Responses). The experimental vision ID was observed in the authenticated `/models` roster on 2026-08-21 and is advertised as image-input capable on the direct Chat Completions route only. Its limits, reasoning, and tool-call flags provisionally inherit Flash, and no funded image round trip was made during this release work. DeepSeek has since retired both `deepseek-v4-flash` and the vision ID; it still accepts them, serves them with V4.1 Flash, and bills them at the `deepseek-flash` price, which Codewhale applies from 2026-09-10 04:00 UTC. The `flash`, `deepseek-v4flash`, `flash-vision`, and `deepseek-v4flashvisionexp` shorthands now select `deepseek-flash`. |\n| `deepseek-anthropic` | `[providers.deepseek_anthropic]` | `DEEPSEEK_API_KEY` | `DEEPSEEK_ANTHROPIC_BASE_URL`; default `https://api.deepseek.com/anthropic` | `deepseek-v4-pro`, `deepseek-v4-flash`; compatibility aliases `deepseek-chat`, `deepseek-reasoner` | Opt-in DeepSeek route for the Anthropic Messages wire protocol. Uses `/v1/messages`, `x-api-key`, and `anthropic-version: 2023-06-01`. Keep `provider = \"deepseek\"` for the default Chat Completions path. |\n| `nvidia-nim` | `[providers.nvidia_nim]` | `NVIDIA_API_KEY`, `NVIDIA_NIM_API_KEY` | `NVIDIA_NIM_BASE_URL`, `NIM_BASE_URL`, `NVIDIA_BASE_URL`; default `https://integrate.api.nvidia.com/v1` | `deepseek-ai/deepseek-v4-pro`, `deepseek-ai/deepseek-v4-flash` | Hosted DeepSeek V4 through NVIDIA NIM. `NVIDIA_NIM_MODEL` is accepted by the TUI config path. |\n| `openai` | `[providers.openai]` | `OPENAI_API_KEY` | `OPENAI_BASE_URL`; default `https://api.openai.com/v1` | `gpt-5.6` (default), `gpt-5.6-sol`, `gpt-5.6-terra`, `gpt-5.6-luna` | Generic OpenAI-compatible route whose built-in endpoint and fallback catalog are native to OpenAI. The [GPT-5.6 family](https://developers.openai.com/api/docs/models/gpt-5.6-sol) uses OpenAI's documented 1.05M context, 128K max output, and reasoning levels. The [GPT-6 family](https://developers.openai.com/api/docs/models/gpt-6-sol) (`gpt-6-sol`, `gpt-6.1-sol`, `gpt-6-luna`, `gpt-6-astra`) is not offered on this Chat Completions route because OpenAI does not support normal tool calling for them over Chat Completions. OpenAI has [deprecated](https://developers.openai.com/api/docs/deprecations) `gpt-5.3-codex` and `gpt-5.1` (replacement `gpt-6-sol`) and `gpt-5.4-nano` (replacement `gpt-6-luna`), all shutting down 2027-04-01, but those replacements need a Responses-wire OpenAI route here. Custom gateways remain free to select an explicit gateway-owned model. `OPENAI_MODEL` is accepted. |\n| `atlascloud` | `[providers.atlascloud]` | `ATLASCLOUD_API_KEY` | `ATLASCLOUD_BASE_URL`; default `https://api.atlascloud.ai/v1` | Default `deepseek-ai/deepseek-v4-flash`; explicit `vendor/model-id` values pass through when AtlasCloud is selected | OpenAI-compatible hosted route. `ATLASCLOUD_MODEL` is accepted by the TUI config path, the static `ModelRegistry` keeps DeepSeek V4 fallback rows, and provider-hinted CLI model IDs are sent to AtlasCloud exactly as requested. Use Atlas Cloud's own catalog or Coding Plan page for the current provider-owned model list and pricing. |\n| `wanjie-ark` | `[providers.wanjie_ark]` | `WANJIE_ARK_API_KEY`, `WANJIE_API_KEY`, `WANJIE_MAAS_API_KEY` | `WANJIE_ARK_BASE_URL`, `WANJIE_BASE_URL`, `WANJIE_MAAS_BASE_URL`; default `https://maas-openapi.wanjiedata.com/api/v1` | `deepseek-reasoner` | OpenAI-compatible hosted route. `WANJIE_ARK_MODEL`, `WANJIE_MODEL`, and `WANJIE_MAAS_MODEL` are accepted. |\n| `volcengine` | `[providers.volcengine]` | `VOLCENGINE_API_KEY`, `VOLCENGINE_ARK_API_KEY`, `ARK_API_KEY` | `VOLCENGINE_BASE_URL`, `VOLCENGINE_ARK_BASE_URL`, `ARK_BASE_URL`; default `https://ark.cn-beijing.volces.com/api/coding/v3` | `DeepSeek-V4-Pro`, `DeepSeek-V4-Flash` | Volcengine/Volcano Engine Ark OpenAI-compatible coding endpoint. `VOLCENGINE_MODEL` and `VOLCENGINE_ARK_MODEL` are accepted. |\n| `openrouter` | `[providers.openrouter]` | `OPENROUTER_API_KEY` | `OPENROUTER_BASE_URL`; default `https://openrouter.ai/api/v1` | `deepseek/deepseek-v4-pro`, `deepseek/deepseek-v4-flash`; recent large IDs include `arcee-ai/trinity-large-thinking`, `minimax/minimax-m3`, `xiaomi/mimo-v2.5-pro`, `qwen/qwen3.6-flash`, `qwen/qwen3.6-35b-a3b`, `qwen/qwen3.6-max-preview`, `qwen/qwen3.6-27b`, `qwen/qwen3.6-plus`, `google/gemma-4-31b-it`, `z-ai/glm-5.1`, `z-ai/glm-5.2`, `moonshotai/kimi-k2.7-code`, `moonshotai/kimi-k2.6` | Additive open-model routing layer. It does not replace DeepSeek; it lets users route supported model IDs through OpenRouter when they choose it. |\n| `orcarouter` | `[providers.orcarouter]` | `ORCAROUTER_API_KEY` | `ORCAROUTER_BASE_URL`; default `https://api.orcarouter.ai/v1` | `deepseek/deepseek-v4.1-flash-1017` (default), `deepseek/deepseek-v4-pro`, `deepseek/deepseek-v4-flash`; router alias `orcarouter/auto`; recent large IDs mirror the OpenRouter namespaced catalog | [OrcaRouter](https://www.orcarouter.ai) OpenAI-compatible aggregation gateway. Shares the namespaced `vendor/model` wire-model format and DeepSeek model set with OpenRouter, so the OpenRouter base-URL and model-normalization rules apply. `ORCAROUTER_MODEL` is accepted. Provider aliases: `orcarouter`, `orca_router`, `orca`. |\n| `xiaomi-mimo` | `[providers.xiaomi_mimo]` | `XIAOMI_MIMO_TOKEN_PLAN_API_KEY`, `MIMO_TOKEN_PLAN_API_KEY`, `XIAOMI_MIMO_API_KEY`, `XIAOMI_API_KEY`, `MIMO_API_KEY` | `XIAOMI_MIMO_BASE_URL`, `MIMO_BASE_URL`, `XIAOMI_MIMO_MODE`, `MIMO_MODE`; default `https://token-plan-sgp.xiaomimimo.com/v1` | Chat: `mimo-v2.6-pro` (default), `mimo-v2.6-flash`, `mimo-v2.5-pro-ultraspeed`; speech/TTS: `mimo-v2.5-tts`, `mimo-v2.5-tts-voicedesign`, `mimo-v2.5-tts-voiceclone`, `mimo-v2-tts` | Xiaomi MiMo OpenAI-compatible chat completions route. Xiaomi [retires](https://mimo.mi.com/docs/updates/deprecate) `mimo-v2.5-pro` and `mimo-v2.5` at 2026-10-21 10:00 UTC+8; Codewhale sends `mimo-v2.6-pro` and `mimo-v2.6-flash` for those configured ids. `mimo-v2.6-pro` and `mimo-v2.6-flash` can use the documented provider-native web-search plugin; ultraspeed, speech/TTS, and custom-compatible routes do not inherit it. Token Plan keys (`tp-...`) use `api-key` auth and the token-plan endpoint by default; pay-as-you-go mode uses standard API keys (`sk-...`) and `https://api.xiaomimimo.com/v1`. It sends `max_completion_tokens` and uses MiMo's `thinking` field for reasoning control. Token Plan cost/usage is credit/quota based; Codewhale shows it as unknown until Xiaomi exposes a reliable balance API. `codewhale speech` / `tts` uses the TTS models. |\n| `novita` | `[providers.novita]` | `NOVITA_API_KEY` | `NOVITA_BASE_URL`; default `https://api.novita.ai/openai/v1` | `deepseek/deepseek-v4-pro`, `deepseek/deepseek-v4-flash` | OpenAI-compatible hosted route for DeepSeek model IDs. Use config or `CODEWHALE_MODEL` / `DEEPSEEK_MODEL` for model overrides. |\n| `fireworks` | `[providers.fireworks]` | `FIREWORKS_API_KEY` | `FIREWORKS_BASE_URL`; default `https://api.fireworks.ai/inference/v1` | `accounts/fireworks/models/deepseek-v4-pro` | OpenAI-compatible hosted route. Use config or `CODEWHALE_MODEL` / `DEEPSEEK_MODEL` for model overrides. |\n| `siliconflow` | `[providers.siliconflow]` | `SILICONFLOW_API_KEY` | `SILICONFLOW_BASE_URL`; default `https://api.siliconflow.com/v1` | `deepseek-ai/DeepSeek-V4-Pro`, `deepseek-ai/DeepSeek-V4-Flash` | OpenAI-compatible hosted route. Official docs use the `.com` endpoint. `SILICONFLOW_MODEL` is accepted. Reasoning aliases `deepseek-reasoner` and `deepseek-r1` map to Pro; `deepseek-chat` and `deepseek-v3` map to Flash. |\n| `siliconflow-CN` | `[providers.siliconflow_cn]` | `SILICONFLOW_API_KEY` | `SILICONFLOW_BASE_URL`; default `https://api.siliconflow.cn/v1` | Uses the SiliconFlow model set | China regional SiliconFlow route. Falls back to `[providers.siliconflow]` for api_key / base_url / model when unset. Select it with `provider = \"siliconflow-CN\"` or `CODEWHALE_PROVIDER=siliconflow-CN`. |\n| `arcee` | `[providers.arcee]` | `ARCEE_API_KEY` | `ARCEE_BASE_URL`; default `https://api.arcee.ai/api/v1` | `trinity-large-thinking`, `trinity-large-preview` | Arcee AI direct OpenAI-compatible route, tracked as 256K-context BF16 serving. `ARCEE_MODEL` is accepted. OpenRouter's `arcee-ai/trinity-large-thinking` remains the OpenRouter namespaced model ID; direct Arcee uses the bare `trinity-large-thinking` ID. |\n| `moonshot` | `[providers.moonshot]` | `MOONSHOT_API_KEY`, `KIMI_API_KEY` | `MOONSHOT_BASE_URL`, `KIMI_BASE_URL`; default `https://api.moonshot.ai/v1` | Direct Moonshot: `kimi-k3`, `kimi-k2.7-code`, `kimi-k2.7-code-highspeed`, `kimi-k2.6`; Kimi Code membership: `k3`, `kimi-for-coding`, `kimi-for-coding-highspeed` at `https://api.kimi.com/coding/v1` | Moonshot/Kimi route. Exact direct `kimi-k3` routes use the documented Formula web-search tool/fiber loop; direct `kimi-k2.6` retains the built-in `$web_search` contract, and exact Kimi Code membership routes use their structured `/search` service. Adjacent paths, K2.7 direct models, and cross-product model IDs do not inherit native search. `kimi` and `kimi-k2` aliases select `kimi-k2.7-code`; `MOONSHOT_MODEL`, `KIMI_MODEL_NAME`, and `KIMI_MODEL` are accepted. Kimi thinking streams through `reasoning_content`; Codewhale keeps it in Thinking cells and replays it for thinking/tool-call continuity. For direct K3, use exact `base_url = \"https://api.moonshot.ai/v1\"` and `model = \"kimi-k3\"`; it is always-thinking and receives top-level `reasoning_effort = \"low\" | \"high\" | \"max\"` (`off` normalizes to `low`), uses only `max_completion_tokens`, and omits `temperature`/`top_p` per the [K3 quickstart](https://platform.kimi.ai/docs/guide/kimi-k3-quickstart). For Kimi Code K3, use a key from the [Kimi Code console](https://www.kimi.com/code/console), exact `base_url = \"https://api.kimi.com/coding/v1\"`, and bare `model = \"k3\"`; `off` becomes enabled `low`, while normal dispatched `auto` selects and sends a concrete Codewhale tier. Only an omitted reasoning setting leaves the provider default in control. That membership route defaults safely to 262,144 context tokens; the [Kimi Code model-tier table](https://www.kimi.com/code/docs/en/kimi-code/models.html) grants Allegretto and higher plans up to 1M, which those plans may express as `context_window = 1048576`. `k3[1m]` is Claude Code-only and Codewhale rejects it. `kimi-for-coding` remains the valid K2.7 membership route, and `kimi-for-coding-highspeed` is its own high-speed roster entry (262,144 context); membership ids are rejected on the direct platform endpoint, and `kimi-k3` stays rejected on the membership endpoint. Billing is decided by the endpoint the route resolves to, judged once against the two exact product endpoints: direct Moonshot (`https://api.moonshot.ai/v1` or the default) bills metered with dollar estimates, the exact Kimi Code membership endpoint bills as Kimi Code quota and never shows dollar estimates, and anything else — a gateway host, a neighboring Kimi-hosted path — reports `cost: unknown` rather than borrowing either product. An imported Kimi Code token with no `base_url` in its table still resolves to the membership endpoint, so it bills as Kimi Code quota and never accrues dollars. A completed turn, parent or sub-agent, is billed from the immutable endpoint receipt its own client was built with, never from a later config re-read: `MOONSHOT_BASE_URL`/`KIMI_BASE_URL` are merged into the *active* provider's table only, and an in-turn provider switch can move the ambient config off the route that actually ran. Legacy `auth_mode = \"kimi_oauth\"` fails to API-key guidance without probing Kimi CLI files. Codewhale does not impersonate `kimi_cli` or `kimi_code_cli`. **China-region keys:** contributor field evidence (@vFONGv, PR #5229, verified on Windows 10) reports that a China-region Moonshot key must be paired with `base_url = \"https://api.moonshot.cn/v1\"`; left on the default international host (`https://api.moonshot.ai/v1`) it fails authentication. We have no China-region key to verify this ourselves, so it is recorded as a user report rather than a tested route. Note also that editing `base_url` alone does not take effect until `codewhale auth set` is re-run for that provider. |\n| `google` | `[providers.google]` | `GOOGLE_API_KEY`, `GEMINI_API_KEY` | `GOOGLE_BASE_URL`, `GEMINI_BASE_URL`; default `https://generativelanguage.googleapis.com/v1beta/openai/` | `gemini-3.1-pro-preview` (default); `/model` also lists `gemini-4-argon`, `gemini-3-pro-preview`, `gemini-3.8-flash`, `gemini-3.6-flash`, `gemini-3.5-flash-lite`, `gemini-2.5-pro`, `gemini-2.5-flash` | Google Gemini as its own backend on the official OpenAI-compatible Chat Completions route. Google [deprecated](https://ai.google.dev/gemini-api/docs/changelog) `gemini-3.7-flash` (served as `gemini-3.8-flash`) and `gemini-3.5-flash` (served as `gemini-3.6-flash`) on 2026-10-08; both ids stay accepted as selector aliases of their replacements. `gemini-4-argon` is paid-tier only. Thinking models capture `extra_content.google.thought_signature` on tool calls and replay it with the assistant tool-call messages; replaying a tool call whose signature was not captured fails closed with an actionable error instead of letting the tool loop break. `gemini-2.5-flash-lite` ships thinking off and degrades with a warning instead. Reasoning effort maps onto the documented `google.thinking_config.thinking_level` (`low`/`high`). The dialect binds to the exact official base URL: a `google` row pointed at another gateway gets plain OpenAI semantics and no signature requirements. Codewhale never reads Google OAuth files; only an AI Studio API key is used. Not live-tested against the real endpoint in this environment. |\n| `zai` | `[providers.zai]` | `ZAI_API_KEY`, `Z_AI_API_KEY`, `ZHIPU_API_KEY`, `GLM_API_KEY` | `ZAI_BASE_URL`, `Z_AI_BASE_URL`; default `https://api.z.ai/api/coding/paas/v4`; general APIs `https://api.z.ai/api/paas/v4` and `https://open.bigmodel.cn/api/paas/v4` | `GLM-5.3` default; `/model` also lists `GLM-5.3-Flash`, `GLM-5.2`, `GLM-5.1`, and `GLM-5-Turbo` | Z.AI GLM Coding Plan route. All three first-party Chat routes (both api.z.ai products and BigModel's general platform endpoint) share one request dialect, so reasoning controls — the GLM-5.2 thinking toggle, tiered effort, and the forced-thinking GLM-5.3 rewrite that sends `off` as `enabled` + `reasoning_effort: \"low\"` — apply on BigModel too; neighboring paths such as `/preview` stay fail-closed. The two general API products expose structured provider-native web search (`search-prime` globally, `search_std` in China); Coding Plan and compatible custom endpoints do not inherit it. `GLM-5.3` is the default and a first-class picker row (`model = \"GLM-5.3\"` or `ZAI_MODEL=GLM-5.3`); `GLM-5.3-Flash` is the 1M multimodal fast sibling (`model = \"GLM-5.3-Flash\"`). `GLM-5.3-FlashX` is not offered because the Coding Plan endpoint this route defaults to does not serve it. An explicit `GLM-5.2` selection keeps its own id. Limits and reasoning options for 5.3 are inherited from `GLM-5.2` until Z.ai publishes distinct 5.3 metadata; 5.3 carries no price. Flash ships the published $0.15/$0.50 list. A live call can still 429 with entitlement code 1311 on accounts that are not provisioned for 5.3. |\n| `stepfun` | `[providers.stepfun]` | `STEPFUN_API_KEY`, `STEP_API_KEY` | `STEPFUN_BASE_URL`, `STEP_BASE_URL`; default `https://api.stepfun.ai/v1`; Coding Plan endpoint `https://api.stepfun.ai/step_plan/v1` | `step-3.7-flash` | StepFun / StepFlash direct OpenAI-compatible route. `/provider` setup asks which billing route the key belongs to — pay-as-you-go or Step Plan — validates the key against the chosen endpoint, and writes the answer to `[providers.stepfun].base_url` only. A base URL that is neither recognized route is left alone and the question is skipped. You can also set `[providers.stepfun].base_url` or `STEP_BASE_URL` to the Coding Plan URL by hand. Offline accounting labels recognized routes as `stepfun-payg` or `stepfun-plan` without persisting the raw endpoint, and only the standard PAYG route receives token pricing. `STEPFUN_MODEL` and `STEP_MODEL` are accepted. |\n| `minimax` | `[providers.minimax]` | `MINIMAX_API_KEY` | `MINIMAX_BASE_URL`; default `https://api.minimax.io/v1`; China `https://api.minimaxi.com/v1` | `MiniMax-M3`, `MiniMax-M2.7`, `MiniMax-M2.7-highspeed`, `MiniMax-M2.5`, `MiniMax-M2.5-highspeed`, `MiniMax-M2.1`, `MiniMax-M2.1-highspeed`, `MiniMax-M2` | MiniMax direct OpenAI-compatible route. MiniMax [closes](https://platform.minimax.io/docs/guides/models-intro) `MiniMax-M2.7`, `MiniMax-M2.7-highspeed`, `MiniMax-M2.5` and `MiniMax-M2.5-highspeed` to new users on 2026-10-19 and retires them on 2026-11-18; they are no longer in the fallback picker, stay accepted for existing configs until then, and `MiniMax-M3` remains the default. Codewhale sends `reasoning_split = true` so MiniMax thinking arrives separately from answer text. Both MiniMax dialects sell pay-as-you-go and Token Plan over the same endpoints and the same key, so billing is classified from the credential *product*, never from the endpoint or from a default. `mode = \"token-plan\"` in `[providers.minimax]`/`[providers.minimax_anthropic]`, or a Token Plan key shaped `sk-cp…`, bills as MiniMax Token Plan quota with no dollar estimates; an explicit pay-as-you-go mode (`pay-as-you-go`/`payg`/`metered`) wins over key shape. The key's product prefix is only visible when the key is in config, bound by `api_key_env`, or exported as `MINIMAX_API_KEY` on an official endpoint — a key saved through `codewhale auth set` (secret store / OS keyring) is deliberately not read to classify billing. With no explicit mode and no visible product marker the route reports `cost: unknown` rather than assuming pay-as-you-go, so a Token Plan account is never charged invented dollars. Custom/gateway endpoints also fail closed with `cost: unknown`. Official M3 input modalities are text, image, and video; M2.7 is text-only. |\n| `minimax-anthropic` | `[providers.minimax_anthropic]` | `MINIMAX_API_KEY` | `MINIMAX_ANTHROPIC_BASE_URL`; default `https://api.minimax.io/anthropic`; China `https://api.minimaxi.com/anthropic` | `MiniMax-M3`, `MiniMax-M2.7` | MiniMax direct Anthropic-compatible Messages route. Keep the `/anthropic` suffix because Codewhale appends `/v1/messages`; the route uses `x-api-key`. M3 supports adaptive or disabled thinking. M2.7 always keeps thinking enabled. |\n| `sglang` | `[providers.sglang]` | Optional `SGLANG_API_KEY` | `SGLANG_BASE_URL`; default `http://localhost:30000/v1` | `deepseek-ai/DeepSeek-V4-Pro`, `deepseek-ai/DeepSeek-V4-Flash` | Self-hosted OpenAI-compatible route. Localhost deployments commonly omit auth. `SGLANG_MODEL` is accepted. |\n| `vllm` | `[providers.vllm]` | Optional `VLLM_API_KEY` | `VLLM_BASE_URL`; default `http://localhost:8000/v1` | `deepseek-ai/DeepSeek-V4-Pro`, `deepseek-ai/DeepSeek-V4-Flash` | Self-hosted vLLM OpenAI-compatible route. Localhost deployments commonly omit auth. `VLLM_MODEL` is accepted. |\n| `ollama` | `[providers.ollama]` | Local optional `OLLAMA_API_KEY` | `OLLAMA_BASE_URL`; default `http://localhost:11434/v1` | live tag from the local catalog; pre-refresh placeholder `unknown`; provider-hinted custom tags pass through | Local Ollama is keyless by default. `OLLAMA_MODEL` is accepted. The header must not paint a hosted id the local daemon did not list. |\n| `ollama-cloud` | `[providers.ollama_cloud]` | `OLLAMA_CLOUD_API_KEY`, then `OLLAMA_API_KEY` | `OLLAMA_CLOUD_BASE_URL`; default `https://ollama.com/v1` | `gpt-oss:120b`; arbitrary provider-owned IDs pass through | Hosted OpenAI-compatible `/v1/chat/completions` route. Save credentials under `ollama-cloud`; the exact released `ollama` + Cloud URL tuple has bounded read-only in-memory compatibility with its legacy table and secret slot. `OLLAMA_CLOUD_MODEL` is accepted. |\n| `huggingface` | `[providers.huggingface]` | `HUGGINGFACE_API_KEY`, `HF_TOKEN` | `HUGGINGFACE_BASE_URL`, `HF_BASE_URL`; default `https://router.huggingface.co/v1` | `deepseek-ai/DeepSeek-V4-Pro`, `deepseek-ai/DeepSeek-V4-Flash` | Hugging Face Inference Providers OpenAI-compatible router route. Accepted aliases: `huggingface`, `hugging-face`, `hugging_face`, `hf`. Org-prefixed model IDs pass through. `HUGGINGFACE_MODEL` and `HF_MODEL` are accepted. Hub browsing/export are separate future features. |\n| `modelscope` | `[providers.modelscope]` | `MODELSCOPE_API_KEY` | `MODELSCOPE_BASE_URL`; default `https://api-inference.modelscope.cn/v1` | `Qwen/Qwen3.5-397B-A17B` (default), `Qwen/Qwen3.5-122B-A10B`, `Qwen/Qwen3.5-27B`, `Qwen/Qwen3.5-35B-A3B`, `Qwen/Qwen3.8-27B`, `Qwen/Qwen3.8-Flash-Next`, `deepseek-ai/DeepSeek-V4-Pro`, `deepseek-ai/DeepSeek-V4-Pro-0813`, `deepseek-ai/DeepSeek-V4.1-Flash`, `ZhipuAI/GLM-4.7-Flash`, `ZhipuAI/GLM-5.2` | ModelScope OpenAI-compatible inference route. Org-prefixed model IDs pass through. `MODELSCOPE_MODEL` is accepted. |\n| `deepinfra` | `[providers.deepinfra]` | `DEEPINFRA_API_KEY`, `DEEPINFRA_TOKEN` | `DEEPINFRA_BASE_URL`; default `https://api.deepinfra.com/v1/openai` | `deepseek-ai/DeepSeek-V4-Pro` (default), `deepseek-ai/DeepSeek-V4-Flash`; also selectable: `deepseek-ai/DeepSeek-V4-Pro-0813`, `deepseek-ai/DeepSeek-V4.1-Flash-1017`, `deepseek-ai/DeepSeek-V4.1-Flash`, `deepseek-ai/DeepSeek-V4-Flash-0731` | DeepInfra OpenAI-compatible route. Drop-in replacement for OpenAI SDK. `deepseek-v4-pro` selects V4-Pro and `deepseek-flash` / `deepseek-v4-flash` select V4-Flash. DeepInfra lists V4-Pro-0813, both V4.1 Flash ids and V4-Flash-0731 at 1,048,576 context and publishes no separate output limit for them, so none is assumed; the V4.1 Flash ids take image input. |\n| `together` | `[providers.together]` | `TOGETHER_API_KEY` | `TOGETHER_BASE_URL`; default `https://api.together.xyz/v1` | `deepseek-ai/DeepSeek-V4-Pro`, `deepseek-ai/DeepSeek-V4-Flash`, `thinkingmachines/inkling` | Together AI OpenAI-compatible route. `TOGETHER_MODEL` is accepted. Model aliases `deepseek-v4-pro` and `deepseek-v4-flash` normalize to Together's org-prefixed IDs; `inkling` and `together-inkling` normalize to Together's published lowercase Inkling wire ID. Inkling uses the exact `none`/`minimal`/`low`/`medium`/`high`/`max` reasoning vocabulary from Thinking Machines' [official model repository](https://huggingface.co/thinkingmachines/Inkling). Together's [launch post](https://www.together.ai/blog/together-ai-brings-thinking-machines-labs-new-model-inkling-on-day-0) currently says Inkling is live with 1M context, while its [model detail page](https://www.together.ai/models/inkling) says coming soon with 256K context and publishes no price. Until Together's active `/models` endpoint and the Models.dev catalog resolve that conflict, Inkling is not seeded into Codewhale's offline picker and no route-specific context or cost is inferred. |\n| `qianfan` | `[providers.qianfan]` | `QIANFAN_API_KEY`, `BAIDU_QIANFAN_API_KEY` | `QIANFAN_BASE_URL`, `BAIDU_QIANFAN_BASE_URL`; default `https://api.baiduqianfan.ai/v1` | `ernie-4.0-turbo-8k`; provider-scoped custom Qianfan service/model IDs pass through | Baidu Qianfan OpenAI-compatible route. Requests use Bearer auth and Chat Completions payloads. `QIANFAN_MODEL` and `BAIDU_QIANFAN_MODEL` are accepted; aliases `baidu-qianfan`, `baidu_qianfan`, and `baidu` resolve to this provider. Tool/function calling is model-scoped in Qianfan docs, so Codewhale preserves the selected wire model and leaves live capability proof to follow-up route/capability work. |\n| `openai-codex` | `[providers.openai_codex]` | Official Sign in with ChatGPT (`codewhale auth chatgpt` / `/provider setup openai-codex`) | Official `https://api.openai.com/v1` | Selected account catalog; configured model IDs remain explicit selections | **Experimental.** Public Responses endpoint (`/v1/responses`) with a validated `chatgpt.tokens.use.direct` grant. Dynamic OSS registration issues a client ID for the selected account/workspace; Codewhale protects and renews its own tokens. The account catalog supplies selectable models; public model lists and legacy Codex tokens do not establish plan permission. No silent billing fallback. See [Sign in with ChatGPT](#sign-in-with-chatgpt) for setup, usage limits, and preview boundaries. |\n| `anthropic` | `[providers.anthropic]` | `ANTHROPIC_API_KEY` | `ANTHROPIC_BASE_URL`; default `https://api.anthropic.com` | `claude-opus-5-5`, `claude-sonnet-5-5` (default), `claude-fable-5-1`, `claude-sonnet-4-6`, `claude-haiku-5-5` | Native Anthropic Messages API route (`/v1/messages`, `x-api-key` + `anthropic-version: 2023-06-01`) — not OpenAI-compatible. Prompt caching via `cache_control` breakpoints, adaptive thinking + `output_config.effort`, signed thinking blocks replayed verbatim, cache telemetry normalized per #2961. The [current models](https://platform.claude.com/docs/en/about-claude/models/overview) have 1M context and 128K output; the auto-router's cheap tier stays `claude-haiku-4-5`; `claude-haiku-5-5` is selectable and priced from the catalog because its rate rises above 100,000 prompt tokens. Codewhale softens a forced `tool_choice` to `auto` for Opus 5.5 and Sonnet 5.5, which reject it, and omits sampling parameters on the models that reject them; `/effort off` on a native route omits the `thinking` block, so the model's own default reasoning behaviour applies. The default is `claude-sonnet-5-5`. No default uses `claude-sonnet-4-5`, which [retires](https://platform.claude.com/docs/en/about-claude/model-deprecations) on 2026-11-30. `ANTHROPIC_MODEL` is accepted. |\n| `openmodel` | `[providers.openmodel]` | `OPENMODEL_API_KEY` | `OPENMODEL_BASE_URL`; default `https://api.openmodel.ai` | `deepseek-v4-flash`; provider-scoped custom model IDs pass through | OpenModel Anthropic-compatible Messages route. Uses `/v1/messages`, Bearer auth, and `anthropic-version: 2023-06-01`; OpenModel selects DeepSeek, DashScope, Xiaomi, Claude, and other routes by model id. `OPENMODEL_MODEL` is accepted. |\n| `sakana` | `[providers.sakana]` | `FUGU_API_KEY`, `SAKANA_API_KEY` | `SAKANA_BASE_URL`; default `https://api.sakana.ai/v1` | `fugu` (default), `fugu-ultra-20260615` | Sakana AI Fugu OpenAI-compatible route. Standard Chat Completions wire protocol; streaming supported. `fugu-ultra-20260615` is the heavy/reasoning variant. Env var aliases: `FUGU_API_KEY` (primary), `SAKANA_API_KEY`; provider aliases: `sakana-ai`, `sakana_ai`, `fugu`. |\n| `longcat` | `[providers.longcat]` | `LONGCAT_API_KEY` | `LONGCAT_BASE_URL`; default `https://api.longcat.chat/openai/v1` | `LongCat-2.0` (default) | Meituan LongCat curated model gateway. OpenAI-compatible Chat Completions wire protocol. Sign up at https://longcat.chat/platform for an API key. Provider aliases: `long-cat`, `meituan-longcat`, `meituan`. |\n| `opencode-go` | `[providers.opencode_go]` | `OPENCODE_GO_API_KEY` | `OPENCODE_GO_BASE_URL`; default `https://opencode.ai/zen/go/v1` | `deepseek-v4-pro` (default), `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` | [OpenCode Go](https://opencode.ai/docs/go/) subscription route using OpenAI-compatible Chat Completions. `OPENCODE_GO_MODEL` is accepted. Codewhale uses bare wire IDs; familiar `opencode-go/<model-id>` input aliases normalize to the bare ID. Go models documented only on the Anthropic `/messages` endpoint are deliberately not advertised by this route until Codewhale supports per-model wire selection. Billing surfaces show the Go allowance instead of token-price estimates. |\n| `opencode-zen` | `[providers.opencode_zen]` | `OPENCODE_ZEN_API_KEY`, fallback `OPENCODE_API_KEY` | `OPENCODE_ZEN_BASE_URL`; default `https://opencode.ai/zen/v1` | `gpt-5.6` (default); current GPT, Claude, Qwen, DeepSeek, MiniMax, GLM, Kimi, Grok, Muse Spark, and free-model IDs | [OpenCode Zen](https://opencode.ai/docs/zen/) model-aware gateway. `OPENCODE_ZEN_MODEL` is accepted, and official `opencode/<model-id>` selectors normalize to bare wire IDs. Each model's wire comes from the curated snapshot, then from the AI SDK package its [Models.dev](https://models.dev) row names: GPT, Grok, and Muse Spark rows use `/responses`; Claude and most Qwen rows use `/messages`; DeepSeek, MiniMax, GLM, Kimi, `qwen3.8-max`, and the free rows use `/chat/completions`. Responses and Chat Completions authenticate with Bearer `Authorization`, while Anthropic Messages uses `x-api-key`; none of these routes use ChatGPT/Codex OAuth guidance or headers. Gemini fails closed because its model-specific Google wire protocol is not implemented; Models.dev rows marked `deprecated` and models no loaded catalog lists also fail closed. See [OpenCode Zen protocol catalog](#opencode-zen-protocol-catalog). |\n| `meta` | `[providers.meta]` | `META_MODEL_API_KEY`, `MODEL_API_KEY` | `META_MODEL_API_BASE_URL`, `MODEL_API_BASE_URL`; default `https://api.meta.ai/v1` | `muse-spark-1.2` (default), `muse-spark-1.3`, `muse-spark-1.3-contributor`, `muse-wonder-1.0` (preview), `muse-spark-1.2-contributor`, `muse-spark-1.1` | [Meta Model API](https://developer.meta.com/ai/resources/blog/build-with-muse-spark/) public-preview route using OpenAI-compatible Chat Completions. The default stays `muse-spark-1.2`, which keeps its wire ID, tool support, 1M context, 32K output metadata, and `none` through `xhigh` reasoning effort; [Meta recommends](https://dev.meta.ai/docs/models.md) `muse-spark-1.3` for new work (1,048,576 context) and it stays selectable. `META_MODEL_API_MODEL` and `MODEL_API_MODEL` are accepted. Provider aliases: `meta-ai`, `meta_model_api`, `muse`, `muse-spark`. |\n| `telecomjs` | `[providers.telecomjs]` | `TELECOMJS_API_KEY` | `TELECOMJS_BASE_URL`; default `https://aigw.telecomjs.com/v1` | `deepseek-v4-pro` conservative fallback; authenticated `/models` rows when a key is configured | TelecomJS TokenHub OpenAI-compatible Chat Completions route. Live catalogs are isolated by provider and key fingerprint, stale rows survive transient refresh failures, and unsupported reasoning request fields are omitted. `TELECOMJS_MODEL` is accepted. Provider aliases: `telecom-js`, `telecom_js`, `telecomjs-cn`, `tokenhub`. |\n| `mistral` | `[providers.mistral]` | `MISTRAL_API_KEY` | `MISTRAL_BASE_URL`; default `https://api.mistral.ai/v1` | `mistral-code-latest` (default; `codestral-latest` accepted as alias), `mistral-medium-latest` (aliases: `mistral-medium-3-5`), `mistral-small-latest` (aliases: `mistral-small-2603`), `mistral-large-latest` | Mistral AI (la Plateforme) OpenAI-compatible Chat route. On the documented first-party HTTPS `/v1` hosts, Medium and Small send adjustable `reasoning_effort` (`none` or `high` only), parse Mistral's polymorphic thinking/text blocks, and replay stored thinking in that same wire shape. Deprecated native Magistral IDs remain explicit-configuration compatibility routes: they are always-reasoning and never receive the adjustable effort field. Code and Large are non-reasoning. A custom `MISTRAL_BASE_URL` keeps generic Chat semantics unless it is one of the documented first-party hosts. `MISTRAL_MODEL` is accepted. Provider aliases: `mistral-ai`, `mistralai`, `la-plateforme`. |\n| `edenai` | `[providers.edenai]` | `EDENAI_API_KEY` | `EDENAI_BASE_URL`; default `https://api.edenai.run/v3`; EU `https://api.eu.edenai.run/v3` | `deepseek/deepseek-v4-pro` (default); live `/models` catalog of `provider/model` ids | Eden AI OpenAI-compatible aggregation gateway. Catalog rows remain provider-scoped; generic reasoning controls are omitted because supported fields depend on the selected upstream family. `EDENAI_MODEL` is accepted. The default `deepseek/deepseek-v4-pro` is listed on the global catalog only; on the EU endpoint set `EDENAI_MODEL` (or `model`) to a row from the EU `/models` list, for example `qwen/deepseek-v4-pro`. Provider aliases: `eden-ai`, `eden_ai`. |\n| `zenmux` | `[providers.zenmux]` | `ZENMUX_API_KEY` | `ZENMUX_BASE_URL`; default `https://zenmux.ai/api/v1` | `deepseek/deepseek-v4.1-flash` (default); live `/models` catalog of `provider/model` ids (keyless-readable) | ZenMux OpenAI-compatible aggregation gateway (~190 models). Catalog rows remain provider-scoped; generic reasoning controls are omitted because supported fields depend on the selected upstream family. `ZENMUX_MODEL` is accepted. Provider aliases: `zen-mux`, `zen_mux`. |\n| `csdn` | `[providers.csdn]` | `CSDN_API_KEY` | `CSDN_BASE_URL`; default `https://ai.csdn.net/api/model/v1` | `glm_for_coding` (default; the Coding Plan's dedicated model id); other marketplace model ids pass through | CSDN 星图 (Starmap) OpenAI-compatible hosted platform. Coding Plan keys and general marketplace keys share the one endpoint, so billing follows the credential product, never the URL alone: routing `glm_for_coding` — the shipped default — or setting `mode = \"coding_plan\"`/`\"plan\"`/`\"subscription\"` bills as CSDN Coding Plan quota with no dollar estimates; any other model, or an explicit `pay-as-you-go`/`metered` mode, bills metered; an unrecognized mode or an endpoint off `ai.csdn.net/api/model/v1` reports `cost: unknown`. `CSDN_MODEL` is accepted. Provider aliases: `csdn-ai`, `csdn_ai`, `csdn-coding-plan`, `csdn_coding_plan`, `starmap`. |\n| `concentrate` | `[providers.concentrate]` | `CONCENTRATE_API_KEY` | `CONCENTRATE_BASE_URL`; default `https://api.concentrate.ai/v1` | `deepseek-v4-pro` (default; a plain catalog id lets the gateway pick the upstream provider); `provider/model` ids such as `openai/gpt-5.6-sol` pin one upstream; `concentrate/auto` is the gateway's own router; unauthenticated live `/v1/models` catalog | Concentrate OpenAI Responses-compatible gateway (`POST /v1/responses`, bearer Universal API key). Opt-in and BYOK only: your key, your Concentrate bill, zero Codewhale fee, no managed default. Requests carry only documented fields (the system prompt rides as a `system` input item). Contract: [API introduction](https://concentrate.ai/docs/api-reference/introduction), [request parameters](https://concentrate.ai/docs/api-reference/endpoint/request-parameters), [streaming](https://concentrate.ai/docs/api-reference/endpoint/streaming), [errors](https://concentrate.ai/docs/api-reference/endpoint/errors). See [Concentrate Notes](#concentrate-notes). |\n| `codewhale` | `[providers.codewhale]` | `CODEWHALE_API_KEY` | `CODEWHALE_API_BASE`; default `https://api.codewhale.net/v1` | `deepseek/deepseek-v4-pro` (default), `anthropic/claude-sonnet-5`, `openai/gpt-5.6` are offline bootstrap rows only; the authenticated `GET /v1/models` listing of the account's connected providers is the catalog authority | Codewhale API: account-backed model access over the provider keys the customer connected to their Codewhale account. One `cwc_key_…` account API key with the `models:infer` scope authenticates every model. Model ids are `provider/model` exactly as the account catalog returns them, and each row states its protocol (`chat-completions` → `/v1/chat/completions`, `anthropic-messages` → `/v1/messages`, `responses` → `/v1/responses`); every protocol uses `Authorization: Bearer`, never `x-api-key`. `CODEWHALE_API_BASE` must be HTTPS except on loopback. Connect provider keys with `codewhale account keys set <provider>`. Provider aliases: `codewhale-api`, `cw-api`, `codewhale-cloud`. |\n| `xai` | `[providers.xai]` | `XAI_API_KEY`, Codewhale-owned device OAuth, or explicit read-only Grok CLI consent | `XAI_BASE_URL`; default `https://api.x.ai/v1` | `grok-4.6` (default), `grok-4.7`, `grok-4.5`, `grok-4.3`, `grok-build`, `grok-composer-2.5-fast`, `grok-4.20-0309-reasoning`, `grok-4.20-0309-non-reasoning` | xAI/Grok OpenAI-compatible Chat Completions route. Grok 4.6 has a 500K context window, text/image input, function calls, structured output, server-side web search, and `low`/`medium`/`high`/`xhigh` reasoning (default `high`). [Grok 4.7](https://docs.x.ai/docs/models/grok-4.7) has the same 500K window, text/image input, function calls, structured output, reasoning ladder and $2.00 / $0.50 cached / $6.00 rates; xAI documents no web-search support for it yet, so Codewhale does not claim it. Grok reasoning cannot be disabled ([reasoning guide](https://docs.x.ai/docs/guides/reasoning)). Its standard rates double when the prompt reaches 200K tokens; the same 2x long-context rule applies to `grok-4.5` (500K context, $2.00 / $0.30 cached / $6.00) and `grok-4.3` (1M context, $1.25 / $0.20 cached / $2.50) per their [model pages](https://docs.x.ai/docs/models/grok-4.5). There is no documented `latest`/`fast` alias and no published numeric output limit. **API-key** (default): Bearer token from console.x.ai via `XAI_API_KEY` / keyring / `api_key`. **OAuth**: `codewhale auth xai-device` uses SSH-friendly device login and Codewhale-owned storage, which may refresh itself. Existing Grok CLI credentials require `codewhale auth external-consent --provider xai --mode read-only`; the granted external file is never refreshed or rewritten. OAuth may return HTTP 403 on some SuperGrok tiers — keep API-key as the reliable fallback. `XAI_MODEL` is accepted. Provider aliases: `x-ai`, `x_ai`, `grok`. |\n| `modelstudio-token-plan` | `[providers.modelstudio_token_plan]` | `MODELSTUDIO_API_KEY`, `DASHSCOPE_API_KEY` | `MODELSTUDIO_TOKEN_PLAN_BASE_URL`; default `https://token-plan.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1` | `qwen3.8-max` (default), `qwen3.7-plus`, `qwen3.7-max`, `qwen3.6-flash`, `deepseek-v4-pro`, `deepseek-v4-flash-0731`, `glm-5.2` | Alibaba Cloud Model Studio Token Plan OpenAI-compatible Chat Completions route. Token Plan Personal and Team share this endpoint. `qwen3.8-max`, `qwen3.7-plus`, and `qwen3.7-max` can use provider-native web search through the Token Plan Responses Harness; the Coding Plan and Anthropic routes do not inherit that capability. Alibaba retired `qwen3.8-max-preview` on 2026-08-05 and routes it to `qwen3.8-max`, so it is no longer listed. All listed models are reasoning-capable text/coding models. DeepSeek and GLM entries are provider-scoped and do not collide with first-party routes. `MODELSTUDIO_TOKEN_PLAN_MODEL` is accepted. Provider aliases: `modelstudio-token-plan`, `alibaba-token-plan`, `dashscope-token-plan`. |\n| `modelstudio-token-plan-anthropic` | `[providers.modelstudio_token_plan_anthropic]` | `MODELSTUDIO_API_KEY`, `DASHSCOPE_API_KEY` | default `https://token-plan.ap-southeast-1.maas.aliyuncs.com/apps/anthropic` | Same model catalog as `modelstudio-token-plan` | Token Plan Anthropic-compatible Messages route (`/apps/anthropic`). Same API key as the OpenAI dialect. Provider aliases: `modelstudio-token-plan-anthropic`, `alibaba-token-plan-anthropic`. |\n| `modelstudio-coding-plan` | `[providers.modelstudio_coding_plan]` | `MODELSTUDIO_API_KEY`, `DASHSCOPE_API_KEY` | `MODELSTUDIO_CODING_PLAN_BASE_URL`; default `https://coding-intl.dashscope.aliyuncs.com/v1` | `qwen3.8-max` (default); same catalog as Token Plan | Alibaba Cloud Model Studio Coding Plan OpenAI-compatible Chat Completions route. `MODELSTUDIO_CODING_PLAN_MODEL` is accepted. Provider aliases: `modelstudio-coding-plan`, `alibaba-coding-plan`, `dashscope-coding-plan`. |\n| `modelstudio-coding-plan-anthropic` | `[providers.modelstudio_coding_plan_anthropic]` | `MODELSTUDIO_API_KEY`, `DASHSCOPE_API_KEY` | default `https://coding-intl.dashscope.aliyuncs.com/apps/anthropic` | Same model catalog as `modelstudio-coding-plan` | Coding Plan Anthropic-compatible Messages route (`/apps/anthropic`). Provider aliases: `modelstudio-coding-plan-anthropic`, `alibaba-coding-plan-anthropic`. |\n\nStepFun's four coding models are available through both its standard API and\n[Step Plan](https://platform.stepfun.ai/docs/en/step-plan/integrations/reasoning-api).\nChoose the billing route in `/provider`, then the model in `/model`; Step 5\nPreview is the default, and an existing Step 3.7 selection remains unchanged. [Step 5 Preview](https://platform.stepfun.ai/docs/en/guides/models/step-5-preview)\nhas a 1M context window. Step 5 and Step 3.7 expose low/medium/high reasoning;\nStep 3.5 Flash 2603 exposes low/high; base Step 3.5 uses provider-default reasoning.\n[Published API prices](https://platform.stepfun.ai/docs/en/guides/pricing/details)\napply only to verified PAYG routes. Step Plan displays subscription allowance.\nSpeech, music and image-generation models use separate interfaces and are not\npresented as coding models. Provider video capability metadata does not imply\nthat every Codewhale client can attach video.\n\n\n### OpenCode Zen protocol catalog\n\nZen Responses and Chat Completions requests authenticate with Bearer\n`Authorization`; Zen Anthropic Messages requests use `x-api-key`. None of these\nroutes add ChatGPT/Codex OAuth headers.\n\nEach Zen model's wire comes from two sources, and neither is guessed from a\nmodel-id family (`qwen3.8-flash` is Messages while `qwen3.8-max` is Chat):\n\n1. **The curated snapshot** compiled into Codewhale, verified against the\n [official endpoint table](https://opencode.ai/docs/zen/) and the `opencode`\n provider in [Models.dev](https://models.dev). It is the offline floor, and\n it wins when a catalog row names a different wire for the same id.\n2. **The Models.dev catalog.** Its `opencode` provider is Zen's published\n catalog: each model row names the AI SDK package OpenCode itself uses, which\n Codewhale maps exactly — `@ai-sdk/openai` to Responses, `@ai-sdk/anthropic`\n to Anthropic Messages, and `@ai-sdk/openai-compatible` (the provider\n default) to Chat Completions. A Zen model released after this build routes\n once the catalog lists it, without a Codewhale release. The interactive TUI\n and `codewhale exec` both load the persisted catalog; refresh it with\n `codewhale models --update`.\n\nThe curated snapshot:\n\n- Responses: `gpt-6-astra`, `gpt-6-sol`, `gpt-6-luna`, `gpt-5.6-sol`,\n `gpt-5.6-terra`, `gpt-5.6-luna`, `gpt-5.5`, `gpt-5.5-pro`, `gpt-5.4`,\n `gpt-5.4-pro`, `gpt-5.4-mini`, `gpt-5.4-nano`, `gpt-5.3-codex`,\n `gpt-5.3-codex-spark`, `gpt-5.2`, `gpt-5.2-codex`, `gpt-5.1`,\n `gpt-5.1-codex`, `gpt-5.1-codex-max`, `gpt-5.1-codex-mini`, `gpt-5`,\n `gpt-5-codex`, `gpt-5-nano`, `grok-4.7`, `grok-4.6`, `grok-4.5`,\n `grok-build-0.1`, `muse-spark-1.3`, `muse-spark-1.3-contributor-free`,\n `muse-spark-1.2`, `muse-spark-1.2-contributor`,\n `muse-spark-1.2-contributor-free`.\n- Anthropic Messages: `claude-fable-5-1`, `claude-fable-5`,\n `claude-opus-5-5`, `claude-opus-5`, `claude-opus-4-8`, `claude-opus-4-7`,\n `claude-opus-4-6`, `claude-opus-4-5`, `claude-sonnet-5-5`,\n `claude-sonnet-5`, `claude-sonnet-4-6`, `claude-sonnet-4-5`,\n `claude-sonnet-4`, `claude-haiku-4-5`, `qwen3.8-flash`, `qwen3.7-max`,\n `qwen3.7-plus`, `qwen3.6-plus`, `qwen3.5-plus`.\n- Chat Completions: `deepseek-v4.1-flash`, `deepseek-v4-pro`,\n `deepseek-v4-flash`, `deepseek-v4-flash-vision-exp`, `minimax-m3`,\n `minimax-m2.7`, `minimax-m2.5`, `glm-5.3-flash`, `glm-5.3`, `glm-5.2`,\n `glm-5.1`, `glm-5`, `kimi-k3`, `kimi-k2.7-code`, `kimi-k2.6`, `kimi-k2.5`,\n `qwen3.8-max`, `big-pickle`, `space-bunny-free`,\n `longcat-2.5-preview-free`, `mimo-v2.6-flash-free`, `mimo-v2.5-free`,\n `ling-3.0-flash-fin-free`, `north-mini-code-free`,\n `nemotron-3-ultra-free`, `nemotron-3.5-lightning-free`,\n `deepseek-v4-flash-free`.\n\nThese fail closed locally, with the reason in the error, instead of reaching\nZen: Gemini (`@ai-sdk/google`, Google's model-specific protocol, which Codewhale\ndoes not speak); a catalog row naming any other package; a catalog row Models.dev\nmarks `deprecated`; and a model neither source lists. A miss never falls back to\nanother Zen wire shape, including when a custom Zen base URL is configured.\n\n### Concentrate Notes\n\nConcentrate (`concentrate`) is an OpenAI **Responses**-compatible gateway:\n`POST https://api.concentrate.ai/v1/responses` with `Authorization: Bearer\n<Universal API key>`. It is opt-in and **BYOK only**.\n\n- **Model ids pass through.** A plain catalog id (`gpt-5.6-sol`,\n `deepseek-v4-pro`) lets the gateway choose the upstream provider;\n `provider/model` (`openai/gpt-5.6-sol`) pins the upstream provider inside the\n gateway; `concentrate/auto` sends the gateway's own `auto` router. Codewhale's\n bare `--model auto` remains the resolver sentinel (provider default), which is\n why the gateway router has its own explicit spelling. Once streaming begins\n the gateway commits to one provider ([auto routing](https://concentrate.ai/docs/api-reference/endpoint/auto-routing)).\n- **Wire.** Fixed Responses dialect. The request carries only documented\n fields — `model`, `input`, `stream`, `max_output_tokens`, `tools`,\n `tool_choice`, `parallel_tool_calls`, `reasoning.effort` — and the system\n prompt travels as a leading `system` input item because `instructions` is\n not in the gateway's [parameter reference](https://concentrate.ai/docs/api-reference/endpoint/request-parameters).\n Streaming follows the typed `response.*` SSE events and ends on\n `response.completed` without a `[DONE]` sentinel ([streaming](https://concentrate.ai/docs/api-reference/endpoint/streaming)).\n- **Errors.** `{ \"error\", \"message\", \"model\"?, \"retry_after\"? }` with 400\n (bad request / unknown model), 401 (invalid key), 402 (insufficient\n credits), 424 (upstream provider unavailable), 429 (rate limit) —\n surfaced verbatim and classified by message ([errors](https://concentrate.ai/docs/api-reference/endpoint/errors)).\n- **Catalog.** `GET /v1/models` needs no key\n ([list models](https://concentrate.ai/docs/api-reference/endpoint/list-models)); the live rows\n are provider-scoped and unclaimed (no pricing or capability claims).\n- **Commercial boundary.** Concentrate's [Terms of Service](https://concentrate.ai/legal/terms-of-service)\n forbid reselling, white-labeling, or providing the service on a\n service-bureau basis without written consent, and its\n [Acceptable Use Policy](https://concentrate.ai/legal/acceptable-use-policy)\n forbids sharing or sublicensing keys. Codewhale therefore ships **no**\n Codewhale-owned Concentrate key, no stored customer key, no default or\n managed routing, and no markup — activation of any hosted lane is gated on\n written consent, terms, and billing approval (tracked in the Codewhale ops\n evidence checklist `concentrate-gateway-20260829/CHECKLIST.md`). Public\n pricing states \"No platform markup on tokens\".\n- **Self-test without a key.** `scripts/concentrate-selftest.sh` boots a\n local stub that speaks the documented contract and drives the real\n `codewhale exec` path through it, asserting the URL, bearer header, model\n passthrough, streaming, and the completed-turn receipt. No network call\n leaves the machine and no account is required.\n\n### Hugging Face Provider vs MCP vs Hub\n\nCodewhale's `huggingface` provider ID is only the OpenAI-compatible chat\ninference route through Hugging Face Inference Providers. It is selected with\n`/provider huggingface`, `CODEWHALE_PROVIDER=huggingface`, or\n`provider = \"huggingface\"`.\n\nHugging Face MCP is a separate external-tool route. Configure it through the\nMCP config described in `docs/MCP.md`, preferably using the settings-generated\nsnippet from <https://huggingface.co/settings/mcp>. In the TUI, `/hf mcp status`\nchecks whether the Hugging Face MCP server appears in the resolved MCP config,\n`/hf mcp setup` prints the settings workflow and a placeholder-only shape, and\n`/hf concepts` explains the provider/MCP/Hub distinction.\n\nHub publishing or repository management remains explicit user action through\nHub-native tooling such as `huggingface_hub` or git. The `/hf` helper does not\nupload to Hugging Face and does not perform direct Hugging Face Hub HTTP search.\n\n### Xiaomi MiMo Notes\n\n`xiaomi-mimo` defaults to `mimo-v2.6-pro` for long-context reasoning and coding\nwork. The chat picker also exposes `mimo-v2.6-flash` (the replacement for the\nretired Omni model `mimo-v2.5`) and `mimo-v2.5-pro-ultraspeed`. Xiaomi MiMo TTS is available through\n`codewhale --provider xiaomi-mimo speech \"text\" --model tts` (or the `tts`\nalias). In Work and Operate, the provider-specific `speech` / `tts` tools are\navailable through deferred discovery when the Xiaomi MiMo route is configured.\n\n`/provider xiaomi-mimo ultraspeed` and `/provider xiaomi-mimo pro-ultraspeed`\nboth select `mimo-v2.5-pro-ultraspeed`. Speech aliases such as `tts`,\n`voice-design`, and `voice-clone` are separate from normal chat defaults.\n\nToken Plan keys default to the Singapore endpoint\n`https://token-plan-sgp.xiaomimimo.com/v1`. If your MiMo account is provisioned\nfor the China region, set `base_url = \"https://token-plan-cn.xiaomimimo.com/v1\"`\nexplicitly in `[providers.xiaomi_mimo]` or set `mode = \"token-plan-cn\"`. Europe\nToken Plan accounts can set\n`base_url = \"https://token-plan-ams.xiaomimimo.com/v1\"` or use\n`mode = \"token-plan-ams\"`; `mode = \"pay-as-you-go\"`\nselects the standard API endpoint and standard MiMo key family. Xiaomi Token\nPlan docs and console expose credit/quota semantics, but Codewhale does not\ncurrently have a documented balance endpoint to poll, so cost display remains\nunknown rather than reusing token-price estimates from another provider.\n\nVoice-design and voice-clone shorthands map to `mimo-v2.5-tts-voicedesign` and\n`mimo-v2.5-tts-voiceclone`. Xiaomi's current\n[image-understanding guide](https://platform.xiaomimimo.com/docs/en-US/usage-guide/multimodal-understanding/image-understanding)\nincludes `mimo-v2.5` for image input. Codewhale exposes image analysis through the\nseparate `[vision_model]` / `image_analyze` path; set that model to\n`mimo-v2.5` when using MiMo for vision.\n\n### OpenRouter-Compatible Base URLs\n\nOpenRouter-compatible gateways should usually stay on the `openrouter`\nprovider with a provider-scoped `base_url` override instead of moving through\nthe generic `openai` route. That keeps OpenRouter-style reasoning, streaming,\ncache usage, and namespaced wire model parsing attached to the selected route:\n\n```toml\nprovider = \"openrouter\"\n\n[providers.openrouter]\napi_key = \"sk-...\"\nbase_url = \"https://openrouter-compatible.example/v1\"\nmodel = \"deepseek/deepseek-v4-pro\"\n```\n\nCodewhale preserves the `deepseek/` wire-model prefix under the OpenRouter\nprovider scope; it does not infer a switch to the direct DeepSeek provider from\nthat model string. Cache fields such as `prompt_cache_hit_tokens`,\n`prompt_cache_miss_tokens`, and `prompt_tokens_details.cached_tokens` are\nparsed when the upstream gateway sends them. If a key/account type omits those\nfields, Codewhale treats them as absent for that response rather than as a\ndifferent provider route.\n\nOrcaRouter (`https://api.orcarouter.ai/v1`) is a dedicated named route\n([OrcaRouter](https://www.orcarouter.ai)) that speaks the same OpenAI\nChat Completions wire protocol and serves the same namespaced\n`vendor/model` catalog. It does not need an OpenRouter-compatible\n`base_url` override: select `provider = \"orcarouter\"` and its namespaced\nwire models (for example `deepseek/deepseek-v4-pro` or its own\n`orcarouter/auto` router) pass through verbatim, exactly as they do on the\nOpenRouter provider scope. A new OrcaRouter session with no saved wire model\nand no catalog row marked as the provider default starts on\n`deepseek/deepseek-v4.1-flash-1017`.\n\n#### OrcaRouter credentials: API key or OAuth 2.0 + PKCE\n\nOrcaRouter accepts two credential sources, and both write the same durable\n`sk-orca-…` key to the same `orcarouter` secret-store slot with\n`auth_mode = \"api_key\"`:\n\n- **API key** — `codewhale auth set --provider orcarouter`, `/provider`, or\n `ORCAROUTER_API_KEY`. Use this when you already have a key.\n- **Connect with OrcaRouter** — `codewhale auth orcarouter` (CLI) or\n `/auth orcarouter` (in-session). This runs an OAuth 2.0 authorization-code\n flow with PKCE (S256) on a loopback redirect. The browser is sent to\n `GET https://www.orcarouter.ai/auth` with `callback_url`, `code_challenge`,\n `code_challenge_method=S256`, `state`, `app_name`, `scope`, and `ref`\n (Codewhale's public OrcaRouter identifier; `app_id` is added once\n OrcaRouter issues one); the code is exchanged at\n `POST https://www.orcarouter.ai/api/v1/auth/keys` for a durable API key.\n There is no client secret; state is compared in constant time before the\n code is used.\n\nAuthentication and inference use different origins: the consent and exchange\nlive on `https://www.orcarouter.ai`; models and chat live on\n`https://api.orcarouter.ai/v1`. Neither is derived from the other, and\n`/v1/auth/keys` on the API origin is not the exchange route. The inference\norigin can be overridden with `ORCA_API_BASE_URL`, `ORCA_BASE_URL` or\n`ORCAROUTER_BASE_URL`; remote origins must be HTTPS, and plain HTTP is\naccepted only on loopback. The sign-in origin stays\n`https://www.orcarouter.ai`: `ORCA_AUTH_BASE_URL` and\n`ORCAROUTER_AUTH_BASE_URL` move it only to a plain-HTTP loopback address for\nlocal test fixtures, any other value is ignored, and `ORCA_BASE_URL` never\nmoves it.\n\nA PKCE-issued key is a durable API key, **not** a refresh token: Codewhale\nstores it, reuses it across restarts, and never sends a refresh grant. Revoke\nthe key on the OrcaRouter console (`/auth orcarouter-revoke` clears the local\ncopy) and sign in again to get a new one. A 401 from the relay marks that\ncredential generation as needing re-authentication rather than silently\nretrying.\n\nThe chat model list is discovered live from `GET https://api.orcarouter.ai/v1/models`\nwith the configured key; its listed prices, including long-prompt price\ntiers, become the route's price estimates. This route speaks Chat Completions,\nso the chat selector keeps only rows whose `supported_endpoint_types` include\n`openai`, leaves out rows offered only on another dialect (such as\nGemini-only image models), and drops image-generation,\nvideo, and rerank rows instead of guessing from a model name. A row that\nstates `architecture.input_modalities` with `image` is image-input capable;\nrows that state no architecture are treated as unknown, not as text-only.\n\nThe desktop app connects OrcaRouter without a terminal through the Runtime\nAPI's `POST /v1/providers/orcarouter/connect`, which runs this same flow in the\nlocal Engine: the app opens the returned consent link in the system browser,\nand the Engine waits on its loopback callback, exchanges the code and stores\nthe key exactly as `codewhale auth orcarouter` does. See\n[RUNTIME_API.md](RUNTIME_API.md).\n\n### Recent OpenRouter Large Models\n\nOpenRouter completions and static registry rows include the April 2026 onward\nlarge models verified through OpenRouter's model metadata:\n`arcee-ai/trinity-large-thinking`, `qwen/qwen3.6-flash`,\n`qwen/qwen3.6-35b-a3b`, `qwen/qwen3.6-max-preview`, `qwen/qwen3.6-27b`,\n`qwen/qwen3.6-plus`, `minimax/minimax-m3`, `xiaomi/mimo-v2.5-pro`,\n`xiaomi/mimo-v2.5`, `moonshotai/kimi-k2.7-code`, `moonshotai/kimi-k2.6`,\n`z-ai/glm-5.1`, `z-ai/glm-5.2`, `z-ai/glm-5-turbo`, `tencent/hy3-preview`,\n`google/gemma-4-31b-it`, `google/gemma-4-26b-a4b-it`, and\n`nvidia/nemotron-3-nano-omni-30b-a3b-reasoning:free`.\n`minimax/minimax-m3` was added from OpenRouter's May 31, 2026 listing as a 1M\ncontext multimodal model for coding, tool use, and long-horizon agentic work.\n`GLM-5.3` is now the default direct Z.AI Coding Plan model; `GLM-5.2` /\n`z-ai/glm-5.2` remain available (explicit selections keep their own id),\n`GLM-5.1` / `z-ai/glm-5.1` remain available as the smaller model,\n`GLM-5.3-Flash` / `z-ai/glm-5.3-flash` is the faster/explore sibling of\n`GLM-5.3`, and `GLM-5-Turbo` / `z-ai/glm-5-turbo` remains the faster sibling\nof `GLM-5.2`.\n`GLM-5.3` / `z-ai/glm-5.3` and `GLM-5.3-Flash` / `z-ai/glm-5.3-flash` are\nfirst-class picker ids on the Z.ai and OpenRouter routes (`/model` after\n`/provider zai`, or `model = \"GLM-5.3-Flash\"`).\nLimits and reasoning options for 5.3 are inherited from\n`GLM-5.2` until Z.ai publishes distinct 5.3 metadata, and 5.3 carries no\nprice. Flash ships the published $0.15/$0.50 list. A live call can still\n429 with entitlement code 1311 on accounts that are not provisioned for 5.3.\n\n## Static Model Registry\n\nThis table records legacy selector compatibility. Current `codewhale model\nresolve`, `codewhale model list`, the picker and Runtime API use the shared\nprovider lake and route resolver. `models --update` refreshes the existing\ncatalog cache; later routes see new metadata, while dispatched requests retain\ntheir admitted limits and price receipt.\n\nThe reviewed `selections` in `crates/config/assets/catalog_corrections.json`\nremain compatibility metadata for legacy callers. They do not establish account\navailability or executable route permission. Public discovery can contain\nmetadata-only models; an authenticated provider roster establishes a different\nfact. A same-named custom endpoint keeps its own declared limits and prices.\n\n`codewhale model resolve MODEL --provider PROVIDER` reports the exact wire ID,\ncanonical ID when declared, limits and implemented capabilities. Runtime\n`GET /v1/providers/PROVIDER/models` adds source, fetch/expiry metadata, published\nprice structure, reasoning options, configured effective limits and the price\nstatus. `account_access: unverified` means these facts have not established the\nselected account's access. Context responses distinguish an active engine\nwindow from the static route's resolution source, including any fallback.\nSee [Catalog refresh](CATALOG_REFRESH.md) for freshness and estimate boundaries.\n\n| Provider | Static registry entries | Tool calls | Registry reasoning flag |\n| --- | --- | --- | --- |\n| `deepseek` | `deepseek-v4-pro`, `deepseek-v4-flash`, `deepseek-v4-flash-vision-exp` | yes | yes |\n| `nvidia-nim` | `deepseek-ai/deepseek-v4-pro`, `deepseek-ai/deepseek-v4-flash` | yes | yes |\n| `openai` | `deepseek-v4-pro`, `deepseek-v4-flash`, `gpt-5.6`, `gpt-5.6-sol`, `gpt-5.6-terra`, `gpt-5.6-luna` | yes | yes |\n| `atlascloud` | `deepseek-ai/deepseek-v4-flash`, `deepseek-ai/deepseek-v4-pro` | yes | yes |\n| `wanjie-ark` | `deepseek-reasoner` | yes | yes |\n| `volcengine` | `DeepSeek-V4-Pro`, `DeepSeek-V4-Flash` | yes | yes |\n| `openrouter` | `deepseek/deepseek-v4-pro`, `deepseek/deepseek-v4-flash`, `arcee-ai/trinity-large-thinking`, `minimax/minimax-m3`, `minimax/minimax-m2.7`, `xiaomi/mimo-v2.5-pro`, `xiaomi/mimo-v2.5`, `qwen/qwen3.6-flash`, `qwen/qwen3.6-35b-a3b`, `qwen/qwen3.6-max-preview`, `qwen/qwen3.6-27b`, `qwen/qwen3.6-plus`, `qwen/qwen3.7-max`, `moonshotai/kimi-k2.7-code`, `moonshotai/kimi-k2.6`, `z-ai/glm-5.1`, `z-ai/glm-5.2`, `z-ai/glm-5.3`, `z-ai/glm-5.3-flash`, `z-ai/glm-5-turbo`, `tencent/hy3-preview`, `google/gemma-4-31b-it`, `google/gemma-4-26b-a4b-it`, `nvidia/nemotron-3-nano-omni-30b-a3b-reasoning:free`, `nvidia/nemotron-3-ultra-550b-a55b` | yes | yes |\n| `orcarouter` | `deepseek/deepseek-v4.1-flash-1017` (default), `deepseek/deepseek-v4-pro`, `deepseek/deepseek-v4-flash`, `orcarouter/auto` | yes | yes |\n| `xiaomi-mimo` | `mimo-v2.6-pro`, `mimo-v2.6-flash`, `mimo-v2.5-pro-ultraspeed`; speech/TTS IDs are selected through `codewhale speech` / `tts` | yes | yes for chat models; no for speech/TTS models |\n| `novita` | `deepseek/deepseek-v4-pro`, `deepseek/deepseek-v4-flash` | yes | yes |\n| `fireworks` | `accounts/fireworks/models/deepseek-v4-pro` | yes | yes |\n| `siliconflow` | `deepseek-ai/DeepSeek-V4-Pro`, `deepseek-ai/DeepSeek-V4-Flash` | yes | yes |\n| `arcee` | `trinity-large-thinking`, `trinity-large-preview`; provider-hinted custom model IDs pass through | yes | yes for `trinity-large-thinking`; no for `trinity-large-preview` |\n| `moonshot` | `kimi-k2.7-code`, `kimi-k2.6` | yes | yes |\n| `zai` | `GLM-5.3`, `GLM-5.3-Flash`, `GLM-5.2`, `GLM-5.1`, `GLM-5-Turbo`; provider-hinted custom model IDs pass through | yes | yes |\n| `stepfun` | `step-5-preview` (default), `step-3.7-flash`, `step-3.5-flash`, `step-3.5-flash-2603` | yes | yes |\n| `minimax` | `MiniMax-M3`, `MiniMax-M2.1`, `MiniMax-M2.1-highspeed`, `MiniMax-M2`; `MiniMax-M2.7`, `MiniMax-M2.7-highspeed`, `MiniMax-M2.5` and `MiniMax-M2.5-highspeed` stay accepted until their 2026-11-18 retirement | yes | yes |\n| `minimax-anthropic` | `MiniMax-M3`, `MiniMax-M2.7` | yes | yes |\n| `sglang` | `deepseek-ai/DeepSeek-V4-Pro`, `deepseek-ai/DeepSeek-V4-Flash` | yes | yes |\n| `vllm` | `deepseek-ai/DeepSeek-V4-Pro`, `deepseek-ai/DeepSeek-V4-Flash` | yes | yes |\n| `ollama` | live local tag; custom tags pass through when provider hint is `ollama` | yes | no |\n| `ollama-cloud` | `gpt-oss:120b`; arbitrary provider-owned model IDs pass through | yes | yes |\n| `huggingface` | `deepseek-ai/DeepSeek-V4-Pro`, `deepseek-ai/DeepSeek-V4-Flash` | yes | no |\n| `modelscope` | `Qwen/Qwen3.5-397B-A17B`, `Qwen/Qwen3.5-122B-A10B`, `Qwen/Qwen3.5-27B`, `Qwen/Qwen3.5-35B-A3B`, `Qwen/Qwen3.8-27B`, `Qwen/Qwen3.8-Flash-Next`, `deepseek-ai/DeepSeek-V4-Pro`, `deepseek-ai/DeepSeek-V4-Pro-0813`, `deepseek-ai/DeepSeek-V4.1-Flash`, `ZhipuAI/GLM-4.7-Flash`, `ZhipuAI/GLM-5.2` | yes | no |\n| `deepinfra` | `deepseek-ai/DeepSeek-V4-Pro`, `deepseek-ai/DeepSeek-V4-Flash`, `deepseek-ai/DeepSeek-V4-Pro-0813`, `deepseek-ai/DeepSeek-V4.1-Flash-1017`, `deepseek-ai/DeepSeek-V4.1-Flash`, `deepseek-ai/DeepSeek-V4-Flash-0731` | yes | yes |\n| `together` | `deepseek-ai/DeepSeek-V4-Pro`, `deepseek-ai/DeepSeek-V4-Flash`, `thinkingmachines/inkling` | yes | yes |\n| `openai-codex` | the ChatGPT plan roster the account actually serves (`gpt-6.1-sol`, `gpt-6-astra`, `gpt-6-sol`, `gpt-6-luna`, `gpt-5.6-sol`, `gpt-5.6-terra`, `gpt-5.6-luna` observed 2026-10-23); `gpt-6.1-sol` is the offline default | yes | yes |\n| `anthropic` | `claude-opus-5-5`, `claude-sonnet-5-5`, `claude-haiku-5-5`, `claude-fable-5-1`, `claude-opus-5`, `claude-opus-4-8`, `claude-sonnet-5`, `claude-sonnet-4-6`, `claude-fable-5`, `claude-haiku-4-5` | yes | yes except `claude-haiku-4-5` |\n| `openmodel` | `deepseek-v4-flash`; provider-scoped custom model IDs pass through | yes | model-dependent |\n| `sakana` | `fugu`, `fugu-ultra-20260615` | yes | yes for `fugu-ultra-20260615` |\n| `longcat` | `LongCat-2.0` | yes | yes |\n| `opencode-go` | `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` | yes | yes |\n| `meta` | `muse-spark-1.3`, `muse-spark-1.3-contributor`, `muse-wonder-1.0`, `muse-spark-1.2`, `muse-spark-1.2-contributor` | yes | yes |\n| `xai` | `grok-4.6`, `grok-4.7`, `grok-4.5`, `grok-4.3`, `grok-build`, `grok-composer-2.5-fast`, `grok-4.20-0309-reasoning`, `grok-4.20-0309-non-reasoning` | yes | yes for `grok-4.6`, `grok-4.7`, `grok-4.5`, `grok-4.3`, `grok-build`, and `grok-4.20-0309-reasoning` |\n| `google` | `gemini-4-argon`, `gemini-3.1-pro-preview`, `gemini-3-pro-preview`, `gemini-3.8-flash`, `gemini-3.6-flash`, `gemini-3.5-flash-lite`, `gemini-2.5-pro`, `gemini-2.5-flash` | yes | yes except `gemini-3.5-flash-lite` |\n| `mistral` | `mistral-code-latest`, `mistral-medium-latest`, `mistral-small-latest`, `mistral-large-latest` | yes | yes for Medium and Small (`reasoning_effort` `none` or `high` on exact first-party routes); deprecated native Magistral remains an always-on explicit compatibility ID; no for Code and Large |\n| `modelstudio-token-plan`, `modelstudio-coding-plan` | `qwen3.8-max`, `qwen3.7-plus`, `qwen3.7-max`, `qwen3.6-flash`, `deepseek-v4-pro`, `deepseek-v4-flash-0731`, `glm-5.2` | yes | yes |\n| `csdn` | `glm_for_coding`; other marketplace model IDs pass through | yes | yes |\n\nAtlasCloud keeps the same default model as the config layer and adds\nprovider-scoped aliases for the Pro and Flash rows. Other AtlasCloud model IDs\nshould still be selected through `ATLASCLOUD_MODEL`, config, or live model\nlisting when available.\n\n## Capability Metadata\n\n`codewhale doctor --json` exposes the `capability` object. It is static\nmetadata, not a live API probe. Current fields are:\n\n`resolved_provider`, `resolved_model`, `context_window`, `max_output`,\n`thinking_supported`, `cache_telemetry_supported`, and `request_payload_mode`.\n\nWhen configuration cannot be loaded or validated, `doctor --json` exits\nnonzero and prints a bounded, secret-redacted JSON error envelope with\n`status = \"error\"` and `error.kind = \"config_validation\"` instead of emitting\nmisleading route or capability metadata.\n\nMost shipped providers use the Chat Completions request payload mode. Native\nMessages routes, including `minimax-anthropic`, use `/v1/messages`, and\n`openai-codex` uses Responses.\n\nFor OpenAI-compatible gateways or self-hosted runtimes whose real window\ndiffers from the static table, set `[providers.<name>] context_window = N`.\nThe configured value becomes the route-effective context window for prompts,\ncontext-pressure checks, compaction, and output-cap budgeting.\n\n`max_output` is optional and truthful: it is `null` (and omitted from the\ncapability struct on the wire) when the route publishes no output maximum we\ncan stand behind — the Kimi Code membership `kimi-for-coding` family is the\ncanonical example, since the membership catalog owns their limits. An unknown\noutput ceiling is never backfilled with a placeholder, and it applies **no**\ncompatibility clamp to a turn's requested `max_tokens`; only a concrete\nroute/offering maximum narrows the request. A model the catalogue simply has no\nrow for is a different fact — absence is not permission, so an uncatalogued id\nkeeps a conservative ceiling. The \"Max output metadata\" column below reads\n`unknown` wherever no documented maximum exists.\n\nThe exact Kimi Code membership roster contains `k3`, `k3-256k`,\n`kimi-for-coding`, and `kimi-for-coding-highspeed`. The two K3 ids share the\nsame reasoning and fixed-sampling contract; `k3-256k` stays at 262,144 tokens,\nwhile bare `k3` can use an entitled 1M override.\n\n| Provider/model class | Context window | Max output metadata | Thinking support | Cache telemetry | FIM endpoint |\n| --- | --- | --- | --- | --- | --- |\n| DeepSeek V4 (`deepseek-v4-pro`, `deepseek-v4-flash`) | 1,000,000 | 384,000 | yes | yes | DeepSeek beta only, on Chat Completions routes; not on the default Flash route, which speaks Responses |\n| DeepSeek V4 Flash Vision experimental (`deepseek-v4-flash-vision-exp`) | 1,000,000 inherited from Flash | 384,000 inherited from Flash | yes, inherited | yes, inherited | not claimed; Chat Completions route only |\n| DeepSeek compatibility aliases (`deepseek-chat`, `deepseek-reasoner`) | 1,000,000 | 384,000 | yes | yes | DeepSeek beta only |\n| NVIDIA NIM V4 registry models | 1,000,000 | 384,000 | yes | yes | not documented in code |\n| Volcengine Ark V4 model IDs | 1,000,000 | 384,000 | yes | yes | not documented in code |\n| OpenRouter, Novita, Fireworks, SiliconFlow, SGLang, and vLLM V4 model IDs | 1,000,000 | 384,000 | yes | no | not documented in code |\n| Xiaomi MiMo `mimo-v2.6-pro`, `mimo-v2.6-flash` | 1,048,576 | 131,072 | yes | no | not documented in code |\n| Xiaomi MiMo `mimo-v2.5-pro-ultraspeed` | 1,000,000 | 131,072 | yes | no | not documented in code |\n| OpenRouter Qwen 3.6 Flash / Plus | 1,000,000 | 65,536 | yes | no | not documented in code |\n| OpenRouter Qwen 3.6 35B / 27B | 262,144 | 262,140 | yes | no | not documented in code |\n| OpenRouter Qwen 3.6 Max Preview | 262,144 | 65,536 | yes | no | not documented in code |\n| OpenAI API `gpt-5.5` | 1,050,000 | 128,000 | yes | no | not documented in code |\n| OpenAI API `gpt-5.6`, `gpt-5.6-sol`, `gpt-5.6-terra`, `gpt-5.6-luna` | 1,050,000 | 128,000 | yes | no | not documented in code |\n| Anthropic API `claude-opus-5-5`, `claude-sonnet-5-5`, `claude-haiku-5-5`, `claude-fable-5-1`, `claude-opus-5`, `claude-opus-4-8`, `claude-sonnet-5`, `claude-sonnet-4-6`, `claude-fable-5` | 1,000,000 | 128,000 | yes | yes | not documented in code |\n| Google Gemini API `gemini-4-argon` | 1,048,576 | not published | model-dependent | no | not documented in code |\n| Google Gemini API `gemini-3.8-flash`, `gemini-3.6-flash`, `gemini-3.5-flash-lite`, `gemini-3.1-pro-preview`, `gemini-2.5-pro`, `gemini-2.5-flash` | 1,048,576 | 65,536 | model-dependent | no | not documented in code |\n| Meta Model API `muse-spark-1.3`, `muse-spark-1.3-contributor`, `muse-wonder-1.0` | 1,048,576 | not published | yes | no | not documented in code |\n| Meta Model API `muse-spark-1.2` | 1,000,000 | 32,000 | yes | no | not documented in code |\n| ChatGPT plan route (`openai-codex`) | conservatively budgeted; account listing does not state a window | no output cap sent in preview | model dependent | no | public `/v1/responses`; no context limit is inferred from account eligibility |\n| OpenModel default/custom model IDs | 200,000 fallback unless model metadata or config overrides it | 64,000 fallback | model-dependent | no | route uses Messages payload at `/v1/messages` |\n| Wanjie Ark `reasoner` / `r1` model IDs | 128,000 | unknown (no documented maximum) | yes | no | not documented in code |\n| Direct Arcee API `trinity-large-thinking` | 262,144 | 262,144 | yes | no | not documented in code |\n| Direct Arcee API `trinity-large-preview` | 262,144 | unknown (no documented maximum) | no in doctor capability metadata | no | not documented in code |\n| Direct Moonshot `kimi-k3` | 1,048,576 | 1,048,576 documented maximum; 131,072 provider default | yes | no | exact route uses `max_completion_tokens` and omits fixed sampling fields ([K3 quickstart](https://platform.kimi.ai/docs/guide/kimi-k3-quickstart)) |\n| Kimi Code membership `k3` | 262,144 safe baseline; 1,048,576 with an explicit entitled-plan override | 131,072 conservative default ceiling; membership maximum is not published | yes | no | exact `https://api.kimi.com/coding/v1` route |\n| Kimi Code membership `k3-256k` | 262,144 fixed | 131,072 conservative default ceiling; membership maximum is not published | yes | no | exact `https://api.kimi.com/coding/v1` route |\n| Direct Moonshot/Kimi K2.7/K2.6 (`kimi-k2.7-code`, `kimi-k2.7-code-highspeed`, `kimi-k2.6`) | 262,144 | 32,768 | yes | no | provider-reported bundled catalog |\n| Kimi Code membership `kimi-for-coding`, `kimi-for-coding-highspeed` | 262,144 | unknown — the membership catalog owns these limits and no client-side ceiling is claimed | yes | no | exact `https://api.kimi.com/coding/v1` route |\n| Direct Z.AI `GLM-5.3` (default) | 1,000,000 | 131,072 | yes | no | live on the GLM Coding Plan; limits inherited from `GLM-5.2` until Z.ai publishes distinct 5.3 numbers; no USD price |\n| Direct Z.AI `GLM-5.3-Flash` | 1,000,000 | 131,072 | yes | no | natively multimodal; $0.15/$0.50 list (2026-08-26); faster/explore sibling of `GLM-5.3` |\n| Direct Z.AI `GLM-5.2` | 1,000,000 | 131,072 | yes | no | not documented in code |\n| Direct Z.AI `GLM-5.1` | 202,752 | 131,072 | yes | no | not documented in code |\n| Direct Z.AI `GLM-5-Turbo` | 202,752 | 131,072 | yes | no | faster/explore sub-agent sibling |\n| Direct MiniMax `MiniMax-M3` | 1,000,000 | 524,288 | yes | no | not documented in code |\n| Direct MiniMax M2.x models | 204,800 | unknown until MiniMax output metadata is promoted | yes | no | not documented in code |\n| MiniMax Messages route (`MiniMax-M3`, `MiniMax-M2.7`) | model-specific values above | model-specific values above | yes | no | route uses `/anthropic/v1/messages` |\n| Generic `openai` and AtlasCloud | 128,000 | unknown (no documented maximum) | no in doctor capability metadata | no | not documented in code |\n| Ollama | 8,192 | unknown (no documented maximum) | no | no | not documented in code |\n| Hugging Face Inference Providers V4 model IDs | 131,072 | unknown (no documented maximum) | yes | no | not documented in code |\n| Other recognized DeepSeek model IDs | 128,000 unless the model name carries an explicit `Nk` hint | unknown (no documented maximum) | no unless V4/reasoner logic matches | DeepSeek/NIM only | DeepSeek beta only |\n\nMiniMax M3 uses input-length and service tiers. Codewhale omits\n`service_tier`, so requests use the standard tier and cost estimates select the\ncorrect standard rate from total input usage. Priority rates are listed to keep\nthe official tier structure visible. Prices are USD per million tokens.\n\n| Model / service tier | Input length | Input | Output | Cache read | Cache write |\n| --- | --- | ---: | ---: | ---: | ---: |\n| `MiniMax-M3` standard | up to 512,000 input tokens | $0.30 | $1.20 | $0.06 | not published |\n| `MiniMax-M3` standard | over 512,000 input tokens | $0.60 | $2.40 | $0.12 | not published |\n| `MiniMax-M3` priority | up to 512,000 input tokens | $0.45 | $1.80 | $0.09 | not published |\n| `MiniMax-M3` priority | over 512,000 input tokens | $0.90 | $3.60 | $0.18 | not published |\n| `MiniMax-M2.7` standard | all supported inputs | $0.30 | $1.20 | $0.06 | $0.375 |\n\nThese values come from the [MiniMax pay-as-you-go pricing\nguide](https://platform.minimax.io/docs/guides/pricing-paygo). M3 thinking is\nadaptive or disabled; the OpenAI-compatible API defaults to adaptive and the\nAnthropic-compatible API defaults to disabled. M2.7 thinking cannot be\ndisabled. Codewhale sends explicit controls when the user selects a reasoning\nmode.\n\nTool-call support is tracked separately by the static `ModelRegistry` and by\nthe endpoint's ability to accept OpenAI-compatible `tools` payloads. A custom\nOpenAI-compatible or local endpoint can still reject tool calls even if\nCodewhale can send the schema.\n\n### Hugging Face Inference Providers Notes\n\nThe shipped Hugging Face route targets the OpenAI-compatible Inference Providers\nrouter at `https://router.huggingface.co/v1`. Configure auth with\n`HUGGINGFACE_API_KEY` first, or `HF_TOKEN` as a fallback. Configure the endpoint\nwith `HUGGINGFACE_BASE_URL` first, or `HF_BASE_URL` as a fallback; configure the\nmodel with `HUGGINGFACE_MODEL` first, or `HF_MODEL` as a fallback.\n\nThis route does not imply Hub browsing, model-card metadata, dataset access,\nJobs, uploads, or export. Those remain explicit Model Lab work items so\nprovider auth and artifact movement stay separate.\n\n### When a Local Model Prints Tool JSON\n\nCodewhale only executes tools when the provider returns Chat Completions\n`tool_calls` or streamed `delta.tool_calls`. If a local model prints text such\nas `{\"name\":\"File\",\"arguments\":{\"action\":\"search_content\",...}}` in the\nassistant message, that is ordinary model output, not an executable tool\nrequest.\n\nFor OpenAI-compatible or local runtimes, check:\n\n- The endpoint accepts the `tools` array in `/v1/chat/completions` requests.\n- The selected model or chat template is configured for function/tool calls.\n- The server returns `tool_calls` in the response rather than plain JSON text.\n- The compatibility layer does not strip tools before forwarding the request.\n- If in doubt, test a small `File` `read` or `search_content` action against a\n known tool-calling model before debugging Codewhale's tool registry.\n\nChanging `provider`, `base_url`, or `model` can select a route that supports the\nOpenAI-compatible payload shape, but Codewhale cannot convert arbitrary JSON\ntext into a trusted tool call after the model has emitted it as prose.\n\nDeepSeek retired `deepseek-chat` and `deepseek-reasoner` on 2026-07-24 at\n15:59 UTC. Codewhale migrates either name to `deepseek-flash` before a\nrequest reaches DeepSeek's first-party OpenAI or Anthropic endpoint. If no\nreasoning tier was configured, `deepseek-chat` also migrates to `off` and\n`deepseek-reasoner` to `high`, preserving their former non-thinking / thinking\nintent; an explicit `reasoning_effort` remains authoritative. The mapping is\ndeliberately not global: Wanjie Ark, aggregators, self-hosted runtimes, and\ncustom endpoints continue to own their model ids.\n\n## Reasoning Effort\n\n`/reasoning <effort>` (and the `reasoning_effort` config key) is translated to\neach provider's wire dialect by the client before the request is sent. `off`\ndisables thinking where the route supports it. Both exact K3 routes map `off`\nto their lowest supported tier, `low`, and the model is never switched to\nsatisfy `off` — but they do so for different reasons:\n\n- **Kimi Code membership K3** (exact `https://api.kimi.com/coding/v1` with\n `model = \"k3\"` or `model = \"k3-256k\"`) — the membership roster declares K3 always-thinking, so `off`\n cannot be honored without changing what the model is. The clamp preserves the\n fixed K3 identity.\n- **Direct Moonshot K3** (exact `https://api.moonshot.ai/v1` with\n `model = \"kimi-k3\"`) — this clamp is *defensive*, not a documented contract.\n The direct platform publishes no `off` state for K3, and Codewhale will not\n assert a fixed-thinking guarantee it cannot verify for a given key's\n entitlement, so the requested `off` is normalized to the lowest tier with the\n live entitlement left unknown.\n\nNormal dispatched\n`auto` uses Codewhale's auto-reasoning selector and sends a concrete tier;\nonly an omitted reasoning setting leaves the provider default in control.\nProviders marked \"omitted\" receive no reasoning fields at all for that tier.\n\n| Provider | `off` | `low`/`medium`/`high` | `max`/`xhigh` |\n| --- | --- | --- | --- |\n| `deepseek`, `deepseek-cn`, `siliconflow`, `siliconflow-CN`, `sglang`, `volcengine`, `atlascloud` | `thinking: {type: disabled}` | `reasoning_effort: \"high\"` + `thinking: {type: enabled}` | `reasoning_effort: \"max\"` + `thinking: {type: enabled}` |\n| `openrouter`, `novita`, other `together` models | `thinking: {type: disabled}` | `reasoning_effort` pass-through + `thinking: {type: enabled}` | `reasoning_effort: \"xhigh\"` + `thinking: {type: enabled}` |\n| `together` + `thinkingmachines/inkling` | `reasoning_effort: \"none\"` | exact `minimal`/`low`/`medium`/`high` `reasoning_effort` | `reasoning_effort: \"max\"` |\n| Direct Moonshot `kimi-k3` at exact `https://api.moonshot.ai/v1` | top-level `reasoning_effort: \"low\"` (effective normalization) | top-level `reasoning_effort: \"low\"` / `\"high\"` (`medium` becomes `high`) | top-level `reasoning_effort: \"max\"` |\n| Kimi Code membership `k3`, `k3-256k` at exact `https://api.kimi.com/coding/v1` | `thinking: {type: enabled, effort: \"low\"}` (effective normalization) | `thinking: {type: enabled, effort: \"low\" | \"high\"}` | `thinking: {type: enabled, effort: \"max\"}` |\n| Other `moonshot` routes | `thinking: {type: disabled}` | `thinking: {type: enabled}` | `thinking: {type: enabled}` |\n| `ollama` | `think: false` | `think: true` | `think: true` |\n| `ollama-cloud` | `reasoning_effort: \"none\"` | exact `low`/`medium`/`high` `reasoning_effort` | `reasoning_effort: \"max\"` |\n| `xiaomi-mimo` | `thinking: {type: disabled}` | `thinking: {type: enabled}` | `thinking: {type: enabled}` |\n| First-party `minimax` `MiniMax-M3` | `reasoning_split: true` + `thinking: {type: disabled}` | `reasoning_split: true` + `thinking: {type: adaptive}`; effective tier granularity unavailable | `reasoning_split: true` + `thinking: {type: adaptive}`; effective tier granularity unavailable |\n| First-party Z.ai `GLM-5.2` | `thinking: {type: disabled}`; no `reasoning_effort` | enabled thinking; only effective `high` adds `reasoning_effort: \"high\"` | enabled thinking + `reasoning_effort: \"max\"` |\n| First-party Z.ai `GLM-5.3` | `thinking: {type: disabled}`; no `reasoning_effort` | enabled thinking; only effective `high` adds `reasoning_effort: \"high\"` | enabled thinking + `reasoning_effort: \"max\"` |\n| First-party Z.ai `GLM-5.3-Flash` | `thinking: {type: disabled}`; no `reasoning_effort` | enabled thinking; only effective `high` adds `reasoning_effort: \"high\"` | enabled thinking + `reasoning_effort: \"max\"` |\n| First-party Z.ai `GLM-5-Turbo` | `thinking: {type: disabled}` | enabled thinking; effort granularity unavailable | enabled thinking; effort granularity unavailable |\n| Compatible gateways configured as `zai` | omitted; effective unavailable | omitted; effective unavailable | omitted; effective unavailable |\n| `nvidia-nim` | `chat_template_kwargs.thinking: false` | `chat_template_kwargs`: `thinking: true` + `reasoning_effort: \"high\"` | `chat_template_kwargs`: `thinking: true` + `reasoning_effort: \"max\"` |\n| `vllm` | `chat_template_kwargs.enable_thinking: false` | `chat_template_kwargs.enable_thinking: true` + `reasoning_effort` low/medium/high | `chat_template_kwargs.enable_thinking: true` + `reasoning_effort: \"high\"` (vLLM has no max tier) |\n| `arcee`, `huggingface` | omitted | `reasoning_effort` pass-through | `reasoning_effort: \"high\"` |\n| `fireworks` | omitted | `reasoning_effort: \"high\"` | `reasoning_effort: \"max\"` |\n| `openai`, `wanjie-ark`, `telecomjs` | omitted | omitted | omitted |\n| `openmodel` | Anthropic Messages adapter handles thinking/output configuration | Anthropic Messages adapter handles thinking/output configuration | Anthropic Messages adapter handles thinking/output configuration |\n| `openai-codex` | Responses API `reasoning` field (handled by the Responses bridge) | Responses API `reasoning` field | Responses API `reasoning` field |\n\nAtlasCloud serves DeepSeek models, so it speaks the DeepSeek reasoning dialect,\nincluding the `max` tier (#3024).\n\nOn the exact MiniMax OpenAI-compatible Chat endpoints, `MiniMax-M3` uses\n`max_completion_tokens`. Other MiniMax models and compatible gateways retain\n`max_tokens`; the MiniMax Anthropic endpoints use the separate Messages\nadapter.\n\n## Drift Check\n\nRun this before changing provider IDs, provider TOML tables, static model\nregistry rows, or provider default strings:\n\n```bash\npython3 scripts/check-provider-registry.py\n```\n\nThe check fails when:\n\n- `docs/PROVIDERS.md` omits a canonical `ProviderKind::as_str()` ID.\n- Descriptor presentation IDs or released wire tags are duplicated or drift\n from intrinsic kinds, or a second provider enum/ordinal bridge is introduced.\n- The shipped-provider table omits or adds a `[providers.*]` TOML table.\n- The static model registry table drifts from providers used by\n `crates/agent/src/lib.rs`.\n- A provider default model or base URL constant in `crates/tui/src/config.rs`\n is no longer mentioned here.\n\n## Provider Gotchas\n\nThree failures that look like something else (measured by a provider operator\non live traffic):\n\n- **A cold model looks broken.** The first call to a model the provider has not\n loaded can take 90 to 150 seconds, then drop to a second or less once warm.\n Raise `[stream].open_timeout_secs` (default 45 seconds, at most 300) before reading a timeout as \"model unavailable\".\n- **Listed is not callable.** A catalog entry can be visible and still fail\n every call. The same error text on every attempt (for example a \"resources\n busy\" 502) is not transient: a genuinely busy upstream returns successes in\n between. Probe a model before trusting it, and stop retrying one that fails\n identically.\n- **A truncated answer can report `stop`.** A reasoning model can spend the\n whole `max_tokens` allowance on reasoning. When the provider reports\n `completion_tokens` exactly equal to the `max_tokens` the request asked for\n and the finish reason is `stop`, Codewhale treats the turn as cut at the\n ceiling rather than finished (`stop_contradicts_output_ceiling` in\n `codewhale-models`); retrying the same prompt stops at the same count, so\n raise the ceiling or shorten the task instead. No inference is made when the\n provider reports no usage.\n\n## Planned, Not Shipped Yet\n\nThese items belong to the v0.8.48+ provider-abstraction milestone or related\nprovider docs work, but they are not native shipped behavior in this checkout:\n\n- A unified `Provider` trait in `codewhale-agent` that owns env precedence,\n secret resolution, base URL normalization, auth-header construction, and\n provider metadata. Those responsibilities are still split across\n `crates/config`, `crates/secrets`, and `crates/engine/src/client.rs`.\n- Hugging Face model passport metadata in the picker, including license, base\n model, context length, chat template, tool-call support, reasoning support,\n and gated/private status.\n\n## Sign in with Claude\n\nThis is an experimental 0.10.2 candidate flow; live sign-in, inference and\nprovider authorization are not release-qualified.\n\nRun `codewhale auth claude` (or `/auth claude` in a running session), open the\nprinted Claude sign-in URL, and paste the complete `code#state` callback.\n`/provider setup anthropic` also offers API-key and Claude Pro / Max choices.\nThen select `anthropic` or its `claude` alias. API keys continue to use separate\nAnthropic API billing; selecting subscription sign-in never falls back to one.\n\nCodewhale stores the grant in its private credentials directory, commits the\nprovider's credential pointer atomically, and refreshes it before requests on a\nworker. Subscription tokens go only to `https://api.anthropic.com`; a custom\nendpoint is rejected. Each request keeps the existing Messages adapter, tool\nprotocol, cache prefix, and Codewhale inference-client identity. The login\nregistration is a separate fact: this candidate uses Claude Code’s OAuth\nclient ID, not a Codewhale-issued registration. No Claude Code credential\nfile or keychain entry is read or modified.\n\n`codewhale auth claude-revoke` (or `/auth claude-revoke`) removes the owned local\ngrant. End remote access separately in your Claude account settings. The\nsubscription route then needs another login; select API-key billing explicitly\nto use an API key. Login and inference follow Anthropic's provider-side\navailability; local checks do not\nestablish that an account is eligible or that live subscription inference works.\n\nReference refreshed on 2026-10-13: upstream Pi is now `earendil-works/pi`.\nIts [v1.2.0 OAuth adapter](https://github.com/earendil-works/pi/blob/06a466935536f8c9bcacb2d5efc0fc021be1bdff/packages/ai/src/auth/oauth/anthropic.ts)\nsupports loopback and copy-code flows and uses `platform.claude.com` token and\ncopy-code endpoints; this candidate still uses `console.anthropic.com`.\nPi also uses [Claude Code inference identity](https://github.com/earendil-works/pi/blob/06a466935536f8c9bcacb2d5efc0fc021be1bdff/packages/ai/src/api/anthropic-messages.ts),\nwhich Codewhale does not adopt. Pi's implementation is a compatibility\nreference, not proof of account eligibility, billing behavior or permission\nto offer the same flow. See Anthropic's current\n[authentication guidance](https://code.claude.com/docs/en/legal-and-compliance#authentication-and-credential-use)\nand [Agent SDK plan guidance](https://support.claude.com/en/articles/15036540-use-the-claude-agent-sdk-with-your-claude-plan).\n"
|
||
},
|
||
{
|
||
"path": "docs/MODEL_LAB.md",
|
||
"sha256": "e4f26dc6c65008e1ef3df19d6f113ed09399d788cadf7b9189214cb52744c4ef",
|
||
"content": "# Model Lab Roadmap\n\nModel Lab is the planned open-model workbench for Codewhale. The north star is\nsimple: Codewhale should make open-source and open-weight models practical in\nterminal coding workflows across every provider that offers them. Model Lab is how\nthose models become discoverable, evaluable, routable, servable, and exportable\nwithout weakening the current terminal-agent contract: local workspace control,\nexplicit provider auth, approval gates, and clear privacy boundaries.\n\nThis document is roadmap language. Some worksets below are roadmap-only.\n\n## Implemented Today\n\n- DeepSeek is the first-class default provider today, with `deepseek-v4-pro`,\n `deepseek-v4-flash`, streaming thinking blocks, Fin routing, `DEEPSEEK_*`\n environment variables, and `~/.deepseek` config compatibility.\n- OpenRouter, Novita, Fireworks, NVIDIA NIM, AtlasCloud, Wanjie Ark, Hugging\n Face Inference Providers, generic OpenAI-compatible endpoints, SGLang, vLLM,\n and Ollama are supported provider paths where their IDs appear in\n `/provider`, `codewhale --provider`, or `codewhale models`.\n- Hugging Face Inference Providers are available through the\n OpenAI-compatible router at `https://router.huggingface.co/v1`. Select the\n route with `huggingface`, `hugging-face`, `hugging_face`, or `hf`; configure\n `HUGGINGFACE_API_KEY` or `HF_TOKEN` for auth.\n- Model auto-routing chooses a concrete DeepSeek model and thinking level per\n turn. It is not a TUI mode.\n- Fin is the fast `deepseek-v4-flash` thinking-off path for routing,\n summaries, cheap checks, RLM child calls, wakeup verification, and\n binary-completion checks.\n- Self-hosted OpenAI-compatible endpoints can be used through SGLang, vLLM,\n Ollama, or the generic `openai` provider configuration.\n\n## Still Planned\n\n- A native Hugging Face Hub browser, model passport picker, or direct Hub search\n workflow. The OpenAI-compatible Hugging Face Inference Providers route is\n implemented separately as a chat provider.\n- Built-in Hugging Face model card, dataset, adapter, safetensors, Spaces, or\n Jobs workflows.\n- Native Unsloth, NeMo, or Arcee integrations.\n- A dedicated Model Lab UI tab.\n- Built-in eval leaderboards, hosted observability, or training-infrastructure\n orchestration.\n\nUntil those land, use the provider paths above, MCP servers, or external\nworkflows explicitly configured by the user.\n\n## Model Lab Principle\n\nModel Lab should help users answer practical questions:\n\n- Which model should handle this turn?\n- Which open or open-weight model can I run locally or through a trusted\n provider?\n- Which provider offers this model with the latency, price, context window,\n license, and privacy posture I need?\n- What did this model cost, how did it perform, and what data left my machine?\n- Can I reproduce, export, or self-host the route?\n\nIt should never hide provider boundaries, silently upload local artifacts, or\ndescribe a model as available before Codewhale can actually route to it.\n\n## Hugging Face Workset\n\nImplemented today:\n\n- Hugging Face Inference Providers as an explicit OpenAI-compatible router\n provider, selected with `huggingface`, `hugging-face`, `hugging_face`, or\n `hf`.\n- Model IDs are sent to the router exactly as selected, including\n org-prefixed Hugging Face model IDs.\n\nPlanned scope:\n\n- Hub API auth and model discovery.\n- Model cards, licenses, tags, safetensors metadata, adapters, and dataset\n links surfaced in a terminal-friendly way.\n- Native Hub browser and model-passport metadata on top of the already separate\n Hugging Face Inference Providers chat route.\n- Hugging Face Jobs as an optional remote execution path for user-approved\n experiments.\n\nNon-goal for now: claiming native Hub search, model passports, Spaces/Jobs, or\nModel Lab UI exists before those surfaces are implemented in code.\nThe inference-provider API key does not imply Hub browsing/export, upload, or\nJobs authorization.\n\n## Unsloth Workset\n\nPlanned scope:\n\n- Fine-tuning recipes and adapter workflows for users who already own the data\n and compute path.\n- Export guidance that keeps dataset, adapter, and checkpoint locations explicit.\n- Compatibility notes for models that can return to local serving or a hosted\n OpenAI-compatible endpoint.\n\n## NeMo Workset\n\nPlanned scope:\n\n- Training and alignment workflow notes for users operating NVIDIA-centric\n infrastructure.\n- Clear boundaries between NVIDIA NIM inference support that exists today and\n future NeMo training or customization workflows.\n\n## Arcee Workset\n\nPlanned scope:\n\n- Small-model routing and specialization experiments.\n- Exportable routes that make it clear when a task is handled by a smaller\n model, Fin, or full DeepSeek reasoning.\n\n## Serving Workset\n\nPlanned scope:\n\n- Better local and private serving ergonomics for SGLang, vLLM, Ollama, and\n OpenAI-compatible gateways.\n- Health checks, model listing, context-window metadata, and route validation.\n- No silent network exposure: public endpoints must be configured explicitly.\n\n## Eval Workset\n\nImplemented authoring foundation:\n\n- The best-of-N Workflow starter can generate 2–16 structured, independent\n worktree candidates with cache-stable shared instructions and a read-only\n review. This is generation/review evidence, not runtime-owned hard-gate proof.\n\nPlanned scope:\n\n- A frozen experimental-search spec that binds the baseline, requested and\n resolved model names, public evidence, and evaluator identity before\n candidate admission (see [Workflow Experimental Search](WORKFLOW_EXPERIMENTAL_SEARCH.md)).\n- Reproducible task suites for coding, review, docs, release checks, and\n long-context workflows.\n- Side-by-side route comparisons where the exact model, provider, thinking\n level, prompt, and tool policy are captured.\n- Runtime-owned hard gates and command scoring after worker write authority is\n revoked; clean-baseline replay; duplicate-patch detection; multi-round\n Pareto/diversity promotion; aggregate receipts over Fleet receipts; and a\n Workflow-panel leaderboard. No winner is applied or merged automatically.\n\n## Observability Workset\n\nPlanned scope:\n\n- Local-first traces for turn routing, tool calls, approvals, cost, cache\n behavior, and context pressure.\n- Export rules that redact secrets and require explicit user action before data\n leaves the machine.\n\n## Training Infra Workset\n\nPlanned scope:\n\n- Recipes for dataset preparation, adapter training, artifact naming, and\n promotion into serving.\n- Separation between local/private artifacts and anything published to a hub or\n registry.\n\n## Privacy And Export Rules\n\n- Local files, prompts, transcripts, traces, model outputs, eval results,\n adapters, datasets, and checkpoints should remain local unless the user\n explicitly chooses a provider or export destination.\n- Provider auth must remain explicit. `DEEPSEEK_*`, OpenRouter,\n `HUGGINGFACE_API_KEY` / `HF_TOKEN`, and self-hosted credentials should not be\n inferred from unrelated config.\n- Exportable artifacts should include provenance: source model, provider,\n route, tool policy, eval inputs, and redaction status.\n- Public sharing, hosted telemetry, sponsorship badges, and external branding\n require maintainer approval.\n"
|
||
}
|
||
]
|
||
},
|
||
{
|
||
"id": "configuration",
|
||
"slug": "configuration",
|
||
"sources": [
|
||
{
|
||
"path": "docs/CONFIGURATION.md",
|
||
"sha256": "618dd41d6edfb50b884d8634663b9d7ab75b1526a3f3e9121903631804e75e43",
|
||
"content": "# Configuration\n\ncodewhale reads configuration from a TOML file plus environment variables.\nAt process startup it may also load literal built-in-provider credentials from\na workspace-local `.env` file. Use the tracked `.env.example` as the template;\ncopy it to `.env`, then add only credential values.\n\nA workspace is not configuration authority. Codewhale therefore ignores\nconfig/profile/home paths, provider/model/base-URL routing, MCP/plugin state,\napproval/sandbox/shell posture, executable paths, runtime settings, and every\nother non-credential `.env` entry. Variable expansion is rejected so a\nrepository cannot substitute an ambient secret into a credential value. Use\n`config.toml`, CLI flags, or values exported by the launching shell for those\nexplicit control-plane settings. `.env` is read from a stable regular-file\nhandle, is capped at 1 MiB, and symbolic links, reparse points, and multiply\nlinked files are rejected.\n\n## Reading and checking configuration from the CLI\n\n`codewhale config get <key>` reads scalar keys, whole tables such as `tools`,\nand nested paths such as `tools.user_input_timeout_seconds`. Displayed tables\nand nested values apply the same recursive credential redaction as `config dump`.\n\n`config set` supports its named scalar keys and the provider, route, and\nnotification commands. It refuses a key that nothing reads and suggests the\nnearest real key (`config set calm_mod on` names `calm_mode`). A settings.toml\nkey such as `calm_mode` or `tool_collapse` is validated and written to the\nuser-global settings.toml, not config.toml; `config get` reads it back from\nthere even when an old config.toml copy (which nothing reads) is still present,\nand names that copy so you can `config unset` it. Settings keys have no project\nscope, so `--project` refuses them. A config.toml key is checked against the\ntype its reader expects and stored as a TOML boolean or number where the\nreader needs one (`yolo = true`, `max_subagents = 4`); `reasoning_effort`\naccepts the same aliases as `/effort` (#6563). Other dotted writes fail before\nmodifying the file and name the TOML table to edit. For example, set a tools\ntimeout in the file as:\n\n```toml\n[tools]\nuser_input_timeout_seconds = 0\n```\n\n`codewhale config doctor` checks credential presence and endpoint shape, and\nwarns about each config.toml root key that nothing reads: a settings.toml key\nleft in config.toml is named as misplaced, and any other unread key gets a\ndid-you-mean. Keys read by any runtime reader are not reported. The warnings do\nnot fail the check, and a clean result does not validate every runtime setting's\nvalue (#6083, #6563).\n\n## Usage and execution statistics\n\n`codewhale stats` is an alias of `codewhale metrics`; both use the same local,\nread-only backend. Use `--json` for the structured `stats` report. For example:\n\n```sh\ncodewhale stats --days 7 --models 10 --tools 20 --roles 10\ncodewhale metrics --project /path/to/workspace --json\n```\n\n`--days N` accepts a positive number of days and conflicts with `--since`.\n`--since 24h` and the existing duration syntax remain available. `--models N`,\n`--tools N` and `--roles N` select the top positive N rows by known tokens,\nidentified tool executions and recorded Fleet tasks respectively. These limits\nonly shorten the display; they do not remove stored receipts.\n\nThe report separates saved-document and Runtime views, frozen native-source\nmoney and token subtotals by actual model, retained tool-result history\nestimates and outcomes, completed compaction observations, and recorded Fleet\nroles. USD and CNY stay separate. A cumulative correction replaces its proven\npredecessor; copied Session/Runtime receipts and inherited Runtime fork rows\nare not added as new provider work. Conflicting source versions are shown\nseparately and excluded from the subtotal. Saved legacy display totals remain\nvisible as document projections without being charged again globally.\n\nCoverage is explicit. A missing quote, usage receipt, tool outcome, output\nshape or recovery link is unknown, not zero. Empty scalar usage from older\ntransport records cannot prove a reported zero. Token counts describe known\nreported data; tool history tokens are estimates of retained text, not bytes\nproven to have been replayed on a provider request. Latest prepared-request\ncomponents and source-linked compaction/role economics are unavailable where\nthey were not captured. The report does not infer provider invoices or prove\ncomplete historical coverage.\n\n`--project WORKSPACE` filters current saved/thread/Fleet views. Original source\nworkspace is not yet captured, so native money and model subtotals are\nunavailable for that filter; mutable current metadata is not used to assign a\ncharge. Older receipt projections without reliable workspace scope are also\nunavailable under this filter. Without it, those legacy receipt views remain\nseparate from native subtotals and may overlap one another.\n\nInspection uses the existing session-size bound (16 MiB per record/frame and\nfor each retained query index). Unreadable, linked, oversized or incomplete\nrecords create visible gaps; an exhausted native index refuses the query\nwithout evicting evidence. Use a narrower time window where possible. Reading\nstatistics does not create a store, repair history, refresh pricing, start a\nmodel call or retire accounting receipts. Existing state-root overrides remain\nthe read boundary.\n\n`/cache calls [count]` shows recent retained native source receipts in the TUI;\nit defaults to 10 and displays at most 50. A source correction updates its row.\nThe view keeps frozen USD/CNY estimates and known subtotals separate from missing\nusage or prices; absent or contradictory cache classes show unknown. The header\ncounts retained sources, not physical requests or complete historical calls.\nThe command does not issue a model request or remove accounting receipts.\n\nPublished prices are frozen for DeepSeek's root, `/v1`, `/beta` and Anthropic\nendpoint variants, and for other providers' default endpoints. Each quote stays\nbound to the exact endpoint, identity, model and dispatch time when saved or replayed.\nOperator endpoint overrides do not become published pricing authorities, and\nsubscription or token-plan routes remain outside money coverage. Signed cloud\nfacts keep their separate route-scoping rules. Recognition as an official\ncredential endpoint alone does not authorize sharing prices across regions or plans.\n\nMissing request outcomes, omitted provider usage, unverified legacy usage and\nusage corrections have distinct cost reasons. Known limitation: saved receipts\nwithout a frozen price are not repriced from today's catalog; their complete\ncost remains unknown even after updating the binary.\n\n## Constitution, project instructions, and repo authority\n\nCodewhale has several instruction surfaces. They are deliberately separate so a\npersonal constitution, repo policy, project instructions, and runtime security\ncontrols do not blur together.\n\n- **Bundled global Constitution** — the compiled base law in the binary. It is\n the default floor for every session.\n- **User-global constitution** — the normal guided setup output. Manage it with\n `/constitution` or `/setup`; Codewhale stores structured data at\n `$CODEWHALE_HOME/constitution.json` (default `~/.codewhale/constitution.json`)\n and renders it into a separate `<codewhale_user_constitution>` prose block.\n This can express preferences and stop conditions, but it does not change\n runtime approval policy, sandbox, shell, network, trust, or MCP permissions.\n- **Repo-local constitution** — optional project policy in\n `.codewhale/constitution.json`, described below.\n- **`AGENTS.md`** — cross-agent **project instructions** (prose). This is the\n canonical file for \"how should an agent work in this repo.\" Run `/init` to\n scaffold one. Instruction files written for other tools (`CLAUDE.md`,\n `.claude/instructions.md`, `.cursorrules`, `.github/copilot-instructions.md`\n and similar) are read only when you opt in with the top-level\n `project_instruction_imports` key, for example `[\"claude\"]`, `[\"cursor\"]` or\n `[\"all\"]` (see `config.example.toml`); Codewhale warns when it finds one it\n did not load.\n\n Codewhale reads one instruction file per directory, from the repository\n root down to the workspace, and puts your global instruction file (such as\n `~/.codewhale/AGENTS.md`) in front of them. The nearest scope reads last\n and wins: the workspace file overrides its parents, and project files\n override the global file. Global and project instructions are budgeted\n separately in estimated tokens (see [Instruction budget](#instruction-budget-instruction_budget-6526)\n below): a scope over its budget drops whole files from the broadest end\n first, and a single file larger than the remaining budget keeps its head\n with a marker that names the bytes and tokens dropped. A file keeps at most\n its first 100 KiB, and a file larger than `file_max_bytes` (8 MiB, lowerable\n only) is skipped with a notice. Every loss — trimmed, skipped, or refused —\n reaches the model and `/context report`.\n- **Memory and handoffs** — recalled state. Useful, but lower authority than\n constitutions and project instructions.\n\n### Managing the user-global constitution (`/setup` and `/constitution`)\n\nThe bundled **working agreement** is the safe default and no longer adds a\nrequired first-run screen. Customize it later through `/constitution` or the\nprogressive `/setup` guide. Provider/model readiness, workspace trust, and\nruntime posture stay separate from this guidance.\n\nOn the **Working agreement** (constitution) step:\n\n- **`1`–`6`** tune the guided draft. **`G`** previews it, and **`G`** again\n ratifies and saves a fresh structured `constitution.json`.\n- **`A`** (shown only when a provider is configured) asks your first configured\n model to draft the constitution. Drafting is **not** saving: the draft is\n rendered through the same preview and you still press **`G`** to ratify\n before anything persists.\n- **`K`** keeps your existing loaded constitution unchanged (shown only when a\n valid file is already present).\n- **`U`** (or `/constitution bundled`) records the bundled/default law.\n\n`/constitution` (alias `/law`) is the primary management surface once you are\nset up. Subcommands: `status` (the default), `preview`, `review`, `repo` (the\nrepo-local law block), `explain`, `base`, `suggestions`, `ratify <id>`,\n`migrate`, `rollback`, `edit`/`guided`, `repair`, `posture`, and `bundled`.\nManaging the constitution never changes runtime approval, sandbox,\nshell, network, trust, default mode, or MCP authority — those stay in runtime\nposture/config.\n\nEach repo can carry two distinct, complementary files:\n\n- **`AGENTS.md`** — ordinary project working instructions.\n- **`.codewhale/constitution.json`** — Codewhale-specific **repo authority /\n prioritization policy**: when local sources conflict, which should Codewhale\n trust first, and what to verify before claiming a task is done. `.codewhale/`\n lives inside the repo (like `.github/`). Example:\n\n ```json\n {\n \"schema_version\": 1,\n \"authority\": [\n \"current user request\",\n \"live code and tests\",\n \"GitHub issue/PR details\",\n \"AGENTS.md\",\n \"memory\",\n \"old handoffs\"\n ],\n \"protected_invariants\": [\n \"do not break old-session transcript replay\"\n ],\n \"branch_policy\": \"PRs target the integration branch, not main\",\n \"verification_policy\": {\n \"before_claiming_done\": [\"run focused tests\", \"read changed files back\"]\n },\n \"escalate_when\": [\n \"a destructive action was not explicitly authorized\"\n ]\n }\n ```\n\n All fields are optional. When present, the file is rendered into the system\n prompt as concise prose in a higher-authority block. Legacy `WHALE.md` files\n are ignored and reported as migration-only diagnostics.\n\n Each `protected_invariants` entry may be either a plain string (advisory\n prose, the historical shape) or an object carrying path globs, which is\n additionally **mechanically enforced** in the tool gate. See\n [Enforced repo-law invariants](#enforced-repo-law-invariants) below.\n\n This is the **repo-local law** layer. When guidance conflicts, the\n **Whose word wins** section of the effective base constitution owns the\n ordering; the bundled source is\n [`BASE_PROMPT`](../crates/engine/src/prompts/text.rs). Use `/constitution base`\n to inspect the effective base, including any opted-in expert override.\n The order in which these files are described or assembled is not an\n authority ranking. Runtime policy (permissions, sandbox and cost limits\n enforced in code) is separate from prompt guidance; editing a constitution\n does not grant permissions.\n\n> **`WHALE.md` is deprecated.** It overlapped confusingly with `AGENTS.md`.\n> Codewhale no longer reads `WHALE.md` as project or global context. If one is\n> present, setup/context diagnostics report it as ignored so you can migrate it.\n> Move ordinary instructions to `AGENTS.md` and Codewhale-specific authority\n> policy to `.codewhale/constitution.json`. Personal standing guidance belongs\n> in `/constitution` / `$CODEWHALE_HOME/constitution.json`. (The global\n> Codewhale Constitution shipped in the model prompt is a separate thing and is\n> unaffected.)\n\n### Enforced repo-law invariants\n\nBy default a `protected_invariants` entry is advisory prose: it is rendered into\nthe prompt as guidance the agent should honor, but nothing stops a write. An\nentry written as an **object with `paths`** is different — it compiles into a\nmechanical write hold that the engine's tool gate evaluates before the write\nruns. The law becomes mechanism, not just a request.\n\nAn enforced entry has this shape:\n\n```json\n{\n \"schema_version\": 1,\n \"protected_invariants\": [\n \"Keep DeepSeek support first-class.\",\n {\n \"text\": \"The wire format is frozen; protocol changes need a human.\",\n \"paths\": [\"crates/protocol/**\"],\n \"action\": \"block\"\n },\n {\n \"text\": \"Release notes need human review.\",\n \"paths\": [\"CHANGELOG.md\"],\n \"action\": \"ask\"\n }\n ]\n}\n```\n\n- `text` — required. The reason surfaced on the hold. An empty `text` is skipped.\n- `paths` — workspace-relative globs (globset syntax, e.g. `crates/protocol/**`,\n `**/secrets.toml`, `CHANGELOG.md`). An object with no usable `paths` stays\n advisory-only despite the object shape.\n- `action` — optional, defaults to `ask`. `ask` force-prompts in Ask and\n Auto-Review; in Full Access it denies the protected write without opening a\n modal. `block` **denies the write outright** in every posture.\n\nSemantics:\n\n- **Tighten-only.** The schema has no allow/widen shape, so law can only *add*\n holds — a crafted constitution can never grant authority or weaken a gate\n above it.\n- **Not bypassable by mode.** Like the built-in safety floor, an `ask` hold\n force-prompts in Ask and Auto-Review. Full Access never opens approval\n modals, so the same hold fails closed as a hard block; `block` always denies.\n Mode cannot turn a hold off.\n- **Repo-local only.** Only the repo's `.codewhale/constitution.json`\n participates. The user-global constitution stays advisory prose and never\n reaches this mechanism.\n- **Fails safe.** A missing file, parse error, or invalid glob degrades to\n fewer or zero rules — never a hold on unprotected paths and never a poisoned\n gate. Across matches the strongest action wins, so `block` outranks `ask`.\n- **Leaves a receipt.** Every hold emits a `tool.repo_law_decision` tool-audit\n event naming the invariant, the matched path, and the source file; the\n approval/denial reason names the invariant too.\n\n**Coverage is deliberately limited.** Holds are evaluated only for the write\ntools `write_file`, `edit_file`, `apply_patch`, and `fim_edit`, and only\nagainst the filesystem targets named in their inputs (`path`/`target`/\n`destination`/`file_path`, `changes[].path`, and unified-diff /\n`apply_patch`-envelope headers). A shell command that writes a protected path is **not** held by\nrepo law — those writes are still governed by the ordinary approval, sandbox,\nand shell-write gates, not by this mechanism.\n\n### Expert full base-prompt override (#3638)\n\nThe global Constitution (the base system prompt, normally compiled in from\n`crates/engine/src/prompts/text.rs` as `BASE_PROMPT`) can be replaced per-user\nwithout rebuilding. This is\nan expert escape hatch, not the normal `/constitution` guided setup output.\nBecause this is a prompt trust boundary, it takes **two deliberate steps** — a\nfile alone is not enough:\n\n1. Drop the replacement at `~/.codewhale/prompts/constitution.md` (under\n `$CODEWHALE_HOME` when set).\n2. Set the explicit opt-in flag `CODEWHALE_ALLOW_BASE_PROMPT_OVERRIDE=1`\n (`true`/`on`/`yes` also accepted).\n\nIf the file exists but the flag is unset, the override is **ignored** (with a\nlog line pointing to the flag) and the bundled Constitution stays in place.\nThis is intended for repurposing the TUI beyond software engineering — e.g.\nlong-form writing or document review — where the engineering-oriented base\nprompt is a poor fit. It is loaded once at startup; a **missing or empty file\nis a no-op**, so existing installs keep the bundled prompt.\n\nScope is deliberately narrow: only the byte-stable **base prompt segment** is\noverridable. Mode deltas, the approval policy, the tool taxonomy, Context\nManagement, and the Compaction Relay are still owned by Codewhale's runtime\nassembly, so an override **cannot remove safety-relevant guidance** (sandbox,\napprovals) — it only swaps the task/voice framing. To customize ordinary\npersonal behavior, prefer `/constitution`; to customize per-repo behavior,\nprefer `AGENTS.md` + `.codewhale/constitution.json` above.\n\n## Where It Looks\n\nDefault config path:\n\n- `~/.codewhale/config.toml`\n- Legacy fallback: `~/.deepseek/config.toml`\n\nOverrides:\n\n- CLI: `codewhale --config /path/to/config.toml`\n- Env: `CODEWHALE_CONFIG_PATH=/path/to/config.toml`\n- Legacy env alias: `DEEPSEEK_CONFIG_PATH=/path/to/config.toml`\n\nIf both are set, `--config` wins. Environment variable overrides are applied after the file is loaded.\n\n### TUI editability audit\n\nInside the TUI, run `/config audit` to see which documented keys can be changed\nfrom the current session, which ones can also be persisted, and which ones stay\nfile-only or restart-only. The audit includes current values for the high-impact\nruntime controls such as `approval_policy`, `allow_shell`,\n`stream_chunk_timeout_secs`, `base_url`, `mcp_config_path`, and the\n`[subagents]` concurrency/depth/timeout keys.\n\nUse the command's \"Command / reason\" column as the source of truth before\nediting by hand. For example, `/config approval_mode on-request --save` writes\ntop-level `approval_policy = \"on-request\"`, while provider base URLs are saved\nbut still require restarting the model client.\n\n### User workspace entries\n\nInteractive Agent sessions expose shell tools by default with approval gating\nunless you explicitly disable them. For a shell opt-in that should live in the\nuser's global config for noninteractive or durable-task profiles rather than in\nthe repository, add a workspace-scoped entry:\n\n```toml\n[workspace.'/absolute/path/to/project']\nallow_shell = true\n```\n\nThe entry applies only when the launched workspace path matches the table key.\nThe legacy `[projects.\"/absolute/path/to/project\"]` table is also accepted for\nthis user-owned override.\n\nIn interactive mode, the per-project overlay\n`<workspace>/.codewhale/config.toml` is applied after this user entry. A\nproject-level `allow_shell = false` can still tighten the session; project-level\n`allow_shell = true` is ignored.\n\n### Per-project overlay (#485)\n\nWhen the TUI starts in a workspace that contains a regular-file\n`<workspace>/.codewhale/config.toml`, the safe values declared in that file are\nmerged on top of the global config. Legacy\n`<workspace>/.deepseek/config.toml` files are still read when the Codewhale path\nis absent. Symlinked project config files are rejected. This lets a repo suggest\na model or tighten local safety posture without touching the user's\n`~/.codewhale/config.toml`. Pass `--no-project-config` to skip the overlay for\none launch.\n\nSupported keys in the project overlay (top-level fields only):\n\n| Key | Effect |\n|---|---|\n| `model` | override `default_text_model` |\n| `reasoning_effort` | force `\"high\"` / `\"max\"` for a complex repo |\n| `approval_policy` | only values that tighten the user's current permission posture |\n| `sandbox_mode` | only values that tighten the user's current sandbox posture |\n| `max_subagents` | clamp sub-agent concurrency for a constrained repo (clamped to 1..=128) |\n| `allow_shell` | `false` can disable shell access; `true` is ignored |\n\nThe overlay is intentionally narrow — it covers the fields a repo\nmaintainer is most likely to want to standardize across contributors.\nCredential, endpoint, provider-selection, MCP config, hooks, skills,\nretry, hotbar bindings, and `instructions = [...]` settings stay user-global.\nIf a repo-local config declares `api_key`, `base_url`, `providers`, `provider`,\n`mcp_config_path`, `notes_path`, `hotbar`, `allow_shell = true`, or `instructions`,\nCodewhale ignores that key and keeps the user's global setting.\n\nThe consolidated `codewhale` runtime uses `~/.codewhale/config.toml` for\nprovider and model settings. `codewhale auth set --provider deepseek` saves\nthe key in the local secret store (a private file under `~/.codewhale/secrets/`\nby default; the OS keychain is an explicit option), rather than writing it\ninto that config file. Legacy `~/.deepseek/config.toml` is migrated on first\nlaunch when needed. `codewhale --model deepseek-v4-flash` is forwarded to the\nTUI as `CODEWHALE_MODEL`. The dispatcher no longer writes `DEEPSEEK_*` twins;\na value you set yourself remains a legacy alias when its `CODEWHALE_*`\nequivalent is unset.\n\n### Account provider keys\n\n`/connect` offers Continue with Codewhale or Connect on this computer. The\naccount path signs in and lists usable model connections reported by the\naccount service. When none are ready, it offers retry or on-computer setup.\nSign-in alone does not supply model access or change the selected model.\nLocal provider keys and local models never need an account; use `/provider`\nor `codewhale auth set --provider <provider>` to manage them.\n\nFor an account that supports saved provider keys, these commands manage them:\n\n```bash\ncodewhale account keys list # configured status, never secret values\ncodewhale account keys remove deepseek # remove the account's saved key\n```\n\nLogin never uploads local keys, and removing an account key does not change\nlocal copies or revoke the key at the provider.\n\nThe account session is separate from provider keys. `/login` signs in inside\nthe TUI; `codewhale login` does so from the shell. `codewhale account status`\ninspects it. `/logout`, `codewhale logout` and `codewhale account logout` remove\nonly the account session for the selected `--profile`. The explicit\n`codewhale logout --all` action also clears saved model credentials.\nAccount sessions use the private `0600`\nCodewhale secrets file, scoped to the profile and account API origin; they do\nnot use the OS keychain. The older `codewhale cloud ...` spelling remains an\nalias for account commands.\n\n### Portable config bundles\n\n`codewhale config export --portable [--project] [--out FILE]` writes a\nportable, secret-free bundle of your configuration: sorted TOML with\ncredential and machine-specific keys (API keys, base URLs, socket paths)\ndropped, never a redacted placeholder in their place. Without `--out` the\nbundle goes to stdout. Typed tables, arrays, numbers, booleans, and datetimes\nremain typed. Machine-bound authority is deliberately non-portable: project\ntrust overlays, credential readers, auto-running hooks, executable LSP\ndefinitions, and local path bindings are omitted rather than copied to a new\nhost. So is trust posture (`yolo`, `allow_shell`, `approval_policy`,\n`sandbox_mode`, `sandbox_network_access`, and the sandbox read-denylist\npaths) and local endpoints or executables (`[lifecycle_outbox]`,\n`[extension_host]`, `[control_socket]`). URLs that carry a credential —\nuserinfo, a token-named query parameter, or a Slack/Discord/Teams webhook\npath — are treated as credentials.\n\n`codewhale config import <FILE|HTTPS_URL|-> [--dry-run] [--yes] [--project]`\napplies a bundle. The envelope is strict (`schema_version = 1`, kind\n`codewhale.portable-config`; unknown fields fail). Import prints a\ndeterministic plan — added / changed / skipped / conflicting / rejected —\nthen asks for consent unless `--yes` is given; headless use requires it.\nCredential-shaped entries are rejected by key name and by value shape;\nrejections name the field, never a value. Remote bundles come from HTTPS\nonly (loopback http excepted) with a 5 MiB cap. Application backs up the\ntarget document to `<config>.bundle-backup-<timestamp>-<random>`, rolls back on any\nfailure, and re-importing an applied bundle changes nothing.\n\nImport also rejects the non-portable authority classes omitted by export,\nincluding nested/camel/dotted credential keys and cookie headers. This keeps a\nhand-authored or remote bundle from reintroducing machine trust, local\ncredential access, or automatically executable commands that a local export\nwould refuse to carry. Structured tables are deep-merged: a portable model or\npreference update does not erase target-local provider credentials, endpoints,\nhooks, or executable definitions that were deliberately omitted from the\nbundle. Arrays and scalar values still replace the corresponding portable\nvalue.\n\nSections map to scope: `[project]` entries land only in the workspace\ndocument (`--project`, which must target an actual workspace config),\n`[global]` only in the user-global one; `preferences`, `profiles`, and\n`plugins` apply at either scope. A global bundle operation refuses a workspace\ndocument just as a project operation refuses the user-global document.\n\n### Credential read precedence (#5197)\n\nCredential reads are **folder-independent by default**: every layer below is\nuser-global or process-scoped, so a key saved in one repo resolves identically\nin every other repo. Repo-local config never carries credential material — the\nproject overlay above reads only its allowlisted keys and ignores `api_key`,\nand a credential write aimed at a workspace-scoped config is rescoped to the\nuser-global `~/.codewhale/config.toml` (#5045, #5193).\n\nFor the active provider, the runtime resolves the API key in this exact order\n(first match wins):\n\n1. **Route-specific auth contract.** Routes whose `auth_mode` disables API\n keys stop here with no credential. OAuth routes use their explicitly\n granted token: the official `openai-codex` plan route accepts only\n Codewhale-owned credentials from `codewhale auth chatgpt`, bound to its\n issued registration and verified account. Imported Codex CLI and process\n tokens cannot authorize this route;\n `[providers.xai] auth_mode = \"oauth\"` reads Codewhale's own xAI\n device-login store (or a consent-granted Grok CLI file).\n2. **Explicit CLI key.** `--api-key` forwarded with its source marker wins\n over every saved slot.\n3. **Config file `api_key`.** The `[providers.<name>] api_key` table slot for\n the active provider. `deepseek-CN` also reads `[providers.deepseek]`. An\n older top-level `api_key` is read as `[providers.deepseek] api_key` (see\n [Legacy top-level `base_url` and `api_key`](#legacy-top-level-base_url-and-api_key)).\n File-owned keys stay bound to their file-owned endpoint: when the\n environment replaces the route's base URL with a custom host, the saved\n key is not sent there.\n4. **`api_key_env` binding.** `[providers.<name>] api_key_env = \"VAR\"` reads\n the named environment variable. For custom providers an unset or empty\n binding is a loud error, not a silent fallback (#5104).\n5. **Secret store.** The durable per-provider slot written by\n `codewhale auth set` (file-backed under `~/.codewhale/secrets/` by\n default; the OS keyring only when explicitly selected). Skipped for named\n custom routes, self-hosted providers, custom endpoints other than an\n explicitly authenticated loopback, and routes whose `auth_mode` needs no\n key.\n6. **Ambient environment.** The provider's own variable\n (`DEEPSEEK_API_KEY`, `OPENROUTER_API_KEY`, `MOONSHOT_API_KEY`, …).\n Ambient keys are only ever sent to the provider's official endpoint and\n are skipped under the same conditions as the secret store.\n7. **Keyless fallback.** Self-hosted providers and loopback endpoints may run\n with no credential; every other route fails with provider-specific setup\n guidance.\n\nLegacy compatibility: `~/.deepseek/config.toml` is migrated into\n`~/.codewhale/config.toml` on first launch, `DEEPSEEK_*` environment\nvariables remain accepted aliases for the `CODEWHALE_*` forms, and\n`DEEPSEEK_SECRET_BACKEND` is the legacy alias for `CODEWHALE_SECRET_BACKEND`.\n\nRun `codewhale auth status` to inspect the active provider's config\nfile, OS keyring backend, environment variable, winning source, and last-four\nlabel without printing the key itself. The command only probes the active\nprovider's keyring entry.\n\nFor hosted, generic OpenAI-compatible, self-hosted, OpenAI Responses, or native\nAnthropic providers, set `provider = \"<id>\"` or pass\n`codewhale --provider <id>`. The canonical provider IDs are `deepseek`,\n`nvidia-nim`, `openai`, `atlascloud`, `wanjie-ark`, `volcengine`,\n`openrouter`, `orcarouter`, `xiaomi-mimo`, `novita`, `fireworks`,\n`siliconflow`, `arcee`, `siliconflow-CN`, `moonshot`, `sglang`, `vllm`,\n`ollama`, `ollama-cloud`, `huggingface`, `modelscope`, `together`, `qianfan`, `openai-codex`,\n`anthropic`, `openmodel`, `zai`, `stepfun`, `minimax`, `deepinfra`,\n`sakana`, `longcat`, `opencode-go`, `opencode-zen`, `meta`, `xai`,\n`mistral`, `telecomjs`, `modelstudio-token-plan`, `google`,\n`edenai`, `concentrate`, `codewhale`, and `custom` (a user-defined OpenAI-compatible endpoint via\n`[providers.<name>]`).\nFor the provider-by-provider registry, including wire protocol, auth variables,\ndefault base URLs, model IDs, and capability metadata, see\n[PROVIDERS.md](PROVIDERS.md).\nThe facade saves provider credentials in the local secret store and forwards\nthe resolved key, base URL, provider, and model to the TUI process. Use\n`codewhale auth set --provider nvidia-nim --api-key \"YOUR_NVIDIA_API_KEY\"` or\n`codewhale auth set --provider openai --api-key \"YOUR_OPENAI_COMPATIBLE_API_KEY\"` or\n`codewhale auth set --provider atlascloud --api-key \"YOUR_ATLASCLOUD_API_KEY\"` or\n`codewhale auth set --provider wanjie-ark --api-key \"YOUR_WANJIE_API_KEY\"` or\n`codewhale auth set --provider xiaomi-mimo --api-key \"YOUR_XIAOMI_KEY\"` or\n`codewhale auth set --provider fireworks --api-key \"YOUR_FIREWORKS_API_KEY\"` or\n`codewhale auth set --provider siliconflow --api-key \"YOUR_SILICONFLOW_API_KEY\"` or\n`codewhale auth set --provider arcee --api-key \"YOUR_ARCEE_API_KEY\"` or the\nmatching provider ID from [PROVIDERS.md](PROVIDERS.md) to save provider keys\nthrough the facade. The generic `openai` provider defaults\nto `https://api.openai.com/v1`, accepts `OPENAI_BASE_URL`, and defaults to\n`gpt-5.6`. A custom OpenAI-compatible gateway can still select its own model\nexplicitly. `atlascloud` defaults to\n`https://api.atlascloud.ai/v1`, accepts `ATLASCLOUD_BASE_URL`, and uses\n`deepseek-ai/deepseek-v4-flash` as its default model. `wanjie-ark` targets\nWanjie Ark's OpenAI-compatible endpoint at\n`https://maas-openapi.wanjiedata.com/api/v1`, defaults to `deepseek-reasoner`,\nand passes model IDs through unchanged because Wanjie model access is\naccount-scoped. SGLang, vLLM, and Ollama are\nself-hosted and can run without an API key by default. Ollama defaults to\n`http://localhost:11434/v1` and sends model tags such as `codewhale-coder:1.3b`\nor `qwen2.5-coder:7b` unchanged. Self-hosted providers and loopback custom\nURLs (`localhost`, `127.0.0.1`, `[::1]`, `0.0.0.0`) do not read the secret store\nunless API-key auth is explicitly requested; use an env var or config-file key\nwhen a local server does require bearer auth.\nOllama Cloud is the separate hosted `ollama-cloud` provider. It defaults to\n`https://ollama.com/v1` and `gpt-oss:120b`; save its key with\n`codewhale auth set --provider ollama-cloud`. Ambient auth reads\n`OLLAMA_CLOUD_API_KEY` first, then Ollama's official `OLLAMA_API_KEY`.\nSiliconFlow defaults to `https://api.siliconflow.com/v1`, accepts\n`SILICONFLOW_BASE_URL`, and uses `deepseek-ai/DeepSeek-V4-Pro` by default.\n`provider = \"siliconflow-CN\"` selects the China regional default\n`https://api.siliconflow.cn/v1` with the `[providers.siliconflow_cn]` table and\n`SILICONFLOW_API_KEY` credential slot.\nArcee AI defaults to `https://api.arcee.ai/api/v1`, accepts `ARCEE_BASE_URL`,\nand uses `trinity-large-thinking` by default for Codewhale agent work.\n`trinity-large-preview` is also listed as a direct Arcee API model; OpenRouter's\n`arcee-ai/trinity-large-thinking` remains the OpenRouter namespaced form, while\nthe direct Arcee provider uses the bare `trinity-large-thinking` ID. Direct\nArcee large-model API calls are tracked as 256K-context BF16 serving; Thinking\nis reasoning-capable, while Preview is not marked as a thinking model.\n\n### OpenRouter vendor pinning\n\nOpenRouter serves each model through several upstream vendors, and Codewhale\ncan pin requests to a vendor with `[providers.openrouter] vendor` (#6007):\n\n```toml\nprovider = \"openrouter\"\n[providers.openrouter]\nmodel = \"deepseek/deepseek-v4-pro\"\nvendor = \"deepinfra\" # copy the vendor slug from the model's OpenRouter page\n```\n\nThis sends `\"provider\": {\"order\": [\"deepinfra\"], \"allow_fallbacks\": false}`\non OpenRouter requests. A base slug can match multiple endpoint variants;\ncopy a full slug such as `deepinfra/turbo` to select one variant. An unavailable\npin fails at OpenRouter. Codewhale's separate `fallback_providers` setting can\nstill switch the whole route after a recoverable error.\n\nThe pin applies across OpenRouter models, including auxiliary requests on that\nroute. Set `vendor = \"\"` to clear it. Reload config or restart to apply edits;\nrequests already in flight keep their captured route. Other providers do not\ninherit the pin. `/preview-request` shows the primary request's routing fields.\n\nModel strings still pass through verbatim: `:floor` sorts by price, `:nitro`\nsorts by throughput, and `@preset/my-team-preset` references an account preset.\nSee OpenRouter's [provider routing](https://openrouter.ai/docs/guides/routing/provider-selection)\nand [presets](https://openrouter.ai/docs/guides/features/presets) documentation.\nCodewhale does not fetch per-vendor endpoint prices or availability; pinned\nusage reports a routing-dependent unknown cost instead of a catalog estimate.\n\n### Custom OpenAI-Compatible Gateways\n\nFor a single third-party service that implements the OpenAI Chat Completions\nAPI, the simplest setup is the built-in `openai` provider name pointed at the\ngateway:\n\n```toml\nprovider = \"openai\"\ndefault_text_model = \"your-model-id\"\n\n[providers.openai]\napi_key = \"YOUR_OPENAI_COMPATIBLE_API_KEY\"\nbase_url = \"https://your-gateway.example/v1\"\n```\n\nPut the endpoint under `[providers.openai]`, not the legacy top-level\n`base_url`, so the OpenAI-compatible provider receives it. `default_text_model`\nis the model ID sent to the gateway; `[providers.openai].model` can be used as\nthe OpenAI-provider-specific override.\n\nIf you keep several OpenAI-compatible gateways, or need a stable name for an\nAgentProfile provider pin, define a user-named custom provider table:\n\n```toml\nprovider = \"lm-studio\"\n\n[providers.lm-studio]\nkind = \"openai-compatible\"\nbase_url = \"http://127.0.0.1:1234/v1\"\napi_key = \"lm-studio\"\nmodel = \"qwen-2.5-7b\"\n```\n\nCustom provider names may be selected with `provider = \"<name>\"`,\n`--provider <name>`, or an AgentProfile `provider = \"<name>\"` when the matching\n`[providers.<name>]` table exists.\n\nA custom table sends Chat Completions requests by default. Add\n`wire = \"responses\"` to send OpenAI Responses requests to\n`<base_url>/responses` instead, or `wire = \"anthropic\"` for Anthropic Messages:\n\n```toml\n[providers.my-gateway]\nkind = \"openai-compatible\"\nbase_url = \"https://gateway.example/v1\"\napi_key_env = \"MY_GATEWAY_API_KEY\"\nmodel = \"gpt-5.6\"\nwire = \"responses\"\n```\n\nStepFun has a first-class provider entry, so keep Coding Plan credentials and\nbase URL scoped to `[providers.stepfun]`:\n\n```toml\nprovider = \"stepfun\"\n\n[providers.stepfun]\napi_key = \"YOUR_STEPFUN_API_KEY\"\nbase_url = \"https://api.stepfun.ai/step_plan/v1\"\nmodel = \"step-3.7-flash\"\n```\n\n`/provider` setup asks which StepFun billing route the key belongs to —\npay-as-you-go (`https://api.stepfun.ai/v1`) or a Step Plan subscription\n(`https://api.stepfun.ai/step_plan/v1`) — and validates the key against the\nendpoint you pick before saving it. The answer is written to\n`[providers.stepfun].base_url` and nowhere else. If that key already holds a\nbase URL Codewhale does not recognize as one of those two routes, the question\nis skipped and your value is left untouched.\n\nAlibaba Bailian / Model Studio DashScope Qwen routes use the same OpenAI\nprovider shape:\n\n```toml\nprovider = \"openai\"\n\n[providers.openai]\napi_key = \"YOUR_DASHSCOPE_API_KEY\"\nbase_url = \"https://dashscope-intl.aliyuncs.com/compatible-mode/v1\"\nmodel = \"qwen-plus\"\ncontext_window = 1000000\n```\n\nUse the regional DashScope `compatible-mode/v1` base URL that matches the\nregion of your API key. Codewhale keeps `qwen-plus` scoped to the `openai`\nprovider route and does not infer a different provider from the model prefix.\nThe same rule applies to all provider-prefixed model strings: a prefix such as\n`deepseek-ai/...` or `deepseek/...` is a provider-owned wire ID under the\nselected provider, not an automatic switch to the DeepSeek provider.\nSet `context_window` to the gateway/model's real total context window when it\ndiffers from Codewhale's static model metadata. See\n[Context length (context window)](#context-length-context-window) for the full\nresolution order and for how to check which value is in effect.\n\nIf the gateway accepts `POST /chat/completions` but rejects\n`/v1/chat/completions`, set a provider-local `path_suffix`:\n\n```toml\n[providers.openai]\nbase_url = \"https://your-gateway.example/v1\"\npath_suffix = \"/chat/completions\"\n```\n\nThe suffix applies only to chat-completion requests. Model listing and\nDeepSeek beta paths keep their built-in routing so a generic gateway override\ndoes not accidentally rewrite `/models` or `/beta/completions`.\n\nFor private gateways with broken or intercepted certificates, use\n`SSL_CERT_FILE` with a trusted CA bundle. The legacy provider-table key\n`insecure_skip_tls_verify = true` is still parsed so `codewhale doctor` can\nreport stale configs, but provider clients reject it instead of disabling TLS\ncertificate verification.\n\nLocal HTTP endpoints such as Ollama, SGLang, and vLLM are allowed by default\nwhen they use localhost or loopback addresses. For a non-local `http://`\ngateway, launch with `DEEPSEEK_ALLOW_INSECURE_HTTP=1` only on a trusted network:\n\n```bash\nDEEPSEEK_ALLOW_INSECURE_HTTP=1 codewhale\n```\n\nThird-party OpenAI-compatible gateways that need extra request headers can set\n`http_headers = { \"X-Model-Provider-Id\" = \"your-model-provider\" }` at the top\nlevel or under a provider table such as `[providers.deepseek]`. When configured,\ncodewhale sends those custom headers on model API requests. The equivalent\nenvironment override is `DEEPSEEK_HTTP_HEADERS`, using comma-separated\n`name=value` pairs such as\n`X-Model-Provider-Id=your-model-provider,X-Gateway-Route=dev`. `Authorization`\nand `Content-Type` are managed by the client and are not overridden by this\nsetting.\n\n### Vision Model\n\nCodewhale's chat provider and `image_analyze` tool are configured separately.\nThe main chat path remains the selected text/tool provider; image analysis runs\nthrough `[vision_model]` when the `vision_model` feature is enabled.\n\nXiaomi's current image-understanding docs include `mimo-v2.5` for image input.\nTo use MiMo for `image_analyze`, configure the vision model explicitly:\n\n```toml\n[features]\nvision_model = true\n\n[vision_model]\nmodel = \"mimo-v2.5\"\napi_key = \"YOUR_XIAOMI_KEY\"\nbase_url = \"https://api.xiaomimimo.com/v1\"\n```\n\nThe example above uses Xiaomi MiMo's pay-as-you-go OpenAI-compatible endpoint.\nIf you are using a Token Plan key (`tp-...`) for `[vision_model]`, you must set\n`base_url` explicitly because this generic OpenAI-compatible block does not\nauto-select MiMo endpoints. Use\n`https://token-plan-sgp.xiaomimimo.com/v1` for Singapore accounts,\n`https://token-plan-cn.xiaomimimo.com/v1` for China-region accounts, or\n`https://token-plan-ams.xiaomimimo.com/v1` for Europe/Amsterdam accounts.\n\n### Auto Model Routing (`[auto]`, `[auto.router]`)\n\nWith `model = \"auto\"`, each turn runs on your **declared default model** unless\nyou have opted into something else. Auto never guesses a cheaper or stronger\nmodel from how a request is worded; the old keyword-and-length heuristic was\nremoved (`auto_route_declared_fallback` in `crates/engine/src/model_routing.rs`).\nTwo optional layers change that:\n\n- an `[auto.router]` classifier, which you write down, that picks a model per\n turn; and\n- `[auto] cost_saving`, which prefers the active provider's fast sibling.\n\nWith neither set, Auto is local and free: the turn uses the default model and\nno classifier call is made.\n\n**There is no default classifier.** With `[auto.router]` unset, no classifier\ncall happens, whatever keys you hold. Holding a DeepSeek key used to elect\n`deepseek-v4-flash` automatically. That was removed because it spent tokens on\na route the user never chose and privileged one provider over the rest\n(`AutoRouterConfig` in `crates/tui/src/config.rs`). Electing a network\nclassifier is now something you write down.\n\nPoint the classifier at any configured provider with `[auto.router]`:\n\n```toml\n[auto.router]\nprovider = \"zai\"\nmodel = \"glm-5-turbo\"\nthinking = \"off\" # optional; defaults to off\ntimeout_secs = 4 # optional; default 4, 0 = default, capped at 300\n```\n\nA classifier call happens only when `[auto.router]` names both `provider` and\n`model` *and* that provider has a key:\n`router_available = router_configured && has_api_key_for(...)` in\n`ModelInventory::from_config` (`crates/engine/src/model_inventory.rs`). If either\ncondition fails, or the classifier call errors or times out, the local\nfallback decides: the default model, or the fast sibling under `cost_saving`.\nThe turn's route receipt (Turn Inspector, Ctrl+Alt+O or `/turn inspect`, \"Model\nroute + tokens/cost\") records which path was taken, and a\nrouter you configured that cannot run or fails (missing key, HTTP error,\ntimeout, invalid answer) is shown as `Auto router: failing — …` rather than\nsilently ignored.\n\n#### Set up model routing\n\n`/router` (also `/model router`) opens one Router setup view with these\npresets. Each writes only `[auto.router]`; none is ever chosen for you.\n\n| Preset | What it writes | Cost and privacy |\n| --- | --- | --- |\n| `/router jev` | Jev, TypeSafe's decision model, over OpenRouter (`typesafe/jev-1.13`) or TypeSafe direct, whichever key you have | About $0.00002 per turn ($0.042 per million input tokens, output free). Your latest request and up to six recent context lines go to OpenRouter → TypeSafe (or TypeSafe). |\n| `/router fast` | The active provider's runnable fast tier with thinking off | Your existing key; the classifier sees the same request text. |\n| `/router off` | Removes `[auto.router]` | No router call; Auto turns use the default model, or the fast tier while `[auto] cost_saving = true` (Off leaves that setting alone). |\n| `/router custom` | Nothing; prints the TOML to edit | — |\n\nChoosing Jev or Fast makes **one test call** with a fixed sample request and\nshows the tier it picked, the probabilities and confidence, the latency and the\nprovider-reported cost. `Enter` (or `/router save <preset>`) then saves through\nthe normal config writer; `Esc` discards it. TypeSafe paused new signups on\n2026-09-22, so OpenRouter is the default route for new users. A TypeSafe key is\nread from `TYPESAFE_API_KEY`, the `typesafe` secret-store entry, or\n`[providers.typesafe] api_key` / `api_key_env`.\n\n#### Decision routers (`kind = \"decision\"`)\n\nA decision router asks a non-generative decision model typed Choice questions\nfor the active provider's `fast` and `strong` tiers and thinking level, then\nreads calibrated probabilities. No prose is parsed.\n\n```toml\n[auto.router]\nkind = \"decision\" # default \"chat\"\nprovider = \"openrouter\" # or \"typesafe\"\nmodel = \"typesafe/jev-1.13\" # \"~typesafe/jev-latest\" also works; TypeSafe direct: \"jev-latest\"\ntimeout_secs = 2\nmin_confidence = 0.5 # default 0.5, clamped to 0..1\n```\n\n- The router is called only when the active provider has a runnable strong/fast\n pair; otherwise there is no call and no spend.\n- An answer below `min_confidence` takes the local fallback. Under\n `[auto] cost_saving`, a `strong` answer also needs a probability of at least\n 0.75, or the turn stays on the fast tier.\n- An unknown `kind`, or a decision `provider` other than `openrouter` /\n `typesafe`, leaves the router unconfigured and shown as failing.\n- `[auto.router] thinking` is ignored for decision routers. Both routes settle tokens through\n the originating session's routed-usage ledger. TypeSafe is a named Custom route\n with unknown billing; its price is never borrowed from the active chat provider.\n Provider-reported cost remains verbatim on the decision receipt.\n- OpenRouter uses `POST /api/alpha/decisions`; TypeSafe uses `POST /v1/systemone`.\n The shared client validates Choice, Noul and Score against the offered questions\n and bounds responses to 256 KiB. Malformed answers fail open with usage retained.\n\n#### Shadow Decision Gate (experimental, off by default)\n\nThe Superfast Decision Gate asks a decision model three typed questions about\nthe latest user message — does it need a tool, can it be answered from the\nconversation, and what is its intent — and logs a conservative recommendation.\nIt is shadow-only: it never changes routing, never skips or delays the model\ncall, and fails open on any error, timeout or malformed answer. It uses the same\nSystem One client as the decision router above; there is no separate HTTP\nclient. It is configured from the environment and reads it when a turn starts:\n\n```sh\nSUPERFAST_ENABLED=1 # off unless set\nSUPERFAST_PROVIDER=typesafe # or openrouter; required when enabled\nSUPERFAST_BASE_URL=http://localhost:8000/v1 # optional TypeSafe-route base, e.g. self-hosted\nSUPERFAST_MODEL=jev-latest # default jev-latest / ~typesafe/jev-latest\nSUPERFAST_TIMEOUT_MS=150 # 1..=10000, default 150\n```\n\n- Enabling the gate never picks an endpoint by itself: without\n `SUPERFAST_PROVIDER` nothing is sent and a warning is logged.\n- The key comes from the same place the decision router reads it. The TypeSafe\n route always authenticates, so a self-hosted server that ignores auth still\n needs a placeholder `TYPESAFE_API_KEY`.\n- Only the latest user message is sent, truncated to 4,000 characters and\n redacted of configured secrets. The log (target `superfast`) carries the\n route, failure class and latency, never prompt text.\n- The gate retains the originating turn's accounting owner and cancellation.\n Its tokens settle through the shared ledger; missing usage or a cancelled/timed\n out request after dispatch creates an explicit coverage gap. Unknown TypeSafe\n pricing is recorded as unpriced rather than free.\n- Provider-reported cost survives rejected answers and late responses in bounded\n receipts on the originating turn or session. These are diagnostic evidence;\n unknown pricing never becomes an authoritative dollar total. Incomplete token\n counters record a coverage gap while preserving the reported raw evidence.\n\nThe wire contracts are documented in [TypeSafe's OpenAPI schema](https://api.typesafe.ai/openapi.json)\nand [OpenRouter's Decisions examples](https://openrouter.ai/blog/insights/what-is-jev/).\n\nThe Decision Gate concept and reference implementation are by Andrea Bruno,\nreleased under CC BY 4.0:\n[harness-superfast](https://github.com/Andrea-Bruno/harness-superfast).\n\nTwo `[auto]` keys shape routing (`AutoConfig` in `crates/tui/src/config.rs`):\n\n```toml\n[auto]\ncost_saving = false # default false\ncross_provider = false # default false\n```\n\n- **`cost_saving`** (default `false`). Without a classifier, Auto pins the\n active provider's validated fast sibling instead of the default model. A\n provider with no runnable fast sibling stays on the default. With a\n classifier, the classifier is told to prefer the fast tier for routine or\n ambiguous work and to pick the strong tier only for clearly agentic,\n multi-step, architecture, security or debugging work. Cost-saving never\n switches provider just to save money.\n- **`cross_provider`** (default `false`). Auto stays on the provider the session\n is configured to use. The classifier is only shown that provider's models,\n and the fallback never leaves it. Setting `cross_provider = true` lets the\n classifier choose among every runnable provider. There is no interactive\n toggle for `cross_provider`; it has to be set in config. A decision router\n always chooses within the active provider.\n\nTo bootstrap MCP and skills directories at their resolved paths, run `codewhale setup`.\nTo only scaffold MCP, run `codewhale mcp init`.\n\nNote: `setup`, `doctor`, `mcp`, `features`, `sessions`, `resume`/`fork`, `exec`,\n`review`, and `eval` are all available from the installed `codewhale` command.\nThe consolidated dispatcher also provides `auth`, `config`, `model`, `thread`, `sandbox`,\n`app-server`, `mcp-server`, `completions`, `login`/`logout`, `account`,\n`metrics`, `update`, `lane`, `workflow`, and `web`. Plain prompts enter the\nin-process TUI runtime. Release installers expose the same bytes as `codew`.\n\n### Startup Update Checks\n\nBy default, the TUI starts a background check for the latest stable Codewhale\nrelease and shows a short toast only when a newer release is available and the\nofficial release assets are complete. The check never blocks startup, never\nblocks a turn, and fails silently when offline.\n\nDisable the startup check entirely for air-gapped, corporate-proxy, or managed\ndesktop environments:\n\n```toml\n[update]\ncheck_for_updates = false\n```\n\n#### Automatic updates\n\n`mode` decides what happens when a newer release is found (release-binary\ninstalls only; package-manager and desktop-app copies always just notify):\n\n```toml\n[update]\nmode = \"auto\" # auto (default): download in the background, apply at next start\n # notify: tell me only; off: no checks\n```\n\n`codewhale config set update.mode notify` writes it. `CODEWHALE_UPDATE_MODE`\noverrides it, and an organization policy file overrides both; see\n[INSTALL.md](INSTALL.md#automatic-updates) for the full flow, signing, the\npolicy file and `codewhale update --rollback`.\n\n#### Throttling\n\nThe answer is cached in `~/.codewhale/update-check.json` and reused for\n`check_interval_hours` (default `6`). Only the *network request* is throttled —\nthe notice still appears on every launch while an update is outstanding. Set `0`\nto check on every launch.\n\n```toml\n[update]\ncheck_interval_hours = 6\n```\n\nA failed check keeps the last answer (so the notice stays) and retries about an\nhour later rather than on every launch, which keeps offline and rate-limited\nmachines quiet.\n\n#### Automatic suppression\n\nChecks are skipped, without contacting the network, when any of these is set to\na non-falsey value:\n\n| Variable | Why |\n| --- | --- |\n| `CODEWHALE_NO_UPDATE_CHECK` | Explicit opt-out. |\n| `NO_UPDATE_NOTIFIER` | The cross-CLI convention, honored for compatibility. |\n| `CI`, `CONTINUOUS_INTEGRATION`, `GITHUB_ACTIONS`, `GITLAB_CI`, `BUILDKITE`, `CIRCLECI`, `JENKINS_URL`, `TEAMCITY_VERSION`, `TF_BUILD` | Automated build; nobody is at the terminal. |\n\nValues of `\"\"`, `0`, `false`, `no`, and `off` do not count as set, so a\n`CI=false` export does not disable checks for ordinary users.\n\n#### Which update command is offered\n\nCodewhale never installs anything on its own — it only tells you an update\nexists. The command it names depends on how the running binary was installed,\ndetected from its path:\n\n| Install | Command offered |\n| --- | --- |\n| GitHub release binary (including Termux) | `codewhale update` |\n| npm (`node_modules` on the path) | `npm install -g codewhale@latest` |\n| Homebrew (`Cellar` / `linuxbrew` prefix) | `brew upgrade codewhale` |\n| `cargo install` (`~/.cargo/bin`) | `cargo install codewhale-cli --locked --force` |\n\nFor package-managed installs the notice also warns against `codewhale update`:\nreplacing a binary Homebrew or npm owns leaves the manager describing a version\nthat is no longer on disk, and the next upgrade silently reverts you.\n\nOverride the detection with `CODEWHALE_INSTALL_METHOD=npm|homebrew|cargo|binary`\nif you relocated the binary somewhere the path heuristics cannot read.\n\nTo redirect the startup check, set `update_uri` to an internal endpoint that\nreturns GitHub-compatible latest-release JSON. Minimal mirror metadata with a\n`tag_name` field is accepted; if `assets` are present, Codewhale requires the\nsame uploaded asset set as the official release before showing the toast.\n\n```toml\n[update]\ncheck_for_updates = true\nupdate_uri = \"https://internal.mirror.example/codewhale/releases/latest\"\n```\n\nWhen `update_uri` is not set, startup checks honor release mirror environment\nvariables such as `CODEWHALE_RELEASE_BASE_URL` before falling back to the\nofficial GitHub API endpoint. If a configured `update_uri` cannot be fetched or\nparsed and a release mirror env var is set, the TUI falls back to that mirror\ninstead of failing startup.\n\n#### What's new after an update\n\nThe first time Codewhale starts after its version goes up, it says so once and\nlinks that version's release notes on codewhale.net:\n\n```text\nUpdated to 0.10.2. Got 12 features, 31 fixes and 8 other changes.\nRelease notes: https://codewhale.net/en/changelog/0.10.2\n```\n\nThe counts come from the changelog compiled into the binary. Entries under\nAdded or New count as features; Fixed, Security and Latest fixes count as\nfixes; Contributors, Thanks, Community and Known limits or issues are not\ncounted; every other section counts as other changes. The link uses your UI\nlanguage where the site has it (`zh` for Chinese) and English otherwise.\n\nThe TUI shows the notice on the startup card and moves it into the transcript\nwhen the card leaves (at once when the session opens from `--resume`,\n`--continue` or a prompt), adding `/change <version>` for reading the notes in\nplace. The `doctor`, `models`,\n`sessions`, `receipts`, `setup`, `init`, `install`, `features`,\n`integrations`, `mcp`, `config`, `model`, `metrics`, `providers`, `account`,\n`login`, `logout` and `update` commands print it to stderr instead, when\nstdout and stderr are both terminals and no JSON output is requested. `exec`,\nservers, JSON output and non-terminal runs never show it; the next start that\ncan show it does.\n\nNothing is shown on a fresh install or after a downgrade. The version that\nlast started is recorded in `~/.codewhale/last-launch.json`. The notice reads\nonly local files, so it is independent of `check_for_updates`. To turn it off:\n\n```toml\n[update]\nshow_whats_new = false\n```\n\n## Workshop output budgets\n\nBy default an oversized tool result uses bounded spillover: a result\nunder the byte threshold stays inline, a larger one gets a head/tail\npreview plus a session artifact the model can read back. There is no\nsynthesis sub-agent and no per-call `raw = true` escape.\n\n`[workshop] large_output_threshold_tokens` and\n`[workshop.per_tool_thresholds]` (exact model-visible tool names such as\n`bash`) only take effect when the process opts in to adaptive evidence\nrouting with `CODEWHALE_ADAPTIVE_OUTPUT_ROUTING=1`; they set where a result\nstops being inline and becomes handle-only evidence.\n\nTwo optional byte ceilings (#5367) apply either way. They raise the\nmodel-visible floor and never lower it:\n\n- `read_result_max_bytes` — cap for a single `read` / `read_file`\n result. Absent keeps the compile-time defaults (100000 bytes for\n `read`, which has no line cap; 16KiB / 500 lines for `read_file`).\n For `read` this is the middle of a three-layer budget: the model's\n own per-call `max_bytes` (hard maximum 500000) raises the budget for\n one call, this setting raises the floor for the whole process, and\n either way 2MiB is the absolute ceiling. Highest wins; neither layer\n can lower a budget the other granted.\n- `tool_result_max_bytes` — cap for a generic tool result after\n spillover. Absent keeps the 12K-character compact floor (48K on\n windows ≥500K tokens). Hard cap is 2MiB.\n\n## Context length (context window)\n\nAlso called context size, context limit, max context, or window. This is the\ntotal token window Codewhale budgets against, and it drives the header/footer\ncontext percent, the auto-compaction trigger, context-pressure checks, and the\nrequest output cap. If Codewhale compacts at 128K on a model you know serves a\n1M window, this is the setting to change (#5134).\n\n**See what is in effect, and where the value came from.** Every one of these\nprints the resolved window *and* its source:\n\n- `/status` — a `Context window:` row with the percent and token counts, and a\n `Window source:` row naming the provenance and the exact key that overrides\n it.\n- `/config` → Provider — `Context window` (your override, or `(not set)`) and\n `Effective context window` (`1048576 tokens · configured`). Typing\n `context length` in the `/config` filter jumps straight to them.\n- `/context report` — `Window: 1048576 tokens (12.4% used, ...; source: configured)`.\n- `/context json` — machine-readable `context_window_tokens` and\n `context_window_source`.\n\n**Change it** with the provider-table key `context_window`:\n\n```toml\n[providers.moonshot]\ncontext_window = 1048576\n```\n\nor from the CLI:\n\n```bash\ncodewhale config set providers.moonshot.context_window 1048576\ncodewhale config unset providers.moonshot.context_window # back to automatic\n```\n\nUse the table for the provider you are actually on (`providers.openai`,\n`providers.deepseek`, `providers.moonshot`, …); `/status` names it for you. The\nvalue is a positive token count for the route's *total* window.\n\nWhen one gateway fronts models with heterogeneous windows, scope the override\nto an exact wire model id with `[providers.<name>.model_context_windows]`:\n\n```toml\n[providers.command_code]\ncontext_window = 204800\n\n[providers.command_code.model_context_windows]\n\"MiniMaxAI/MiniMax-M2.5\" = 204800\n\"google/gemini-3.1-flash-lite\" = 1000000\n```\n\nKeys are the exact wire model ids the route sends (dotted and `org/model`\nspellings both work as TOML keys when quoted); each value must be a positive\ntoken count. A matching entry beats the provider-level `context_window` for\nthat model only — every other model on the provider still resolves against\n`context_window` and the rungs below. From the CLI:\n\n```bash\ncodewhale config set 'providers.command_code.model_context_windows.\"MiniMaxAI/MiniMax-M2.5\"' 204800\ncodewhale config unset 'providers.command_code.model_context_windows.\"MiniMaxAI/MiniMax-M2.5\"'\n```\n\n### How the effective window is resolved\n\nFirst match wins, and the source label each surface prints is exactly this\nrung:\n\n1. `configured (per-model)` — a `[providers.<name>.model_context_windows]`\n entry keyed by the route's exact wire model id. A hard override for that\n model only; it never rewrites another model's window.\n2. `configured` — `[providers.<name>] context_window` in `config.toml`. A hard\n override for every model on the provider: nothing below it can raise or\n lower the result. Read-time aliases:\n `contextWindow`, `context_window_tokens`, `contextWindowTokens`,\n `context_length`, `contextLength`.\n3. `provider-reported` — route-scoped 1M metadata a provider actually reported\n for the Kimi Code `k3` route, when it was observed within the last 24 hours.\n4. `static Kimi Code safe floor` — 262,144 tokens for Kimi Code memberships,\n because 1M access is plan-gated (Allegretto and above).\n5. `catalog` — the bundled route catalog (hand-curated offerings first, then\n the bundled Models.dev rows). The official `openai-codex` roster supplies\n account-specific model choices, not context-window metadata. Refresh it\n with `codewhale models --update --provider openai-codex`; Codewhale does\n not read `$CODEX_HOME` for this route.\n6. `model-name hint` — an `_Nk` suffix parsed from the model name itself\n (`qwen3-32b-256k` → 256,000), vendor-agnostic. A naming convention the\n serving engine may not honor is not a fact about the route, so this rung\n sits *below* the catalog: any catalog row for the same id beats it (#5441).\n7. `fallback` — the static per-provider capability table: 200,000 for\n Anthropic-wire routes, 128,000 for `openai-codex`, 8,192 for Ollama,\n otherwise Codewhale's static per-model metadata, and finally 128,000 when\n the model is unknown.\n\n### What \"(unverified)\" means\n\nThe `model-name hint` and `fallback` rungs still drive real budgets — the\ncompaction trigger, the context meter, and the output reservation all use the\nnumber — but they are guesses, not capabilities anyone checked. Every surface\nthat renders one of these windows appends `(unverified)` to its source label\n(the status line, the context-pressure message, `/status`, `/config`, and the\nmodel picker chip), so a window you did not configure and no provider reported\ncan never read as a verified limit (#5239, #5441). The `context_window` and\n`model_context_windows` provider-table keys above are the fix: a configured\nwindow is a hard override and renders as `configured` (or\n`configured (per-model)`) with no marker.\n\nOutput ceilings follow the same rule (#5440): an Anthropic-family model the\ncatalog does not describe keeps the 64K Messages floor as its clamp, labeled\n`unverified` (or an \"assumed floor\") instead of `documented`. The official\nChatGPT plan preview does not accept `max_output_tokens`; Codewhale sends no\noutput-token cap on that route. A local request budget is not an upstream\nlimit or a guarantee about the completed response length.\n\nThere is no environment variable for the context window; the provider-table\n`context_window` and per-model `model_context_windows` keys are the user\nknobs. They are the right ones to set when a gateway or self-hosted runtime\nserves a window Codewhale's catalog does not model — per-model when only some\nof a provider's routes differ, provider-wide when they all do. Codewhale will\nnot invent a window it cannot justify — it falls back to a conservative value,\nlabels it `fallback`, and marks it `(unverified)` at every surface that shows\nit.\n\n### Adjacent knobs\n\n- `auto_compact_threshold_percent` (settings.toml; also accepted as\n `auto_compact_threshold`; `10`–`100`, default `80`): the share of the full\n route context window at which auto-compaction fires, clamped so it can never\n cross the spendable input ceiling after output reservation and headroom.\n Editable from `/config`. Raising the window without touching this raises the\n absolute compaction point along with it.\n- `auto_compact` (settings.toml, on/off): turns automatic compaction off\n entirely; `/compact` and Ctrl+L stay available.\n- `[compaction] summary_instructions` and\n `[compaction] retained_user_message_tokens` (config.toml): standing\n summarizer instructions and the verbatim user-message retention budget. See\n the `compaction.*` entry in the config-key reference below.\n- `CODEWHALE_MAX_OUTPUT_TOKENS` (environment variable; legacy alias\n `DEEPSEEK_MAX_OUTPUT_TOKENS`): overrides the requested output cap. Without an\n override, Codewhale starts at the safe `65536` request cap and intersects it\n with any smaller documented model or route ceiling; a catalog `max_output`\n such as DeepSeek V4's 384K remains a capability ceiling, not the amount every\n response requests. Explicit overrides are preserved within the resolved\n route context window and any route output ceiling, and preflight/emergency\n budgeting reserves the same effective value that can reach the wire. A\n separately documented route input ceiling also clamps preflight and\n compaction even when the total context window is larger. A blank canonical\n variable falls through to a nonblank legacy value; a nonblank invalid or\n zero canonical value is authoritative and falls back to the safe automatic\n default instead of activating a stale legacy setting. There is no\n `max_output_tokens` key in `config.toml`.\n\nBefore compaction replaces conversation history, Codewhale durably saves the\noriginal messages to the session's `artifacts/context-transfer-<id>.json` and,\nwhen a model summary is produced, its handoff to the matching `.md` file.\nThese use the existing session artifact store and persistence redaction.\nA failed write aborts compaction without replacing context. Pruning-only passes\nsave the original messages without making an extra model call. The model can\nread them back through `context_archive_search` (ASCII case-insensitive\nsubstring search returning short snippets and an archive id) and\n`context_archive_get` (paged read of one message by id); the handoff note names\nboth so the model knows the archive exists. Pressure metadata\nshows estimated input tokens and the configured trigger; it is an estimate, not\nan exact promise about a provider's remaining context.\n\nCompaction history is available through `codewhale metrics` (or `--json`) and\n`audit.log` in the Codewhale home. Completed passes record their trigger,\nsummary/pruning path, message and estimated-token counts, effective threshold,\nand summarizer token usage. Automatic refusals are recorded once per turn with\ntheir reason. These are local diagnostics, not provider invoice totals; earlier\nartifacts are not retroactively counted. A text-mode `exec` that attempts\ncompaction saves its owning session at turn completion so its recovery artifacts\nremain discoverable. A killed process may leave artifacts without that final\nsession snapshot; the audit writer reports I/O failures instead of inventing data.\n\nSee [Settings File](#settings-file-persistent-ui-preferences) for the\ncompaction settings and [Token Quantities and\nDrivers](#token-quantities-and-drivers) for what each displayed token number\nactually measures.\n\n## Profiles\n\nYou can define multiple profiles in the same file:\n\n```toml\ndefault_text_model = \"deepseek-flash\"\n\n[providers.deepseek]\napi_key = \"PERSONAL_KEY\"\n\n[profiles.work.providers.deepseek]\napi_key = \"WORK_KEY\"\nbase_url = \"https://api.deepseek.com/beta\"\n\n[profiles.nvidia-nim]\nprovider = \"nvidia-nim\"\ndefault_text_model = \"deepseek-ai/deepseek-v4-pro\"\n\n[profiles.nvidia-nim.providers.nvidia_nim]\napi_key = \"NVIDIA_KEY\"\nbase_url = \"https://integrate.api.nvidia.com/v1\"\n\n[profiles.fireworks]\nprovider = \"fireworks\"\ndefault_text_model = \"accounts/fireworks/models/deepseek-v4-pro\"\n\n[profiles.siliconflow]\nprovider = \"siliconflow\"\ndefault_text_model = \"deepseek-ai/DeepSeek-V4-Pro\"\n\n[profiles.siliconflow.providers.siliconflow]\nbase_url = \"https://api.siliconflow.com/v1\"\n\n[profiles.openai-compatible]\nprovider = \"openai\"\n\n[profiles.openai-compatible.providers.openai]\nbase_url = \"https://openai-compatible.example/v4\"\nmodel = \"glm-5\"\n\n[profiles.atlascloud]\nprovider = \"atlascloud\"\n\n[profiles.atlascloud.providers.atlascloud]\nbase_url = \"https://api.atlascloud.ai/v1\"\nmodel = \"deepseek-ai/deepseek-v4-flash\"\n\n[profiles.sglang]\nprovider = \"sglang\"\n\n[profiles.sglang.providers.sglang]\nbase_url = \"http://localhost:30000/v1\"\nmodel = \"deepseek-ai/DeepSeek-V4-Pro\"\n\n[profiles.vllm]\nprovider = \"vllm\"\n\n[profiles.vllm.providers.vllm]\nbase_url = \"http://localhost:8000/v1\"\nmodel = \"deepseek-ai/DeepSeek-V4-Pro\"\n\n[profiles.ollama]\nprovider = \"ollama\"\n\n[profiles.ollama.providers.ollama]\nbase_url = \"http://localhost:11434/v1\"\nmodel = \"qwen2.5-coder:7b\" # an installed chat-capable tag\n\n[profiles.ollama-cloud]\nprovider = \"ollama-cloud\"\n\n[profiles.ollama-cloud.providers.ollama_cloud]\nbase_url = \"https://ollama.com/v1\"\nmodel = \"gpt-oss:120b\"\n```\n\nSelect a profile with:\n\n- CLI: `codewhale --profile work`\n- Env: `DEEPSEEK_PROFILE=work`\n\nIf a profile is selected but missing, codewhale exits with an error listing available profiles.\n\nFor local Ollama, omit `providers.ollama.model` to select a tag whose native\ncatalog confirms completion support. `/provider ollama`, exec, and Runtime API\ndefault-model admission refresh that catalog; embedding-only catalogs have no\nautomatic chat default. An explicit model keeps its exact tag.\n\nA loaded model's `/api/ps` context observation is usable for 60 seconds.\n`/api/show` reports the trained maximum, which does not establish the served\nwindow. An unloaded model or an expired observation uses the unverified 8192-token\nfallback; explicit context-window configuration still takes precedence.\n\n## Legacy top-level `base_url` and `api_key`\n\nOlder releases kept DeepSeek's endpoint and key at the top of `config.toml`,\nand each reader decided for itself which other routes inherited them. They\nnow live in provider tables only. An older file keeps working unchanged:\nevery load reads the top-level keys as if they were already in their table,\nby one rule, per file and per `[profiles.<name>]`:\n\n1. `provider = \"custom\"` with no `[providers.custom]` table: the endpoint, key\n and a copy of the model become `[providers.custom]`.\n2. An endpoint on another vendor's official host (for example\n `integrate.api.nvidia.com`, `xiaomimimo.com`, `openrouter.ai`, or the\n ChatGPT Codex endpoint) belongs to that vendor's table, so a DeepSeek route\n never sends requests there.\n3. Anything else belongs to `[providers.deepseek]`, which DeepSeek-CN also\n reads for its endpoint and key.\n\nThe key goes with DeepSeek (or the literal custom route). It follows the\nendpoint to another vendor only when the same table sets `provider` to that\nvendor, the host is that vendor's own, and the vendor's table has no key. When\nno `provider` is set, a NIM host still selects `nvidia-nim` and\n`api.deepseeki.com` still selects `deepseek-cn`. A `[vision_model]` without a\nkey of its own keeps using the top-level key. A profile's top-level\n`base_url` now overrides the base file's `[providers.deepseek] base_url`\n(before, the base table silently won).\n\nWhen a top-level value and its table disagree, the table's `base_url` and the\ntop-level `api_key` are used, which is what the runtime sent before.\n\nLoading never rewrites the file. The next save Codewhale makes (for example\n`codewhale config set`, `/config ... --save`, or `auth set`) moves the keys,\nkeeps comments, writes a one-time credential-free copy of the old file to\n`config.toml.pre-migrate.bak`, and prints one line saying what moved. A\ndisagreeing pair is never resolved in the file on its own; both values stay\nuntil you choose, and `codewhale config doctor` / `codewhale doctor` report\nit (sources only, never values). `codewhale config get|set|unset base_url`\n(and `api_key`) address the active provider's table.\n\nTo tidy the file yourself:\n\n```bash\ncodewhale config migrate --dry-run # show what would move\ncodewhale config migrate # move it (backup first)\ncodewhale config migrate --prefer top-level # resolve a conflict: keep the top-level value\ncodewhale config migrate --prefer table # resolve a conflict: keep the table value\n```\n\n## Environment Variables\n\nMost runtime environment variables override config values. API-key variables are\nfallbacks after saved config and keyring credentials.\n\nThe three user-facing slots — provider, model, base URL — expose `CODEWHALE_*`\naliases. When both forms are set the `CODEWHALE_*` value wins; the\n`DEEPSEEK_*` form is kept for older shells:\n\n- `CODEWHALE_PROVIDER` (preferred) / `DEEPSEEK_PROVIDER` (legacy alias) —\n `deepseek|deepseek-anthropic|nvidia-nim|openai|atlascloud|wanjie-ark|volcengine|openrouter|xiaomi-mimo|novita|fireworks|siliconflow|arcee|siliconflow-CN|moonshot|sglang|vllm|ollama|ollama-cloud|huggingface|modelscope|together|qianfan|openai-codex|anthropic|openmodel|zai|stepfun|minimax|deepinfra|mistral`\n- `CODEWHALE_MODEL` (preferred) / `DEEPSEEK_MODEL` (legacy alias) — default model for the active provider\n- `CODEWHALE_BASE_URL` (preferred) / `DEEPSEEK_BASE_URL` (legacy alias) — base URL for the active provider\n\n`CODEWHALE_BASE_URL` applies to the **active** route only. A request pinned to\nanother provider — a subagent or fleet child, a routed tool, the per-turn\nauto-router, a picker preview — resolves its endpoint from that provider's own\n`[providers.<table>]`, then its provider-scoped variable (`MOONSHOT_BASE_URL`,\n`OPENAI_BASE_URL`, …), then that provider's default. It never inherits the\nactive session's host, and a custom route with no configured `base_url` fails\nclosed on a loopback placeholder rather than borrowing another provider's\nendpoint. The environment writes the value into the active identity's own\ntable: DeepSeek-CN falls back to `[providers.deepseek]` for its endpoint and\nkey, but never to a DeepSeek value the environment addressed to DeepSeek alone.\nA managed-config overlay that supplies or reselects the effective\nroute's endpoint takes the generic override away from every route.\n\nRemaining variables:\n\n- `CODEWHALE_LOCAL_PROXY_FALLBACK` (`0`) — keeps reqwest's build-time proxy snapshot; the loopback fallback is on by default. Read once when the first client is built.\n- `DEEPSEEK_API_KEY`\n- `DEEPSEEK_ANTHROPIC_BASE_URL`\n- `DEEPSEEK_HTTP_HEADERS` (custom model request headers, comma-separated `name=value` pairs)\n- `DEEPSEEK_DEFAULT_TEXT_MODEL` (extra legacy alias of `DEEPSEEK_MODEL`)\n- `DEEPSEEK_STREAM_IDLE_TIMEOUT_SECS` (stream idle timeout in seconds; default `900`, clamped to `1..=3600`)\n- `DEEPSEEK_STREAM_OPEN_TIMEOUT_SECS` (connection setup + response-header wait in seconds; default `45`, clamped to `5..=300`; distinct from the per-chunk idle timeout; `stream.open_timeout_secs` (legacy `tui.stream_open_timeout_secs` fallback) wins when positive)\n- `CODEWHALE_CACHE_MAXIMAL` (`1`/`true`/`on`/`yes`) — cache-maximal context mode (#528). When on, the Repo Working Set block materializes the **full current contents** of the top active files into the system prompt each turn (deterministic order, byte-bounded), instead of only listing their paths. The block stays byte-stable while those files are unchanged so DeepSeek's KV prefix cache keeps hitting; editing a file cache-misses from its block onward. Off by default (path list only). Byte caps default to 24 KB per file / 96 KB total.\n- `NVIDIA_API_KEY` or `NVIDIA_NIM_API_KEY` (when provider is `nvidia-nim`)\n- `NVIDIA_NIM_BASE_URL`, `NIM_BASE_URL`, or `NVIDIA_BASE_URL`\n- `NVIDIA_NIM_MODEL`\n- `OPENAI_API_KEY`\n- `OPENAI_BASE_URL`\n- `OPENAI_MODEL`\n- `ATLASCLOUD_API_KEY`\n- `ATLASCLOUD_BASE_URL`\n- `ATLASCLOUD_MODEL`\n- `WANJIE_ARK_API_KEY`, `WANJIE_API_KEY`, or `WANJIE_MAAS_API_KEY`\n- `WANJIE_ARK_BASE_URL`, `WANJIE_BASE_URL`, or `WANJIE_MAAS_BASE_URL`\n- `WANJIE_ARK_MODEL`, `WANJIE_MODEL`, or `WANJIE_MAAS_MODEL`\n- `VOLCENGINE_API_KEY`, `VOLCENGINE_ARK_API_KEY`, or `ARK_API_KEY`\n- `VOLCENGINE_BASE_URL`, `VOLCENGINE_ARK_BASE_URL`, or `ARK_BASE_URL`\n- `VOLCENGINE_MODEL` or `VOLCENGINE_ARK_MODEL`\n- `OPENROUTER_API_KEY`\n- `OPENROUTER_BASE_URL`\n- `OPENROUTER_MODEL`\n- `XIAOMI_MIMO_TOKEN_PLAN_API_KEY`, `MIMO_TOKEN_PLAN_API_KEY`, `XIAOMI_MIMO_API_KEY`, `XIAOMI_API_KEY`, or `MIMO_API_KEY`\n- `XIAOMI_MIMO_BASE_URL` or `MIMO_BASE_URL`\n- `XIAOMI_MIMO_MODEL` or `MIMO_MODEL`\n- `XIAOMI_MIMO_MODE` or `MIMO_MODE` (`token-plan-sgp`, `token-plan-cn`,\n `token-plan-ams`, or `pay-as-you-go`)\n- `NOVITA_API_KEY`\n- `NOVITA_BASE_URL`\n- `NOVITA_MODEL`\n- `FIREWORKS_API_KEY`\n- `FIREWORKS_BASE_URL`\n- `FIREWORKS_MODEL`\n- `HUGGINGFACE_API_KEY` or `HF_TOKEN` (`HF_TOKEN` is a fallback alias accepted when provider is `huggingface`)\n- `MODELSCOPE_API_KEY`\n- `HUGGINGFACE_BASE_URL` or `HF_BASE_URL`\n- `HUGGINGFACE_MODEL` or `HF_MODEL`\n- `SILICONFLOW_API_KEY`\n- `SILICONFLOW_BASE_URL`\n- `SILICONFLOW_MODEL`\n- `ARCEE_API_KEY`\n- `ARCEE_BASE_URL`\n- `ARCEE_MODEL`\n- `TOGETHER_API_KEY`\n- `TOGETHER_BASE_URL`\n- `TOGETHER_MODEL`\n- `QIANFAN_API_KEY` or `BAIDU_QIANFAN_API_KEY`\n- `QIANFAN_BASE_URL` or `BAIDU_QIANFAN_BASE_URL`\n- `QIANFAN_MODEL` or `BAIDU_QIANFAN_MODEL`\n- `OPENAI_CODEX_ACCESS_TOKEN` or `CODEX_ACCESS_TOKEN` (legacy process tokens; ignored by the official ChatGPT plan route)\n- `OPENAI_CODEX_BASE_URL` or `CODEX_BASE_URL`\n- `OPENAI_CODEX_MODEL` or `CODEX_MODEL`\n- `OPENAI_CODEX_ACCOUNT_ID` or `CODEX_ACCOUNT_ID` (legacy identity hints; ignored by the official ChatGPT plan route)\n- `CODEWHALE_CHATGPT_NEW_ACCOUNT=1` explicitly replaces the single selected ChatGPT registration after a validated browser sign-in\n- `ANTHROPIC_API_KEY`\n- `ANTHROPIC_BASE_URL`\n- `ANTHROPIC_MODEL`\n- `ZAI_API_KEY` or `Z_AI_API_KEY`\n- `ZAI_BASE_URL` or `Z_AI_BASE_URL`\n- `ZAI_MODEL` or `Z_AI_MODEL`\n- `STEPFUN_API_KEY` or `STEP_API_KEY`\n- `STEPFUN_BASE_URL` or `STEP_BASE_URL`\n- `STEPFUN_MODEL` or `STEP_MODEL`\n- `MINIMAX_API_KEY`\n- `MINIMAX_BASE_URL`\n- `MINIMAX_MODEL`\n- `DEEPINFRA_API_KEY` or `DEEPINFRA_TOKEN`\n- `DEEPINFRA_BASE_URL`\n- `DEEPINFRA_MODEL`\n- `MISTRAL_API_KEY`\n- `MISTRAL_BASE_URL`\n- `MISTRAL_MODEL`\n- `MOONSHOT_API_KEY` or `KIMI_API_KEY`\n- `MOONSHOT_BASE_URL` or `KIMI_BASE_URL`\n- `MOONSHOT_MODEL`, `KIMI_MODEL_NAME`, or `KIMI_MODEL`\n- `SGLANG_BASE_URL`\n- `SGLANG_MODEL`\n- `SGLANG_API_KEY` (optional; many localhost SGLang servers do not require auth)\n- `VLLM_BASE_URL`\n- `VLLM_MODEL`\n- `VLLM_API_KEY` (optional; many localhost vLLM servers do not require auth)\n- `OLLAMA_BASE_URL`\n- `OLLAMA_MODEL`\n- `OLLAMA_API_KEY` (optional; many localhost Ollama servers do not require auth)\n- `OLLAMA_CLOUD_BASE_URL`\n- `OLLAMA_CLOUD_MODEL`\n- `OLLAMA_CLOUD_API_KEY` (preferred Cloud key; `OLLAMA_API_KEY` is the official fallback)\nFor every product-level `CODEWHALE_*` variable below, the matching legacy\n`DEEPSEEK_*` name is still read as a compatibility fallback; when both are set,\nthe `CODEWHALE_*` value wins.\n\n- `CODEWHALE_TIMING=1` emits local JSON phase durations on stderr for startup, model preparation, tool planning, approval waits and postprocessing. Nested intervals overlap. The records contain only phase, process ID, end time and elapsed microseconds; stream-json stdout is unchanged.\n- `CODEWHALE_LOG_LEVEL` or `RUST_LOG` (`info`/`debug`/`trace` enables lightweight verbose logs)\n- `CODEWHALE_SKILLS_DIR`\n- `CODEWHALE_MCP_CONFIG`\n- `CODEWHALE_NOTES_PATH`\n- `CODEWHALE_MEMORY` (`1|on|true|yes|y|enabled` turns user memory on)\n- `CODEWHALE_MEMORY_PATH`\n- `CODEWHALE_TELEMETRY` / `DEEPSEEK_TELEMETRY` (legacy alias) — anonymous usage\n counting is on by default in the current 0.9.12 source, with a disclosure\n naming Codewhale and PostHog and an easy durable opt-out. Prior explicit\n declines remain off. Accepts `0|1|true|false|yes|no|on|off|enabled|\n disabled`. An explicit \"off\" is a **floor**: it beats `--telemetry true` and\n `telemetry = true` in config, and a value this list cannot read also resolves\n to off, because a typo in a kill switch must never resolve to \"on\". See\n [`TELEMETRY.md`](TELEMETRY.md).\n- `CODEWHALE_TELEMETRY_ENDPOINT` / `DEEPSEEK_TELEMETRY_ENDPOINT` (legacy alias)\n — `https://`, or plain `http://` only for loopback. Overrides the config file.\n Unset selects the shipped default,\n `https://telemetry.codewhale.net/v1/telemetry`; setting it to the **empty\n string** routes batches to a local dry-run file and contacts nobody. Either\n way it only decides where a session sends — it cannot override an opt-out.\n- `CODEWHALE_ALLOW_SHELL` (`1`/`true` enables)\n- `CODEWHALE_APPROVAL_POLICY` (`on-request|untrusted|never`)\n- `CODEWHALE_SANDBOX_MODE` (`read-only|workspace-write|danger-full-access|external-sandbox`)\n- `CODEWHALE_NO_NEW_PRIVS` (`0`/`false`/`no`/`off`/`disabled` opts out) — Linux only. The\n TUI process sets the kernel's irreversible no-new-privileges flag at startup\n as defense-in-depth, which blocks `sudo`/`su`/setuid helpers for Codewhale's\n whole process tree. The flag is already skipped when the startup sandbox\n mode resolves to `danger-full-access` (#5723), so this variable is the\n explicit override in the remaining cases: set it to a falsey value before\n launching if you administer through Codewhale as a wheel-group user under a\n narrower posture and need escalation to work (#5413), or set a truthy value\n to force the flag on even under `danger-full-access`. Unset, the posture\n decides; the other startup hardening (no ptrace, no core dumps) always\n stays on.\n- `CODEWHALE_MANAGED_CONFIG_PATH`\n- `CODEWHALE_REQUIREMENTS_PATH`\n- `CODEWHALE_MAX_SUBAGENTS` (clamped to `1..=128`)\n- `CODEWHALE_TASKS_DIR` (runtime task queue/artifact storage, default\n `~/.codewhale/tasks`, with legacy `~/.deepseek/tasks` fallback when only the\n legacy directory exists)\n- `CODEWHALE_RUNTIME_DIR` (override the runtime thread store root). Interactive\n sessions default to `$CODEWHALE_HOME/sessions/<session-id>/runtime` so each\n Codewhale process owns its own store (#5630). The store is single-owner: a\n second process on the **same** root fails at startup. Set this variable to\n share one store across processes, or when the runtime API server should use a\n stable non-session path. Unset, the API/server path remains\n `$CODEWHALE_HOME/tasks/runtime`. Legacy alias: `DEEPSEEK_RUNTIME_DIR`.\n- `CODEWHALE_ALLOW_INSECURE_HTTP` (`1`/`true` allows non-local `http://` base URLs; default is reject)\n- `CODEWHALE_FORCE_HTTP1` (`1|true|yes|on` pins the HTTP client to HTTP/1.1, disabling HTTP/2; useful on Windows or behind proxies that mishandle long-lived H2 streams)\n- `CODEWHALE_HOME` (override the base data directory; defaults to `~/.codewhale`).\n If you previously exported `DEEPSEEK_HOME`, rename it to `CODEWHALE_HOME`;\n the old env var is not used for new Codewhale state paths.\n- `CODEWHALE_RELEASE_BASE_URL` (release asset mirror used by `codewhale update`\n and by TUI startup update checks when `[update].update_uri` is not set, or as\n a fallback when that configured URI cannot be fetched)\n- `CODEWHALE_AUTOMATIONS_DIR` (override the automations storage directory; uses\n `~/.codewhale/automations` by default, with legacy `~/.deepseek/automations`\n fallback when only the legacy directory exists)\n- `NO_ANIMATIONS` (`1|true|yes|on` forces `low_motion = true` and\n `fancy_animations = false` at startup, regardless of the saved\n settings; see [`docs/ACCESSIBILITY.md`](./ACCESSIBILITY.md)).\n- `SSL_CERT_FILE` — corporate-proxy / TLS-inspecting MITM users\n point this at a PEM bundle (or single DER cert) and the cert(s)\n get added alongside the platform's system trust store. Failures\n log a warning and continue — the existing system roots still\n apply.\n\n### Instruction sources (`instructions = [...]`, #454)\n\nAdd a list of additional system-prompt sources that get\nconcatenated, in declared order, alongside the auto-loaded\n`AGENTS.md`:\n\n```toml\ninstructions = [\n \"./AGENTS.md\",\n \"~/.codewhale/global.md\",\n \"~/team/agents-shared.md\",\n]\n```\n\nRules:\n\n- Paths run through `expand_path` so `~` and env vars work.\n- These entries are **global-scope** instruction authority: they are charged\n to the global budget together with the global `AGENTS.md`/`instructions.md`\n fallbacks and `~/.claude/CLAUDE.md`.\n- A file keeps at most its first 100 KiB. A file larger than\n `[instruction_budget] file_max_bytes` (8 MiB by default) is skipped with a\n notice. Missing and empty files are skipped with a notice so a stale entry\n doesn't fail the launch.\n- Only user-owned config, profiles, and managed config may set this array.\n Project config (`<workspace>/.codewhale/config.toml`, or legacy\n `<workspace>/.deepseek/config.toml`) ignores `instructions` so a cloned repo\n cannot choose arbitrary local files to place into the prompt.\n\n### Instruction budget (`[instruction_budget]`, #6526)\n\nStanding instruction files are assembled into two independent scopes and\nbudgeted in **estimated tokens** — the same conservative estimator `/context`\nuses, never a `bytes/4` shortcut, which underestimates Cyrillic by 30–50%.\n\n- **global** — `~/.codewhale/AGENTS.md` (or the `.agents`/`.deepseek`\n fallbacks), the global `instructions.md` fallbacks, `~/.claude/CLAUDE.md`\n as a last resort when `claude_fallback` is on, and the\n `instructions = [...]` array.\n- **project** — the repository-root → workspace `AGENTS.md` chain, co-read\n foreign files, and the `.codewhale/rules`/`.claude/rules` block.\n\nEach scope is capped at `min(absolute_tokens, percent × usable_window)`. The\nusable window is the route window minus the output reservation the request\nwill actually carry and a safety reserve of 10% of the window (minimum 2,048\ntokens). A route whose context window is unknown has no usable window: the\npercentage ceilings and the guard are reported as `unknown` and only the\nabsolute caps apply.\n\n```toml\n[instruction_budget]\nglobal_budget_tokens = 100000\nproject_budget_tokens = 100000\nglobal_budget_percent = 25\nproject_budget_percent = 25\nstatic_prefix_guard_percent = 80\nfile_max_bytes = 8388608\npolicy = \"trim\"\nclaude_fallback = false\n```\n\nWith a 64K (65,536-token) output reservation:\n\n| window | usable | per scope | both scopes | guard 80% |\n| --- | --- | --- | --- | --- |\n| 128,000 | 49,664 | 12,416 | 24,832 | 39,731 |\n| 1,000,000 | 834,464 | 100,000 (absolute cap) | 200,000 | 667,571 |\n\nBehaviour:\n\n- **Nothing is trimmed while a source is within its scope budget.**\n- A scope over budget drops whole files from the broadest end first — the\n root-most ancestor goes first, the workspace file last — and a single file\n that alone exceeds the remaining budget keeps its **head** and drops its\n tail with a marker carrying bytes and estimated tokens.\n- Every instruction, rules, and configured file retains at most its first\n 100 KiB of text, ending in a marker that names the bytes kept and dropped.\n- Rules are charged to the project scope after the instruction chain and keep\n a head-first trim.\n- `file_max_bytes` is a hard ceiling that can only be lowered from its 8 MiB\n default: a larger file is **skipped**, never trimmed, and the read is\n bounded on the open file, so a FIFO or a growing file cannot exhaust memory.\n The same applies to a whitespace-only file and to files beyond the\n 50-per-directory rules cap.\n- `policy = \"trim\"` (default) trims and reports. `policy = \"refuse\"` stops the\n request before dispatch when a scope budget, the rules budget, or the\n 100 KiB retention would trim, or when the request prefix exceeds the guard;\n skips stay skips, because one bad file must not make the project unopenable.\n- `static_prefix_guard_percent` caps the request prefix: every system block\n plus the tool schemas the request carries. When it trips, the two scopes are\n narrowed largest first until the prefix fits, only the installed instruction\n block is replaced, and the change is recorded as a declared prefix change.\n If the prefix still exceeds the guard afterwards, the residual excess is\n reported rather than looped on. The guard must leave at least 10 points\n above the two scope percents. Prompts owned by a host (child agents,\n isolated chat, RLM) are measured and can be refused, but are never trimmed.\n- `claude_fallback = true` (opt in; default `false`): a directory with no\n `AGENTS.md` (or codewhale `instructions.md`) uses its `CLAUDE.md`. A skipped\n `AGENTS.md` counts as absent; a file already loaded from a wider scope (a\n symlink) is a reported duplicate, not a fallback trigger. Other tools'\n instruction files are never imported unless you opt in here or through\n `project_instruction_imports`.\n- Every loss — trimmed, skipped, refused — reaches the model as one\n runtime-provenance message whenever the list changes, and appears per source\n in `/context report`. `codewhale doctor`'s report is a preview built from the\n current files and config; it does not claim a request passed the tool-schema\n guard.\n- The table is **user config only**, like `instructions`: project config is\n ignored and a cloned repository cannot shrink the global budget or force\n `refuse`. An invalid table (unknown key, zero budget, percent outside\n 1–100, guard without room left, `file_max_bytes` outside 1–8388608) fails\n the launch.\n\n### Hooks\n\nHooks are a **TUI runtime feature**. They fire from the interactive TUI and\nthe engine turn loop it drives; `codewhale exec`, the CLI subcommands, the\napp-server / ACP surfaces, and the `workflow` tool do not fire them.\n\n[`docs/HOOKS.md`](HOOKS.md) is the authoritative reference for all eleven hook\nevents — their firing points, environment variables, stdin payloads, timeout\nand background semantics, and which three of them can steer Codewhale. The\nsections below cover the configuration surface and the steering contracts in\nmore depth.\n\nTwo contract points worth reading there before writing a hook:\n\n- `background = true` means **submitted and never awaited**. The hook still\n gets the documented stdin payload and the same timeout, but it has no exit\n code and cannot steer.\n- A condition that references context its event never carries (an `exit_code`\n condition outside `tool_call_after` / `on_error`, a `mode` condition on\n `shell_env`, a tool condition on a non-tool event) is **rejected at load**,\n logged, and shown in `/hooks list`. It does not silently never match.\n Rejection is per entry, so a broken hook never drops another one that merely\n shares its `name` or is likewise unnamed.\n\n### `/hooks` listing\n\nRun `/hooks` (or `/hooks list`) inside the TUI to see every\nconfigured lifecycle hook grouped by event, including each\nhook's name, command preview, effective timeout, and condition. When\n`[hooks].default_timeout_secs` is set it replaces every per-hook\n`timeout_secs`, and the listing shows that effective value and names the\noverride rather than echoing the per-hook number. A\n`default_timeout_secs = 0` is rejected at load — it would expire every hook\nin the config immediately — so the override is ignored, per-hook\n`timeout_secs` applies, the listing shows that per-hook value with no\noverride provenance, and the rejection appears under `configuration\nproblems`. The\n`[hooks].enabled` flag's state is shown at the top so it's\nobvious when hooks are globally suppressed, and any entry rejected\nat load is listed under `configuration problems` with the reason.\nHooks are configured under `[[hooks.hooks]]` entries — see\n[`docs/HOOKS.md`](HOOKS.md) for the full schema.\n\n### Mutable `message_submit` hooks\n\n`message_submit` hooks run before a submitted message is added to\nhistory or sent to the model. Unlike observer-only lifecycle hooks,\nnon-background `message_submit` hooks can replace or block the\nsubmitted text.\n\n```toml\n[[hooks.hooks]]\nevent = \"message_submit\"\ncommand = \"~/.codewhale/hooks/inject-context.sh\"\ntimeout_secs = 2\ncontinue_on_error = true\n```\n\nThe hook receives JSON on stdin:\n\n```json\n{\n \"event\": \"message_submit\",\n \"text\": \"original user text\",\n \"text_bytes\": 18,\n \"text_original_bytes\": 18,\n \"text_truncated\": false,\n \"session_id\": \"sess_12345678\",\n \"workspace\": \"/path/to/workspace\",\n \"mode\": \"agent\",\n \"model\": \"deepseek-chat\",\n \"total_tokens\": 1234\n}\n```\n\nThe entire serialized document is capped at 32 KiB. Codewhale retains the\nlargest UTF-8-safe `text` prefix that fits after JSON escaping and bounded\nmetadata, and the three `text_*` fields make truncation explicit. Immediate\nmessages, restored queue entries, merged steers, and prior-hook replacements\nall cross this same serialization boundary.\n\nIf the hook exits `0` and prints JSON with a non-empty string `text` field,\nthat value replaces the submitted text:\n\n```json\n{ \"text\": \"replacement user text\" }\n```\n\nExit `0` with empty stdout, or stdout JSON without `text`, leaves\nthe current text unchanged. A JSON `text` field must not be empty;\n`{\"text\":\"\"}` is treated as invalid stdout and ignored. Exit `2`\nblocks the submission before the turn starts; a structured `reason` field can\nprovide the bounded, redacted status message shown in the TUI. Raw stdout,\nstderr, and process-error text are not copied into denial receipts.\nOther non-zero exits follow the hook's `continue_on_error` setting.\nTimeouts and spawn failures are also surfaced as transient TUI status\nmessages when `continue_on_error = true` lets submission continue.\n\nMultiple `message_submit` hooks run in config order, and each hook\nreceives the text produced by the previous hook. Hooks marked\n`background = true` are observer-only and cannot transform or block\nthe message — they still receive the same stdin payload and the same\nenvironment, they are simply never awaited. Existing environment\nvariables remain available.\n`shell_env` hooks keep their existing `KEY=VALUE` stdout contract;\nJSON stdout contracts exist for `message_submit` (above) and\n`tool_call_before` (below).\n\n### `tool_call_before` decision hooks\n\n`tool_call_before` hooks run before each tool call executes. In\naddition to the legacy hard deny (exit code `2`, which always wins\nregardless of stdout), a foreground hook may print a JSON decision on\nstdout with exit code `0`:\n\n```json\n{\n \"decision\": \"allow\" | \"deny\" | \"ask\",\n \"reason\": \"human-readable explanation (used for deny)\",\n \"updatedInput\": { \"command\": \"ls -la\" },\n \"additionalContext\": \"text appended to the tool result for the model\"\n}\n```\n\nAll fields are optional. Empty stdout, non-JSON stdout, and JSON\nwithout a `decision` field behave exactly as before (allow). An\nunrecognized `decision` string logs a fixed warning without echoing the\nuntrusted value and is treated as allow.\n\n- `deny` blocks the tool; the model receives a permission-denied tool\n result containing `reason`.\n- `ask` forces the interactive approval prompt in Ask and Auto-Review even for\n tools that would otherwise auto-run. Full Access does not open tool-approval\n prompts, so hook `ask` does not downgrade that posture.\n- `updatedInput` must be a JSON object; it replaces the tool input\n before execution. When several hooks supply it, the last hook wins.\n- `additionalContext` is appended to the tool result sent back to the\n model as `[hook context] ...`. Multiple hooks' contexts are\n concatenated.\n\nWhen multiple hooks match, precedence is deny > ask > allow. Hooks\nmarked `background = true` cannot steer tool calls — they are\nsubmitted and never awaited, so they have no verdict to contribute.\n\nA foreground hook that produced no verdict at all — it hit its timeout, the\nprocess could not be started, or a strict process exited non-zero without an\nexplicit JSON decision — is not treated as permission. If\n*that* hook is configured with `continue_on_error = false`, the outcome\ndenies the tool call and the denial names the hook and a bounded reason.\nStrictness is read off the hooks that actually matched this call, so a\nstrict gate scoped to another tool cannot deny it, and a lenient hook's\ntimeout does not deny merely because a strict hook exists elsewhere in\nconfig. Under the default `continue_on_error = true` the outcome is\nlogged and the call proceeds.\n\n`reason` and `additionalContext` are capped (2 000 characters per field,\n8 000 for the concatenated context of one call) and stripped of control\ncharacters before they reach the TUI or the model.\n\nExample deny hook:\n\n```toml\n[[hooks.hooks]]\nevent = \"tool_call_before\"\ncommand = '''echo '{\"decision\":\"deny\",\"reason\":\"blocked by project policy\"}' '''\ncondition = { type = \"tool_name\", name = \"exec_shell\" }\n```\n\nExample ask hook (force approval for every MCP tool):\n\n```toml\n[[hooks.hooks]]\nevent = \"tool_call_before\"\ncommand = '''echo '{\"decision\":\"ask\"}' '''\ncondition = { type = \"tool_name\", name = \"mcp__*\" }\n```\n\nExample input rewrite:\n\n```toml\n[[hooks.hooks]]\nevent = \"tool_call_before\"\ncommand = \"~/.codewhale/hooks/clamp-shell-timeout.sh\"\ncondition = { type = \"tool_name\", name = \"exec_shell\" }\n```\n\nwhere the script reads the hook context, then prints\n`{\"updatedInput\": {...}}` with the adjusted arguments.\n\n`tool_name` conditions support `*` globs: `mcp__*` matches every MCP\ntool (e.g. `mcp__github__create_issue`) but not built-ins like\n`read_file`; exact names keep matching exactly. Other regex\nmetacharacters in the pattern are matched literally.\n\n### Project-local hooks\n\nRepositories can ship policy in `<workspace>/.codewhale/hooks.toml`,\nusing the same shape as the `[hooks]` table (top-level fields plus\n`[[hooks]]` entries). Project hooks are executable shell\nconfiguration, so Codewhale only loads them after the workspace has\nbeen trusted in user-owned config through the trust prompt or a\n`[projects.\"<workspace>\"] trust_level = \"trusted\"` entry. Session\n`/trust on` mode does not enable repo-supplied hooks by itself, and\nrepo-local legacy markers such as `.deepseek/trusted` do not enable\nproject hooks. Once trusted, project hooks are appended after global\nhooks from `config.toml`, so they run last and, for `updatedInput`,\nwin ties. A malformed trusted project file logs a warning and startup\nfalls back to global hooks only.\n\n```toml\n# .codewhale/hooks.toml\n[[hooks]]\nevent = \"tool_call_before\"\ncommand = '''echo '{\"decision\":\"deny\",\"reason\":\"no shell in this repo\"}' '''\ncondition = { type = \"tool_name\", name = \"exec_shell\" }\n```\n\n### Turn-end observer hooks\n\n`turn_end` hooks observe the end of each model turn after post-turn\nstate, usage totals, cost accounting, notifications, receipts, and\nqueue recovery have been updated. They receive JSON on stdin and are\nobserver-only: stdout is ignored, failures are logged as warnings, and\nthe hook cannot block user input, mutate the transcript, or change the\nnext queued follow-up.\n\nObserver-only UI events share one 32-entry queue and two persistent workers;\nthe terminal loop uses non-blocking submission and does not create a thread per\nevent. A full queue or unavailable dispatcher drops that observer event and is\nkept as an event-specific error toast, independent of ordinary agent/turn\nstatus text.\n\n```toml\n[[hooks.hooks]]\nevent = \"turn_end\"\ncommand = \"~/.codewhale/hooks/turn-audit.sh\"\ntimeout_secs = 2\ncontinue_on_error = true\n```\n\nThe payload includes common hook metadata plus post-turn accounting:\n\n```json\n{\n \"event\": \"turn_end\",\n \"session_id\": \"sess_12345678\",\n \"workspace\": \"/path/to/workspace\",\n \"mode\": \"agent\",\n \"created_at\": \"2026-07-12T10:30:00+00:00\",\n \"model_backed\": true,\n \"provider\": \"deepseek\",\n \"model\": \"deepseek-chat\",\n \"billing_surface\": null,\n \"turn_id\": \"turn_12345678\",\n \"status\": \"completed\",\n \"error\": null,\n \"duration_ms\": 1834,\n \"usage\": {\n \"input_tokens\": 1200,\n \"output_tokens\": 180,\n \"prompt_cache_hit_tokens\": 900,\n \"prompt_cache_miss_tokens\": 300,\n \"prompt_cache_write_tokens\": 0,\n \"reasoning_tokens\": null,\n \"reasoning_replay_tokens\": null\n },\n \"totals\": {\n \"session_tokens\": 1380,\n \"conversation_tokens\": 1380,\n \"input_tokens\": 1200,\n \"output_tokens\": 180\n },\n \"tool_count\": 2,\n \"queued_message_count\": 1,\n \"stop_hook_active\": false\n}\n```\n\n`created_at` anchors time-window pricing; `provider` and `model` identify the\neffective route used for model-backed turns. `billing_surface` is an optional,\nnon-secret classification derived from the endpoint that actually served the\nturn. Recognized StepFun routes emit `stepfun-payg` or `stepfun-plan`; the raw\nbase URL is never written to hook or runtime records. Runtime `TurnRecord`\nexports call the same field `effective_billing_surface`, which `scorecard`\naccepts as an alias. This keeps subscription quota separate from token-priced\nusage. Unrecognized and custom endpoints remain `null` and unpriced.\n\nShell-only lifecycle completions set `model_backed` to `false` and may report a\n`null` provider; offline scorecards exclude those records from model token and\ncost totals. Completion-only shell, manual-compaction, and purge events that do\nnot have a matching `TurnStarted` retain the observer notification with a\nsynthetic `lifecycle_<uuid>` turn id and the time the completion was observed.\n\nFor `interrupted` or `failed` turns, `status` reflects that terminal\nstate and `error` carries the engine error string when one is available.\n`stop_hook_active` is reserved for future re-entry protection and is\ncurrently always `false`.\n\n### Sub-agent lifecycle hooks\n\n`subagent_spawn` and `subagent_complete` hooks observe sub-agent lifecycle\nevents. They receive bounded JSON metadata on stdin and are observer-only:\nhook failures are logged as warnings and do not block sub-agent scheduling,\nchange prompts, or change results. For these observer events,\n`continue_on_error` has no effect: later matching hooks still run even when an\nearlier hook exits non-zero.\n\n```toml\n[[hooks.hooks]]\nevent = \"subagent_complete\"\ncommand = \"~/.codewhale/hooks/subagent-audit.sh\"\ntimeout_secs = 2\ncontinue_on_error = true\n```\n\n`subagent_spawn` receives:\n\n```json\n{\n \"event\": \"subagent_spawn\",\n \"agent_id\": \"agent_12345678\",\n \"session_id\": \"sess_12345678\",\n \"workspace\": \"/path/to/workspace\",\n \"mode\": \"agent\",\n \"model\": \"deepseek-chat\",\n \"total_tokens\": 1234,\n \"prompt_preview\": \"bounded prompt preview\",\n \"prompt_truncated\": false\n}\n```\n\n`subagent_complete` receives the same common fields plus terminal metadata:\n\n```json\n{\n \"event\": \"subagent_complete\",\n \"agent_id\": \"agent_12345678\",\n \"session_id\": \"sess_12345678\",\n \"workspace\": \"/path/to/workspace\",\n \"mode\": \"agent\",\n \"model\": \"deepseek-chat\",\n \"total_tokens\": 1234,\n \"status\": \"completed\",\n \"result_preview\": \"bounded result preview\",\n \"result_truncated\": false\n}\n```\n\nPreviews are capped before delivery so lifecycle hooks do not receive full\nsub-agent prompts, transcripts, or unbounded results. Use the transcript handle\nreturned by `agent` when full sub-agent details are needed.\n\n### Running-turn input\n\nComposer shortcuts keep the same role throughout a session:\n\n- **Enter** sends when idle and queues a next-turn follow-up while busy. The\n behavior does not change before versus after the provider's first token.\n- With an empty composer and queued follow-ups visible, **Enter** sends the\n oldest queued follow-up into the active turn now.\n- **Ctrl+Enter** (or **Cmd+Enter** when the terminal forwards it) explicitly\n steers the active turn. It sends normally when idle.\n- By default, **Shift+Enter**, **Alt+Enter**, and **Ctrl+J** insert a newline.\n- Set `composer_multiline_mode = true` to make **Enter** insert a newline and\n **Alt+Enter** send instead. **Shift+Enter** also sends when the terminal\n reports it distinctly; legacy terminals may report it as plain **Enter**.\n **Ctrl+J** still inserts a newline, and supported **Ctrl+Enter** /\n **Cmd+Enter** behavior is unchanged.\n- **Ctrl+G** and **Ctrl+S** only stash drafts; they never send or steer.\n\n### Composer stash (`/stash`, Ctrl+G / Ctrl+S)\n\nPress **Ctrl+G** in the composer to park the current draft to\n`~/.codewhale/composer_stash.jsonl`. `/stash list` shows parked\ndrafts with one-line previews and timestamps; `/stash pop`\nrestores the most recently parked draft (LIFO); `/stash clear`\nwipes the file. Capped at 200 entries; multiline drafts round-trip intact.\n**Ctrl+S** remains an alias in terminals that forward it; Cursor and VS Code\nreserve Ctrl+S for Save, so Ctrl+G is the portable default.\n\n## Settings File (Persistent UI Preferences)\n\ncodewhale also stores user preferences in:\n\n- `~/.codewhale/settings.toml` on new installs\n- `~/.deepseek/settings.toml` or the legacy platform config-dir\n `deepseek/settings.toml` when an existing settings file is present\n\nNotable settings include `auto_compact`, which uses a model-aware default-on\npolicy for known context windows up to the 1M-token V4 class. Automatic\ncompaction runs before the active model limit and carries the compacted summary\nforward into the next request. The trigger defaults to\n`auto_compact_threshold_percent = 80`. Users who prefer manual continuity can\npersist `auto_compact = false`; manual `/compact` / Ctrl+L remains available.\nYou can inspect or update these from the TUI with `/settings` and `/config`\n(interactive editor).\n\nCommon settings keys:\n\n- `theme` (`system`, `terminal`, `underwater`, `underwater-retro`,\n `shoreline`, `shoreline-light`, `dark`, `light`, `grayscale`,\n `catppuccin-mocha`, `tokyo-night`, `dracula`, `gruvbox-dark`, `claude`,\n `matrix`, `solarized-light`, `uwu`; default `underwater`): `underwater` is\n the dark navy fresh-install default, `shoreline` is its warm charcoal\n alternative and `shoreline-light` the light variant, `system` follows terminal\n background detection, `dark`/`light` use the Codewhale Whale pair,\n `terminal` inherits the host terminal, `grayscale` is the low-opinion\n black/white theme, and the named community presets apply across the TUI.\n Aliases such as `whale`, `mono`, `black-white`, `tokyonight`, and `gruvbox`\n are accepted. In Whale, cobalt blue owns action/focus, seafoam owns live\n work, Signal Gold owns human decisions and the whale, coral owns warnings,\n rose owns danger, violet owns Operate, and green remains completed/verified.\n Text labels, markers, and motion policy carry the same states when color is\n unavailable; color is never the only cue.\n User-authored overlays live only at `~/.codewhale/themes/<name>.json` (or\n `$CODEWHALE_HOME/themes/<name>.json`) and are selected with\n `/theme custom:<name>`. The filename is a bounded slug, symlinks and files\n over 64 KiB are refused, colors must be `#RRGGBB`, and unknown fields fail\n validation. `/theme schema` prints the embedded JSON Schema and `/theme path`\n shows the exact directory. An overlay names one compiled `base` theme and\n changes only listed semantic colors; it cannot include or read another file.\n Open `/theme` to browse valid overlays, preview them live, and keep the\n active `custom:<name>` selector when the picker is opened without moving.\n- `auto_compact` (on/off, model-aware default on for known context windows\n unless explicitly configured)\n- `auto_compact_threshold_percent` (10-100, default `80`): pre-send\n auto-compaction threshold used only when `auto_compact` is enabled.\n- `paste_burst_detection` (on/off, default on): fallback rapid-key paste\n detection for terminals that do not emit bracketed-paste events. This is\n independent of terminal bracketed-paste mode.\n- `work_surface_placement` (`bottom`, `top`, `left`, `right`, or `off`;\n default `bottom`): places the workbar — Tasks / To-do / Workers — under the\n composer (the default bottom workbar), above the transcript, in a side\n workbar, or hides it entirely (`off`). Side choices fall back to the top\n layout on narrow terminals without changing the saved preference. Set it\n live with `/config work_surface_placement right --save` (or `left` / `top` /\n `bottom` / `off`).\n- `rail_panel` (`tasks`, `agents`, `background`, `files`, `notepad`,\n `context`, `git`, `price`; default `tasks`, alias key `rail`): which panel\n the workbar shows. Panel selection is orthogonal to placement. `tasks` is\n the full live work list (to-dos, then sub-agents); `agents` narrows to the\n sub-agent rows; `background` lists background shells and automations;\n `files` lists touched files; `notepad` shows the workspace notes; `context`\n is a read-only session-facts list; `git` shows branch status; `price`\n shows cost. In every panel except `context`, rows are selectable and\n clickable and open their detail surface. `Alt+!`/`Alt+@`/`Alt+#`/`Alt+$`\n switch panels live.\n- `work_surface_top_height` (2–16) and `work_surface_side_width` (26–80):\n ceilings for the top strip's height and the side workbar's width. Both are\n normally persisted by dragging the divider rather than edited by hand; the\n strip still auto-fits its content below the ceiling.\n- `focus_texture` (`off`, `scrim`, or `grain`; default `off`): focus-context\n texture for modal views. `scrim` dims the already-rendered background\n outside the focused modal toward the theme surface; `grain` sprinkles\n sparse dots over blank cells there. The texture is static (no time\n component, so it is unaffected by `low_motion`), never writes over a cell\n that carries text, and preserves the 4.5:1 body-text contrast floor\n wherever both colors are resolvable. It is skipped entirely on frames\n below the ambient-life minimum size and when the focused modal already\n covers 90% or more of the frame. Set it live with\n `/config focus_texture scrim --save`.\n- `mention_menu_limit` (integer, default `128`): maximum number of\n `@`-mention popup candidates retained before the composer renders the\n visible window. The visible rows still depend on terminal height.\n- `mention_walk_depth` (integer, default `10`): maximum workspace depth for\n `@`-mention completion walks. Set to `0` for unlimited depth in deeply\n nested workspaces; keep the default in very large repos unless needed.\n- `mention_menu_behavior` (`fuzzy`, `browser`; default `fuzzy`): controls how\n `@`-mention completions are populated. `fuzzy` searches the workspace and\n applies mention frecency. `browser` lists only the immediate children of the\n currently typed directory segment in deterministic alphabetical order.\n- `show_thinking` (on/off)\n- `thinking_default_expanded` (on/off, default off): renders thinking blocks\n expanded initially when `show_thinking` is enabled. Space still toggles the\n selected block, and it decides only the blocks you have not touched: a block\n you expanded or collapsed yourself keeps that state if you change this\n setting later. This is useful in SSH/tmux environments where the Space\n binding may be intercepted.\n- `thinking_preview_lines` (integer, default `2`): how many body rows a\n **collapsed** completed thought still shows. `0` is header-only; `10` is\n the older dump. Live streaming preview is unchanged. Expand a block with\n Space, or set `thinking_default_expanded` to open every block.\n- `help_expand_groups` (on/off, default off): start Help/shortcuts with every\n group expanded. Default folds the long tail (Grok-style); type-to-filter\n still unfolds matches.\n- `pin_last_prompt` (on/off, default on): pin the last user prompt at the top\n of the transcript viewport after it scrolls off.\n- `show_tool_details` (on/off)\n- `inline_diffs` (`full`, `summary`, or `off`; default `full`): controls the\n inline presentation of successful structured File mutations. `full` shows a\n bounded red/green diff and semantic statistics, `summary` keeps only the\n statistics, and `off` keeps the calm changed-file outcome. All three retain\n the exact applied change in the selected File receipt's Alt/Option+V detail.\n Failure and cancellation never render a successful diff. Save the choice\n with `/config inline_diffs <mode> --save`.\n- `locale` (`auto`, `en`, `ja`, `zh-Hans`, `zh-Hant`, `pt-BR`, `es-419`, `vi`,\n `ko`; default `auto`): UI chrome locale. `auto` checks `LC_ALL`,\n `LC_MESSAGES`, then `LANG`; unsupported locale selections resolve to English.\n Every shipped pack holds full `en.json` parity, so no string falls back\n to English. The runtime also exposes the resolved locale in the system\n prompt as the fallback natural language for V4 reasoning and replies when the\n latest user message is ambiguous. Clear user language still takes priority;\n Chinese turns should produce Chinese `reasoning_content` and Chinese final\n replies even when the resolved locale is English.\n- `background_color` (`#RRGGBB`, `RRGGBB`, or `default`): optional main TUI\n background color applied to the root, header, transcript, and footer\n surfaces while preserving panel contrast.\n- `cost_currency` (`usd`, `cny`; default `usd`): currency used by the footer,\n context panel, `/cost`, `/tokens`, and long-turn notification summaries. The\n aliases `rmb` and `yuan` normalize to `cny`.\n- `default_mode` (`agent`, `plan`, or `operate`; legacy values are accepted for migration but are not live mode vocabulary)\n- `launch_screen` (legacy, migration-only): this historical `on`/`off` value\n is still accepted when reading an existing settings file, but it no longer\n changes behavior and is omitted from new saves. A fresh interactive launch\n always opens Tideline Startup; only an explicit resume or an explicit\n initial prompt enters the live session directly.\n- `sidebar_focus` (legacy, migration-only): the classic right sidebar this key\n configured was removed in the 0.9.4 rail unification. The key is still read\n once so old settings carry forward, then folds into the live keys:\n `pinned`/`work`/`plan`/`todos` become `rail_panel = \"pinned\"`,\n `agents`/`subagents` become `rail_panel = \"agents\"`, `context`/`session`\n become `rail_panel = \"context\"`, `tasks`/`auto` (the old default) become the\n `tasks` panel, `sessions` enables `sessions_rail`, and `hidden` turns the\n workbar off via `work_surface_placement = \"off\"`. An explicit `rail_panel`\n in the file always wins over the migrated value. Configure the workbar with\n `rail_panel` and `work_surface_placement`, not this key.\n- `sessions_rail` (`on`/`off`; default `off`): show the persistent Sessions\n list in the workbar. Rows list this workspace's recent\n non-archived sessions, newest first, with the active one marked; activating a\n row opens the session picker preselected on it (`/sessions open <id>`), so\n resume keeps its single implementation. Rows are projected from cached\n session metadata — the list never reads a transcript per frame, and never\n contacts a provider.\n- `session_auto_resume` (`on`/`off`; default `off`): reattach to this\n workspace's most recent session when Codewhale starts. Off by default so\n plain `codewhale` keeps starting fresh. `--resume`, `--continue`, and\n `--fresh` always take precedence. When it is on, startup still refuses to\n resume a session that is archived, fails to load, or is recorded against a\n different workspace; each of those falls back to a fresh transcript and says\n which session was skipped and why. It applies to the interactive launch only\n — `codewhale \"<prompt>\"` and `codewhale exec` are never silently prefixed\n with a prior conversation.\n- `max_input_history` (number of submitted input history entries; cleared\n drafts are also kept locally for composer history search). Note the spelling:\n the serde field on disk is `max_input_history`\n (`crates/tui/src/settings.rs:426`, default 100). `max_history` is the key\n name accepted by `/config set` and `settings.set()` (`settings.rs:1388`), not\n a settings.toml key — writing `max_history` into the file is silently\n ignored.\n- `default_model` (model name override)\n\n`/task digest` (alias `/tasks digest`) renders the canonical Work Graph\noperations and four-state To-do list as plain text, running work first. It\nreads the same snapshots as the styled Work surface and owns no parallel\nprogress state.\n\nPlan and Work are the everyday visible modes in the UI; Operate is an explicit\npreview entry while its Workflow control surface is still being built. Switch\nbetween them with `/mode`. For compatibility, older settings files with\n`default_mode = \"normal\"` still load as `agent`.\n\nLocalization scope is tracked in [LOCALIZATION.md](LOCALIZATION.md). The v0.7.6\ncore pack covers high-visibility TUI chrome only; provider/tool schemas,\npersonality prompts, and full documentation remain English unless explicitly\ntranslated later.\n\nReadability semantics:\n\n- Selection uses a unified style across transcript, composer menus, and modals.\n- Footer hints use a dedicated semantic role (`FOOTER_HINT`) so hint text stays readable across themes.\n\n### Token Quantities and Drivers\n\nDeepSeek V4 prefix caching makes token labels matter. These quantities are kept\nseparate:\n\n| Quantity | Meaning | Allowed to drive |\n|---|---|---|\n| Active request input estimate | Conservative estimate of the next request's live system prompt and transcript payload. | Header/footer context percent, auto-compaction trigger, opt-in Flash seam trigger, and emergency overflow preflight. |\n| Reserved response headroom | The effective request cap plus `1024` safety tokens. Normal no-override requests start at `65536`; a smaller route/provider ceiling narrows that value, and an explicit output override raises it only within the resolved route window and output ceiling. The identical cap reaches the wire and drives preflight, except on a route that states an output limit above half its known context window with no explicit output override: there compaction aims to keep a quarter of the window free (at most `65536`, and never more than the request cap), preflight refuses a request only when fewer than `128` output tokens would fit, and every request's `max_tokens` is what the window has left after that request's estimated input and the `1024` safety tokens, never above the request cap. Reasoning effort does not add a second hidden reservation. A separately published route input ceiling independently clamps the spendable input budget. | Emergency overflow budget checks only. |\n| Cumulative API usage | Provider-reported input plus output tokens summed across completed API calls; multi-tool turns may count the same stable prefix more than once. | Session usage and approximate cost telemetry only. |\n| Prompt cache hit/miss | Provider cache telemetry for the most recent call when available. | Cache-hit display and cost estimation only; never compaction or seam triggers. |\n| Context percent | Active request input estimate divided by the model context window. | Display only; it mirrors the active-input basis used by context safeguards. |\n| Cost estimate | Approximate spend from provider usage and configured DeepSeek rates. | Display only. |\n\nFor known context-window models, including 1M-class V4 models, replacement\ncompaction is enabled by default unless the user explicitly configures\n`auto_compact = false`. It fires at the active model's compaction threshold and\nreplaces old history with recent user context followed by one ordinary\ncheckpoint message. The standing system prompt remains unchanged. Unknown model\nids remain opt-in.\n\n### Command Migration Notes\n\nIf you are upgrading from older releases:\n\n- Old: `/codewhale`\n New: `/links` (aliases: `/dashboard`, `/api`)\n- Old: `/set model deepseek-reasoner`\n New: `/config` and edit the `model` row to `deepseek-v4-pro` or `deepseek-flash`\n- Old: visible `Normal` mode or `default_mode = \"normal\"`\n New: use `Agent` / `default_mode = \"agent\"`; legacy `normal` still maps to `agent`\n- Old: discover `/set` in slash UX/help\n New: use `/config` for editing and `/settings` for read-only inspection\n\n## Key Reference\n\n### Kimi Code membership model IDs\n\nThe exact `https://api.kimi.com/coding/v1` endpoint accepts `k3`, `k3-256k`,\n`kimi-for-coding`, and `kimi-for-coding-highspeed`. Use `k3-256k` for a fixed\n262,144-token K3 window; use bare `k3` with `context_window = 1048576` only\nwhen the membership plan includes the 1M entitlement. Both K3 ids use the same\nreasoning contract, and all four membership ids omit generic sampling fields.\n\n### Core keys (used by the TUI/engine)\n\n- `provider` (string, optional): `deepseek` (default), `deepseek-anthropic`, `nvidia-nim`, `openai`, `atlascloud`, `wanjie-ark`, `volcengine`, `openrouter`, `xiaomi-mimo`, `novita`, `fireworks`, `siliconflow`, `arcee`, `siliconflow-CN`, `moonshot`, `sglang`, `vllm`, `ollama`, `ollama-cloud`, `huggingface`, `modelscope`, `together`, `qianfan`, `openai-codex`, `anthropic`, `openmodel`, `zai`, `stepfun`, `minimax`, `deepinfra`, `sakana`, `longcat`, `opencode-go`, `meta`, `mistral`, `telecomjs`, `xai`, `orcarouter`, `modelstudio-token-plan`, `google`, `edenai`, or `custom`. Legacy `deepseek-cn` configs are still accepted as an alias for `deepseek`; DeepSeek uses the same official host [`https://api.deepseek.com`](https://api-docs.deepseek.com/) worldwide. `deepseek-anthropic` targets DeepSeek's Anthropic Messages-compatible endpoint at `https://api.deepseek.com/anthropic` using `DEEPSEEK_API_KEY`; `nvidia-nim` targets NVIDIA's NIM-hosted DeepSeek endpoints through `https://integrate.api.nvidia.com/v1`; `openai` targets a generic OpenAI-compatible endpoint, defaulting to `https://api.openai.com/v1`; `atlascloud` targets AtlasCloud's OpenAI-compatible endpoint at `https://api.atlascloud.ai/v1`; `wanjie-ark` targets Wanjie Ark's OpenAI-compatible endpoint at `https://maas-openapi.wanjiedata.com/api/v1`; `volcengine` targets Volcengine Ark's OpenAI-compatible coding endpoint at `https://ark.cn-beijing.volces.com/api/coding/v3`; `openrouter` targets `https://openrouter.ai/api/v1`; `xiaomi-mimo` targets Xiaomi MiMo's OpenAI-compatible endpoint, using `https://token-plan-sgp.xiaomimimo.com/v1` by default for Token Plan keys (`tp-...`) and `https://api.xiaomimimo.com/v1` for pay-as-you-go keys. For Token Plan accounts outside the Singapore default, set `base_url` explicitly or use `mode = \"token-plan-cn\"` for China and `mode = \"token-plan-ams\"` for Europe/Amsterdam; `novita` targets `https://api.novita.ai/openai/v1`; `fireworks` targets `https://api.fireworks.ai/inference/v1`; `siliconflow` targets SiliconFlow, defaulting to `https://api.siliconflow.com/v1`; `arcee` targets Arcee AI's OpenAI-compatible endpoint at `https://api.arcee.ai/api/v1`; `siliconflow-CN` targets the SiliconFlow China regional endpoint through `[providers.siliconflow_cn]`; `moonshot` targets Moonshot/Kimi, defaulting to `https://api.moonshot.ai/v1`; `sglang` targets a self-hosted OpenAI-compatible endpoint, defaulting to `http://localhost:30000/v1`; `vllm` targets a self-hosted vLLM OpenAI-compatible endpoint, defaulting to `http://localhost:8000/v1`; `ollama` targets Ollama's OpenAI-compatible endpoint, defaulting to `http://localhost:11434/v1`; `huggingface` targets Hugging Face Inference Providers at `https://router.huggingface.co/v1`; `modelscope` targets ModelScope's OpenAI-compatible inference API at `https://api-inference.modelscope.cn/v1`; `together` targets Together AI at `https://api.together.xyz/v1`; `qianfan` targets Baidu Qianfan at `https://api.baiduqianfan.ai/v1`; `openai-codex` targets official ChatGPT plan inference with Codewhale-owned OAuth at `https://api.openai.com/v1`; `anthropic` targets Claude's native Messages API; `openmodel` targets OpenModel's Anthropic-compatible Messages API at `https://api.openmodel.ai`; `zai` targets Z.ai at `https://api.z.ai/api/coding/paas/v4`; `stepfun` targets StepFun at `https://api.stepfun.ai/v1`; `minimax` targets MiniMax at `https://api.minimax.io/v1`; `deepinfra` targets DeepInfra at `https://api.deepinfra.com/v1/openai`; `sakana` targets Sakana AI Fugu at `https://api.sakana.ai/v1`; `longcat` targets Meituan LongCat at `https://api.longcat.chat/openai/v1`; `opencode-go` targets the subscription-backed OpenCode Go model-aware route (Chat Completions, Responses, or Messages according to the documented model) at `https://opencode.ai/zen/go/v1`; `meta` targets Meta Model API; `mistral` targets Mistral AI's OpenAI-compatible endpoint at `https://api.mistral.ai/v1`; `telecomjs` targets TelecomJS TokenHub at `https://aigw.telecomjs.com/v1`; and `xai` targets xAI's API-key or OAuth route.\n- `opencode-zen` (string provider value): selects the model-aware OpenCode Zen gateway through `[providers.opencode_zen]`. The default base URL is `https://opencode.ai/zen/v1`, the default model is `gpt-5.6`, and credentials come from `api_key`, `OPENCODE_ZEN_API_KEY`, or fallback `OPENCODE_API_KEY`—never ChatGPT/Codex OAuth. `OPENCODE_ZEN_BASE_URL` and `OPENCODE_ZEN_MODEL` are accepted. The selected model's wire comes from the curated Zen snapshot, then from the AI SDK package its Models.dev `opencode` row names (refresh with `codewhale models --update`): GPT, Grok, and Muse Spark use Responses; Claude and most Qwen rows use Anthropic Messages; DeepSeek, MiniMax, GLM, Kimi, `qwen3.8-max`, and the free rows use Chat Completions. Gemini, Models.dev rows marked `deprecated`, and models no loaded catalog lists fail closed because Codewhale has no proven supported wire contract for them. See the exact current model groups in [`PROVIDERS.md`](PROVIDERS.md#opencode-zen-protocol-catalog).\n- `minimax-anthropic` (string provider value): selects MiniMax's Anthropic-compatible Messages route through `[providers.minimax_anthropic]`. The default Base URL is `https://api.minimax.io/anthropic`; set `https://api.minimaxi.com/anthropic` for China. Keep the `/anthropic` suffix because Codewhale appends `/v1/messages`. The route uses `MINIMAX_API_KEY` and defaults to `MiniMax-M3`; `MiniMax-M2.7` is also registered. Official M3 input modalities are text, image, and video, with adaptive or disabled thinking. M2.7 is text-only and always keeps thinking enabled.\n- `api_key` (string, required for hosted providers): must be non-empty for DeepSeek/hosted providers (or set the provider API key env var). Self-hosted SGLang, vLLM, and local `ollama` can omit it. `ollama-cloud` requires a key saved for that provider or supplied by `OLLAMA_CLOUD_API_KEY`, then `OLLAMA_API_KEY`.\n- `auth_mode` (string, optional provider-table key): selects a provider-specific authentication contract. Kimi Code membership uses `auth_mode = \"api_key\"` (or omit the field), a key created in the [Kimi Code console](https://www.kimi.com/code/console), `base_url = \"https://api.kimi.com/coding/v1\"`, and bare `model = \"k3\"` for K3. Codewhale gives that route a safe 262,144-token baseline; set `context_window = 1048576` only when the Kimi Code plan includes 1M access (Allegretto and above). `k3[1m]` is a Claude Code-only convention, not an API model ID, and Codewhale rejects it instead of silently changing the wire model or assuming an entitlement. `model = \"kimi-for-coding\"` remains the valid K2.7 compatibility route available to all Kimi Code members. Legacy `auth_mode = \"kimi_oauth\"` fails closed with API-key guidance and never probes, reads, refreshes, or rewrites `kimi_cli`/`kimi_code_cli` credential files. First-class OAuth requires Codewhale's own vendor-registered client identity and remains tracked in #4417.\n- `base_url` (string, optional, `[providers.<name>]` key; see [Legacy top-level `base_url` and `api_key`](#legacy-top-level-base_url-and-api_key) for the older top-level spelling): defaults to `https://api.deepseek.com/beta` for DeepSeek's OpenAI-compatible Chat Completions API, including legacy `provider = \"deepseek-cn\"` configs. Other defaults are `https://api.deepseek.com/anthropic` for `deepseek-anthropic`, `https://integrate.api.nvidia.com/v1` for `nvidia-nim`, `https://api.openai.com/v1` for `openai`, `https://api.atlascloud.ai/v1` for `atlascloud`, `https://maas-openapi.wanjiedata.com/api/v1` for `wanjie-ark`, `https://ark.cn-beijing.volces.com/api/coding/v3` for `volcengine`, `https://openrouter.ai/api/v1` for `openrouter`, `https://token-plan-sgp.xiaomimimo.com/v1` for `xiaomi-mimo` when the API key starts with `tp-...` and `https://api.xiaomimimo.com/v1` otherwise, `https://api.novita.ai/openai/v1` for `novita`, `https://api.fireworks.ai/inference/v1` for `fireworks`, `https://api.siliconflow.com/v1` for `siliconflow`, `https://api.siliconflow.cn/v1` for `siliconflow-CN`, `https://api.arcee.ai/api/v1` for `arcee`, `https://api.moonshot.ai/v1` for `moonshot`, `https://api.minimax.io/v1` for `minimax`, `https://api.openmodel.ai` for `openmodel`, `https://api.z.ai/api/coding/paas/v4` for `zai`, `https://api.stepfun.ai/v1` for `stepfun`, `https://api.deepinfra.com/v1/openai` for `deepinfra`, `https://api.sakana.ai/v1` for `sakana`, `https://router.huggingface.co/v1` for `huggingface`, `https://api-inference.modelscope.cn/v1` for `modelscope`, `https://api.together.xyz/v1` for `together`, `https://api.baiduqianfan.ai/v1` for `qianfan`, `https://api.openai.com/v1` for `openai-codex`, `https://api.anthropic.com` for `anthropic`, `https://api.mistral.ai/v1` for `mistral`, `http://localhost:30000/v1` for `sglang`, `http://localhost:8000/v1` for `vllm`, `http://localhost:11434/v1` for `ollama`, and `https://ollama.com/v1` for `ollama-cloud`. Set `base_url = \"https://token-plan-cn.xiaomimimo.com/v1\"` for China-region Xiaomi MiMo Token Plan accounts or `base_url = \"https://token-plan-ams.xiaomimimo.com/v1\"` for Europe/Amsterdam accounts. Mistral-specific reasoning fields and polymorphic replay are enabled only on the documented first-party HTTPS `/v1` hosts; a custom Mistral base URL keeps generic Chat semantics. Set `https://api.deepseek.com` or `https://api.deepseek.com/v1` explicitly to opt out of DeepSeek beta features.\n- `ollama-cloud` route: select `provider = \"ollama-cloud\"`, configure `[providers.ollama_cloud]` when overriding the default `https://ollama.com/v1` / `gpt-oss:120b` tuple, and save a key from [Ollama account settings](https://ollama.com/settings/keys) with `codewhale auth set --provider ollama-cloud`. Ambient precedence is `OLLAMA_CLOUD_API_KEY`, then `OLLAMA_API_KEY`; arbitrary Ollama model IDs pass through unchanged.\n- Legacy Ollama Cloud migration: a released `provider = \"ollama\"` config whose normalized `[providers.ollama].base_url` is exactly `https://ollama.com/v1` is upgraded to the `ollama-cloud` runtime identity in memory. Only that exact tuple may read its old `ollama` provider table and secret slot. The config and secrets are never rewritten, and neighboring paths, HTTP downgrades, lookalike hosts, or an explicit `ollama-cloud` selection never consume the fallback.\n- `telecomjs` base URL and catalog: `[providers.telecomjs]` defaults to `https://aigw.telecomjs.com/v1`; `TELECOMJS_BASE_URL` overrides it. With `TELECOMJS_API_KEY`, `/models` refreshes a key-scoped catalog without mixing rows into another provider.\n- `edenai` gateway: select `provider = \"edenai\"`; `[providers.edenai]` defaults to `https://api.edenai.run/v3` and `deepseek/deepseek-v4-pro`. `EDENAI_API_KEY`, `EDENAI_BASE_URL`, and `EDENAI_MODEL` are accepted. Use `EDENAI_BASE_URL = \"https://api.eu.edenai.run/v3\"` for Eden AI's documented EU endpoint; the default `deepseek/deepseek-v4-pro` is only listed on the global catalog, so pair the EU endpoint with an EU-listed model such as `qwen/deepseek-v4-pro` via `EDENAI_MODEL` or `model`. The provider refreshes Eden AI's `/models` catalog, but leaves model-specific reasoning controls untouched because the gateway spans multiple model families.\n- `codewhale` (Codewhale API): select `provider = \"codewhale\"`; `[providers.codewhale]` defaults to `https://api.codewhale.net/v1` and `deepseek/deepseek-v4-pro`. The credential is a Codewhale account API key (`cwc_key_…`) with the `models:infer` scope, read from `CODEWHALE_API_KEY` or the `codewhale` secret-store slot; `codewhale account api-keys create --name <name> --use` mints one and saves it locally. `CODEWHALE_API_BASE` overrides the origin and must be HTTPS except on loopback. The model catalog is the account's own authenticated `GET /v1/models`: ids are `provider/model` and each row states its wire (`chat-completions` → `/v1/chat/completions`, `anthropic-messages` → `/v1/messages`, `responses` → `/v1/responses`). Connect the underlying provider keys with `codewhale account keys set <provider>`.\n- `concentrate` gateway: select `provider = \"concentrate\"`; `[providers.concentrate]` defaults to `https://api.concentrate.ai/v1` and `deepseek-v4-pro` over the OpenAI Responses wire. `CONCENTRATE_API_KEY`, `CONCENTRATE_BASE_URL`, and `CONCENTRATE_MODEL` are accepted. Model ids pass through verbatim (`gpt-5.6-sol`, `openai/gpt-5.6-sol`, or `concentrate/auto` for the gateway router). BYOK only; see [PROVIDERS.md](PROVIDERS.md#concentrate-notes).\n- `mistral` model and reasoning contract: `[providers.mistral]` defaults to `mistral-code-latest`; `MISTRAL_MODEL` overrides it and the generic `CODEWHALE_MODEL` override wins when both are set. The current picker also lists `mistral-medium-latest`, `mistral-small-latest`, and `mistral-large-latest`. On exact first-party HTTPS `/v1` routes, Medium and Small accept only `reasoning_effort = \"none\" | \"high\"` and replay polymorphic thinking blocks. Deprecated native Magistral IDs may still be configured explicitly, remain always-reasoning, and never receive the adjustable effort field.\n- `context_window` (integer, optional provider-table key): override the total context window for the active `[providers.<name>]` route when an OpenAI-compatible gateway, hosted model alias, or self-hosted runtime has a different limit than Codewhale's static model table. For example, `[providers.openai] context_window = 1000000` lets an OpenAI-compatible DashScope/Qwen route budget against a 1M-token window instead of the conservative fallback. For Kimi Code K3, keep `model = \"k3\"` and set `[providers.moonshot] context_window = 1048576` only when the membership plan includes 1M access; otherwise omit it to retain the 262,144-token safe baseline. The value must be greater than 0 and affects prompt context notes, compaction thresholds, context-pressure checks, and request output caps. Full resolution order, and how to see which rung produced the current window: [Context length (context window)](#context-length-context-window).\n- `path_suffix` (string, optional provider-table key): override the chat-completions path for OpenAI-compatible gateways that do not serve `/v1/chat/completions`. For example, `[providers.openai] path_suffix = \"/chat/completions\"` sends chat requests to the unversioned base URL plus `/chat/completions`; `models` and `beta/*` requests keep their normal routing.\n- `reasoning_stream_style` (string, optional provider-table key): override how streaming reasoning is separated from answer text for the active provider route. Use `separate_field` for `reasoning_content` / `reasoning` deltas, `inline_tags` for gateways that stream `<think>...</think>` inside `delta.content`, or `none` to render incoming content exactly as answer text. When unset, every Chat Completions route uses `separate_field` (Mistral's first-party route uses its typed thinking blocks); set `none` only for a gateway that streams its answer inside `reasoning_content`.\n- `[providers.<name>.auth]` (table, optional): provider-scoped auth source metadata. `source = \"command\"` stores a command argv plus optional `timeout_ms`; `source = \"secret\"` stores a `secret_id`. This slice lets provider readiness, `/provider`, and doctor JSON report the auth source class without exposing command argv output or secret values; executing commands and resolving external secret material is handled by the follow-up resolver work.\n- `insecure_skip_tls_verify` (bool, optional provider-table key): legacy compatibility key, disabled by default. When true on the active provider table, provider clients reject the configuration instead of skipping TLS certificate verification. Use `SSL_CERT_FILE` for corporate or private CA bundles; `codewhale doctor` reports stale uses of this setting.\n- `default_text_model` (string, optional): defaults to `deepseek-flash` for DeepSeek and `deepseek-anthropic`, `gpt-5.6` for OpenAI, `grok-4.6` for xAI, `deepseek-ai/deepseek-v4-pro` for NVIDIA NIM, `deepseek-ai/deepseek-v4-flash` for AtlasCloud, `deepseek-reasoner` for Wanjie Ark, `DeepSeek-V4-Pro` for Volcengine Ark, `deepseek/deepseek-v4-pro` for OpenRouter and Novita, `mimo-v2.6-pro` for Xiaomi MiMo, `accounts/fireworks/models/deepseek-v4-pro` for Fireworks, `deepseek-ai/DeepSeek-V4-Pro` for SiliconFlow and DeepInfra, `trinity-large-thinking` for Arcee AI, `kimi-k2.7-code` for Moonshot, `MiniMax-M3` for MiniMax, `GLM-5.3` for Z.ai, `step-3.7-flash` for StepFun, `ernie-4.0-turbo-8k` for Qianfan, `fugu` for Sakana AI, `deepseek-ai/DeepSeek-V4-Pro` for SGLang/vLLM, no fixed id for local Ollama (Codewhale adopts a chat-capable tag from the live local catalog — coder or tool-capable tags first, then the largest context — and never an embedding or reranker model), and `gpt-oss:120b` for Ollama Cloud. Hugging Face and Together AI both default to `deepseek-ai/DeepSeek-V4-Pro`; `openai-codex` defaults to `gpt-6.1-sol`; `anthropic` defaults to `claude-sonnet-5-5`; `openmodel` defaults to `deepseek-v4-flash`. Current public DeepSeek IDs include `deepseek-v4-pro` and `deepseek-flash` (V4.1 Flash, shipped as the unversioned id), both with 1M context windows, 384K max output, and thinking mode enabled by default. DeepSeek's live pricing/model page now labels the Pro backend `DeepSeek-V4-Pro-0813`; the callable API ID remains `deepseek-v4-pro`, so Codewhale does not send the backend label or the Claude Code-specific `deepseek-v4-pro[1m]` selector. DeepSeek retired `deepseek-chat` and `deepseek-reasoner` on July 24, 2026; direct first-party routes migrate both to `deepseek-flash`, with omitted reasoning settings preserving their former non-thinking (`off`) and thinking (`high`) intent. DeepSeek has also retired the `deepseek-v4-flash` and `deepseek-v4-flash-vision-exp` models: it still accepts both names, serves them with V4.1 Flash, and bills them at the `deepseek-flash` price, which Codewhale applies to turns from 2026-09-10 04:00 UTC. The `flash`, `deepseek-v4flash`, `flash-vision`, and `deepseek-v4flashvisionexp` shorthands select `deepseek-flash`. Explicit `reasoning_effort` wins, and provider-owned ids on Wanjie Ark, aggregators, self-hosted runtimes, and custom endpoints are not globally rewritten. SiliconFlow retains its own mapping: `deepseek-reasoner` and `deepseek-r1` select its Pro model while `deepseek-chat` and `deepseek-v3` select Flash. Provider-specific mappings translate `deepseek-v4-pro` / `deepseek-v4-flash` to each provider's model ID where supported. OpenRouter also recognizes recent large IDs such as `arcee-ai/trinity-large-thinking`, `minimax/minimax-m3`, `minimax/minimax-m2.7`, `xiaomi/mimo-v2.5-pro`, `qwen/qwen3.6-flash`, `qwen/qwen3.6-35b-a3b`, `qwen/qwen3.6-max-preview`, `qwen/qwen3.6-27b`, `qwen/qwen3.6-plus`, `qwen/qwen3.7-max`, `google/gemma-4-31b-it`, `moonshotai/kimi-k2.7-code`, `moonshotai/kimi-k2.6`, `nvidia/nemotron-3-nano-omni-30b-a3b-reasoning:free`, and `nvidia/nemotron-3-ultra-550b-a55b`; direct Arcee uses bare IDs such as `trinity-large-thinking` and `trinity-large-preview`; direct Moonshot recognizes `kimi-k3`, `kimi-k2.7-code`, and `kimi-k2.6`. The exact Kimi Code endpoint recognizes bare `k3` for K3 and `kimi-for-coding` for K2.7; those membership IDs are distinct from the direct Moonshot IDs and are never rewritten across routes. Direct MiniMax recognizes `MiniMax-M3` and the documented M2.x chat model IDs; direct Z.ai recognizes `GLM-5.3` (the default), `GLM-5.2`, `GLM-5.1`, and `GLM-5-Turbo`, and OpenRouter recognizes the matching `z-ai/glm-5.1`, `z-ai/glm-5.2`, `z-ai/glm-5.3`, and `z-ai/glm-5-turbo` IDs — `GLM-5.3` has been live on the Z.ai Coding Plan since 2026-08-13; it inherits its catalog metadata from `GLM-5.2` until Z.ai publishes distinct 5.3 numbers and carries no price, and an explicit `GLM-5.2` selection keeps its own id; direct Sakana recognizes `fugu` and `fugu-ultra-20260615`; direct Xiaomi MiMo recognizes chat IDs `mimo-v2.6-pro`, `mimo-v2.6-flash`, and `mimo-v2.5-pro-ultraspeed` (the retired `mimo-v2.5-pro` and `mimo-v2.5` are sent as `mimo-v2.6-pro` and `mimo-v2.6-flash`), while TTS IDs are selected through `codewhale speech` / `tts`. Generic `openai`, `atlascloud`, `wanjie-ark`, `xiaomi-mimo`, `arcee`, `moonshot`, `minimax`, `openmodel`, `zai`, `stepfun`, `qianfan`, `sakana`, local Ollama, and Ollama Cloud model IDs are passed through unchanged after known aliases are normalized. OpenRouter and SiliconFlow provider configs with a custom `base_url` also preserve explicit model values, which lets OpenAI-compatible gateways accept bare model IDs. Use `/models` or `codewhale models` to discover live IDs from your configured endpoint. `CODEWHALE_MODEL` overrides this for a single process; `DEEPSEEK_MODEL` is the legacy alias.\n- TelecomJS uses `deepseek-v4-pro` only as a conservative pre-refresh fallback. Once its key-scoped `/models` catalog is available, the picker uses those live rows; Codewhale omits unsupported reasoning request fields on this route.\n- `reasoning_effort` (string, optional): `auto`, `off`, `minimal`, `low`, `medium`, `high`, `xhigh`, `ultra` (`ultracode` is an alias of `ultra`), or `max`; defaults to the configured UI tier. DeepSeek Platform receives top-level `thinking` / `reasoning_effort` fields. Ollama Cloud's OpenAI-compatible Chat Completions route preserves its documented `none` / `low` / `medium` / `high` / `max` ladder (`off` is sent as `none`; `xhigh` and `ultracode` normalize to `max`). Direct xAI `grok-4.7` and `grok-4.6` on exact `https://api.x.ai/v1` receive top-level `reasoning_effort = \"low\" | \"medium\" | \"high\" | \"xhigh\"` (the ladder comes from the bundled catalog row; `grok-4.5` maps `xhigh` to `high`, and rows without a documented effort such as `grok-4.3` get no field); Grok reasoning cannot be disabled, so `off` normalizes to `high`, `max`/`ultracode` to `xhigh`, and `auto` leaves the field omitted so xAI's documented default `high` applies. A custom xAI-compatible `base_url` does not inherit that dialect. Direct Moonshot `kimi-k3` on exact `https://api.moonshot.ai/v1` is always-thinking and receives only top-level `reasoning_effort = \"low\" | \"high\" | \"max\"`; `off` normalizes to `low`, and `medium` to `high`. Kimi Code membership `k3` on exact `https://api.kimi.com/coding/v1` instead receives nested `thinking.effort`, and its `off` setting also normalizes to enabled `low`. Normal dispatched `auto` uses Codewhale's auto-reasoning selector and sends a concrete route-normalized tier; only an omitted reasoning setting leaves the provider default in control. Neighboring gateways and model/endpoint combinations retain the generic Moonshot contract. OpenAI Codex normalizes stale `off` to `low` and sends `max` / `ultracode` as Responses `xhigh`. Z.ai receives documented `thinking` controls and treats enabled thinking as the GLM coding high/max lane. NVIDIA NIM receives equivalent settings through `chat_template_kwargs`.\n- `verbosity` (string, optional): `normal` or `concise`. `normal` keeps the\n default conversational prompt. `concise` appends a prompt discipline block\n for direct, low-chatter output; CLI noninteractive commands (`exec` and\n `eval`) default to `concise` unless config/env/CLI overrides it.\n Override per process with `CODEWHALE_VERBOSITY` or the legacy\n `DEEPSEEK_VERBOSITY` alias.\n- `telemetry` (bool, optional): anonymous usage counting, **`true` by default\n in the current 0.9.12 source**. Notice version `5` names Codewhale and PostHog\n and describes the opt-out policy; no acceptance is invented for a default\n user. Existing explicit declines remain off. An explicit `false` here is the\n durable *opt-out*: it deletes the random install id, truncates buffered and\n dry-run events, and leaves a tombstone reasserted while the setting is false.\n It is a floor: `--telemetry true` and `CODEWHALE_TELEMETRY=1` lose to it.\n Use `/settings` or `codewhale config set telemetry true` to turn counting\n back on explicitly for new sessions through the existing privacy transition. `CODEWHALE_TELEMETRY`\n (legacy alias `DEEPSEEK_TELEMETRY`) and `--telemetry false` provide a run-scoped\n kill switch that stops collection and delivery without erasing the owner's\n state. A repo-local `.codewhale/config.toml` cannot set this preference.\n `codewhale config telemetry` shows the disclosure; `codewhale config get\n telemetry` reports preference and privacy status. Full schema and opt-out\n behavior: [`TELEMETRY.md`](TELEMETRY.md).\n- `telemetry_endpoint` (string, optional): where batches are POSTed. Leaving it\n unset selects the shipped default,\n **`https://telemetry.codewhale.net/v1/telemetry`** — the first-party ingest\n service described in [`TELEMETRY.md`](TELEMETRY.md), whose source is in\n `telemetry-ingest/`. This key decides only *where* a permitted session sends;\n it cannot override an opt-out. Setting it\n to the **empty string** is how you stay enabled and contact nobody: each batch\n is then written to `$CODEWHALE_HOME/telemetry/dryrun.jsonl` and no HTTP client\n is constructed at all, so you can read exactly what would have been sent. Any\n other value replaces the default outright. `https://` is required; plain\n `http://` is accepted only for loopback hosts, and there is no environment\n variable that overrides that refusal. A rejected endpoint turns telemetry off\n for the run rather than falling back to plaintext or to the default. Override\n per process with `CODEWHALE_TELEMETRY_ENDPOINT` (legacy alias\n `DEEPSEEK_TELEMETRY_ENDPOINT`), where an empty value means the same \"contact\n nobody\". A repo-local `.codewhale/config.toml` cannot set it.\n- `allow_shell` (bool, optional): in interactive TUI Agent sessions, omitting\n this keeps shell tools available with approval prompts; setting it to `false`\n hides shell tools. Headless, durable-task, and other noninteractive profiles\n keep the conservative omitted-field default and require `allow_shell = true`\n to expose shell. Plan mode always hides shell; Full Access enables shell and\n auto-approval.\n- `approval_policy` (string, optional): `on-request`, `untrusted`, or `never`. Runtime `approval_mode` editing in `/config` also accepts `on-request` and `untrusted` aliases.\n- `[approval] default_selection` (string, optional): which option an approval\n card highlights when it first appears — `deny` (default) or `allow_once`.\n `deny` means a reflexive Enter on a card you have not read refuses the call.\n The highlighted row is tagged `(Enter)`; press `y` to allow once.\n Set `allow_once` to restore the pre-v0.9.6 Enter-to-approve muscle memory\n (#5293). It moves the highlight only: which calls are prompted for is still\n `approval_policy` plus the rules in `permissions.toml`.\n\n ```toml\n [approval]\n default_selection = \"allow_once\"\n ```\n- `[approval] timeout_seconds` (integer, optional): bound how long an\n approval may wait — the TUI card and the Runtime API approvals the desktop\n app and web use alike. When the window elapses the call is refused\n (fail-closed) and recorded as a timeout, not as the operator's denial.\n Omitted or `0` waits indefinitely, which is the default everywhere: no\n approval is ever denied on your behalf unless you set this. Values above\n 24h clamp with a warning (#6101).\n\n ```toml\n [approval]\n timeout_seconds = 300\n ```\n- `sandbox_mode` (string, optional): `read-only`, `workspace-write`, `danger-full-access`, `external-sandbox`.\n The value can only narrow the sandbox the permission mode picks (Full\n Access: no wrapper; Ask and Auto-Review: `workspace-write`; Plan:\n `read-only`), so `danger-full-access` and `external-sandbox` take effect\n only in Full Access (`yolo`).\n Platform support is not identical. macOS uses Seatbelt when its runtime\n probe succeeds. Linux uses bubblewrap by default whenever `/usr/bin/bwrap`\n is installed and a probe proves it can confine a child; `prefer_bwrap =\n false` opts out and reports no OS command sandbox. Windows does not currently advertise an OS sandbox; its planned helper contract starts\n with process-tree containment only and must not be described as read-only\n filesystem isolation, workspace-write enforcement, network blocking,\n registry isolation, or AppContainer isolation until those are implemented.\n- The cross-layer relationship between mode admission, hooks, registered tool\n requirements, typed rules, auto-review, repo law, human approval, and the\n execution sandbox is defined in\n [Authorization order](AUTHORIZATION_ORDER.md).\n- **Read deny-list.** Every sandbox posture — `read-only` included — grants\n read access to the whole filesystem; the postures differ in what they may\n *write* and whether they may reach the network. The read deny-list narrows\n that, in two scopes. Codewhale's own file tools (`read`, `grep`,\n `file_search`, `read_media`) refuse every listed path. A shell command\n refuses only the *key material and other-principal* entries — SSH and GPG\n keys, `/etc/shadow`, `/etc/sudoers`, system and login keychains, browser\n profiles, other agents' credential stores, Codewhale's own secret stores,\n runtime daemon sockets, and anything in `sandbox_denied_read_paths`. A\n tool's own auth config is refused to the model but stays readable to the\n tool: `gh` cannot start without `~/.config/gh/hosts.yml`, so denying it to\n the process would disable the CLI without keeping its token out of reach.\n - `sandbox_read_denylist_defaults` (bool, default `true`): apply the built-in\n credential-store set — `~/.ssh`, `~/.gnupg`, cloud credential directories\n (`~/.aws`, `~/.config/gcloud`, `~/.azure`, `~/.kube`, …), `~/.netrc`,\n `~/.npmrc`, `~/.git-credentials`, macOS keychains, browser profiles,\n Codewhale's own secret stores, and `.env` files (but not `.env.example`\n and friends). Ordinary source, `Cargo.toml`, `~/.gitconfig`, `~/.cargo`,\n and `~/.npm` stay readable so builds and tests still work. Set `false` to\n restore the pre-0.9.12 full-disk-read behavior.\n - `sandbox_denied_read_paths` (list of paths): additional denied subpaths.\n `~` expands. These can never be exempted.\n - `sandbox_read_denylist_exempt` (list of paths): subtract a path from the\n *built-in defaults* when a project genuinely needs it. Deny wins over\n allow: this never reopens anything in `sandbox_denied_read_paths`.\n\n Exemption granularity is **whole-rule**, not per-file. An exempt path\n removes a built-in rule only when the rule's own path is at or below it,\n so exempting `~/.ssh/config` does nothing: the `~/.ssh` rule still denies\n it, because `~/.ssh` does not lie within `~/.ssh/config`. To reopen that\n one file you must exempt `~/.ssh` itself — which also reopens the private\n keys next to it. That is the documented tradeoff: there is no shipped way\n to narrow a built-in rule to \"everything except one file\"; copy what you\n need out of the denied tree instead. The one name-shaped rule, `.env`\n files, is exempted by *name* rather than by path: any exempt entry whose\n file name is exactly `.env` — bare `.env`, `~/.env`,\n `some/project/.env` — disables the entire `.env` filename rule, i.e.\n every `.env` and `.env.<name>` on disk rather than one project's.\n (`.env.example` and friends are never denied, so they need no exemption.)\n\n Enforced at two points, with different reach. Codewhale's own in-process\n tools, which the OS sandbox never wraps, refuse every rule — `read_file` /\n `read` / `read_media` for contents, `list_dir` / `file_search` for\n *enumeration* (listing a denied directory, or searching one, is refused just\n as Seatbelt blocks its readdir; a name search rooted above a denied tree\n skips entries inside it). Sandboxed shell commands (Seatbelt last-match-wins\n `deny file-read*` rules; bubblewrap masks each path) refuse only the\n key-material and other-principal rules, so an authenticated CLI can still\n read its own config. A refused read is always an explicit error, never an\n empty result, and the error names the path as the caller spelled it rather\n than a symlink target's real location.\n\n **This is defense-in-depth, not a security boundary.** It does not stop a\n hardlink to a denied file, a secret already copied into the workspace, an\n indirect read (`ssh-agent`, `security find-generic-password`, `aws sts …`),\n reads under `danger-full-access` shell commands, reads by MCP servers or\n other unwrapped child processes, or exfiltration of anything that *was*\n read. Keep least-privilege credentials and short-lived tokens doing the real\n work.\n- `permissions.toml` (sibling file, optional): typed permission rule records\n loaded next to `config.toml`, for example `~/.codewhale/permissions.toml`.\n This active user file is the only permission-rule source today; project\n config overlays do not load a project-local `permissions.toml`. A rule's\n optional `workspace` field is its repository scope, not a second source.\n Manually authored `[[rules]]` entries accept `tool`, optional `command` or\n `path`, optional absolute `workspace`, optional `command_exact = true`, and\n optional `action = \"deny\" | \"ask\" | \"allow\"`; omitted `action` defaults to\n `\"ask\"`. `workspace` limits a rule to that repository, while\n `command_exact = true` changes a command rule from the historical\n arity-aware prefix match to a complete-command match. `deny` blocks matching\n invocations before mode-based\n approval handling, `allow` skips approval for matching invocations, and\n `ask` forces approval only in modes that can prompt. Outside the TUI\n auto-approve path, a matching `ask` rule under `approval_policy = \"never\"`\n is rejected because no prompt can be shown. In Full Access / auto-approval sessions,\n `ask` rules do not downgrade the session into prompting or blocking; explicit\n `deny` rules still block according to the current execution-policy logic.\n\n In a supported approval card, press `S` to allow the request once and append\n exact `action = \"ask\"` rules to this file. For eligible safe requests, choose\n **Always allow this exact rule in this repo** (shortcut `P`) to append an\n `action = \"allow\"` rule with the current absolute `workspace` scope.\n Remembered shell grants set `command_exact = true`, so later commands with\n extra arguments do not inherit the grant. File and patch grants retain the\n exact workspace-relative paths produced by the existing validation path.\n Supported saves are intentionally narrow:\n `exec_shell` stores the exact approved command string; `write_file` and\n `edit_file` store the exact workspace-relative file path; `apply_patch`\n stores one exact workspace-relative `path` rule per validated touched file\n from apply-patch preflight. Existing exec command matching remains\n arity-aware for manually authored prefix rules; approval-card allow grants\n use complete-command matching. File paths are normalized to the same\n workspace-relative form used by runtime matching.\n\n `read_file` rules can still be authored manually when you want future reads\n of a specific path to ask, allow, or deny, but the approval UI does not save\n `read_file` rules. Commands classified as requiring approval or dangerous,\n critical approval cards, and repo-law prompts cannot save allow grants and\n continue to require review.\n\n `/permissions` (or `/permissions list`) is the narrow rule-management\n surface. It lists each numbered rule with the active user-file source, its\n exact effective matcher (tool-wide, command prefix, exact command, or exact\n normalized path), global or repository scope, and whether that scope\n applies in the current workspace. `/config ask-rules` remains a compatibility\n entry to the same list.\n\n Deletion is review-gated: `/permissions remove <number>` only previews the\n selected rule and prints a confirmation command. That command carries an\n opaque token bound to the exact file bytes and rule index; if another writer\n changes `permissions.toml`, confirmation fails instead of deleting a rule\n that moved into the old position. Confirmed removal and approval-card appends\n share the adjacent `permissions.toml.lock`, preserve unrelated TOML comments\n and formatting, and atomically replace the file. The running TUI reloads the\n user ruleset without clearing session-only approvals.\n\n This editor intentionally does not create or rewrite rules, persist deny\n choices from approval cards, expand globs, or create broad\n directory/recursive rules. Author those supported exact/prefix records\n manually when needed.\n- `[[hotbar]]` (array of tables, optional): user-owned 1-8 slot bindings for\n the TUI hotbar. Each entry has `slot`, `action`, and optional `label`.\n The Hotbar is hidden by default (#3807): omitting `hotbar` and setting\n `hotbar = []` both mean no hotbar. `/hotbar on` writes the eight default\n slots (`slash.workflow`, `slash.goal`, `slash.auto`, `mode.plan`,\n `mode.agent`, `mode.operate`, `palette.open`, `sidebar.toggle`) as explicit\n `[[hotbar]]` tables. When one or more `[[hotbar]]` tables are present, that\n list is the whole bar; missing slots stay empty. Invalid slots outside `1..=8` are skipped with a warning, duplicate\n slots use the later entry, and unknown action IDs are kept so the UI can show\n a disabled/unknown cell instead of silently deleting user config. Trusted\n user config, profiles, and managed config replace the whole list; project\n overlays cannot change hotbar bindings. Setup or wizard flows that persist\n hotbar bindings write this same schema to the resolved `~/.codewhale/config.toml`\n path, preserving legacy `~/.deepseek/config.toml` only when that fallback file\n is already the active config.\n\n ```toml\n [[hotbar]]\n slot = 1\n action = \"mode.plan\"\n label = \"Plan\"\n\n [[hotbar]]\n slot = 2\n action = \"session.compact\"\n ```\n- `[auto_review]` (table, optional): tool-call review policy — a deterministic floor plus a model guardian tier.\n This layer sits on top of the existing permission posture; it can hold or block a\n tool call, but it is not an auto-push, auto-merge, or hosted review service.\n Block rules are checked first, then the built-in safety floor, then allow\n rules. In Ask, a safety hold opens approval that no session grant or allow\n rule can answer; in Auto-Review or a non-interactive `never` posture it\n fails closed as a hard block. Full Access skips the built-in floor. The\n safety floor covers publish-like actions, destructive background/headless\n actions, and `curl`/`wget` uploads whose request body carries a credential\n file or a secret-named variable, or reads stdin fed from one of those or\n from `env`/`printenv`, even if an allow rule matches; a shell command,\n shell input or verifier over 64 KiB or 4,096 words whose text contains\n `curl` or `wget` anywhere, or one nested too deeply to read, is held too.\n Outside Full\n Access it also refuses, in every posture, a\n shell command, shell input or verifier containing a hidden Unicode character\n (bidirectional controls, zero-width and other invisible characters, tag\n characters, stray variation selectors, and terminal control characters\n except ASCII control keys in shell input), naming the code point and its\n position.\n\n ```toml\n [[auto_review.allow]]\n id = \"read-only-inspection\"\n action_kind = \"read\"\n reason = \"Read-only inspection is safe to run automatically.\"\n\n [[auto_review.block]]\n id = \"no-release-publish\"\n action_kind = \"publish\"\n reason = \"Release and publish actions require maintainer review.\"\n ```\n\n Rule matchers are exact `tool` and/or `action_kind`. At least one matcher is\n required. `action_kind` accepts the six decision-relevant kinds `read`,\n `write`, `shell`, `external`, `publish`, and `destructive`. Invalid names\n fail config validation instead of silently broadening into another policy\n class. In block rules, the old names remain conservative compatibility\n aliases: `network`, `git`, `mcp_action`, `browser`, and `unknown` map to\n `external`; `secret` maps to `destructive`; and `mcp_read` maps to `read`.\n Retired narrow kinds in allow rules fail validation rather than widening to\n a broader class. The retired `text_contains` matcher likewise fails\n validation instead of silently broadening an old intent-dependent rule.\n Fallback holds in interactive Auto-Review escalate to one stateless guardian\n request. The request contains the exact held call and deterministic\n observations as separate JSON fields. Conversation history, skill\n instructions, attached file contents, and other expanded model context are\n excluded. The guardian does not infer user intent or compute an authorization\n score. It exposes no tools and returns a risk level, allow/deny, and a\n rationale. High or critical risk cannot auto-run even if the model says\n allow. An oversized exact call is denied rather than truncated. Exactly one\n reviewer request is made; incomplete or malformed output, timeout,\n cancellation, provider failure, or an empty rationale all fail closed. The\n deterministic floor is never model-reviewed, and headless adapters use the\n deterministic-only tier. The pinned Codex, Kimi, and DeepSeek source\n boundaries are linked from [Permission Posture](MODES.md#permission-posture).\n Reviewer outcomes emit `tool.auto_review` audit events with\n `gate = \"guardian\"`.\n\n Auto-review decisions emit `tool.auto_review` audit events with\n `gate = \"deterministic\"` when tool\n audit logging is enabled. Future PreToolUse/PostToolUse hooks can add\n observer input around this layer, but the configured auto-review policy is\n evaluated before a tool call is allowed to proceed.\n- `managed_config_path` (string, optional): managed config file loaded after user/env config.\n- `requirements_path` (string, optional): requirements file used to enforce allowed approval/sandbox values.\n- `max_subagents` (int, optional): defaults to `64` and is clamped to `1..=128`.\n- `subagents.*` (optional compatibility table): manual per-role model pins\n for direct and Workflow `agent` starts. An explicit saved profile wins,\n then a manual role pin, then a unique saved role pin. Conflicting tool\n `model` or `model_strength` choices are refused before admission. Unpinned\n roles allow task model/strength choices before inherited defaults.\n `[subagents.roles.<role>] model = \"provider/model\"` folds into the existing override\n map and wins over `[subagents.models]`, then the convenience keys. Structured\n canonical role keys win over legacy aliases. Only this structured syntax\n separates the explicit provider from the model suffix; unknown providers\n fail before admission. Bare structured model ids inherit the active provider.\n Legacy scalar/map values preserve namespaced provider-owned ids unchanged.\n Supported convenience keys are\n `default_model`, `worker_model`, `scout_model`, `planner_model`,\n `reviewer_model`, `custom_model`, `max_concurrent`, `max_admitted`,\n `launch_concurrency`, `token_budget`, `api_timeout_secs`, and\n `heartbeat_timeout_secs`. The v0.9.x keys `explorer_model`, `awaiter_model`,\n and `review_model` remain accepted as aliases. The `[subagents]\n max_concurrent` value overrides\n top-level `max_subagents` and is also clamped to `1..=128`. `[subagents]\n max_admitted` (aliases: `max_total`, `admission_limit`) is the bounded total\n of queued plus running sub-agents; it defaults to `1024`\n (`MAX_SUBAGENT_ADMISSION`, `crates/tui/src/config/subagent_limits.rs:21`,\n applied at `config.rs:6400`) so high-fanout turns can queue and drain while\n runtime launch pressure remains bounded, and is clamped to\n `max_concurrent..=1024`. `[subagents]\n launch_concurrency` sets how many direct children start at once before the\n rest queue for a launch slot; it defaults to the resolved `max_subagents` cap\n and is clamped to `1..=max_subagents` (the deprecated\n `interactive_max_launch` key is accepted as an alias, with the new key\n winning when both are set). `[subagents] token_budget` is an optional\n aggregate token ceiling for each root `agent` run and its descendants; unset\n or `0` preserves unlimited legacy behavior. `[subagents] api_timeout_secs`\n controls the per-step API timeout for sub-agent model calls and is clamped to\n `1..=3600`, with `0` or unset preserving the 600 second default. A timeout\n after possible dispatch interrupts with a preserved checkpoint and unknown\n outcome. Only a transient connection timeout proven to precede dispatch uses\n exponential backoff, up to 5 retries.\n `[subagents] heartbeat_timeout_secs` controls stale running agent cleanup,\n defaults to `300`, and is clamped to `30..=3600` while staying above the\n resolved API timeout. `[subagents.providers.<provider>]` accepts the same\n fanout, depth, budget, and timeout knobs (`enabled`, `max_concurrent`,\n `max_admitted`, `launch_concurrency`, `max_depth`, `token_budget`,\n `api_timeout_secs`, `heartbeat_timeout_secs`) and inherits the global\n `[subagents]` value for any key you omit. Provider keys accept canonical\n names such as `deepseek`, `zai`, `openrouter`, `anthropic`, plus convenience\n aliases such as `glm` for Z.ai and `deepseek_api` for direct DeepSeek:\n\n ```toml\n [subagents]\n max_concurrent = 20\n launch_concurrency = 20\n max_admitted = 200\n max_depth = 6\n\n [subagents.providers.deepseek]\n max_concurrent = 20\n launch_concurrency = 20\n max_admitted = 200\n\n [subagents.providers.glm]\n max_concurrent = 4\n launch_concurrency = 3\n max_admitted = 12\n max_depth = 2\n\n [subagents.providers.openrouter]\n max_concurrent = 5\n launch_concurrency = 3\n max_admitted = 20\n ```\n\n `/config subagents status` prints both global values and the active\n provider's resolved profile so rate-limit tuning is visible in the TUI.\n `[subagents.models]` accepts lower-case Fleet role keys such as `worker`,\n `scout`, `planner`, `reviewer`, `builder`, and `verifier`; legacy type keys\n remain accepted during v0.9.x. Values are validated\n against the active provider at spawn time; direct DeepSeek requires DeepSeek\n IDs, while OpenAI-compatible/custom provider routes pass explicit model IDs\n through to that provider. To route a child to a different provider than the\n parent session, save a Fleet/AgentProfile with explicit `provider` and\n `model` fields (including user-named custom providers such as `lm-studio`)\n and call `agent(profile: \"...\")`; see [SUBAGENTS.md](SUBAGENTS.md).\n- `skills_dir` (string, optional): defaults to `~/.codewhale/skills` (each skill is\n a directory containing `SKILL.md`). After workspace trust, project skills\n take precedence, with `.codewhale/skills` ahead of compatible roots such as\n `.agents/skills`. Flat `./skills` requires explicit opt-in below. The runtime\n also discovers global agentskills.io-compatible `~/.agents/skills` and the\n broader Claude-ecosystem\n `~/.claude/skills`. With the default bundle policy and no `skills_dir` override, first launch installs versioned bundled skills for common\n workflows including skill creation, delegation, MCP/plugin scaffolding,\n documents, presentations, spreadsheets and PDFs. Feishu/Lark is outside the\n default pack. Only\n Codewhale-owned roots (`<workspace>/.codewhale/skills` and\n `~/.codewhale/skills`) are writable install/import targets; compatible harness\n roots stay read-only. Bare `/skills` opens Extensions → Skills (owned-only,\n zero network); `/skills manage` opens the manager. See [SKILLS.md](SKILLS.md)\n for the manager, audit statuses, provenance markers, and mutation rules, and\n [CLAUDE_PLUGIN_COMPAT.md](CLAUDE_PLUGIN_COMPAT.md) for the supported boundary\n between portable `SKILL.md` bundles and Claude Code plugin runtimes.\n- `[skills.bundled].install` (`\"auto\"` default, `\"all\"`, `\"none\"`): controls bundle writes independently of discovery. `auto` keeps deletion-as-opt-out and disables installation when `skills_dir` is set. `all` repairs enabled missing skills into `$CODEWHALE_HOME/skills` (normally `~/.codewhale/skills`) even with a discovery override. `none` writes nothing and leaves the marker untouched. Modified and managed bodies are preserved in every mode.\n- `[skills.bundled].selected` / `disabled` / `explicit_only` (arrays of names, default `[]`): selection limits installation and discovery; empty selects the whole bundle. Disabled wins and reveals lower-root copies. Explicit-only retains user invocation while removing the shipped copy from the model index and model loading. Aliases share canonical policy. These settings affect recognized global bundle copies only and never delete files. Profile fields merge separately; `[]` clears an inherited list. `doctor` warns about unknown/ignored entries and `/skills inspect` reports per-path reasons. See [bundled skills](SKILLS.md#bundled-skills).\n- `[skills].listing_max_description_chars` (integer, default `120`, range `1..=40000`): ambient-index description cap, shared by parent and child prompts and `/skills inspect`.\n- `[skills].listing_budget_fraction` (finite number, default `0.002`, greater than `0` and at most `1`): share of the route window for that same index, estimated at four characters per token and clamped to `2400..=40000` characters. Explicit profile fields override inherited values independently; `/context` uses the same resolution. Full skill loading is unaffected.\n- `[skills].scan_codewhale_only` (bool, default `false`): when `true`, session\n skill discovery ignores cross-tool roots such as `.claude/skills`,\n `.opencode/skills`, `.cursor/skills`, and `~/.agents/skills`. Codewhale still\n scans `<workspace>/.codewhale/skills`, `~/.codewhale/skills`, and any explicit\n `skills_dir` override. The Skills Manager can still toggle a local compatible\n audit scan independently of this runtime knob — see [SKILLS.md](SKILLS.md).\n- `[skills].flat_workspace_root` (bool, default `false`): opt in to the flat\n `<workspace>/skills` compatibility root after workspace trust. Without this\n opt-in it is an audit candidate only; an explicit `skills_dir` remains an\n alternative. `scan_codewhale_only = true` excludes the flat compatibility\n root regardless of this flag, unless it is the explicit `skills_dir`.\n- `[skills].registry_url` / `[skills].max_install_size_bytes` (optional): used by\n `/skills --remote`, `/skills suggest <task>`, `/skills sync`, and `/skill\n install|update`. The default manager open path does not contact the registry.\n- `[verifier].enabled` (bool, default `false`): enables automatic\n claim-of-done verifier preview once that runtime trigger is active. The\n manual `run_verifiers` tool is still available when this is false.\n- `mcp_config_path` (string, optional): defaults to `~/.codewhale/mcp.json`, with\n legacy `~/.deepseek/mcp.json` fallback when the Codewhale path is absent.\n Custom paths must be absolute; a relative value falls back to the user-global\n path so changing the launch directory cannot silently change the MCP pool.\n It is visible in `/config` and can be changed from the TUI. The new path is\n used immediately by `/mcp`; run `/mcp reload` to rebuild the model-visible\n MCP tool pool without restarting the TUI. A headless runtime still needs a\n restart because its `ConfigReload` request does not refresh MCP connections.\n- `notes_path` (string, optional): defaults to `~/.codewhale/notes.txt`, with\n legacy `~/.deepseek/notes.txt` fallback when the Codewhale path is absent, and\n is used by the model-visible `note` tool.\n- `[memory].enabled` (bool, optional): defaults to `false`. When `true`,\n the TUI loads the user memory file into a `<user_memory>` prompt block,\n enables `# foo` quick-capture in the composer, surfaces the `/memory`\n slash command, and registers the `remember` tool. The same toggle is\n available via `DEEPSEEK_MEMORY=on`.\n- `memory_path` (string, optional): anchors the native memory store. The\n configured filename is **not** the file that is written. Under the Native\n backend (the only backend) the store is re-rooted to\n `<parent-of-memory_path>/memory/global/MEMORY.md` — so the default\n `~/.codewhale/memory.md` yields `~/.codewhale/memory/global/MEMORY.md`\n (plus workspace-scoped files and a rebuildable SQLite FTS5 index). See\n [`MEMORY.md`](MEMORY.md) for the full feature surface (`# foo` composer\n prefix, `/memory` slash command, `remember` tool, opt-in toggle).\n- `snapshots.*` (optional): side-git workspace snapshots for file rollback:\n - `[snapshots].enabled` (bool, default `true`)\n - `[snapshots].max_age_days` (int, default `7`)\n - snapshots live under\n `~/.codewhale/snapshots/<project_hash>/<worktree_hash>/.git`, with legacy\n `~/.deepseek/snapshots/...` fallback when only the legacy state exists, and\n never use the workspace's own `.git` directory\n- `context.*` (optional):\n - `[context].project_pack` (bool, default `false`): include a deterministic\n project context pack (a large pretty-printed directory listing) in the\n stable prompt prefix (#4781). Useful for weak tool-calling models; the\n model can rebuild the same information with one `File` call.\n - The removed seam-manager keys (`enabled`, `verbatim_window_turns`,\n `l1_threshold`, `l2_threshold`, `l3_threshold`, `seam_model`) are\n ignored if an older config still carries them; they no longer load.\n- `compaction.*` (optional, config.toml): how a compaction pass behaves once\n it fires. `auto_compact` / `auto_compact_threshold_percent` (settings.toml)\n still decide *when* it fires. Both keys are absent by default, and absent\n means the built-in behaviour, unchanged:\n - `[compaction].summary_instructions` (string, default empty): standing\n operator instructions appended to the summarizer prompt as a clearly\n delimited \"Additional instructions from the operator\" section on **every**\n pass, manual and automatic — the effort-free counterpart to a one-off\n `/compact <focus>`, which still composes after this text. Useful for\n \"always list exact file paths and line numbers\", \"always restate open\n decisions and their trade-offs\", \"always write a TL;DR first\". Truncated\n at 4 000 characters with a warning naming the key; whitespace-only reads\n as unset. The summarizer still runs with no system prompt and no tools —\n this suffix is the only operator-authored input it sees.\n - `[compaction].retained_user_message_tokens` (int, default `20000`, clamped\n to `2000`–`200000`; also accepted as `retained_user_message_max_tokens`):\n token budget for the recent plain user messages kept **verbatim** in the\n replacement history. Raising it keeps more of the user's own earlier\n messages instead of only whatever the lossy summary captured; the\n last-round survival contract still applies on top. The `/compact` receipt\n names the effective budget and whether operator instructions were applied,\n so you can tell the knob took effect.\n- `retry.*` (optional): HTTP retry settings. The following backoff fields govern\n ordinary transport retries; HTTP 429 uses the independent policy below:\n - `[retry].enabled` (bool, default `true`)\n - `[retry].max_retries` (int, default `3`): ordinary transport retries\n - `[retry].initial_delay` (float seconds, default `1.0`)\n - `[retry].max_delay` (float seconds, default `60.0`)\n - `[retry].exponential_base` (float, default `2.0`)\n - `[retry].jitter` (bool, default `true`): randomize ordinary transport backoff\n - `[retry].jitter_factor` (float, default `0.1` = ±10%; clamps to `0.0..=1.0`)\n - `[retry].respect_retry_after` (bool, default `true`): wait for a server\n `Retry-After` preference retained for legacy retry configuration; it does not\n disable HTTP 429 server minima\n\n HTTP 429 recovery has its own validated `[retry.rate_limit]` fields:\n\n - `max_attempts` (integer, default `20`, range `1..=1000`): total attempts,\n including the first\n - `recovery_budget_secs` (integer seconds, default `600`, range `1..=86400`)\n - `initial_delay_secs` (integer seconds, default `5`)\n - `max_delay_secs` (integer seconds, default `60`), with\n `1 <= initial_delay_secs <= max_delay_secs <= 86400`\n\n This policy always honors server minima and uses equal jitter, independently\n of the legacy `initial_delay`, `max_delay`, `exponential_base`, `jitter`,\n `jitter_factor` and `respect_retry_after` fields. `max_retries = 0` does not\n disable 429 recovery; set `retry.rate_limit.max_attempts = 1` to disable only\n automatic 429 retries, or `retry.enabled = false` to disable this HTTP retry\n loop. A safe pre-dispatch HTTP/1.1 fallback inside a stream-opening operation\n remains possible. The one-hour limit on actual provider pauses and automatic sleeps\n still applies. See [HTTP rate-limit retries](rate-limit-retries.md) for the\n budget, server-minimum and active-time boundaries.\n\n `[retry]` schedules HTTP-request retries inside the client. Each physical\n streaming request gets its own `stream.open_timeout_secs` response-header\n wait. Provider backoff and honored `Retry-After` waits happen between\n attempts, outside that timer, so the configured retry count remains\n reachable. Inference retries require an ordinary rate limit or a proven\n transient connection failure before dispatch. Only that connection failure\n can switch once to HTTP/1.1; a header stall cannot. Both inference clients\n disable redirects and reqwest protocol retries. Explicit plan-window\n exhaustion is a terminal quota error. Server failures, uncertain timeouts\n and interrupted streams preserve unknown coverage and stop without\n automatically reissuing inference. Read-only catalog GETs retain bounded\n retries at their actual method owner. The `[stream]` keys below bound safe\n pre-dispatch recovery and stream consumption; legacy `tui.stream_max_*`\n keys remain configuration fallbacks.\n- `[notifications]`: notification delivery, attention, categories and audio share one\n policy. `quiet = true`, `method = \"off\"`, `condition = \"never\"` and disabled\n categories suppress both the banner and Codewhale's selected sound.\n- `notifications.method`: `auto` (default), `osc9`, `kitty`, `ghostty`, `bel`, `off`.\n- `notifications.condition`: `unfocused` (default), `always`, `never`. When absent,\n the legacy `tui.notification_condition` remains the fallback. `always` also\n bypasses the duration threshold; `unfocused` requires two seconds away.\n- `notifications.threshold_secs`: nonnegative integer, default `30`.\n- `notifications.include_summary`: boolean, default `false`.\n- `notifications.sound`: optional `off`, `whale`, `bell`, `beep`, `file`.\n A selected value controls audio across enabled categories. Absent keeps legacy\n `completion_sound` and `event_sound` choices; `off` overrides both.\n- `notifications.sound_file`: custom local WAV path for `sound = \"file\"` or legacy\n `completion_sound = \"file\"`.\n- `notifications.subagent_completion`: `always`, `final-only` (default), `off`.\n Covers all background work that finishes: sub-agents, background shells and\n durable tasks. `final-only` sends one notice naming everything that finished\n once no agent, workflow or durable task is still running; a running shell\n (a dev server, a watcher) never holds it back. `always` sends one per item.\n- `notifications.quiet`: boolean, default `false`.\n- `notifications.events`: six boolean categories, all enabled by default; see below.\n- `notifications.completion_sound`: legacy completion cue, default `off`, with the\n same values as `sound`. Used only when `sound` is absent.\n- `notifications.event_sound`: legacy `enabled` (default `false`), `events`\n (default `[\"turn-complete\", \"approval-needed\"]`), and `quiet` (default `false`).\n `min_interval_ms` (default `2000`) applies to each category's audio in both modes.\n- `tui.alternate_screen` (string, optional, default `auto`): which screen an interactive session starts on. `auto` and `always` start on the TUI-owned alternate screen; `never` starts in inline mode — a ratatui viewport the full height of the terminal with no alternate screen, so the shell's scrollback survives the session and stays scrollable after exit. `/fullscreen` and `/inline` switch it in-process; a switch that the terminal refuses rolls back and says why. Inline mode paints the whole transcript inside its viewport — nothing is written into the host scrollback while the session runs.\n- `tui.mouse_capture` (bool, optional, default `true` on non-Windows terminals and on Windows Terminal/ConEmu/Cmder when the alternate screen is active; `false` on legacy Windows console and inside JetBrains JediTerm — PyCharm/IDEA/CLion/etc. — where mouse-event escapes leak into the input stream as garbled text, see #878 / #898): enable internal mouse scrolling, transcript selection, right-click context actions, and transcript scrollbar dragging. TUI-owned drag selection copies the intersected cells, removes visual wrap-column line breaks from paragraphs, and keeps selection scoped to the transcript pane; the payload is Markdown source by default, see `tui.selection_copy_markdown` below. Set this to `false` or run with `--no-mouse-capture` for raw terminal selection; set it to `true` or run with `--mouse-capture` to opt in anywhere it's defaulted off. On raw terminal selection, especially on legacy Windows console or when mouse capture is disabled, selection may cross the right workbar and include visual wraps because the terminal, not the TUI, owns the selection.\n On Linux, finishing a transcript or composer selection quietly copies text to\n PRIMARY, leaving the regular clipboard unchanged. Middle-click inside the\n composer pastes PRIMARY at the pointer without submitting it. This uses native\n X11 or Wayland data control; compositors must support PRIMARY selection. Over\n SSH without a forwarded graphical display, use your terminal's selection/paste\n gestures or `--no-mouse-capture`. Explicit Copy still uses the regular clipboard.\n\n- `tui.selection_copy_markdown` (bool, optional, default `true`): copy TUI-owned\n transcript selections (drag release, context-menu Copy, and `Cmd+C`/`Ctrl+C`\n on an active selection) as Markdown source instead of rendered text. Every\n intersected cell serializes through the same canonical projection `Ctrl-Y`\n and `/copy` use — user and assistant cells keep their authored Markdown,\n other cells keep their full transcript form — partial intersections round out\n to whole cells, cells join with blank lines, and a toast names the copied\n cell count. Set `false` to copy the rendered text as displayed. Composer\n selections and the Linux PRIMARY auto-copy are unchanged; PRIMARY always\n carries rendered text.\n\n- `tui.stream_chunk_timeout_secs` (int, optional, default `900`): per-SSE-chunk idle timeout for streamed model responses. Slow local or compatible servers can raise this with `/config stream_chunk_timeout_secs <seconds>` (add `--save` to write canonical `stream.chunk_timeout_secs`); `0` maps to the default and explicit values must be `1..=3600`. The legacy `DEEPSEEK_STREAM_IDLE_TIMEOUT_SECS` env var is still honored when this key is omitted.\n- `tui.osc8_links` (bool, optional, default on for macOS/Linux, off for Windows): emit OSC 8 escape sequences around URLs in transcript output so supporting terminals (iTerm2, Terminal.app 13+, Ghostty, Kitty, WezTerm, Alacritty, recent gnome-terminal/konsole) can open them with the terminal's link gesture—usually Cmd-click on macOS and Ctrl-click on Linux/Windows. Terminals without OSC 8 support render the plain label and ignore the escape. The escapes are emitted out-of-band (not inside buffer cells), so column corruption is not a concern; set `false` only for terminals that misrender the OSC 8 terminator itself. Windows legacy consoles default off; opt in with `true`.\n- `tui.max_model_steps` (int, optional, default uncapped): optional model-step ceiling for one ordinary turn. Omission or `0` leaves model steps uncapped; explicit positive values are preserved (`4294967295` is the compatibility value for no limit). Headless `exec` and Fleet workers also have no implicit model-step ceiling; `exec --max-turns N` and positive worker budgets still apply. At exhaustion the turn ends `Failed` with `Maximum model steps reached before completion (limit: N)` after one bounded final-report response when needed. Cumulative wall-clock and per-stream limits remain independent. Active interactive goal turns use `goal.max_steps` instead (also uncapped by default); see the Goal loop section below.\n- `tui.turn_wall_clock_secs` (int, optional, default: no limit): cumulative per-turn wall-clock budget in seconds, measured across every model step of one turn (not per request). Time blocked on a human approval is excluded. Omitted or `0` means no limit; positive values are preserved without an added runtime ceiling. When exhausted the turn stops before authorizing another billable request with a message naming the limit and the key to raise.\n- `tui.stream_max_resumes` (int, optional, default `3`): turn-level recovery after a proven transient connection failure before request dispatch. `0` disables this additional recovery; values clamp to `0..=10`. Missing response headers, empty or partial streams, sleep and network drops never authorize inference replay.\n- `tui.stream_max_transparent_retries` (int, optional, default `2`): retained for configuration compatibility, clamped to `0..=10`. Interrupted streams no longer trigger transparent inference reissues, including before visible content.\n- `tui.stream_max_errors` (int, optional, default `5`): retained for configuration compatibility; `0` selects the default and positive values clamp to `1..=50`. Core ends an uncertain stream on its first read error or error frame regardless of this value.\n- `tui.stream_open_timeout_secs` (int, optional, default `45`): wait for a streaming request's response headers (connection setup included). A header stall ends with an unknown outcome and does not replay over HTTP/1.1. Omitted or `0` falls back to `CODEWHALE_STREAM_OPEN_TIMEOUT_SECS`, then the default; values clamp to `5..=300`.\n- `tui.connect_timeout_secs` (int, optional, default `30`): TCP/TLS connect timeout for the model HTTP client. Omitted or `0` uses the default; values clamp to `1..=300`.\n- `tui.force_http1` (bool, optional, default `false`): pin the model HTTP client to HTTP/1.1, for provider edges or proxies that mishandle long-lived HTTP/2 streams. `CODEWHALE_FORCE_HTTP1=1` does the same; either one pins.\n- `tui.stream_max_content_mb` (int, optional, default `10`) and `tui.stream_max_duration_secs` (int, optional, default `1800`): per-step caps on one stream's accumulated content and wall-clock duration. `0` selects the default; values clamp to `1..=512` MB and `10..=86400` seconds.\n- `transcript.prose_measure` (positive integer, optional, default absent = full width): wrap cap, in columns, for prose cells — user messages, assistant answers, and reasoning/thinking blocks — in the live transcript (#5436). Absent (or `0`) spends the full content width, consistent with tool/status cells and the #5322 wide-frame decision; the former 105-column prose rail is gone. Set a positive whole number (e.g. `prose_measure = 120` under `[transcript]`) to restore a bounded reading measure on ultrawide terminals. Narrow terminals always keep their content width — the cap clamps from above only. Tool, diff, and status cells never inherit this cap. Invalid values (negative or non-integer) are rejected at startup with a `transcript.prose_measure` config error. Resolved once per render pass, so the main transcript cache and the full-screen overlay always agree on the effective width.\n- `hooks` (optional): lifecycle hooks configuration (see `config.example.toml`).\n- `features.*` (optional): feature flag overrides (see below).\n\n### Stream and transport settings\n\n`[stream]` is the canonical table for model-stream policy and its HTTP clients.\n`codewhale config dump` and `codewhale config get stream` show effective values\nfrom the runtime resolver, including environment fallbacks and clamps; this does\nnot persist defaults. Set or unset individual fields with, for example,\n`codewhale config set stream.open_timeout_secs 120` and\n`codewhale config unset stream.open_timeout_secs`. Per-run\n`--set stream.open_timeout_secs=120` uses the same validation.\n\n| `[stream]` key | Default | Accepted behavior | Legacy `[tui]` fallback |\n| --- | --- | --- | --- |\n| `open_timeout_secs` | 45 | Positive values clamp to 5–300; 0 or omission falls through to the environment/default | `stream_open_timeout_secs` |\n| `chunk_timeout_secs` | 900 | 0 selects default; positive values clamp to 1–3600 | `stream_chunk_timeout_secs` |\n| `max_resumes` | 3 | Proven transient pre-dispatch recovery only; 0 disables, maximum 10 | `stream_max_resumes` |\n| `max_transparent_retries` | 2 | Compatibility value; interrupted streams never reissue inference | `stream_max_transparent_retries` |\n| `max_stream_errors` | 5 | Compatibility value; Core ends on the first uncertain stream error | `stream_max_errors` |\n| `max_duration_secs` | 1800 | Per-stream wall clock; 0 selects default, positive values clamp to 10–86400 | `stream_max_duration_secs` |\n| `max_content_mb` | 10 | Per-stream content; 0 selects default, positive values clamp to 1–512 MiB | `stream_max_content_mb` |\n| `connect_timeout_secs` | 30 | TCP/TLS setup; 0 selects default, positive values clamp to 1–300 | `connect_timeout_secs` |\n| `force_http1` | false | Boolean; a truthy environment pin always enables HTTP/1.1 | `force_http1` |\n| `tcp_keepalive_secs` | 30 | Idle time before TCP keepalive probes; 0 disables, positive values clamp to 1–3600 | none |\n| `http2_keep_alive_interval_secs` | 15 | PING interval on active HTTP/2 connections; 0 disables, positive values clamp to 1–3600 | none |\n| `http2_keep_alive_timeout_secs` | 20 | PING acknowledgement deadline; 0 selects default, positive values clamp to 1–3600 | none |\n\nFor each field, an explicit canonical value wins over the legacy `[tui]` field,\nincluding zero or false; an absent canonical field preserves the legacy value.\nThe existing environment precedence is unchanged: a positive configured header\nwait wins over `CODEWHALE_STREAM_OPEN_TIMEOUT_SECS` (then its `DEEPSEEK_` alias);\nzero falls through to those variables. An omitted chunk timeout uses\n`CODEWHALE_STREAM_IDLE_TIMEOUT_SECS` (then its `DEEPSEEK_` alias), whereas an\nexplicit zero uses 900. `CODEWHALE_FORCE_HTTP1` (legacy `DEEPSEEK_FORCE_HTTP1`)\nis ORed with the selected config flag, so a false config flag cannot defeat a\ntruthy environment pin. Profile overlays merge canonical fields independently.\n\nHTTP transport values apply to all newly constructed model, catalog and HTTP/1\nfallback clients. They do not rebuild an active client, govern MCP/other network\nservices, or send HTTP/2 PINGs on idle pooled connections. HTTP/2 knobs have no\neffect when HTTP/1.1 is pinned. The operating system controls TCP probe details;\nthese settings do not disable certificate validation. `[retry]` still owns the\nHTTP-request backoff schedule independently of stream-level budgets.\n\n### Workspace notes\n\n`/note` manages a simple notes file in the current workspace at\n`.codewhale/notes.md` (legacy `.deepseek/notes.md` is the fallback path when\nno `.codewhale/notes.md` exists yet).\nExisting `/note <text>` usage still appends a note.\nThe management forms are:\n\n| Command | Action |\n|---|---|\n| `/note <text>` | Append a note (legacy shorthand) |\n| `/note add <text>` | Append a note explicitly |\n| `/note list` | List notes with temporary 1-based numbers |\n| `/note show <n>` | Show the full note at number `n` |\n| `/note edit <n> <text>` | Replace note `n` with new text |\n| `/note remove <n>` | Delete note `n`; `rm` and `delete` are aliases |\n| `/note clear` | Empty the workspace notes file |\n| `/note path` | Show the resolved workspace notes path |\n\nThe numbers shown by `/note list` are not stored in the file; they are derived\nfrom the current order each time notes are read. This keeps the file format\ncompatible with the existing `---`-separated notes.\n\n### User memory\n\nUser memory is split across one top-level path setting and one opt-in\ntoggle table:\n\n```toml\n# Anchors the store only — actual writes go to\n# ~/.codewhale/memory/global/MEMORY.md (see MEMORY.md).\nmemory_path = \"~/.codewhale/memory.md\"\n\n[memory]\nenabled = true\n```\n\nNotes:\n\n- `memory_path` stays at the top level beside `notes_path` and\n `skills_dir`; it is not nested under `[memory]`.\n- The configured path is an **anchor**: its parent directory gains\n `memory/global/MEMORY.md`, workspace-scoped files, and `index.db`.\n Pointing `memory_path` at the native layout path itself would double-nest\n (`…/memory/global/memory/global/MEMORY.md`); keep the legacy-style\n anchor filename.\n- `DEEPSEEK_MEMORY_PATH` overrides the anchor path from the environment.\n- `DEEPSEEK_MEMORY=on` (also `1`, `true`, `yes`, `y`, or `enabled`)\n flips the feature on without editing `config.toml`.\n- The feature is inert when disabled: no file is injected, `# foo`\n falls through to normal message submission, and the model does not\n see the `remember` tool.\n- See [`MEMORY.md`](MEMORY.md) for examples and the full `/memory`\n command surface.\n\n### Goal loop (`[goal]`)\n\nOperate-mode goals run to their completion gate with no default token, time, or\ncontinuation ceiling (#5052). Token/time budgets, when explicitly supplied,\nare telemetry only and do not stop a goal. Users who want a circuit breaker can\nopt into one:\n\n```toml\n[goal]\n# Optional safety backstop on automatic goal continuation passes.\n# Default: 0 (unlimited). Set a positive value to opt into a ceiling.\nmax_continuations = 100\n\n# Optional cancellable quiet period between successful turns. This is useful\n# for coordinator goals that should poll on a cadence instead of keeping one\n# provider turn open. Default: 0 (continue immediately).\ncontinuation_delay_seconds = 300\n\n# Optional per-turn model-step ceiling while a goal is active (#6512).\n# Default: uncapped (0 or absent). Positive values are preserved.\n# This bounds each provider turn, never the number of continuation passes.\nmax_steps = 0\n\n# Optional independent judges (#6013). Default: false (the model's own\n# update_goal call is final). When true, one adversarial critique of the claim\n# runs before `update_goal` completes (complete_judge) or blocks (blocked_judge)\n# the goal. Each judged call is billed as auxiliary usage.\ncomplete_judge = false\nblocked_judge = false\n```\n\nThe effective delay is capped at 86,400 seconds (24 hours); use an automation\nfor schedules that are less frequent than once per day.\n\nWhen an explicit backstop fires, the goal pauses with a status message naming\n`[goal] max_continuations` and a warning is logged; resume the goal after\ninspecting progress, or raise/disable the backstop.\n\n`[goal] max_steps` governs one engine turn at a time. Like ordinary interactive\nturns, omitted or `0` leaves model steps uncapped. Explicit per-invocation\nceilings — `exec --max-turns N`, child-worker caps — always win over it. At exhaustion\nit gets one bounded final report and the turn classifies as budget-exhausted. An\nunfinished goal then pauses with the BudgetLimit reason instead of re-arming\nanother goal turn — resume it explicitly after reviewing the report. Wall-clock\nand stream protections are separate and still apply.\n\nWith `[goal] complete_judge = true`, a refuted completion claim keeps the goal\nactive and records the judge's findings as critical gaps. A missing,\nunparseable, uncertain or failed verdict never completes the goal; it records\none fixed gap, so a persistent judge outage ends in the existing no-progress\npause rather than a silent pass. With `[goal] blocked_judge = true`, a refuted\nblocker keeps the goal active, and an unverifiable one hands the goal back as a\nyield that your next message resumes. The judge reads only the objective, the\nevidence and the verification receipt, not the transcript or the diff. The\noperator `POST /v1/threads/{id}/goal/complete|block` routes and `/goal`\ncommands stay ungated.\n\nThe delay starts only after a successful turn while an explicitly created goal\nis still active. `/goal pause`, `/goal done`, `/goal blocked`, `/goal clear`,\nEsc, or Ctrl+C cancels a pending continuation before another provider request\nstarts. Failed turns and policy/route failures never schedule another turn.\nOnly the numeric cadence is stored in config; no prompt, credential, or secret\nis persisted for the loop.\n\n### Reasoning-only recovery (`[reasoning_only]`)\n\nWhen a model finishes with only reasoning and no answer or tool call, Codewhale\ncan request an answer again. A clean stop permits two retries by default:\nthe first keeps the request unchanged, and later retries carry a request-scoped\nnudge. The default nudge is `Continue: give your answer, or make the next tool call.`\n\n```toml\n[reasoning_only]\nmax_reprompts = 2\non_output_limit = false\n```\n\nSet `on_output_limit = true` to also retry `length`/`max_tokens` stops that\nproduced no answer or tool call, including providers that did not stream their\nhidden reasoning. This option is off by default. Output-limit retries carry the\nnudge immediately and share the same `max_reprompts` allowance. Set\n`max_reprompts = 0` to disable both reasoning-only and output-limit recovery;\ntransport and empty-stop recovery have their own existing limits.\n\nThis applies to ordinary interactive, exec and Runtime API engines and Fleet\nworkers. Workers use the captured parent configuration. RLM keeps its separate\ncompletion/FINAL/REPL stop protocol and does not use this option.\n\nEvery retry is a fresh request and may repeat the model's reasoning. It does not\ncarry hidden reasoning forward, raise output limits or bypass route, context,\nturn, cancellation or tool-admission controls. Each request can incur provider\ncost. Existing output-limit overrides still apply, and request preparation may\nreduce or refuse an allowance when input grows. Restart existing engines after\nchanging these settings.\n\nThe nudge appears in one outbound request without joining conversation history\nor later resumed model context. Retry status retains its stable protocol prefix\nand localizes its human text. Its receipt is visible in the transcript; Fleet\nalso stores an owner-bound prepared-request receipt in its existing private\nartifact before dispatch. A prepared receipt does not claim a completed model\ncall. Existing version-1 artifacts remain readable.\n\nTo customize or disable the nudge:\n\n```toml\n[reasoning_only]\nreprompt_message = \"Give your answer, or make the next tool call.\"\n```\n\nAn empty `reprompt_message = \"\"` disables the nudge, including on output-limit\nretries; the retry still counts against `max_reprompts` and uses the unchanged\nrequest. Configuration with recovery enabled does not guarantee the model will\ncomplete a long proof within its allowance.\n\n### Notifications\n\nNotification controls are available in the existing `/config` settings view,\nfrom terminal commands, and through the CLI. All write the same `config.toml`\nkeys. Terminal changes apply immediately; add `--save` to keep them. CLI writes\napply when the next process loads its configuration.\n\n```sh\ncodewhale config set notifications.sound whale\ncodewhale config set notifications.events.approval-needed false\ncodewhale config set notifications.quiet true\ncodewhale config get notifications\ncodewhale config unset notifications.quiet\n```\n\n```text\n/config notifications sound whale --save\n/config notifications condition unfocused --save\n/config notifications quiet true\n/config notifications status\n```\n\nNested CLI edits preserve TOML types, unrelated keys and comments. Unset removes\nonly the selected leaf. `notifications.sound legacy` in the TUI, or CLI unset of\n`notifications.sound`, restores previous sound choices. Invalid values are\nrejected before file or session changes. In an active TUI profile, saved edits\nupdate its notification table when it owns one; otherwise they update the\ninherited root table. The settings detail keeps saved and current values distinct.\n\n```toml\n[notifications]\nmethod = \"auto\" # auto | osc9 | kitty | ghostty | bel | off\ncondition = \"unfocused\" # unfocused | always | never\nthreshold_secs = 30\ninclude_summary = false\nsound = \"whale\" # optional; sound is opt-in, not enabled by default\nquiet = false\n\n[notifications.events]\nturn-complete = true\nsubagent-terminal = true\napproval-needed = true\ninput-needed = true\nelevation-needed = true\nmodel-notify = true\n```\n\n`quiet = true` mutes every category without changing saved choices. `method =\n\"off\"` also stops both banner and selected audio. Disabling a category stops its\nsound. Attention and duration gates apply before any sink runs. Title animation\ncompletion is silent; only the authorized notification event can request audio.\nSuccessful turn completion is a notification category; failed/cancelled turns do\nnot create a success notification.\n\n`auto` chooses a recognized terminal protocol or the existing macOS native\nfallback; unknown terminals remain unsupported and never invent a bell. `bel`\nis an audio-only transport: one selected cue is dispatched, without a second\ntransport bell. With `sound = \"off\"`, that transport is silent. `osc9`, `kitty`\nand `ghostty` use their terminal notification protocols; tmux passthrough is\npreserved. Terminal/OS notification preferences still govern display, attribution\nand any sound the host itself adds.\n\nBy default the terminal must stay unfocused for two seconds. `condition =\n\"always\"` allows foreground notifications and bypasses the duration threshold;\n`\"never\"` suppresses all delivery. The canonical condition takes precedence over\nlegacy `[tui].notification_condition`.\n\nThe bundled `whale` is a 1.55-second original whale-inspired cue, with no\nthird-party recording. It remains an opt-in candidate pending listening approval.\nWAV playback uses a background worker: macOS `/usr/bin/afplay`, Linux `aplay`\nfrom [ALSA utilities](https://github.com/alsa-project/alsa-utils), or Windows\n`PlaySoundW`. A missing player/file or unsupported platform has no fallback bell.\nOnly one WAV plays at a time. A worker-start receipt is a dispatch attempt, not\nproof of audible playback or OS acceptance. The existing macOS `osascript`\nbanner retains Script Editor attribution; this Core change does not provide a\nbranded native Apps banner.\n\n#### Previous sound settings\n\nWhen `notifications.sound` is absent, completion uses a non-off\n`completion_sound` selection, and other events use the existing event allow-list.\nThese are compatibility inputs to the same audio decision, not separate playback\npaths. When the global sound is selected, it takes precedence over the previous\ncompletion/event choices. The default remains silent unless a legacy sound or\nexplicit `bel` transport was already selected.\n\n```toml\n[notifications.event_sound]\nenabled = false\nevents = [\"turn-complete\", \"approval-needed\"]\nmin_interval_ms = 2000\nquiet = false\n```\n\nLegacy event cues use one bell for completion, subagent completion, input and\nmodel notices; approval/elevation cues use two. The per-category repeat interval\nsurvives settings refreshes. Old unknown event names are ignored on load; new\nCLI/TUI edits require names from the six categories above.\n\nLocal approval/input/elevation prompts and error receipts remain available when\nexternal notifications are muted. Action prompts use the selected UI language\nand retire when that request settles. Repeated live notices keep their first\nexpiry; a later routine update does not hide an unresolved warning at completion.\nOptional plugin suggestion toasts and contextual tips share one session guidance\nbudget. `/config contextual_tips off` hides those toasts while preserving\nrequired notices and explicit plugin review requests.\n\n#### What a notification can contain\n\nA desktop notification is a glance surface: on macOS it can appear on the\nlock screen, and on every platform it is visible to anyone near the machine.\nCodewhale therefore builds notifications from a typed payload with a fixed\nper-event disclosure policy rather than from whatever text was on hand:\n\n| Event | Shown | Never shown |\n|---|---|---|\n| Turn complete | status line (+ elapsed/cost when `include_summary`), preview of the assistant's reply | — |\n| Sub-agent finished | status line, agent id, preview of the child's summary line | — |\n| Approval needed | the tool name | the tool description, the command, the arguments |\n| Input needed | \"Answer the question in the terminal to continue\" | the question |\n| Sandbox elevation needed | the tool name and the denial reason | the command |\n| `notify` tool | model-supplied title and body | — |\n\nEvery field is capped (80 characters for the status line, 120 for the\nidentifier, 200 for the preview), stripped of control bytes and escape\nsequences, and passed through a redactor that replaces credential-shaped\nstrings with `[redacted]`, reduces absolute local paths to `…/basename`,\nand replaces raw tool JSON with `[details hidden]`. The redactor is\ndeliberately over-eager: an unbroken 40-character run has no word\nstructure, so it is redacted even when it is not a secret.\n\n#### macOS: why the banner says \"Script Editor\"\n\nOn macOS terminals that provide no notification escape of their own —\nApple Terminal, the VS Code and JetBrains embedded terminals, plain tmux\nwithout `LC_TERMINAL` — `method = \"auto\"` falls back to `osascript`'s\n`display notification`. That command posts on behalf of the *bundled*\nhost process, and `/usr/bin/osascript` is unbundled, so macOS attributes\nthe banner to `com.apple.ScriptEditor2`. That attribution supplies the\nScript Editor icon and owns the System Settings → Notifications entry\n(alert style, previews, Do Not Disturb). `display notification` has no\nicon parameter, so this cannot be fixed from the notification code; it\nneeds Codewhale to ship a real `.app` bundle. Tracked in\n[#4834](https://github.com/codewhale-hq/CodeWhale/issues/4834). In the meantime,\niTerm2, WezTerm, Ghostty, and kitty are matched first and use their own\nnotification protocols, and `method = \"osc9\"` / `\"bel\"` / `\"off\"` opt out\nof the `osascript` path explicitly.\n\n## Automations in the terminal\n\nOpen `/automation`, then choose **New automation** (`n`) or **Edit automation**\n(`e`). The form edits the name, multiline prompt, schedule, model, workspace,\nand enabled status. `Tab` moves between fields; `Enter` inserts a newline in\nthe prompt. Schedule presets include daily, weekly, hourly, once, and a custom\nRRULE. Time fields accept `HH:MM`; their arrow controls change the time by\n15 minutes. Weekly day buttons and model search support mouse and keyboard.\n\nThe next-run preview uses the scheduler's local time zone, shown with its UTC\noffset. An enabled automation can run after **Save** (`Ctrl+S`); a paused one\nhas no scheduled next run. **Cancel** (`Esc`) discards the draft. Editing keeps\nexisting permission settings, custom schedules, and additional workspace\nentries unless the corresponding supported field is explicitly changed.\n\nSchedules are evaluated in the machine's local time zone against the wall\nclock. A wall time that does not exist on a spring-forward day is skipped and\nan ambiguous fall-back time fires once. Occurrences missed while Codewhale was\nclosed, asleep, or still running the previous occurrence are coalesced: the\nnext start runs one catch-up occurrence and then continues from the next\nfuture slot, never replaying every missed slot. An occurrence never starts\nwhile an earlier run of the same automation is still queued or running. Each\nrun is recorded durably with its status, timing and error; a run that needs a\ntool approval has no operator to ask and fails once the approval wait expires.\n\nChoosing a concrete model pins both the model and its exact configured\nprovider, including named custom routes. Later changes to the active provider\ndo not move that automation's pin. The default-model choice and legacy\ndefinitions without a provider pin keep the runtime's existing default\nbehavior. New automation records use schema v2 and tasks use v3 so older\nruntimes reject records whose provider pins they cannot preserve.\n\n## Lifecycle Outbox (`[lifecycle_outbox]`)\n\nThe lifecycle outbox is an opt-in, machine-readable stream of session,\nturn, and sub-agent lifecycle events. With a path configured, Codewhale\nappends one JSON line per event to that file — for interactive TUI\nsessions *and* headless `codewhale exec` runs — so a supervisor\n(terminal multiplexer wrapper, automation harness, alerting setup) can\nreact to what happened without scraping the screen or installing per-hook\nshell commands. Unset or empty `path` = the feature is **off** and\nbehavior is unchanged.\n\n```toml\n[lifecycle_outbox]\npath = \"~/.codewhale/notifications/outbox.jsonl\" # unset/empty = OFF\nwebhook_url = \"\" # optional; POSTs events as JSON when set\nwebhook_token = \"\" # optional bearer token for webhook_url\n```\n\n### Events emitted\n\n| Event | Kind | Fired at |\n|---|---|---|\n| `turn_start` | `turn.started` | a new turn begins (TUI TurnStarted; `exec` at message dispatch) |\n| `turn_end` | `turn.completed` / `turn.failed` / `turn.interrupted` | turn completion, kind projected from the turn status |\n| `turn_stalled` | `turn.stalled` | the stall watchdog recovers a wedged turn |\n| `subagent_spawn` | `subagent.spawned` | a sub-agent is spawned |\n| `subagent_complete` | `subagent.completed` | a sub-agent reaches a terminal state |\n| `session_start` | `session.started` | interactive session start |\n| `session_end` | `session.ended` | interactive session end |\n\n### File contract\n\nEach line is a `RuntimeEventEnvelope`:\n\n```json\n{\"schema_version\": 1, \"seq\": 3, \"event\": \"turn_start\", \"kind\": \"turn.started\",\n \"thread_id\": \"…\", \"turn_id\": \"…\", \"item_id\": null, \"timestamp\": \"…\",\n \"created_at\": \"…\", \"payload\": {…}}\n```\n\n- `seq` is monotonic per outbox file and recovers from the last written\n line when a new process opens the file.\n- Lines are written one complete JSON line per append, serialized by an\n internal writer task and flushed before the next event; concurrent\n sessions writing the same path do not interleave bytes mid-line, but\n separate processes each continue from their own recovered `seq`, so seqs\n can repeat across processes sharing one file — prefer one file per\n process for strict uniqueness.\n- Parent directories are created lazily on the first event.\n- Payloads are constructed from bounded, pre-redacted fields only — never\n raw tool arguments, environment, or full transcript text. Free-form\n fields (error messages, previews) are capped at the notification limits\n (80 headline / 120 detail / 200 preview characters) and stripped of\n control bytes.\n\n### Webhook delivery\n\nWith `webhook_url` set, every event is additionally POSTed as\n`{\"at\": \"<ISO 8601 timestamp>\", \"event\": {…}}` with\n`Authorization: Bearer <webhook_token>` when a token is configured.\nDelivery is best-effort: failures are logged and dropped, never retried\ninto the agent loop, and a failing webhook never blocks the local file\nappend.\n\n## Control Socket (`[control_socket]`)\n\nPer-session control surface for supervised operation: with the feature\nenabled, the interactive TUI binds one unix domain socket per *running*\nsession at `<sessions-dir>/<session-id>/control.sock` (mode `0600`;\n`<sessions-dir>` is the same directory the session store uses, typically\n`~/.codewhale/sessions`). The socket is removed with the session's\nartifact directory, and a stale socket left by a crashed process is taken\nover by the next launch. Unset or `enabled = false` = the feature is\n**off** (the default) and behavior is unchanged. Unix-only; on other\nplatforms the key parses but no socket is bound.\n\n```toml\n[control_socket]\nenabled = false # default: OFF\n```\n\nThe socket speaks newline-framed JSON-RPC, one request per connection:\nwrite one request line, read one response line, close.\n\n```json\n{\"id\":\"1\",\"method\":\"message\",\"params\":{\"text\":\"hello\"}}\n{\"id\":\"2\",\"method\":\"interrupt\",\"params\":{}}\n{\"id\":\"3\",\"method\":\"status\",\"params\":{}}\n```\n\n- `message` — delivers `text` as a structured user message through the\n ordinary composer dispatch path; dispatched immediately when idle,\n queued when a turn is in flight (the response's `delivery` field says\n which).\n- `interrupt` — the Esc-shaped cancel of the active turn; `cancelled`\n reports whether active work was in flight.\n- `status` — answers `turn_state` (`idle` / `in_progress` / `waiting`) and\n `goal` (`objective`, `status`, `paused`).\n\nSuccess responses echo the request id with a `type`-tagged result;\nfailures carry `error.code` (`invalid_request`, `command_error`,\n`timeout`, `server_unavailable`). Requests are bounded at 1 MiB per line\nand a handler that does not answer within 5 s is reported as `timeout`.\n\n## Tool Catalog\n\nCodewhale loads a small core native tool catalog by default and leaves less\ncommon native tools discoverable through ToolSearch. To keep specific native\ntools loaded on every request, add them to `[tools].always_load`:\n\n```toml\n[tools]\nalways_load = [\"Git\", \"notify\"]\n```\n\n### Script tools and overrides\n\nScripts in `~/.codewhale/tools/` (or `[tools].plugin_dir`) that start with a\n`# name:` header become model-visible tools, and `/plugin tools` lists them.\nThe script reads the tool's JSON input on stdin and writes a JSON\n`ToolResult` (`{\"content\": \"...\", \"success\": true}`) on stdout.\n\n```sh\n#!/usr/bin/env sh\n# name: word_count\n# description: Count words in the given text\n# schema: {\"type\":\"object\",\"properties\":{\"text\":{\"type\":\"string\"}}}\n# approval: required\n```\n\n`# approval:` takes `suggest` (the default) or `required`; either way the\ntool follows the session's approval setting. A script cannot approve itself:\n`approval: auto` is no longer supported, so such a script gets the default,\nand the runtime log (`~/.codewhale/logs/`) and `/plugin tools` name it.\n\nA script cannot replace a built-in tool either. A script whose `# name:` is\nalready registered is not loaded. `[tools.overrides]` may disable a built-in,\nor add a script or command tool under a name of its own:\n\n```toml\n[tools.overrides]\n\"Web\" = { type = \"disabled\" } # turn a built-in off\n\"audited_shell\" = { type = \"script\", path = \"audit-shell.sh\" } # a new tool\n\"Bash\" = { type = \"script\", path = \"audit-shell.sh\" } # refused: Bash is built in\n```\n\nA `script` or `command` override keyed by a built-in is refused, and the\nbuilt-in stays active. A status line names the key once per session, and the\nruntime log records it. To route a\nbuilt-in through your own wrapper, disable the built-in and register the\nwrapper under a new name. An override keyed by a drop-in script's name still\nreplaces that script. Relative `path` values resolve against the plugin\ndirectory.\n\n### `request_user_input` limits\n\n`request_user_input` asks the user a short batch of multiple-choice questions.\nBoth ceilings are configurable (#5949): raise `user_input_max_questions` when a\nresearch or planning workflow legitimately needs more clarifications, lower it\nwhen interactive triage should stay terse.\n\n```toml\n[tools]\nuser_input_max_questions = 6 # default 6, clamped to 1..=10\nuser_input_max_options = 4 # default 4, clamped to 2..=10\n```\n\nThe effective values are applied in three places at once: the tool's JSON\nschema (`minItems` / `maxItems`), its model-visible description, and the\npayload validator. A rejected payload names the key to raise, so the model can\neither resize the batch or tell the user which setting to change.\n\n### User-input wait timeout\n\nQuestions from `request_user_input` wait until answered or canceled by default\n(#6003). An omitted setting or `0` leaves the wait unbounded; a positive value\ncancels the question when that many seconds pass, capped at 86,400 (24 hours).\nHeadless `exec` runs have no\nresponder, so `request_user_input` is withheld there by default:\nthe model reports the tool absent and finishes instead of stalling.\n\n```toml\n[tools]\nuser_input_timeout_seconds = 300 # opt into 5 minutes; omitted or 0 waits indefinitely; maximum 86400\n```\n\nThis key governs question waits only. Approvals have their own clock,\n`[approval] timeout_seconds`, which waits indefinitely unless you set it.\nWall-clock and stream protections elsewhere are unaffected.\n\n## Feature Flags\n\nFeature flags live under the `[features]` table and are merged across profiles.\nDefaults are enabled for built-in tooling, so you only need to set entries you\nwant to force on or off.\n\n```toml\n[features]\nshell_tool = true\nsubagents = true\nweb_search = true # enables deferred Web; the flag name is retained for config compatibility\napply_patch = true\nmcp = true\nexec_policy = true\ncode_mode = true # execute_tools composes MCP/plugin/native calls; false defers it behind tool_search\nverify_tool = true # agent-callable `verify` self-critique; false removes it from the tool catalog\nvision_model = false # true routes image analysis to the [vision_model] model (see above)\nextension_host = false # experimental: run reviewed plugins' native code (see EXTENSIONS.md)\n```\n\n`code_mode` is on by default: `execute_tools` is advertised from the first\nrequest and nested calls go through the same permission gate as direct calls\n(see [Tool surface](TOOL_SURFACE.md#code-mode-execute_tools)). Set\n`code_mode = false` to defer it behind `tool_search` again.\n\n`extension_host` is experimental and off by default. Turning it on lets reviewed\nplugins run their `native` TypeScript/JavaScript tool and slash-command code in\na separate Node (or, opt-in, Bun) process; toggling it in either direction\nchanges the plugin activation policy, so every plugin is reviewed again after a\nrestart. Its `[extension_host]` table (`runtime`, `node`, `bun`) and what the\nhost's sandbox does on each platform are in\n[Writing an extension tool](EXTENSIONS.md).\n\n`[extension_host] mcp_backend` (user config only) chooses the MCP client\nindependently of the `extension_host` flag: `\"host\"` (default) uses the official\nMCP SDK behind the Rust broker; `\"rust\"` keeps the previous Rust client for one\nrelease. A host refusal never falls back to Rust. See\n[MCP connection lifecycle](MCP.md#connection-lifecycle).\n\nEvery flag has a row in [`docs/features.toml`](features.toml), the feature\nregistry; a test fails when a flag and its row disagree.\n\nYou can also override features for a single run:\n\n- `codewhale --enable web_search`\n- `codewhale --disable subagents`\n\nUse `codewhale features list` to inspect known flags and their effective state.\nThe native `/config` view also includes a read-only **Experimental** section\nfor experimental feature flags. It shows each flag's effective enabled/disabled\nstate and whether that state comes from the default or a configured override.\nChange feature flags in `[features]` or with `--enable` / `--disable`; the\n`/config` section is an audit surface, not a stability promise. Goal and\nWorkflow preview rows may appear there as reserved entries until those workflows\ngraduate behind real gated flags.\n\n## Web Search Provider\n\n`web_search` uses keyless Firecrawl by default. Runtime failure or an exhausted\nkeyless quota degrades visibly through DuckDuckGo and Bing. China deployments\ncan explicitly select Baidu, Metaso, Volcengine, or a trusted SearXNG endpoint;\nCodewhale does not guess geography from locale or model provider.\n\nConfigured API providers are attempted first. Runtime failure or an empty\nresult visibly degrades through DuckDuckGo and then Bing; the structured search\nreceipt records every hop. Missing configuration and network-policy denials\nfail closed without sending the query to another provider.\n\n**Provider-native search.** On routes whose provider offers its own web-search\ntool (OpenAI, xAI, Anthropic, DeepSeek, Kimi and others), that search can run\nahead of the configured provider. It is a separate model call on the active\nroute. `[search] native` decides the order:\n\n- unset (default): native search leads only when no search provider is\n configured; a provider chosen in `[search] provider`,\n `CODEWHALE_SEARCH_PROVIDER`, or a Tavily key wins;\n- `native = true`: native search leads even when a provider is pinned;\n- `native = false`: native search is never used.\n\nNative citations are link-checked before they are returned. Each cited URL is\nrequested through the same guarded fetch as `Web` fetch (SSRF, DNS and redirect\nchecks, `[network]` policy): a `HEAD`, then a one-byte ranged `GET` when `HEAD`\nis refused, at most four at a time and five seconds each. The request uses the\naddress as it will appear in `results`, without userinfo, fragment or\ncredential-like query parameters; an address whose query string repeats the\nsearch query is not requested and is removed. Only sources that\nanswer with a 2xx status stay in `results`; the receipt lists each removed one\nas `citation_unverified` with its reason. When no citation passes, or when the\nquery asks for `recency` (no native search tool can enforce a time window), the\nsearch falls through to the configured provider. A citation whose host the\n`[network]` policy denies is removed. A citation whose host the policy would\nask you about is not fetched: it stays in `results` and the receipt records it\nas `citation_unchecked` until you allow the host.\n\nThe provider's prose answer is returned whole in a separate\n`unverified_synthesis` field: it is the provider model's summary, and neither\nits claims nor its links are checked. Oversized tool output spills to a session\nartifact the model can page back.\n\n**Recency and locale.** `recency` and `locale` are forwarded where the\nbackend's API takes them, and the search receipt reports each as honored or\nignored:\n\n| Backend | Recency | Locale |\n| --- | --- | --- |\n| Firecrawl | `tbs=qdr:d/w/m/y` | `country` from the region (`de-DE` → `DE`); a bare language is ignored |\n| Tavily | `time_range` | not sent (Tavily takes country names) |\n| SearXNG | `time_range` | `language`, as given |\n| Serply | not sent (undocumented) | `hl` language, `gl` country |\n| DeepSeek | not sent | not sent (`localization` takes an IP address) |\n| Provider-native | declined; the search falls through to the configured provider | not sent |\n\nRecency is rounded up to the backend's nearest window (day, week, month,\nyear), so `recency = 10` searches the last month. Other backends ignore both\nknobs and say so in the receipt.\n\nFor a private/internal search service that serves DuckDuckGo-compatible HTML,\nkeep `provider = \"duckduckgo\"` and set `base_url`; Codewhale appends the `q`\nquery parameter to that endpoint and applies network policy to its host.\nCustom endpoints do not fall back to public Bing. `CODEWHALE_SEARCH_BASE_URL`\ncan override this per process; `DEEPSEEK_SEARCH_BASE_URL` remains accepted as\nthe legacy alias.\n\n**SearXNG** ([docs](https://docs.searxng.org/dev/search_api.html)) uses the\nconfigured instance's JSON API. Set `provider = \"searxng\"` and\n`base_url = \"https://your-searxng.example\"`; Codewhale calls\n`/search?q=...&format=json`. Codewhale does not use a public SearXNG instance\nby default because public instances often disable JSON output or rate-limit API\ntraffic.\n\nSelf-host it as a separate process (Docker is fine); Codewhale never bundles or\nmanages the search engine itself:\n\n- Enable JSON on the instance (`settings.yml`, `search.formats` must include\n `json`) and restart it. An HTML-only instance answers the API with HTTP 403;\n Codewhale reports that as a JSON/API-access problem on the SearXNG hop rather\n than silently returning no results.\n- Bind it to loopback, or to a host and port your network policy allows. The\n instance keeps its own engine list, limiter, and limits.\n- Point Codewhale at it with `[search] provider = \"searxng\"` and `base_url`\n (required; either the root URL or the `/search` endpoint). No instance ships\n as a default, and none is discovered automatically.\n- `codewhale doctor --probe-search` sends a transport-only `HEAD` to that\n origin — no `q=`, no credentials, no redirects, no audit receipt — so a green\n probe proves reachability and network-policy admission, not that JSON is on.\n\nConfirm the JSON API itself before assuming a Codewhale bug:\n\n```sh\ncurl -sS \"$BASE/search?q=codewhale&format=json\" | jq '.results[0] | {title,url,score}'\n```\n\nCodewhale ranks the returned rows by `score`, highest first, and applies\n`max_results` to that ranking; rows an instance reports without a usable score\nkeep their original relative order.\n\n**Metaso** ([metaso.cn](https://metaso.cn)) requires a user-supplied key. Set\n`METASO_API_KEY` or `[search] api_key`; Codewhale does not ship a shared key.\n\n**Firecrawl** ([docs](https://docs.firecrawl.dev/sdks/cli)) searches Firecrawl\nCloud without a key using its bounded per-IP daily quota. Set\n`FIRECRAWL_API_KEY` or `[search] api_key` for authenticated limits. Codewhale\nsends no `Authorization` header in keyless mode.\n\n**Baidu** uses Baidu AI Search at\n`https://qianfan.baidubce.com/v2/ai_search/web_search`. Set\n`BAIDU_SEARCH_API_KEY` or `[search] api_key`. This is a search-tool backend\nonly; it does not add a Baidu model provider.\n\n**Sofya** ([sofya.co](https://sofya.co)) returns full extracted page content\nrather than snippets. Set `[search] api_key` to your `ay_live_...` key, or the\n`SOFYA_API_KEY` env var. This is a search-tool backend only; it does not add a\nSofya model provider.\n\n**Serply** ([serply.io](https://serply.io)) returns Google organic results with\ntitle, URL, and snippet. Set `[search] api_key` to your Serply key, or the\n`SERPLY_API_KEY` env var. This is a search-tool backend only; it does not add a\nSerply model provider.\n\n**DeepSeek** ([docs](https://api-docs.deepseek.com/guides/search)) uses\n`POST https://api.deepseek.com/v1/tools/search`. Set `provider = \"deepseek\"`\n(`deep-seek` and `deepseek-search` are also accepted) and supply `[search] api_key`\nor the `DEEPSEEK_API_KEY` env var, in that order. The saved DeepSeek model-provider\nkey is not reused by this search backend. Queries are truncated to 128 Unicode\ncharacters. The backend clamps counts to 1–50; the shared search tool currently\ncaps requests at 10 results. Query operators such as `site:`, `lang:`,\n`after:`, and `before:` are passed through; the `recency` and `locale` knobs are\nignored. Results include title, URL, and snippet, falling back to summary when a\nsnippet is empty.\n\n**Tavily** ([tavily.com](https://tavily.com)) is selected automatically when a\nTavily key is present and no provider is pinned: `TAVILY_API_KEY` set, or\n`[search] api_key` / `CODEWHALE_SEARCH_API_KEY` in the `tvly-` family. Doctor\nreports that as `source: tavily key`. Autodetect is runtime-only — Codewhale\nnever writes `[search] provider` for it, and `TAVILY_API_KEY` is never merged\ninto `[search] api_key`. An explicit `[search] provider` or\n`CODEWHALE_SEARCH_PROVIDER` always wins, so `provider = \"firecrawl\"` keeps\nFirecrawl even with a Tavily key in the environment. Pinned `tavily` accepts\nany non-empty `[search] api_key` and is configured by that key or\n`TAVILY_API_KEY`; with both empty it fails closed.\n\n```toml\n[search]\nprovider = \"firecrawl\" # also duckduckgo | bing | tavily | bocha | metaso | searxng | baidu | volcengine | sofya | serply | deepseek\n# base_url = \"https://search.example/\" # optional with provider = \"duckduckgo\"; required with \"searxng\"\n# api_key = \"YOUR_KEY\" # optional for firecrawl; required by the other API providers\n# native = false # provider-native search: unset = only when no provider is configured\n```\n\n## Local Media Attachments\n\nUse `@path/to/file` in the composer to add local text file or directory context\nto the next message. Use `/attach <path>` for local image/video media paths, or\n`Ctrl+V` to attach an image from a local clipboard or an explicitly forwarded\nX11/Wayland clipboard. SSH terminal paste without a forwarded graphical display\nis text-only; use the local terminal's paste command (`Cmd+V` on macOS or\n`Ctrl+Shift+V` on Linux/Windows), and use `/attach <path>` for remote image\nfiles. OpenSSH loopback X11 displays are detected automatically. For an\nexplicitly forwarded Wayland or non-loopback X11 display, set\n`CODEWHALE_SSH_CLIPBOARD=graphical`; set it to `terminal` to force terminal\ntransfer instead of an ambient remote display. DeepSeek's public Chat\nCompletions API currently accepts text message\ncontent, so media attachments are sent as explicit local path references instead\nof native image/video payloads.\nAttachment rows appear above the composer before submit; move to the start of\nthe composer, press `↑` to select an attachment row, then press `Backspace` or\n`Delete` to remove it without editing the sample text by hand.\n\n## Managed Configuration and Requirements\n\ncodewhale supports a policy layering model:\n\n1. user config + profile + env overrides\n2. managed config (if present)\n3. requirements validation (if present)\n\nBy default on Unix:\n- managed config: `/etc/deepseek/managed_config.toml`\n- requirements: `/etc/deepseek/requirements.toml`\n\nRequirements file shape:\n\n```toml\nallowed_approval_policies = [\"on-request\", \"untrusted\", \"never\"]\nallowed_sandbox_modes = [\"read-only\", \"workspace-write\"]\n```\n\nIf configured values violate requirements, startup fails with a descriptive error.\n\n## Notes On `codewhale doctor`\n\n`codewhale doctor` follows the same config resolution rules as the rest of the\nTUI. That means `--config`, `CODEWHALE_CONFIG_PATH`, and the legacy\n`DEEPSEEK_CONFIG_PATH` are respected, and MCP/skills\nchecks use the resolved `mcp_config_path` / `skills_dir` (including env overrides).\n\nTo bootstrap missing MCP/skills paths, run `codewhale setup --all`. You can\nalso run `codewhale setup --skills --local` to create the owned project\n`.codewhale/skills` directory. Project skills load after workspace trust.\n\nBoth plain `codewhale doctor` and `codewhale doctor --json` are structural and\noffline by default. They do not check the release service, hosted provider\nAPIs, local provider endpoints, or MCP processes, and they do not load a\nworkspace credential `.env`. Use `--check-updates`,\n`--probe-api`, `--probe-local`, or `--probe-mcp` to opt into the corresponding\nlive boundary; `--probe-local` may start a desktop-managed service such as\nOllama. Only the explicit API/local probe paths may load workspace credential\n`.env` values. Live flags conflict with `--json`, so machine-readable doctor\noutput is always offline. Top-level keys include `version`, `paths`, `secret_backend`,\n`config_path`, `config_present`, `workspace`, `api_key.source`,\n`api_key.availability`, `base_url`,\n`default_text_model`, `mcp`, `skills`, `tools`, `plugins`, `sandbox`,\n`platform`, `api_connectivity`, and `capability`. CI consumers should rely on\n`api_key.source` (`config_declared`/`env_declared`/`external_auth_declared`/\n`secret_store_unprobed`/`secret_store_unavailable`/`oauth_unprobed`/\n`external_consent`/`none`/`local_runtime`/`unknown`) and\n`api_key.availability`\n(`present`/`not_required`/`not_probed`/`unavailable`/`unknown`) rather than parsing the\nhuman-readable `doctor` text. Source is declaration metadata, not proof that a\ncredential exists or works. Only a non-empty, non-sentinel literal config value\nis structurally `present`; no-auth and local routes are `not_required`. Environment,\nexternal-auth, OAuth, consent, and secret-store declarations remain `not_probed`\nand cannot make structural Setup or Fleet readiness true. A secret-store sentinel\non a named/custom endpoint that is prohibited from using the shared store is\n`secret_store_unavailable`/`unavailable`, while `unknown` remains reserved for\nthe absence of a supported structural conclusion. Exact and whitespace-wrapped\nlegacy sentinels are never treated as literal credentials. The structural\nloader still honors safe environment routing/model/policy fields, but it never\nmaterializes environment HTTP headers, sandbox API keys, or search API keys;\nonly an explicit API/local probe switches to the normal credential-loading\npath. An opted-in update check also emits only typed generic failures: untrusted\nrelease metadata and transport errors are not echoed.\n\nIf configuration loading or validation fails, `doctor --json` returns nonzero\nand prints a bounded JSON error envelope with\n`status = \"error\"` and `error.kind = \"config_validation\"`. It does not emit a\nnormal route or capability report—or the underlying possibly sensitive error—\nfor an invalid configuration.\n\nMCP entries are configuration diagnostics unless an explicit MCP command is\nrun. `mcp.probe_scope` is `configuration`, `mcp.live_health_checked` is false,\nand each server separates `checks.configuration` / `checks.command` from\n`checks.process_reachable`, `checks.protocol_initialized`, and\n`checks.backend_tool_health`. The latter three remain `not_checked` in doctor\noutput. Run `codewhale mcp validate` to explicitly start enabled servers and\nverify protocol initialization/discovery; backend health still requires an\nappropriate explicit tool call. Doctor reports only safe structural MCP fields:\nURL userinfo/path/query/fragment and raw command arguments, environment values,\nheader values, and token material are omitted. Provider URLs follow the same\nrule and expose only `scheme://host[:explicit-port]`.\n\nThe `capability` key contains per-provider capability info derived from\nstatic knowledge (release docs, API guides) rather than live API probes.\nTop-level sub-keys: `resolved_provider`, `resolved_model`, `context_window`,\n`max_output`, `thinking_supported`, `cache_telemetry_supported`,\nand `request_payload_mode`.\n\nUse `capability.context_window` and `capability.max_output` for model-limit\nchecks in CI scripts; do not treat `capability.max_output` as the per-turn\nrequest budget. Use `capability.thinking_supported` to decide whether to\nconfigure reasoning effort.\n\n## Setup status, clean, and extension dirs\n\n`codewhale setup` accepts a few flags beyond the existing `--mcp`,\n`--skills`, `--local`, `--all`, and `--force`:\n\n- `--status` — print a compact one-screen status (api key, base URL, model,\n MCP/skills/tools/plugins counts, sandbox, `.env` presence). Read-only and\n network-free; safe to run in CI. If `.env` is missing and `.env.example` is\n present in the workspace, the status output points at `cp .env.example .env`.\n- `--tools` — scaffold `~/.codewhale/tools/` with a `README.md` describing the\n self-describing frontmatter convention (`# name:` / `# description:` /\n `# usage:`) and an `example.sh` that follows it. The directory is\n intentionally not auto-loaded; wire individual scripts into the agent via\n MCP, hooks, or skills.\n- `--plugins` — scaffold `~/.codewhale/plugins/` with a `README.md` and an\n `example/plugin.toml` plus a namespaced example Skill. Bundles are discovered\n read-only, untrusted, and disabled; review them through `/plugin` before\n enabling. v0.9.1 activates only declared Skills and MCP servers. See\n [PLUGIN_BUNDLES.md](PLUGIN_BUNDLES.md).\n- `--all` now scaffolds MCP + skills + tools + plugins together.\n- `--clean` — list `~/.codewhale/sessions/checkpoints/latest.json` and\n `offline_queue.json` if they exist. Legacy\n `~/.deepseek/sessions/checkpoints/` files are not scanned automatically; set\n `CODEWHALE_HOME=~/.deepseek` for a one-off legacy cleanup. Pass `--force` to\n actually remove matched files. This never touches real session history or the\n task queue.\n\n`--status` and `--clean` are mutually exclusive with the scaffold flags.\n\n## Why the engine strips XML/`[TOOL_CALL]` text\n\ncodewhale sends and receives tool calls only over the API tool channel\n(structured `tool_use` / `tool_call` items). The streaming loop in\n`crates/engine/src/core/engine.rs` recognizes a fixed set of fake-wrapper start\nmarkers — `[TOOL_CALL]`, `<codewhale:tool_call`, `<tool_call`, `<invoke `,\n`<function_calls>` — and scrubs them from visible assistant text without ever\nturning them into structured tool calls. When a wrapper is stripped, the loop\nemits one compact `status` notice per turn so the user can see why their\nvisible text shrank. Treat any change that re-enables text-based tool\nexecution as a regression; the protocol-recovery tests in\n`crates/tui/tests/integration/protocol_recovery.rs` lock the contract.\n\n## Model-bound redaction (`[redaction] model_bound`)\n\nCodewhale masks credential-looking values in tool output **before it is sent\nto an upstream model** — the \"model boundary\". A file read by a tool can\ncontain a configured API key, a bare provider token, or a credential-shaped\nopaque string, and the model must not see those bytes. This backstop is\nseparate from the display/export scrubbers: it decides what the model itself\ncan quote back, and it is deliberately conservative (`CredentialShaped`\npolicy, see `crates/config/src/persistence.rs`), so ordinary code and config\nstay byte-exact while keys, JWTs, bearer tokens, PEM blocks, and long opaque\nruns are masked.\n\nTurning that masking **off** is a security decision, so it is not a plain\nboolean:\n\n```toml\n[redaction]\nmodel_bound = \"disabled\" # \"enabled\" (default) | \"disabled\"\n```\n\nSetting `\"disabled\"` only records a *request*. It takes effect only when all\nof these are true:\n\n1. You restart the interactive TUI.\n2. The startup gate appears and you press `1`/`Y` on its first stage\n (\"confirm and disable\"). This only advances to a second, final-confirmation\n stage - the gate repeats the red warning and asks \"are you really sure?\".\n3. On that second stage you press `1`/`Y` again. The gate is rendered with the\n same explicit-key discipline as workspace trust - `Enter` never confirms by\n reflex, and `2`/`U` on the second stage steps back.\n4. Only that second confirmation persists a receipt to\n `~/.codewhale/redaction-state.json` (next to `config.toml`) and rebuilds\n the engine with masking off for the rest of this launch and future ones.\n\nThe receipt is bound to the config it was made against and is valid only\nwhile that config still requests `\"disabled\"`. Setting `model_bound` back\nto `\"enabled\"` - or rewriting `config.toml` in any way after the\nconfirmation - invalidates it, so requesting `\"disabled\"` again later\nasks for a fresh confirmation on the next launch. The receipt checks both the\nconfig contents and modification time; missing, unreadable, or malformed config\nand older receipts without this binding keep masking enabled. Legacy-home\ninstalls store the receipt beside their resolved config file.\n\nUntil a confirmation exists, the effective mode is always `\"enabled\"`:\n\n- Choosing `2`/`U` (\"keep masking on\") leaves the config field untouched, so\n the next launch asks again. Edit the field back to `\"enabled\"` to stop being\n asked.\n- Non-interactive entry points (`codewhale exec`, hooks, automations, headless\n agents) never confirm anything and never apply an unconfirmed request.\n- Routing/classification summaries and durable goal-state text keep their own\n always-on redaction regardless of this switch; the opt-out exists so the\n model can quote file bytes for exact edits, not to relax stored state.\n\nThe config value itself is forgiving: `true`/`false`, `\"on\"`/`\"off\"`, and\n`\"enabled\"`/`\"disabled\"` (any casing) all parse, with `false`/`\"off\"` meaning\n`\"disabled\"`.\n\nA confirmed opt-out still sends your configured API keys to the provider you\nare already talking to. Only use it when the model must read and edit files\nthat contain real credentials.\n\n### Stored sessions\n\nThe masking runs once, when tool output enters the transcript, so saved\nsession files (`~/.codewhale/sessions/*.json` and their checkpoints) and\nRuntime API thread items (their text and tool metadata) do not store a\ncredential-shaped value from tool output. With the confirmed opt-out above,\ntool output is stored as the model saw it.\n\nNot yet masked: large outputs spilled to `~/.codewhale/tool_outputs`, shell\ncompletion evidence artifacts, and tool-call *inputs* (for example a\n`curl -H \"Authorization: Bearer …\"` command line). `doctor` and\n`scrub-secrets` do not check those either.\n\nSessions saved by builds before this change may still hold credentials in\ntheir tool output; nothing rewrites them automatically. `codewhale doctor`\nreports them under **Stored Sessions** (it checks the newest 50 files), and\nthis command finds and masks them across every saved session:\n\n```sh\ncodewhale sessions scrub-secrets # report only\ncodewhale sessions scrub-secrets --apply # rewrite the affected files\n```\n\n`--apply` rewrites each file under the same per-session lock a save takes,\nso it never loses a concurrent save. A session that is still open can write\nits in-memory copy back on its next save, so close open sessions first, and\nrotate any credential that was exposed — masking a stored copy cannot un-leak it.\n"
|
||
},
|
||
{
|
||
"path": "docs/LEGACY_PATHS.md",
|
||
"sha256": "5f862bcc0c1f85d4b0c1c62df27fc149de292025c5eb007a50fb75d7f04a09eb",
|
||
"content": "# Legacy `.deepseek/` compatibility paths — audit & migration status (#3068)\n\n> 阅读简体中文版:[zh_hans/LEGACY_PATHS.md](zh_hans/LEGACY_PATHS.md)。\n\n## v0.9.10 cleanse ledger\n\nThis is the release-window decision record for residue that can otherwise look\ninterchangeable in a text search. A compatibility entry must name the contract\nthat still requires it; new production code must use a current path.\n\n| Class | Surface | Decision / contract |\n|---|---|---|\n| DELETE | Retired underwater build-version test override | Removed in `1cc4d1d1b`; the owner test was already deleted and the production-only build version remains authoritative. |\n| MIGRATE | Cross-task Agent Mail | `bd998495a` and managed-app `4ff7bc50d` replace guessed event aliases and same-session mailbox conflation with one durable typed runtime protocol. |\n| MIGRATE | TUI `status_message` writes | Active residue; touched flows must move to the typed toast/current-state owner. No new writes are allowed. |\n| COMPATIBILITY | `.deepseek/` state and stored provider aliases | Read fallback for upgraded installations and stored provider identity; writes remain Codewhale-only. Details are below. |\n| COMPATIBILITY | `agent_message` transcript/protocol decoding | Persisted transcripts and tool wire values require decoding; it is not the cross-task Agent Mail transport. |\n| COMPATIBILITY | Managed runtime envelope schema 1 | Previously persisted/enrolled runner replay; current local Codewhale Agent Mail producer is schema 2 and both normalize to the managed envelope. |\n| CURRENT | DeepSeek provider support | A real selectable provider, not product branding; provider-scoped names and configuration remain. |\n| CURRENT | Same-session subagent mailbox | Start/status/peek/message/followup/interrupt/wait/cancel stays scoped to a parent runtime session. |\n| CURRENT | Web locale dictionaries | The application and documentation routes have no page-local `isZh` branches; new copy continues through dictionaries. |\n| CURRENT | Safety, authorization, protocol, persistence, migration, and data-integrity tests | These protect external or durable contracts and are not copy/layout cleanup candidates. |\n\nSnapshot on 2026-08-19: `10,954` Rust `#[test]` attributes under\n`crates/tui`, `523` TUI `status_message` source lines, and zero page-local\n`isZh` files under `apps/web/app` in the paired managed-app candidate. Counts\nare inventory signals, not deletion targets.\n\nCodewhale was renamed from DeepSeek-TUI. To avoid breaking existing installs, the runtime reads\nstate from the new `~/.codewhale/` location but **falls back** to the legacy `~/.deepseek/` location,\nand always **writes** to `~/.codewhale/`. This doc audits each legacy reference and records a\nkeep / deprecate / remove decision so the migration is auditable.\n\n## The canonical resolver (use this for new code)\n\nState-dir APIs are exported by `codewhale-config`; their implementation is\n`crates/config/src/state_dirs.rs`, with root selection in `crates/paths/src/lib.rs`.\nThe paths in this audit show defaults. `CODEWHALE_HOME` selects the primary\nhome and prevents fallback outside that root.\n\n| Symbol | Purpose |\n|---|---|\n| `CODEWHALE_APP_DIR` / `LEGACY_APP_DIR` | Default directory names from `codewhale-paths`. |\n| `codewhale_home()` | Selected primary home, defaulting to `~/.codewhale`. |\n| `legacy_deepseek_home()` | Selected legacy home, defaulting to `~/.deepseek`. |\n| `resolve_state_dir(subdir)` | Read the primary directory; fall back only if it is absent and no explicit primary home was selected. |\n| `ensure_state_dir(subdir)` | Write under the primary home; guarded migration can relocate owned legacy state and reports what moved. |\n\nCompatible harness roots are read-only discovery inputs, not owned migration\nor deletion targets.\n\n## Per-path decisions\n\n**Decision for all legacy references below: keep-as-fallback.** Removing the `.deepseek` fallback\nwould strand users who upgraded in place and never re-ran onboarding. Revisit only after a release\nthat actively migrates `~/.deepseek/` → `~/.codewhale/` on first run and a deprecation window.\n\n| Reference | Routed through `resolve_state_dir`? | Decision |\n|---|---|---|\n| `config::resolve_state_dir` / `ensure_state_dir` | n/a (the resolver itself) | keep — canonical |\n| `crates/skills/src/roots.rs` (`~/.deepseek/skills`) | no — compatible-root enumeration | Keep read-only harness discovery; this is not an owned write target. |\n| `crates/engine/src/prompts.rs` (`LEGACY_HANDOFF_RELATIVE_PATH = \".deepseek/handoff.md\"`) | no — explicit legacy const | keep — explicit legacy handoff fallback |\n| `crates/tui/src/workspace_trust.rs` | no — hardcoded | keep-as-fallback; follow-up |\n| `crates/engine/src/session_manager.rs` | no — hardcoded | keep-as-fallback; follow-up |\n| `crates/runtime/src/skill_state.rs` | no — hardcoded | keep-as-fallback; follow-up |\n| `crates/engine/src/tools/skill.rs` | no — hardcoded | keep-as-fallback; follow-up |\n| `crates/snapshot/src/lib.rs` | no — hardcoded | keep-as-fallback; follow-up |\n| `crates/runtime/src/workspace_discovery.rs` | no — hardcoded | keep-as-fallback; follow-up |\n\n## Follow-up source changes\n\nThe table retains the original audit with moved owners corrected. Verify each\ncurrent caller before changing it. Reuse its existing issue and resolver;\npreserve explicit-home isolation, read fallback, ownership and recovery.\nUse existing focused checks. This document does not authorize moving or\ndeleting another harness's roots.\n"
|
||
}
|
||
]
|
||
},
|
||
{
|
||
"id": "modes",
|
||
"slug": "modes",
|
||
"sources": [
|
||
{
|
||
"path": "docs/MODES.md",
|
||
"sha256": "f07a2fce8d2ef02692fdc9298911ebc99dbd7940af6a3b88d396c039dc7e4608",
|
||
"content": "# Modes and Permission Postures\n\n> 阅读简体中文版:[zh_hans/MODES.md](zh_hans/MODES.md)\n\nCodewhale has three related concepts:\n\n- **TUI mode**: what kind of visible interaction you're in (Plan/Work/Operate).\n- **Permission posture**: how aggressively the UI asks before executing tools.\n- **Workflow**: named steps that coordinate sub-agents, with dependencies and\n results, available in any TUI mode.\n\nModel selection is separate. `--model auto` and `/model auto` route each turn to\na concrete model and thinking level; they are not TUI modes and are not part of\nthe `Tab` cycle.\n\nWorkflow runs named steps through the same sub-agent runtime; Fleet manages\ntheir saved roles and model assignments. A workflow adds ordering, result\nhandoffs, gates and a progress view. The active mode and permission posture\nstill control what each step may execute.\n\nBefore assigning steps, query `agent(action=\"roster\")` for the saved Fleet\nmodels and roles. A plan child can select a listed model with `model`, or use\nthe saved `role`/`profile` assignment. An Exact Fleet fixes each member's route.\n\n## TUI Modes\n\nPress `Tab` to complete composer menus or cycle through the visible modes\nwhen the composer is empty: **Plan → Work → Operate → Plan**. `Tab` never sends\nor queues composer text; use `Enter` to send or queue it.\nPress `Shift+Tab` to cycle permission posture (Ask → Auto-Review → Full Access).\nEvery change of effective permission writes a `Permissions changed to …` line\nin the transcript.\nPress `Ctrl+T` to cycle reasoning effort.\nRun `/mode` to open the mode picker, or switch directly with `/mode work`,\n`/mode plan`, or `/mode operate`.\n\n- **Plan**: design-first prompting. The stable primitive names remain familiar, but the runtime centrally refuses file mutation and shell execution. Read-only inspection and policy-allowed research, including deferred Web search/fetch, remain available. When a Plan turn successfully finishes a nonempty response, including a prose-only plan, the TUI asks how to continue: Work (Ask), Work (Auto-Review), or keep planning. Work sends the exact approved response as a normal follow-up and preserves that turn's existing To-dos, or adds one pending plan item without replacing unrelated work.\n- **Work** (internally `agent`): ordinary multi-step execution. The first-turn toolbox includes `read`, `write`, `edit`, `file_search`, `bash`, `agent`, `workflow`, `todo_write`, and `load_skill`, plus `create_goal`, `get_goal`, `update_goal`, and `task_shell_wait` so goal and background-shell controls are available without discovery. The agent calls `create_goal` when a request is a durable objective; the host never infers a goal from wording, and the contract must not tell the model otherwise. Approval, sandbox, repository law, and managed policy decide what may execute.\n- **Operate**: Work at full strength, the \"ultra\" mode. Same tools and authority as Work; the difference is how they are used. Each substantive request becomes a goal (`create_goal`), the plan stays visible (`todo_write`), multi-part work runs through `workflow` and parallel `agent` workers, non-trivial changes get an independent reviewer before they are called done, long commands keep running in the background, and recurring or watch-type work is offered as an `automation` (created after approval). It keeps going until the goal is verified, blocked on you, or paused.\n\n`Act` and `/mode act` remain compatibility aliases for Work. Saved settings\nstill normalize to the internal value `agent`.\n\n### Tool availability by mode\n\n| Tool family | Plan | Work | Operate |\n|:---|:---:|:---:|:---:|\n| `read` and policy-allowed deferred research tools | yes | yes | yes |\n| `write` and `edit` | visible names; execution denied | approval- and policy-gated | same as Work |\n| `bash` | visible name; execution denied | approval- and policy-gated | same as Work; delegation is preferred when parallelism or isolation helps |\n| `agent` | yes, subject to child-depth authority | yes, subject to child-depth authority | yes, subject to child-depth authority |\n| Deferred native, MCP, and plugin tools | discoverable through `tool_search` when policy permits | same | same |\n| Paid or external-service tools | follows permission posture | follows permission posture | follows permission posture |\n| Access outside the workspace root | explicit trusted paths only | only through trusted paths or trust mode | same trusted-path/trust policy as Work; fleet profiles never widen it |\n\nOperate changes scheduling emphasis, not authority. It neither adds a\nmode-specific tool denial nor bypasses the active approval, sandbox, shell,\nask-rule, repository-law, or managed-policy boundary. Plan remains the\nmode-specific execution boundary for shell and write-capable tools; that\nauthority difference does not require a different primitive vocabulary.\n\n### Operate loop (one screen)\n\n```text\nUser message\n → small / chat / one-file? → parent does it (Work-equivalent tools)\n → multi-step work? → goal → named steps + dependencies + completion checks\n → Workflow phases → independent sub-agents in parallel\n → collect results → check evidence → hand off to the next phase\n → missing required result? → stop dependent work and repair the step\n → one independent task? → one direct sub-agent\n → parent integrates results and reports completed, blocked and next steps\n```\n\nLifecycle claims stay exact: dispatched ≠ settled ≠ verified.\n\n`allow_shell` controls whether `bash` can execute; it does not rename the tool\nor make mode the approval authority. Durable tasks and automation keep\nconservative omitted-field defaults and receive shell authority only when their\nsettings explicitly grant it. Stateful terminal/background controls are\nspecialized deferred tools rather than fields on the small foreground `bash`\nschema. Full Access changes the permission posture: the deterministic\nAuto-Review floor applies in the reviewing postures and is skipped under Full\nAccess, while repository-policy holds remain authoritative.\n\nAction-capable modes can discover the deferred `rlm` family through\n`tool_search`; its `open`, `eval`, `configure`, and `close` actions own persistent\nRLM sessions. The legacy split `rlm_*` spellings remain replay-only aliases.\nInside an RLM Python REPL, `sub_query_batch` fans out 1-16 cheap parallel child\ncalls pinned to `deepseek-v4-flash`.\n\nThe fast `deepseek-v4-flash` / thinking-off path is called Fin in the product\nlanguage. Fin is a seam for routing, summaries, cheap child calls, and\ncoordination work; it does not change approval behavior.\n\nThe orchestration controls remain available without taking over the starting\nscreen: `/auto` turns on Auto-Review so the agent just works, `/goal` keeps one\nobjective across turns, and `/workflow` prepares a repeatable ordered or\nfan-out workflow. They are directly callable and searchable through the full\ncommand palette, but are not pinned to the starter slash menu, idle welcome,\nfooter, or default Hotbar. A bare `/` instead opens the small task-oriented\nstarter set; use `/help` or the command palette for the complete inventory.\n\n`/goal <objective>` sets a session objective with an optional token budget and\nkeeps active objectives visible as Work context. The agent may also create the\ngoal itself when a direct request describes a verifiable end state that will\ntake more than one turn (\"until the tests pass\", \"make X work end to end\"); it\nthen shows one receipt line and you can `/goal pause` or `/goal clear` it. Bare\n`/goal` shows progress (state, elapsed, continuations, and how to continue when\nno turn is running); with no goal and no conversation yet it prints usage.\n`/goal pause` stops goal continuation without changing the objective, `/goal\nresume` resumes and sends the objective back into the turn, `/goal complete`\nmarks it done, `/goal blocked` marks it blocked, and `/goal clear` removes it. Goal state does not change the active TUI mode,\npermission posture, or model route. This remains distinct from `--model auto`, which\nonly controls model and thinking selection.\n\nWorkflow builds on the same separation: a goal can ask the agent to keep\nworking, while Workflow supplies the repeatable workflow/progress surface for\nlarge fanout. In the UI, a Workflow run should be shown as an overlay on the\nmain screen, not as another mode beside Plan, Work, and Operate.\n\nApp-server clients can persist a thread-scoped goal with `thread/goal/set`, read\nit with `thread/goal/get`, and clear it with `thread/goal/clear`. That persisted\nrecord carries `active`, `paused`, `blocked`, `usage_limited`, `budget_limited`,\nor `complete` status plus token/time accounting fields for clients that need\nthread resume semantics.\n\n## Mode Persistence\n\nChoosing a mode interactively also sets the mode a fresh session starts in.\nTab/Shift+Tab cycling, the `Alt+A` / `Alt+P` / `Alt+Y` shortcuts, the hotbar's\nPlan/Work/Operate actions, and `/mode` all write `default_mode` to\n`~/.codewhale/settings.toml`, so switching to Operate survives a restart. The\nwrite happens off the event loop; if it fails, the TUI says so in a warning\ntoast rather than reverting silently on the next launch.\n\nMode, thinking level, and the model picker share one serialized writer, so the\nselection you made last is the one on disk — a burst of Tab presses cannot end\nup persisting whichever write happened to finish last — and a mode write never\nrolls back an unrelated key such as `default_model`.\n\nRestoring a saved session deliberately does **not** rewrite the startup\ndefault: it re-installs the mode that session was in. The legacy `yolo` entry point installs Work\nplus Full Access, and `agent` is what it persists — `yolo` is a permission\nalias, never a startup mode.\n\nRe-selecting the mode you are already in is not a no-op. After a restored\nsession the live mode and `default_mode` routinely disagree, so choosing the\nlive mode again is how you make it durable; Codewhale confirms with a\n\"saved as startup default\" receipt rather than reporting \"already in that mode\".\n\nMode and permissions can change while a turn is running, from any surface\n(Tab, Shift+Tab, `/mode`, the hotbar, `/config mode …`). The change does not\nrestart a response that is already streaming. The engine applies it at its next\nstep in the same turn: before the next tool batch, at every approval gate, and\nbefore the next model request, which carries a new mode notice. A call that was\nstill waiting for approval, or that no approval covers, goes back to the model\nto retry under the new posture; a call you already approved runs only if the\nnew posture is equal or broader. The status line says the change is pending\nuntil that step, and the engine reports `Permissions: … · <mode>` once it\napplies.\n\nModel, thinking level, and provider are still refused while a turn runs, on\nevery surface (`/model`, `/provider`, `/config <key> <value>`,\n`/config preset`): they change the route the running request is on. Press Esc\nto interrupt first.\n\nCodewhale writes `settings.toml` under a lock that spans processes, and replaces\nthe file atomically, so a second Codewhale instance on the same home directory\ncannot lose your selection or read a half-written file. At exit, queued writes\nare flushed before the terminal is restored; anything that failed is printed on\nthe way out instead of disappearing with the alternate screen.\n\n## Compatibility Notes\n\n- Older settings files with `default_mode = \"normal\"` still load as `agent`; saving rewrites the normalized value.\n\n## Escape Key Behavior\n\n`Esc` is a cancel stack, not a mode switch.\n\n- Close slash menus or transient UI first.\n- Cancel the active request if a turn is running.\n- Discard a queued draft if the composer is empty.\n- Clear the current input if text is present.\n- Otherwise it is a no-op.\n\n## Permission Posture\n\nPermission posture controls tool approval and whether a turn may pause for a\nmissing user decision. It is one layer of the full\n[authorization order](AUTHORIZATION_ORDER.md), not a bypass for tool admission,\nrepository law, or sandbox enforcement. Cycle it with `Shift+Tab`, or edit it\nat runtime:\n\n```text\n/config\n# edit the approval_mode row to: suggest | auto | never\n```\n\nLegacy note: `/set approval_mode ...` was retired in favor of `/config`.\n\n- `suggest` (**Ask**, default): tool approvals may interrupt, and Codewhale asks\n when an unresolved user choice materially changes authority, cost, scope, or\n outcome.\n- `auto` (**Auto-Review**): reviews tool calls automatically. Deliberate user\n questions remain available in interactive sessions through `request_user_input`;\n a question parks the turn until answered, canceled, or its configured timeout\n expires. Headless `exec` withholds that tool because it has no responder.\n Tool safety holds remain separate from user questions. Two layers decide approvals. The\n **deterministic floor** (configured block rules plus the built-in safety\n floor) allows proven-safe calls and hard-blocks publish-like actions and\n destructive background/headless work; it is never model-reviewed. Fallback\n holds — calls the deterministic engine could not prove safe — escalate to a\n one-shot **model guardian** (v0.9.8) that returns risk, allow/deny, and a\n rationale. The guardian sees the exact held call and deterministic\n observations in separate JSON fields; conversation history, skill\n instructions, attachments, and expanded model context are excluded. It does\n not infer user intent or compute a generic user-intent score.\n High or critical risk cannot auto-run even if the model says allow. It has no\n tools, remembers no rules, and denies rather than truncates an oversized\n exact call. Exactly one reviewer request is made; incomplete or malformed\n output, timeout, cancellation, or provider failure fails closed. Repeated\n denials stop the turn: after three guardian denials in a row, or ten among\n the turn's last fifty reviews, the turn ends as interrupted with a status\n line, and calls still in that batch are refused without another review.\n Only explicit denials count (not timeouts or failures), an allowed review\n resets the run of three, the count starts fresh with each turn, and a child\n agent counts its own turns. Headless\n adapters use the deterministic-only tier. Repo-law\n holds that explicitly require a person block in Auto-Review rather than\n opening a hidden approval modal.\n\nThe LLM reviewer is closest to OpenAI Codex's experimental Auto-Review at\ncommit [`6fc6b9d6d2580d62622fc9884b5f5707f6505a5e`](https://github.com/openai/codex/tree/6fc6b9d6d2580d62622fc9884b5f5707f6505a5e).\nCodex's [guardian entry point](https://github.com/openai/codex/blob/6fc6b9d6d2580d62622fc9884b5f5707f6505a5e/codex-rs/core/src/guardian/mod.rs)\nreconstructs conversation context and runs a dedicated review session.\nCodewhale deliberately adopts only the exact-action structured decision,\n90-second deadline, fail-closed result, and per-turn denial circuit breaker. It\ndoes not copy Codex's transcript reconstruction, user-authorization score,\nreviewer tools, retries, persistent review session, or denial ledger.\n\nKimi Code at commit\n[`1414d4602898f406e540b23342cb18db23ff9efc`](https://github.com/MoonshotAI/kimi-code/tree/1414d4602898f406e540b23342cb18db23ff9efc)\nalso has no LLM reviewer. Its ordered\n[permission policy](https://github.com/MoonshotAI/kimi-code/blob/1414d4602898f406e540b23342cb18db23ff9efc/packages/agent-core-v2/src/agent/permissionPolicy/permissionPolicyService.ts)\napplies explicit deny rules and then its\n[Auto policy](https://github.com/MoonshotAI/kimi-code/blob/1414d4602898f406e540b23342cb18db23ff9efc/packages/agent-core-v2/src/agent/permissionPolicy/policies/auto-mode-approve.ts)\nreturns `approve` directly. Codewhale uses the deterministic floor and guardian\ndescribed above, while keeping deliberate user questions available.\n\nThe sandbox and escalation baseline is grounded in DeepSeek Harness\n`0.1.0-rc.5` at\ncommit [`47f943859bef60e4160492346772ded9b24f765a`](https://github.com/deepseek-ai/deepseek-harness/tree/47f943859bef60e4160492346772ded9b24f765a):\nits [sandbox contract](https://github.com/deepseek-ai/deepseek-harness/blob/47f943859bef60e4160492346772ded9b24f765a/docs/subsystems/sandbox.md)\ndefines per-call `read-only`, `workspace-write`, and `danger-full-access`\nboundaries and forbids silent unconfined fallback; its\n[approval contract](https://github.com/deepseek-ai/deepseek-harness/blob/47f943859bef60e4160492346772ded9b24f765a/docs/subsystems/approval.md)\ngrants only `allowed-once` and fails closed on rejection, cancellation, or an\nunavailable answerer; and its\n[sandbox result contract](https://github.com/deepseek-ai/deepseek-harness/blob/47f943859bef60e4160492346772ded9b24f765a/packages/shell/bash-sandbox/README.md)\ntells the model to retry a denied command exactly once with the narrowest wider\nmode plus a justification. DeepSeek Harness does not add an LLM reviewer to\nthat path. Codewhale's autonomous posture adds only the single stateless\nguardian request described above; deterministic hard blocks remain\nnon-bypassable in the reviewing postures.\n- `bypass` (**Full Access**): ordinary tool calls do not show approval prompts,\n while deliberate user questions remain available. The deterministic\n Auto-Review floor is skipped — publish-like commands, destructive\n background/headless work, and the Windows npm-launcher platform hold do not\n strand a call the person already granted. Non-bypassable registered holds\n auto-approve instead of opening a contradictory modal. Repository-law and\n managed-policy holds fail closed as hard blocks instead of contradicting\n Full Access with an approval modal.\n- `never`: blocks any tool that is not considered safe/read-only; deliberate\n user questions remain available.\n\nThe effective posture and its question discipline are projected into every\nturn from the same runtime authority that gates tools. A mode/posture change is\ntherefore visible to the next turn. Untrusted runtime-generated input is\nnarrowed before metadata is built and cannot invent approval authority. An\nexplicit Full Access sub-agent handoff preserves the parent's standing posture\nso ordinary child work does not begin prompting again.\n\n### Children (sub-agents and fleet workers)\n\nChildren inherit the session posture faithfully rather than a bare\nauto-approve bit:\n\n- **Auto-Review**: a worker's held call goes through the same deterministic\n policy (proven-safe calls run; publish-like and destructive background work\n is hard-blocked) and, for holds it cannot prove safe, the same one-shot\n model guardian using the child's own session client. No prompt is ever\n opened for a child; an unavailable guardian denies, fail closed.\n- **Ask**: a call the role may delegate runs. A held call is raised as an\n approval prompt in the parent's UI (`agent:<id>:approval:<n>`) when the\n host is an interactive TUI; the worker waits visibly (`waiting for user`)\n and the person's answer is routed back to it, whether the parent turn is\n idle or itself awaiting an approval. Hosts that cannot prompt deny with the\n reason.\n- **Full Access**: calls run, including destructive detached work — the\n Auto-Review floor is skipped exactly as it is for the parent turn. The\n execution envelope still applies and never widens.\n\nRole posture and the execution envelope are checked before and after this\ngate and never widen. Every decision a person did not make at a prompt is\nwritten to the audit log and to the child's transcript as a one-line note\n(`Auto-Review allowed 'bash' (low risk, model guardian): …`), visible when\nthe worker is focused.\n\n## Small-Screen Status Behavior\n\nWhen terminal height is constrained, the status area compacts first so header/chat/composer/footer remain visible:\n\n- Loading and queued status rows are budgeted by available height.\n- Queued previews collapse to compact summaries when full previews do not fit.\n- `/queue` workflows remain available; compact status only affects rendering density.\n\n## Workspace Boundary and Trust Mode\n\nBy default, file tools are restricted to the `--workspace` directory. Enable trust mode to allow file access outside the workspace:\n\n```text\n/trust on\n```\n\nBare `/trust` (like `/trust status`) only *reports* the current setting — it\ndoes not enable anything. Use `/trust off` to restrict access again.\n\nFull Access enables trust mode automatically.\n\n## MCP Behavior\n\nMCP tools are exposed as `mcp_<server>_<tool>` and use the same approval flow as\nbuilt-in tools. Read-only MCP helpers may auto-run in Ask and Auto-Review when\npolicy permits; MCP tools with possible side effects require approval. Full\nAccess does not bypass hard policy holds.\n\nA tool's own MCP annotations count only as far as their source is trusted. A\ntool from a reviewed, enabled plugin that declares `readOnlyHint: true` runs\nlike the built-in read helpers, with no prompt; the same claim from any other\nserver is ignored. A tool that declares `destructiveHint: true` never gets\nthat relaxation, even from a reviewed plugin, and its approval card says the\nserver marked it destructive. Full Access still runs it without a prompt, like\nany other tool that would ask.\n\nSee `MCP.md`.\n\n## Related CLI Flags\n\nRun `codewhale --help` for the canonical list. Common flags:\n\n- `-p, --prompt <TEXT>`: one-shot prompt mode (prints and exits)\n- `codewhale exec --auto --output-format stream-json <PROMPT>`: run the tool-backed non-interactive agent and emit one JSON object per line for harnesses and backend wrappers. `--auto` enables Full Access by default (no Codewhale shell sandbox, trust mode on); add `--sandbox workspace-write` to keep the shell sandbox and file-tool workspace restrictions, with trust mode off. Any `--sandbox` other than `danger-full-access` (or the equivalent `sandbox_mode` in config) keeps trust mode off; `external-sandbox` relies on your own isolation. Exec runs that offer tools are saved in every output format. Exit codes: `0` on success and `1` on failure. Network and timeout errors do not authorize a supervisor to repeat the command: its request may already have run or its user message may already be saved. The terminal stream `metadata` event retains `error_category` for diagnosis; it is not an idempotency receipt.\n- Put exec flags before the prompt. A prompt starting with `-` needs `--` first, for example `codewhale exec -- \"- explain this option\"`. A lone `-` is literal text. Known exec long flags (including `--flag=value`) after the prompt are rejected unless they follow an explicit `--`, which marks them as literal prompt text.\n- `codewhale exec --prompt-file <PATH>` / `cat prompt.txt | codewhale exec --prompt-file -`: read the prompt from a file or stdin instead of argv, for prompts past the OS per-argument limit (~128 KiB on Linux). Conflicts with a positional prompt, and a positional `-` is literal prompt text; `--prompt-file -` is refused with `--parent-death-watch`, which owns stdin\n- `codewhale exec --resume <ID|PREFIX> <PROMPT>` / `--session-id <ID|PREFIX>`: continue a saved session non-interactively\n- `codewhale exec --continue <PROMPT>`: continue the most recent saved session for this workspace non-interactively\n- `codewhale fork <ID|PREFIX>` / `codewhale fork --last`: copy a saved session into a new sibling session; forked sessions retain additive parent-session metadata and show that lineage in session listings\n- `--model <MODEL>`: when using the `codewhale` facade, forward a DeepSeek model override to the TUI\n- `--workspace <DIR>`: workspace root for file tools\n- `-r, --resume <ID|PREFIX|latest>`: resume a saved session\n- `-c, --continue`: resume the most recent session in this workspace\n- `--max-subagents <N>`: clamp to `1..=128`\n- `--full-access`: start with Full Access, no approval prompts and no Codewhale sandbox; the same as `yolo = true`. `serve --acp --full-access` and `--full-access serve --acp` are equivalent.\n- `--mouse-capture` / `--no-mouse-capture`: opt in or out of internal mouse scrolling, transcript selection, right-click context actions, and transcript scrollbar dragging. Mouse capture is enabled by default on non-Windows terminals and on Windows Terminal/ConEmu/Cmder so drag selection copies only transcript text, removes visual wrap-column line breaks from paragraphs, and stays scoped to the transcript pane; hold Shift while dragging or use `--no-mouse-capture` for raw terminal selection. It defaults off on legacy Windows console (CMD without `WT_SESSION` / `ConEmuPID`) and inside JetBrains JediTerm — PyCharm/IDEA/CLion/etc. — where the terminal advertises mouse support but forwards SGR mouse events as raw text (#878, #898). Use `--mouse-capture` to opt in anywhere it's defaulted off. Raw terminal selection may cross the right workbar and include visual wraps because the terminal, not the TUI, owns the selection.\n- `--profile <NAME>`: select config profile\n- `--config <PATH>`: config file path\n- `-v, --verbose`: verbose logging\n\n## Branching and Rollback\n\nCodewhale has three related but intentionally separate recovery paths:\n\n- `codewhale fork <ID>` creates a new saved session from an existing saved\n conversation and records the source session id. This is the safe way to\n explore a different answer path without overwriting the original session.\n- Esc-Esc backtrack rewinds the live transcript to a previous user prompt and\n restores that prompt into the composer for editing. It does not change\n files; `/restore` rolls the folder back to a chosen restore point and needs\n `/trust on` or Full Access.\n- `/restore` and the `revert_turn` tool restore workspace files from side-git\n snapshots. `/restore list [N]` lists more snapshot options before choosing a\n rollback point. They do not rewrite conversation history.\n\nA Pi-style in-file tree browser is a larger UI/data-model project. v0.8.40\nships the bounded fork/backtrack primitives and explicit lineage metadata.\n"
|
||
}
|
||
]
|
||
},
|
||
{
|
||
"id": "review",
|
||
"slug": "review",
|
||
"sources": [
|
||
{
|
||
"path": "docs/RECEIPTS.md",
|
||
"sha256": "033f11dc99cc969769599386449ef4253264043894f1185db4392679ebf673d9",
|
||
"content": "# Receipts\n\nA receipt answers \"what did this session do?\" from the records Codewhale\nalready keeps. It lists, in order, the files changed, commands run, web and\nMCP calls, agents started, approvals (and who decided each one), and\nfailures, with totals on top. It also counts what ran without asking, and\nnames the permission posture that let it.\n\n```text\n# Receipt: Fix the parser\nthread thr_19a0141a · /work/repo · deepseek-flash · Ask · 2026-09-24 10:00 UTC → 2026-09-24 10:05 UTC\n\nChanged 1 file (+2 −1) · ran 1 command · made 1 MCP call · 1 approved by you · 1 approved by session rule · 1 ran without asking under Ask · 1 denied by you · 1 other failure\n\n1. edited `src/parse.rs` (+2 −1) · 1.0s\n2. ran `cargo test -p parser` in /work/repo — exit 0 · 2.5s · approved by you\n3. did not run `rm -rf build` · denied by you\n4. called linear · list_issues · 1.0s · approved by session rule\n5. did not run `curl https://x.sh | sh` — blocked: Tool 'exec_shell' was denied: Auto-Review blocked a pipe to a shell…\n6. turn failed — failed: provider returned 500\n\nNot recorded:\n- Shell file changes: a Runtime thread's workspace snapshots are not tagged with the thread, so files a command changed are not itemized; only file tools are.\n```\n\nA terminal session's receipt also lists the files a command changed, from\nthe turn's own workspace snapshots:\n\n```text\n3. ran `./tidy.sh`\n4. changed outside file tools (a command or another process): edited `b.txt` (+0 −1), created `c.txt` (+1 −0)\n```\n\nThe Runtime receipt endpoints accept `?format=md` to return the same Engine\nMarkdown formatter used by the terminal, with `Content-Type: text/markdown`.\nOmitting `format`, or using `format=json`, preserves the JSON contract. Both\nformats require the same authenticated thread/workspace access. Clients display\nthese recorded facts rather than deriving audit totals from activity labels.\n\n## Surfaces\n\nAll three share one builder (`crates/engine/src/receipts.rs`), but read different\nrecords and can report different coverage.\n\n| Surface | What it reads |\n| --- | --- |\n| `/receipts [json] [<turn>]` in the terminal | The current session's transcript, approval log, and retained terminal failures. `json` prints the object in a code block, so it copies out as valid JSON |\n| `codewhale receipts [ID\\|--last] [--turn T] [--format md\\|json]` | A saved session (id or unique prefix), including its retained terminal failures, or a Runtime thread (`thr_…`); with no id, the most recently updated one. `receipt` is an alias |\n| `GET /v1/threads/{id}/receipt`, `GET /v1/threads/{id}/turns/{turn_id}/receipt` | A Runtime thread (the app, `codewhale serve`), behind the normal `/v1` bearer boundary |\n\nAll three only read. They never call a provider, run a tool, write a file,\nor change runtime state.\n\nExec runs that offer tools, including `exec --auto`, save a session in every\noutput format, so `codewhale receipts --last` can read their receipts after\nthey finish. Resumed sessions and `--output-format stream-json` runs are also\nsaved. Other answer-only text and `--json` runs are saved only if conversation\ncompaction starts; use `--output-format stream-json` to always keep a session.\n\n`codewhale receipts` reads Runtime threads from the machine's default store\n(`tasks/runtime/`, or `$CODEWHALE_RUNTIME_DIR`). A thread kept in one\nterminal session's own store (`sessions/<id>/runtime/`) is not found by id;\nthe API reads whatever store its server owns.\n\n## Where the facts come from\n\n| Session kind | Record | Holds |\n| --- | --- | --- |\n| Terminal session | `sessions/<id>.json` | Every tool call and its result text, in order; each prompt's `<turn_meta>` names the posture the turn ran under; `turn_outcomes` records terminal turn status and errors |\n| Terminal session | `sessions/<id>/approval_receipts.jsonl` | Every approval ask and decision, with time and who decided |\n| Runtime thread | `tasks/runtime/turns`, `items` | Every tool call with its input, status, start and end time, and structured result (exit code, diff, agent status); each turn's `permission_posture` |\n| Runtime thread | `tasks/runtime/events/<thread>.jsonl` | `approval.required` / `approval.decided`, with the flags that say who decided |\n\nThe saved-session CLI and live `/receipts` read terminal turn failures,\nincluding model errors in exec runs, as `turn_failed` actions and count them\ntoward failures even when no tool failed. Truncating the conversation removes\noutcomes beyond the retained history, so later turns cannot inherit their failures.\nSaved sessions keep only the newest 64 recorded failed-turn outcomes. Older outcomes\nand legacy sessions without this record cannot provide a complete terminal\nfailure history from tool results alone.\n\n### Who decided an approval\n\n| Receipt says | Meaning | Recorded as |\n| --- | --- | --- |\n| approved by you / denied by you | A person answered: the terminal card, the app, the web mirror, or an API client acting for them | `decided_by: \"user\"`; Runtime event with no `auto` flag |\n| … by session rule | A remembered \"for this session\" rule answered | `decided_by: \"session_rule\"`; Runtime event with `auto` and a `grant_id` |\n| … by posture | The mode or permission posture answered without a prompt | `decided_by: \"posture\"`; Runtime event with `auto` or `posture` |\n| approval timed out | The card expired unanswered | outcome `timeout` |\n| turn stopped while waiting / nobody could be asked | Codewhale could not ask anyone: the turn had ended or stopped, or the request never reached a person. Counted as not answered, never as a denial, even where the record says `denied` | `decided_by: \"host\"` |\n| approved / denied, with no \"by\" | A record written before 0.10.1, or a sub-agent's request. The totals say \"(decider not recorded)\" | no `decided_by` |\n\nThe totals line counts approvals and denials separately, by who gave them:\n`1 approved by you · 1 denied by you` is two decisions.\n\n### Ran without asking\n\nMost calls never produce an approval. Under Full Access nothing asks; under\nAsk, reads and allowed tools run without a prompt, and a remembered rule can\nskip one. Those calls leave no approval record, so the receipt counts them\ninstead: `ran_without_asking` is every file change, command, code run, web\nor MCP call, and agent start that ran with no approval on record. The totals\nline names the postures the turns ran under (`9 ran without asking under\nFull Access`). Reads are not counted, and neither is a call that did not\nstart (below).\n\n### Blocked before it ran\n\nCodewhale answers a call it will not run with an error result, the same way\na tool reports a failure: an Auto-Review or guardian block, a tool-policy or\nallow-list denial, a sandbox escalation the posture cannot grant, input that\ndid not parse, or a tool that is not available. None of these writes an\napproval, so a receipt reads the result itself:\n\n- **Blocked:** the result is Codewhale's own refusal text (`Tool 'x' was\n denied: …`, `Invalid input for tool …`, `BLOCKED: …`, a validation\n feedback line with `\"side_effect_status\":\"not_started\"`, or a Runtime\n thread's `Failed to authorize tool execution: …`). Listed as\n `did not run … — blocked: <reason>`, status `blocked`, counted in\n `blocked` (`N blocked before running` in the totals line), and never as\n run, failed, or ran without asking. Every permission denial the engine\n writes keeps the `Tool 'x' was denied:` lead, including ones that name\n their own fix (Plan mode, `allow_shell`); sessions saved before 0.10.1\n wrote some of those without it, and such a call reads as a failure.\n- **Only Codewhale's own words count.** An MCP server's or GitHub's reply,\n a fetched page, a program's output (code tools), and a sub-agent's words\n can say anything, so a failed call to one of those is judged by the\n metadata Codewhale wrote for it, never its text: an MCP server that\n answers `BLOCKED: …` or `{\"side_effect_status\":\"not_started\"}` is listed\n as a failed call that ran. `BLOCKED:` counts only from the shell tools,\n and the validation feedback line only as the result's last line.\n- **Stopped at an approval prompt with no approval record** (a sub-agent,\n or a session older than the approval log): the result says\n `Tool 'x' denied by user`, which the engine also writes when nobody could\n be asked. Listed as `did not run …` with no decider, since the text does\n not prove who said no.\n- **Ran and failed:** the result holds an exit code or a line the shell\n writes only after a process ran (`Command exited with code N`,\n `Command failed (exit code N)`, a timeout or cancel line). Counted as a\n command and a failure.\n- **Not shown either way:** a command whose error has neither. Listed as\n `tried to run … — error, no exit code: <error>`, status `unknown`, not\n counted as run, with a `not_recorded` note. Other tools that return an\n error are counted as failures, since the error came from the tool.\n\nTerminal sessions started before 0.9.10 (2026-08-20) have no approval log,\nso their receipts do not count this and say why.\n\n### Not recorded\n\nThe receipt says so instead of guessing:\n\n- **Shell file changes in a Runtime thread.** A terminal session reads the\n files a command changed from the workspace snapshots Codewhale takes\n before and after each turn (`git diff` between the two, inside the\n snapshot side repo; at most 50 paths a turn). A Runtime thread's snapshots\n are not tagged with the thread, so its receipt lists only file tools. A\n terminal turn with no snapshot pair (snapshots off, the workspace too large\n for them, or pruned: the newest 50 are kept) says so. A snapshot\n difference covers anything that wrote to the workspace during the turn,\n including you or another program, not only the command. It leaves out\n what snapshots do not track: ignored and skipped paths (`.gitignore`\n entries, `.env`, `node_modules`, `target`, and the like) and anything\n outside the workspace (`~/.ssh`, `/tmp`), so an empty list does not mean\n a command wrote nothing. A turn is matched to its snapshots by its prompt;\n when another turn has the same prompt (\"continue\"), the snapshots' turn\n number must agree too, or the turn is counted as having no pair rather\n than given another turn's files. Paths print with control characters\n escaped (`\\n`, `\\u{1b}`), so a file name cannot add a line to the\n receipt or send the terminal an escape sequence.\n- **Terminal-session exit codes, durations, and timestamps.** A terminal\n session saves each call and its result text, not the structured result. A\n failed shell call's exit code is read from the shell tool's own closing\n line (`Command exited with code N`); a passing one shows no code.\n- **Line counts for whole-file writes** in terminal sessions, and whether the\n file existed before.\n- **Who decided** for approvals recorded before 0.10.1 and for sub-agent\n approvals.\n- **Which calls asked first** in terminal sessions started before 0.9.10.\n- **Whether a failed command started** when its error has no exit code and\n no shell status line, and whether a call with no result ran at all.\n- **Why a call ran without asking** beyond the turn's posture: the record\n does not say whether the posture, an allow rule, or a remembered grant let\n it through.\n\n## Non-Goals\n\nA receipt is not a safety certification, a provider compatibility\ncertification, or a hosted attestation (`claim_ceiling` in the JSON says so).\nIt exports no reasoning text and no raw tool output. Commands, search\nqueries, and error lines are bounded (200, 120, and 160 characters) and pass\nthrough the shared secret redactor. A receipt lists at most 2,000 actions;\ntotals always cover every action, and `omitted_actions` counts the rest.\n\n## JSON shape\n\n`--format json`, `/receipts json`, and the API return the same object:\n\n```json\n{\n \"schema_id\": \"codewhale.receipt/v1\",\n \"source\": {\n \"kind\": \"thread\",\n \"id\": \"thr_19a0141a\",\n \"title\": \"Fix the parser\",\n \"workspace\": \"/work/repo\",\n \"model\": \"deepseek-flash\",\n \"started_at\": \"2026-09-24T10:00:00Z\",\n \"updated_at\": \"2026-09-24T10:05:00Z\"\n },\n \"postures\": [\"Ask\"],\n \"totals\": {\n \"files_changed\": 1, \"files_changed_outside_file_tools\": 0,\n \"files_created\": 0, \"files_deleted\": 0,\n \"lines_added\": 2, \"lines_removed\": 1, \"line_counts_complete\": true,\n \"commands\": 1, \"commands_failed\": 0, \"code_runs\": 0, \"network\": 0,\n \"mcp_calls\": 1, \"plugin_calls\": 0, \"subagents\": 0,\n \"approvals\": {\n \"total\": 3, \"approved\": 2, \"denied\": 1, \"timed_out\": 0,\n \"not_answered\": 0, \"pending\": 0,\n \"approved_by\": { \"you\": 1, \"session_rule\": 1, \"posture\": 0, \"not_recorded\": 0 },\n \"denied_by\": { \"you\": 1, \"session_rule\": 0, \"posture\": 0, \"not_recorded\": 0 }\n },\n \"ran_without_asking\": 1, \"failures\": 1, \"blocked\": 1,\n \"other_tool_calls\": 0\n },\n \"actions\": [\n {\n \"seq\": 2, \"turn\": \"turn_1\", \"at\": \"2026-09-24T10:02:00Z\",\n \"call_id\": \"call_test\", \"tool\": \"exec_shell\",\n \"kind\": \"command\", \"command\": \"cargo test -p parser\",\n \"cwd\": \"/work/repo\", \"exit_code\": 0,\n \"status\": \"ok\", \"duration_ms\": 2500,\n \"approval\": {\n \"decision\": \"approved\", \"decided_by\": \"user\",\n \"at\": \"2026-09-24T10:01:59Z\"\n }\n }\n ],\n \"omitted_actions\": 0,\n \"not_recorded\": [\"Shell file changes: …\"],\n \"claim_ceiling\": [\n \"local_record_only\",\n \"not_safety_certification\",\n \"not_provider_compatibility_certification\"\n ]\n}\n```\n\n`kind` is one of `file_change` (`files[]` with `path`, `change` =\n`edited|created|deleted|written`, optional `lines_added`/`lines_removed`),\n`workspace_change` (`files[]` a turn's snapshots show changed that no file\ntool names, and `truncated` when more than 50 did), `command` (`command`, `cwd`, `exit_code`), `code` (`exit_code`, `nested[]`\ntool calls an `execute_tools` program made), `network` (`action`, `host`,\n`query`), `mcp` (`server`, `plugin`), `subagent` (`name`, `agent_id`,\n`outcome`), `approval` (an approval with no matching call), `tool` (any other\ncall, listed only when it failed), or `turn_failed`. `status` is `ok`,\n`failed` (ran and failed), `not_run` (held at approval), `blocked`\n(refused before it started; the reason is in `error`), `interrupted`,\n`running`, or `unknown` (no result, or a command error that does not show\nwhether it started). A terminal session's `turn` is the turn number; a\nthread's is the turn id. `/receipts 7` or `--turn 7` for a turn the session\ndoes not have is an error, not an empty receipt.\n\n## `audit.log` is not the receipt\n\n`~/.codewhale/audit.log` is a security-event log: credential saves and\nclears, hook environment key names, compaction passes, goal completions, the\nterminal's own approval routing, Auto-Review verdicts (`tool.gate.decision`,\nsince 0.10.1), and outbound network decisions when `[network]` auditing is\non. It has never held commands or file changes, and\nturns run by the app or `codewhale serve` write no approvals there. Their\napprovals are in the session's `approval_receipts.jsonl` and the thread's\nevent log, which is where receipts read them.\n\nA quiet `audit.log` does not mean nothing ran. It gets an approval line only\nwhen the terminal routes an approval request. Since 0.8.66\n(`1c68e3bb32`, 2026-06-29) the engine decides auto-allowed calls itself, so\nthey never become requests; under Full Access almost nothing does. On one\ndeveloper machine the last `tool.approval.*` line was written on 2026-08-19,\nthe last `tool.approval.auto_approve` line on 2026-06-30, and the writes\nafter that were test runs, which since `244368675b` go to a scratch log. Use\na receipt to see what ran.\n\n## Review Receipts\n\n`codewhale review --write-receipt` writes a local JSON receipt for the reviewed\ndiff under the Codewhale state directory (`review-receipts/`) unless\n`--receipt-path <path>` is provided. This is a pre-push handoff artifact: it\nrecords what diff was reviewed and what the review reported, without pushing,\ntagging, opening a PR, or claiming to replace maintainer review.\n\nThe current receipt includes:\n\n- `diff_fingerprint`: SHA-256 of the reviewed diff.\n- `provider` and `model`: the routed review provider/model.\n- `checks_run`: local checks attached to the receipt when available. Empty\n means no checks were attached; attached checks must report a passing status.\n- `findings`: structured issue/suggestion counts and issue locations when the\n review output is structured.\n- `unresolved_risk`: a conservative summary derived from unresolved findings.\n- `review_content_sha256`: SHA-256 of the review text.\n- `coverage` (PR receipts): the exact base/head and complete-diff\n fingerprint, ordered per-pass diff fingerprints/file counts, and one\n response-content hash for every completed pass. Manifest-backed PR receipts\n use schema version 2 so older readers reject rather than misinterpret them.\n\nThe receipt deliberately does not include the raw diff body. Re-run\n`codewhale review --write-receipt` after changing the diff; reviewers should\ncompare the `diff_fingerprint` before reusing a receipt in a PR handoff.\n\n`codewhale review --check-receipt` is the local pre-push gate. It does not call\na model; it compares the current diff fingerprint with a supplied receipt\n(`--receipt-path <path>`) or the latest matching local receipt. The check exits\nnonzero when the diff no longer matches, the receipt schema is unsupported, the\nreceipt has unresolved risk, or an attached check did not pass.\n\nBy default receipt generation rejects a PR that needs more than one\n`--max-chars` pass before calling a model. An explicit `--max-passes N` admits\nat most N complete ordered PR passes; any missing, malformed, reordered or\nstale pass prevents a receipt. Receipt checking is provider-free and validates\nthe exact stored manifest without authorizing another run. Neither mode\nfingerprints a truncated prefix. A receipt for\n`review --base <base-sha> --path <path>` covers only that selected path at the\nchecked-out revision; validate it with the same base, path, and input limit.\nIt does not cover the rest of a pull request or prove that separately reviewed\nchanges work together. With `--base`, the reviewer also reads the\n`base..HEAD` commit messages (merges excluded, capped at 16,000 characters) as\nthe author's stated intent; the receipt fingerprints the diff, not those\nmessages.\n\n## Builder Rules\n\nThe builder is deterministic and conservative:\n\n1. A thread receipt loads the thread, its turns, the items each turn lists\n (plus items that name the turn but are not listed yet, for a live turn),\n and the thread's `approval.*` events. A turn id from another thread is\n rejected.\n2. A session receipt reads `tool_use`/`tool_result` pairs from the\n transcript and replays the approval log; a log that does not replay is\n reported, not half-used.\n3. Approvals attach to their call by tool call id. One with no matching call\n is listed on its own.\n4. File changes come from a tool's structured mutation record (the applied\n diff) when saved, otherwise from the call's own input (an edit's\n replacement text, a patch's hunks). A failed call changed nothing and\n carries no counts.\n5. Nothing is derived from model prose. Two fixed kinds of text Codewhale\n itself writes are read: the shell's status lines (for an exit code) and\n its refusal text (for a call it blocked); see \"Blocked before it ran\".\n"
|
||
},
|
||
{
|
||
"path": "docs/CONFIGURATION.md",
|
||
"sha256": "618dd41d6edfb50b884d8634663b9d7ab75b1526a3f3e9121903631804e75e43",
|
||
"content": "# Configuration\n\ncodewhale reads configuration from a TOML file plus environment variables.\nAt process startup it may also load literal built-in-provider credentials from\na workspace-local `.env` file. Use the tracked `.env.example` as the template;\ncopy it to `.env`, then add only credential values.\n\nA workspace is not configuration authority. Codewhale therefore ignores\nconfig/profile/home paths, provider/model/base-URL routing, MCP/plugin state,\napproval/sandbox/shell posture, executable paths, runtime settings, and every\nother non-credential `.env` entry. Variable expansion is rejected so a\nrepository cannot substitute an ambient secret into a credential value. Use\n`config.toml`, CLI flags, or values exported by the launching shell for those\nexplicit control-plane settings. `.env` is read from a stable regular-file\nhandle, is capped at 1 MiB, and symbolic links, reparse points, and multiply\nlinked files are rejected.\n\n## Reading and checking configuration from the CLI\n\n`codewhale config get <key>` reads scalar keys, whole tables such as `tools`,\nand nested paths such as `tools.user_input_timeout_seconds`. Displayed tables\nand nested values apply the same recursive credential redaction as `config dump`.\n\n`config set` supports its named scalar keys and the provider, route, and\nnotification commands. It refuses a key that nothing reads and suggests the\nnearest real key (`config set calm_mod on` names `calm_mode`). A settings.toml\nkey such as `calm_mode` or `tool_collapse` is validated and written to the\nuser-global settings.toml, not config.toml; `config get` reads it back from\nthere even when an old config.toml copy (which nothing reads) is still present,\nand names that copy so you can `config unset` it. Settings keys have no project\nscope, so `--project` refuses them. A config.toml key is checked against the\ntype its reader expects and stored as a TOML boolean or number where the\nreader needs one (`yolo = true`, `max_subagents = 4`); `reasoning_effort`\naccepts the same aliases as `/effort` (#6563). Other dotted writes fail before\nmodifying the file and name the TOML table to edit. For example, set a tools\ntimeout in the file as:\n\n```toml\n[tools]\nuser_input_timeout_seconds = 0\n```\n\n`codewhale config doctor` checks credential presence and endpoint shape, and\nwarns about each config.toml root key that nothing reads: a settings.toml key\nleft in config.toml is named as misplaced, and any other unread key gets a\ndid-you-mean. Keys read by any runtime reader are not reported. The warnings do\nnot fail the check, and a clean result does not validate every runtime setting's\nvalue (#6083, #6563).\n\n## Usage and execution statistics\n\n`codewhale stats` is an alias of `codewhale metrics`; both use the same local,\nread-only backend. Use `--json` for the structured `stats` report. For example:\n\n```sh\ncodewhale stats --days 7 --models 10 --tools 20 --roles 10\ncodewhale metrics --project /path/to/workspace --json\n```\n\n`--days N` accepts a positive number of days and conflicts with `--since`.\n`--since 24h` and the existing duration syntax remain available. `--models N`,\n`--tools N` and `--roles N` select the top positive N rows by known tokens,\nidentified tool executions and recorded Fleet tasks respectively. These limits\nonly shorten the display; they do not remove stored receipts.\n\nThe report separates saved-document and Runtime views, frozen native-source\nmoney and token subtotals by actual model, retained tool-result history\nestimates and outcomes, completed compaction observations, and recorded Fleet\nroles. USD and CNY stay separate. A cumulative correction replaces its proven\npredecessor; copied Session/Runtime receipts and inherited Runtime fork rows\nare not added as new provider work. Conflicting source versions are shown\nseparately and excluded from the subtotal. Saved legacy display totals remain\nvisible as document projections without being charged again globally.\n\nCoverage is explicit. A missing quote, usage receipt, tool outcome, output\nshape or recovery link is unknown, not zero. Empty scalar usage from older\ntransport records cannot prove a reported zero. Token counts describe known\nreported data; tool history tokens are estimates of retained text, not bytes\nproven to have been replayed on a provider request. Latest prepared-request\ncomponents and source-linked compaction/role economics are unavailable where\nthey were not captured. The report does not infer provider invoices or prove\ncomplete historical coverage.\n\n`--project WORKSPACE` filters current saved/thread/Fleet views. Original source\nworkspace is not yet captured, so native money and model subtotals are\nunavailable for that filter; mutable current metadata is not used to assign a\ncharge. Older receipt projections without reliable workspace scope are also\nunavailable under this filter. Without it, those legacy receipt views remain\nseparate from native subtotals and may overlap one another.\n\nInspection uses the existing session-size bound (16 MiB per record/frame and\nfor each retained query index). Unreadable, linked, oversized or incomplete\nrecords create visible gaps; an exhausted native index refuses the query\nwithout evicting evidence. Use a narrower time window where possible. Reading\nstatistics does not create a store, repair history, refresh pricing, start a\nmodel call or retire accounting receipts. Existing state-root overrides remain\nthe read boundary.\n\n`/cache calls [count]` shows recent retained native source receipts in the TUI;\nit defaults to 10 and displays at most 50. A source correction updates its row.\nThe view keeps frozen USD/CNY estimates and known subtotals separate from missing\nusage or prices; absent or contradictory cache classes show unknown. The header\ncounts retained sources, not physical requests or complete historical calls.\nThe command does not issue a model request or remove accounting receipts.\n\nPublished prices are frozen for DeepSeek's root, `/v1`, `/beta` and Anthropic\nendpoint variants, and for other providers' default endpoints. Each quote stays\nbound to the exact endpoint, identity, model and dispatch time when saved or replayed.\nOperator endpoint overrides do not become published pricing authorities, and\nsubscription or token-plan routes remain outside money coverage. Signed cloud\nfacts keep their separate route-scoping rules. Recognition as an official\ncredential endpoint alone does not authorize sharing prices across regions or plans.\n\nMissing request outcomes, omitted provider usage, unverified legacy usage and\nusage corrections have distinct cost reasons. Known limitation: saved receipts\nwithout a frozen price are not repriced from today's catalog; their complete\ncost remains unknown even after updating the binary.\n\n## Constitution, project instructions, and repo authority\n\nCodewhale has several instruction surfaces. They are deliberately separate so a\npersonal constitution, repo policy, project instructions, and runtime security\ncontrols do not blur together.\n\n- **Bundled global Constitution** — the compiled base law in the binary. It is\n the default floor for every session.\n- **User-global constitution** — the normal guided setup output. Manage it with\n `/constitution` or `/setup`; Codewhale stores structured data at\n `$CODEWHALE_HOME/constitution.json` (default `~/.codewhale/constitution.json`)\n and renders it into a separate `<codewhale_user_constitution>` prose block.\n This can express preferences and stop conditions, but it does not change\n runtime approval policy, sandbox, shell, network, trust, or MCP permissions.\n- **Repo-local constitution** — optional project policy in\n `.codewhale/constitution.json`, described below.\n- **`AGENTS.md`** — cross-agent **project instructions** (prose). This is the\n canonical file for \"how should an agent work in this repo.\" Run `/init` to\n scaffold one. Instruction files written for other tools (`CLAUDE.md`,\n `.claude/instructions.md`, `.cursorrules`, `.github/copilot-instructions.md`\n and similar) are read only when you opt in with the top-level\n `project_instruction_imports` key, for example `[\"claude\"]`, `[\"cursor\"]` or\n `[\"all\"]` (see `config.example.toml`); Codewhale warns when it finds one it\n did not load.\n\n Codewhale reads one instruction file per directory, from the repository\n root down to the workspace, and puts your global instruction file (such as\n `~/.codewhale/AGENTS.md`) in front of them. The nearest scope reads last\n and wins: the workspace file overrides its parents, and project files\n override the global file. Global and project instructions are budgeted\n separately in estimated tokens (see [Instruction budget](#instruction-budget-instruction_budget-6526)\n below): a scope over its budget drops whole files from the broadest end\n first, and a single file larger than the remaining budget keeps its head\n with a marker that names the bytes and tokens dropped. A file keeps at most\n its first 100 KiB, and a file larger than `file_max_bytes` (8 MiB, lowerable\n only) is skipped with a notice. Every loss — trimmed, skipped, or refused —\n reaches the model and `/context report`.\n- **Memory and handoffs** — recalled state. Useful, but lower authority than\n constitutions and project instructions.\n\n### Managing the user-global constitution (`/setup` and `/constitution`)\n\nThe bundled **working agreement** is the safe default and no longer adds a\nrequired first-run screen. Customize it later through `/constitution` or the\nprogressive `/setup` guide. Provider/model readiness, workspace trust, and\nruntime posture stay separate from this guidance.\n\nOn the **Working agreement** (constitution) step:\n\n- **`1`–`6`** tune the guided draft. **`G`** previews it, and **`G`** again\n ratifies and saves a fresh structured `constitution.json`.\n- **`A`** (shown only when a provider is configured) asks your first configured\n model to draft the constitution. Drafting is **not** saving: the draft is\n rendered through the same preview and you still press **`G`** to ratify\n before anything persists.\n- **`K`** keeps your existing loaded constitution unchanged (shown only when a\n valid file is already present).\n- **`U`** (or `/constitution bundled`) records the bundled/default law.\n\n`/constitution` (alias `/law`) is the primary management surface once you are\nset up. Subcommands: `status` (the default), `preview`, `review`, `repo` (the\nrepo-local law block), `explain`, `base`, `suggestions`, `ratify <id>`,\n`migrate`, `rollback`, `edit`/`guided`, `repair`, `posture`, and `bundled`.\nManaging the constitution never changes runtime approval, sandbox,\nshell, network, trust, default mode, or MCP authority — those stay in runtime\nposture/config.\n\nEach repo can carry two distinct, complementary files:\n\n- **`AGENTS.md`** — ordinary project working instructions.\n- **`.codewhale/constitution.json`** — Codewhale-specific **repo authority /\n prioritization policy**: when local sources conflict, which should Codewhale\n trust first, and what to verify before claiming a task is done. `.codewhale/`\n lives inside the repo (like `.github/`). Example:\n\n ```json\n {\n \"schema_version\": 1,\n \"authority\": [\n \"current user request\",\n \"live code and tests\",\n \"GitHub issue/PR details\",\n \"AGENTS.md\",\n \"memory\",\n \"old handoffs\"\n ],\n \"protected_invariants\": [\n \"do not break old-session transcript replay\"\n ],\n \"branch_policy\": \"PRs target the integration branch, not main\",\n \"verification_policy\": {\n \"before_claiming_done\": [\"run focused tests\", \"read changed files back\"]\n },\n \"escalate_when\": [\n \"a destructive action was not explicitly authorized\"\n ]\n }\n ```\n\n All fields are optional. When present, the file is rendered into the system\n prompt as concise prose in a higher-authority block. Legacy `WHALE.md` files\n are ignored and reported as migration-only diagnostics.\n\n Each `protected_invariants` entry may be either a plain string (advisory\n prose, the historical shape) or an object carrying path globs, which is\n additionally **mechanically enforced** in the tool gate. See\n [Enforced repo-law invariants](#enforced-repo-law-invariants) below.\n\n This is the **repo-local law** layer. When guidance conflicts, the\n **Whose word wins** section of the effective base constitution owns the\n ordering; the bundled source is\n [`BASE_PROMPT`](../crates/engine/src/prompts/text.rs). Use `/constitution base`\n to inspect the effective base, including any opted-in expert override.\n The order in which these files are described or assembled is not an\n authority ranking. Runtime policy (permissions, sandbox and cost limits\n enforced in code) is separate from prompt guidance; editing a constitution\n does not grant permissions.\n\n> **`WHALE.md` is deprecated.** It overlapped confusingly with `AGENTS.md`.\n> Codewhale no longer reads `WHALE.md` as project or global context. If one is\n> present, setup/context diagnostics report it as ignored so you can migrate it.\n> Move ordinary instructions to `AGENTS.md` and Codewhale-specific authority\n> policy to `.codewhale/constitution.json`. Personal standing guidance belongs\n> in `/constitution` / `$CODEWHALE_HOME/constitution.json`. (The global\n> Codewhale Constitution shipped in the model prompt is a separate thing and is\n> unaffected.)\n\n### Enforced repo-law invariants\n\nBy default a `protected_invariants` entry is advisory prose: it is rendered into\nthe prompt as guidance the agent should honor, but nothing stops a write. An\nentry written as an **object with `paths`** is different — it compiles into a\nmechanical write hold that the engine's tool gate evaluates before the write\nruns. The law becomes mechanism, not just a request.\n\nAn enforced entry has this shape:\n\n```json\n{\n \"schema_version\": 1,\n \"protected_invariants\": [\n \"Keep DeepSeek support first-class.\",\n {\n \"text\": \"The wire format is frozen; protocol changes need a human.\",\n \"paths\": [\"crates/protocol/**\"],\n \"action\": \"block\"\n },\n {\n \"text\": \"Release notes need human review.\",\n \"paths\": [\"CHANGELOG.md\"],\n \"action\": \"ask\"\n }\n ]\n}\n```\n\n- `text` — required. The reason surfaced on the hold. An empty `text` is skipped.\n- `paths` — workspace-relative globs (globset syntax, e.g. `crates/protocol/**`,\n `**/secrets.toml`, `CHANGELOG.md`). An object with no usable `paths` stays\n advisory-only despite the object shape.\n- `action` — optional, defaults to `ask`. `ask` force-prompts in Ask and\n Auto-Review; in Full Access it denies the protected write without opening a\n modal. `block` **denies the write outright** in every posture.\n\nSemantics:\n\n- **Tighten-only.** The schema has no allow/widen shape, so law can only *add*\n holds — a crafted constitution can never grant authority or weaken a gate\n above it.\n- **Not bypassable by mode.** Like the built-in safety floor, an `ask` hold\n force-prompts in Ask and Auto-Review. Full Access never opens approval\n modals, so the same hold fails closed as a hard block; `block` always denies.\n Mode cannot turn a hold off.\n- **Repo-local only.** Only the repo's `.codewhale/constitution.json`\n participates. The user-global constitution stays advisory prose and never\n reaches this mechanism.\n- **Fails safe.** A missing file, parse error, or invalid glob degrades to\n fewer or zero rules — never a hold on unprotected paths and never a poisoned\n gate. Across matches the strongest action wins, so `block` outranks `ask`.\n- **Leaves a receipt.** Every hold emits a `tool.repo_law_decision` tool-audit\n event naming the invariant, the matched path, and the source file; the\n approval/denial reason names the invariant too.\n\n**Coverage is deliberately limited.** Holds are evaluated only for the write\ntools `write_file`, `edit_file`, `apply_patch`, and `fim_edit`, and only\nagainst the filesystem targets named in their inputs (`path`/`target`/\n`destination`/`file_path`, `changes[].path`, and unified-diff /\n`apply_patch`-envelope headers). A shell command that writes a protected path is **not** held by\nrepo law — those writes are still governed by the ordinary approval, sandbox,\nand shell-write gates, not by this mechanism.\n\n### Expert full base-prompt override (#3638)\n\nThe global Constitution (the base system prompt, normally compiled in from\n`crates/engine/src/prompts/text.rs` as `BASE_PROMPT`) can be replaced per-user\nwithout rebuilding. This is\nan expert escape hatch, not the normal `/constitution` guided setup output.\nBecause this is a prompt trust boundary, it takes **two deliberate steps** — a\nfile alone is not enough:\n\n1. Drop the replacement at `~/.codewhale/prompts/constitution.md` (under\n `$CODEWHALE_HOME` when set).\n2. Set the explicit opt-in flag `CODEWHALE_ALLOW_BASE_PROMPT_OVERRIDE=1`\n (`true`/`on`/`yes` also accepted).\n\nIf the file exists but the flag is unset, the override is **ignored** (with a\nlog line pointing to the flag) and the bundled Constitution stays in place.\nThis is intended for repurposing the TUI beyond software engineering — e.g.\nlong-form writing or document review — where the engineering-oriented base\nprompt is a poor fit. It is loaded once at startup; a **missing or empty file\nis a no-op**, so existing installs keep the bundled prompt.\n\nScope is deliberately narrow: only the byte-stable **base prompt segment** is\noverridable. Mode deltas, the approval policy, the tool taxonomy, Context\nManagement, and the Compaction Relay are still owned by Codewhale's runtime\nassembly, so an override **cannot remove safety-relevant guidance** (sandbox,\napprovals) — it only swaps the task/voice framing. To customize ordinary\npersonal behavior, prefer `/constitution`; to customize per-repo behavior,\nprefer `AGENTS.md` + `.codewhale/constitution.json` above.\n\n## Where It Looks\n\nDefault config path:\n\n- `~/.codewhale/config.toml`\n- Legacy fallback: `~/.deepseek/config.toml`\n\nOverrides:\n\n- CLI: `codewhale --config /path/to/config.toml`\n- Env: `CODEWHALE_CONFIG_PATH=/path/to/config.toml`\n- Legacy env alias: `DEEPSEEK_CONFIG_PATH=/path/to/config.toml`\n\nIf both are set, `--config` wins. Environment variable overrides are applied after the file is loaded.\n\n### TUI editability audit\n\nInside the TUI, run `/config audit` to see which documented keys can be changed\nfrom the current session, which ones can also be persisted, and which ones stay\nfile-only or restart-only. The audit includes current values for the high-impact\nruntime controls such as `approval_policy`, `allow_shell`,\n`stream_chunk_timeout_secs`, `base_url`, `mcp_config_path`, and the\n`[subagents]` concurrency/depth/timeout keys.\n\nUse the command's \"Command / reason\" column as the source of truth before\nediting by hand. For example, `/config approval_mode on-request --save` writes\ntop-level `approval_policy = \"on-request\"`, while provider base URLs are saved\nbut still require restarting the model client.\n\n### User workspace entries\n\nInteractive Agent sessions expose shell tools by default with approval gating\nunless you explicitly disable them. For a shell opt-in that should live in the\nuser's global config for noninteractive or durable-task profiles rather than in\nthe repository, add a workspace-scoped entry:\n\n```toml\n[workspace.'/absolute/path/to/project']\nallow_shell = true\n```\n\nThe entry applies only when the launched workspace path matches the table key.\nThe legacy `[projects.\"/absolute/path/to/project\"]` table is also accepted for\nthis user-owned override.\n\nIn interactive mode, the per-project overlay\n`<workspace>/.codewhale/config.toml` is applied after this user entry. A\nproject-level `allow_shell = false` can still tighten the session; project-level\n`allow_shell = true` is ignored.\n\n### Per-project overlay (#485)\n\nWhen the TUI starts in a workspace that contains a regular-file\n`<workspace>/.codewhale/config.toml`, the safe values declared in that file are\nmerged on top of the global config. Legacy\n`<workspace>/.deepseek/config.toml` files are still read when the Codewhale path\nis absent. Symlinked project config files are rejected. This lets a repo suggest\na model or tighten local safety posture without touching the user's\n`~/.codewhale/config.toml`. Pass `--no-project-config` to skip the overlay for\none launch.\n\nSupported keys in the project overlay (top-level fields only):\n\n| Key | Effect |\n|---|---|\n| `model` | override `default_text_model` |\n| `reasoning_effort` | force `\"high\"` / `\"max\"` for a complex repo |\n| `approval_policy` | only values that tighten the user's current permission posture |\n| `sandbox_mode` | only values that tighten the user's current sandbox posture |\n| `max_subagents` | clamp sub-agent concurrency for a constrained repo (clamped to 1..=128) |\n| `allow_shell` | `false` can disable shell access; `true` is ignored |\n\nThe overlay is intentionally narrow — it covers the fields a repo\nmaintainer is most likely to want to standardize across contributors.\nCredential, endpoint, provider-selection, MCP config, hooks, skills,\nretry, hotbar bindings, and `instructions = [...]` settings stay user-global.\nIf a repo-local config declares `api_key`, `base_url`, `providers`, `provider`,\n`mcp_config_path`, `notes_path`, `hotbar`, `allow_shell = true`, or `instructions`,\nCodewhale ignores that key and keeps the user's global setting.\n\nThe consolidated `codewhale` runtime uses `~/.codewhale/config.toml` for\nprovider and model settings. `codewhale auth set --provider deepseek` saves\nthe key in the local secret store (a private file under `~/.codewhale/secrets/`\nby default; the OS keychain is an explicit option), rather than writing it\ninto that config file. Legacy `~/.deepseek/config.toml` is migrated on first\nlaunch when needed. `codewhale --model deepseek-v4-flash` is forwarded to the\nTUI as `CODEWHALE_MODEL`. The dispatcher no longer writes `DEEPSEEK_*` twins;\na value you set yourself remains a legacy alias when its `CODEWHALE_*`\nequivalent is unset.\n\n### Account provider keys\n\n`/connect` offers Continue with Codewhale or Connect on this computer. The\naccount path signs in and lists usable model connections reported by the\naccount service. When none are ready, it offers retry or on-computer setup.\nSign-in alone does not supply model access or change the selected model.\nLocal provider keys and local models never need an account; use `/provider`\nor `codewhale auth set --provider <provider>` to manage them.\n\nFor an account that supports saved provider keys, these commands manage them:\n\n```bash\ncodewhale account keys list # configured status, never secret values\ncodewhale account keys remove deepseek # remove the account's saved key\n```\n\nLogin never uploads local keys, and removing an account key does not change\nlocal copies or revoke the key at the provider.\n\nThe account session is separate from provider keys. `/login` signs in inside\nthe TUI; `codewhale login` does so from the shell. `codewhale account status`\ninspects it. `/logout`, `codewhale logout` and `codewhale account logout` remove\nonly the account session for the selected `--profile`. The explicit\n`codewhale logout --all` action also clears saved model credentials.\nAccount sessions use the private `0600`\nCodewhale secrets file, scoped to the profile and account API origin; they do\nnot use the OS keychain. The older `codewhale cloud ...` spelling remains an\nalias for account commands.\n\n### Portable config bundles\n\n`codewhale config export --portable [--project] [--out FILE]` writes a\nportable, secret-free bundle of your configuration: sorted TOML with\ncredential and machine-specific keys (API keys, base URLs, socket paths)\ndropped, never a redacted placeholder in their place. Without `--out` the\nbundle goes to stdout. Typed tables, arrays, numbers, booleans, and datetimes\nremain typed. Machine-bound authority is deliberately non-portable: project\ntrust overlays, credential readers, auto-running hooks, executable LSP\ndefinitions, and local path bindings are omitted rather than copied to a new\nhost. So is trust posture (`yolo`, `allow_shell`, `approval_policy`,\n`sandbox_mode`, `sandbox_network_access`, and the sandbox read-denylist\npaths) and local endpoints or executables (`[lifecycle_outbox]`,\n`[extension_host]`, `[control_socket]`). URLs that carry a credential —\nuserinfo, a token-named query parameter, or a Slack/Discord/Teams webhook\npath — are treated as credentials.\n\n`codewhale config import <FILE|HTTPS_URL|-> [--dry-run] [--yes] [--project]`\napplies a bundle. The envelope is strict (`schema_version = 1`, kind\n`codewhale.portable-config`; unknown fields fail). Import prints a\ndeterministic plan — added / changed / skipped / conflicting / rejected —\nthen asks for consent unless `--yes` is given; headless use requires it.\nCredential-shaped entries are rejected by key name and by value shape;\nrejections name the field, never a value. Remote bundles come from HTTPS\nonly (loopback http excepted) with a 5 MiB cap. Application backs up the\ntarget document to `<config>.bundle-backup-<timestamp>-<random>`, rolls back on any\nfailure, and re-importing an applied bundle changes nothing.\n\nImport also rejects the non-portable authority classes omitted by export,\nincluding nested/camel/dotted credential keys and cookie headers. This keeps a\nhand-authored or remote bundle from reintroducing machine trust, local\ncredential access, or automatically executable commands that a local export\nwould refuse to carry. Structured tables are deep-merged: a portable model or\npreference update does not erase target-local provider credentials, endpoints,\nhooks, or executable definitions that were deliberately omitted from the\nbundle. Arrays and scalar values still replace the corresponding portable\nvalue.\n\nSections map to scope: `[project]` entries land only in the workspace\ndocument (`--project`, which must target an actual workspace config),\n`[global]` only in the user-global one; `preferences`, `profiles`, and\n`plugins` apply at either scope. A global bundle operation refuses a workspace\ndocument just as a project operation refuses the user-global document.\n\n### Credential read precedence (#5197)\n\nCredential reads are **folder-independent by default**: every layer below is\nuser-global or process-scoped, so a key saved in one repo resolves identically\nin every other repo. Repo-local config never carries credential material — the\nproject overlay above reads only its allowlisted keys and ignores `api_key`,\nand a credential write aimed at a workspace-scoped config is rescoped to the\nuser-global `~/.codewhale/config.toml` (#5045, #5193).\n\nFor the active provider, the runtime resolves the API key in this exact order\n(first match wins):\n\n1. **Route-specific auth contract.** Routes whose `auth_mode` disables API\n keys stop here with no credential. OAuth routes use their explicitly\n granted token: the official `openai-codex` plan route accepts only\n Codewhale-owned credentials from `codewhale auth chatgpt`, bound to its\n issued registration and verified account. Imported Codex CLI and process\n tokens cannot authorize this route;\n `[providers.xai] auth_mode = \"oauth\"` reads Codewhale's own xAI\n device-login store (or a consent-granted Grok CLI file).\n2. **Explicit CLI key.** `--api-key` forwarded with its source marker wins\n over every saved slot.\n3. **Config file `api_key`.** The `[providers.<name>] api_key` table slot for\n the active provider. `deepseek-CN` also reads `[providers.deepseek]`. An\n older top-level `api_key` is read as `[providers.deepseek] api_key` (see\n [Legacy top-level `base_url` and `api_key`](#legacy-top-level-base_url-and-api_key)).\n File-owned keys stay bound to their file-owned endpoint: when the\n environment replaces the route's base URL with a custom host, the saved\n key is not sent there.\n4. **`api_key_env` binding.** `[providers.<name>] api_key_env = \"VAR\"` reads\n the named environment variable. For custom providers an unset or empty\n binding is a loud error, not a silent fallback (#5104).\n5. **Secret store.** The durable per-provider slot written by\n `codewhale auth set` (file-backed under `~/.codewhale/secrets/` by\n default; the OS keyring only when explicitly selected). Skipped for named\n custom routes, self-hosted providers, custom endpoints other than an\n explicitly authenticated loopback, and routes whose `auth_mode` needs no\n key.\n6. **Ambient environment.** The provider's own variable\n (`DEEPSEEK_API_KEY`, `OPENROUTER_API_KEY`, `MOONSHOT_API_KEY`, …).\n Ambient keys are only ever sent to the provider's official endpoint and\n are skipped under the same conditions as the secret store.\n7. **Keyless fallback.** Self-hosted providers and loopback endpoints may run\n with no credential; every other route fails with provider-specific setup\n guidance.\n\nLegacy compatibility: `~/.deepseek/config.toml` is migrated into\n`~/.codewhale/config.toml` on first launch, `DEEPSEEK_*` environment\nvariables remain accepted aliases for the `CODEWHALE_*` forms, and\n`DEEPSEEK_SECRET_BACKEND` is the legacy alias for `CODEWHALE_SECRET_BACKEND`.\n\nRun `codewhale auth status` to inspect the active provider's config\nfile, OS keyring backend, environment variable, winning source, and last-four\nlabel without printing the key itself. The command only probes the active\nprovider's keyring entry.\n\nFor hosted, generic OpenAI-compatible, self-hosted, OpenAI Responses, or native\nAnthropic providers, set `provider = \"<id>\"` or pass\n`codewhale --provider <id>`. The canonical provider IDs are `deepseek`,\n`nvidia-nim`, `openai`, `atlascloud`, `wanjie-ark`, `volcengine`,\n`openrouter`, `orcarouter`, `xiaomi-mimo`, `novita`, `fireworks`,\n`siliconflow`, `arcee`, `siliconflow-CN`, `moonshot`, `sglang`, `vllm`,\n`ollama`, `ollama-cloud`, `huggingface`, `modelscope`, `together`, `qianfan`, `openai-codex`,\n`anthropic`, `openmodel`, `zai`, `stepfun`, `minimax`, `deepinfra`,\n`sakana`, `longcat`, `opencode-go`, `opencode-zen`, `meta`, `xai`,\n`mistral`, `telecomjs`, `modelstudio-token-plan`, `google`,\n`edenai`, `concentrate`, `codewhale`, and `custom` (a user-defined OpenAI-compatible endpoint via\n`[providers.<name>]`).\nFor the provider-by-provider registry, including wire protocol, auth variables,\ndefault base URLs, model IDs, and capability metadata, see\n[PROVIDERS.md](PROVIDERS.md).\nThe facade saves provider credentials in the local secret store and forwards\nthe resolved key, base URL, provider, and model to the TUI process. Use\n`codewhale auth set --provider nvidia-nim --api-key \"YOUR_NVIDIA_API_KEY\"` or\n`codewhale auth set --provider openai --api-key \"YOUR_OPENAI_COMPATIBLE_API_KEY\"` or\n`codewhale auth set --provider atlascloud --api-key \"YOUR_ATLASCLOUD_API_KEY\"` or\n`codewhale auth set --provider wanjie-ark --api-key \"YOUR_WANJIE_API_KEY\"` or\n`codewhale auth set --provider xiaomi-mimo --api-key \"YOUR_XIAOMI_KEY\"` or\n`codewhale auth set --provider fireworks --api-key \"YOUR_FIREWORKS_API_KEY\"` or\n`codewhale auth set --provider siliconflow --api-key \"YOUR_SILICONFLOW_API_KEY\"` or\n`codewhale auth set --provider arcee --api-key \"YOUR_ARCEE_API_KEY\"` or the\nmatching provider ID from [PROVIDERS.md](PROVIDERS.md) to save provider keys\nthrough the facade. The generic `openai` provider defaults\nto `https://api.openai.com/v1`, accepts `OPENAI_BASE_URL`, and defaults to\n`gpt-5.6`. A custom OpenAI-compatible gateway can still select its own model\nexplicitly. `atlascloud` defaults to\n`https://api.atlascloud.ai/v1`, accepts `ATLASCLOUD_BASE_URL`, and uses\n`deepseek-ai/deepseek-v4-flash` as its default model. `wanjie-ark` targets\nWanjie Ark's OpenAI-compatible endpoint at\n`https://maas-openapi.wanjiedata.com/api/v1`, defaults to `deepseek-reasoner`,\nand passes model IDs through unchanged because Wanjie model access is\naccount-scoped. SGLang, vLLM, and Ollama are\nself-hosted and can run without an API key by default. Ollama defaults to\n`http://localhost:11434/v1` and sends model tags such as `codewhale-coder:1.3b`\nor `qwen2.5-coder:7b` unchanged. Self-hosted providers and loopback custom\nURLs (`localhost`, `127.0.0.1`, `[::1]`, `0.0.0.0`) do not read the secret store\nunless API-key auth is explicitly requested; use an env var or config-file key\nwhen a local server does require bearer auth.\nOllama Cloud is the separate hosted `ollama-cloud` provider. It defaults to\n`https://ollama.com/v1` and `gpt-oss:120b`; save its key with\n`codewhale auth set --provider ollama-cloud`. Ambient auth reads\n`OLLAMA_CLOUD_API_KEY` first, then Ollama's official `OLLAMA_API_KEY`.\nSiliconFlow defaults to `https://api.siliconflow.com/v1`, accepts\n`SILICONFLOW_BASE_URL`, and uses `deepseek-ai/DeepSeek-V4-Pro` by default.\n`provider = \"siliconflow-CN\"` selects the China regional default\n`https://api.siliconflow.cn/v1` with the `[providers.siliconflow_cn]` table and\n`SILICONFLOW_API_KEY` credential slot.\nArcee AI defaults to `https://api.arcee.ai/api/v1`, accepts `ARCEE_BASE_URL`,\nand uses `trinity-large-thinking` by default for Codewhale agent work.\n`trinity-large-preview` is also listed as a direct Arcee API model; OpenRouter's\n`arcee-ai/trinity-large-thinking` remains the OpenRouter namespaced form, while\nthe direct Arcee provider uses the bare `trinity-large-thinking` ID. Direct\nArcee large-model API calls are tracked as 256K-context BF16 serving; Thinking\nis reasoning-capable, while Preview is not marked as a thinking model.\n\n### OpenRouter vendor pinning\n\nOpenRouter serves each model through several upstream vendors, and Codewhale\ncan pin requests to a vendor with `[providers.openrouter] vendor` (#6007):\n\n```toml\nprovider = \"openrouter\"\n[providers.openrouter]\nmodel = \"deepseek/deepseek-v4-pro\"\nvendor = \"deepinfra\" # copy the vendor slug from the model's OpenRouter page\n```\n\nThis sends `\"provider\": {\"order\": [\"deepinfra\"], \"allow_fallbacks\": false}`\non OpenRouter requests. A base slug can match multiple endpoint variants;\ncopy a full slug such as `deepinfra/turbo` to select one variant. An unavailable\npin fails at OpenRouter. Codewhale's separate `fallback_providers` setting can\nstill switch the whole route after a recoverable error.\n\nThe pin applies across OpenRouter models, including auxiliary requests on that\nroute. Set `vendor = \"\"` to clear it. Reload config or restart to apply edits;\nrequests already in flight keep their captured route. Other providers do not\ninherit the pin. `/preview-request` shows the primary request's routing fields.\n\nModel strings still pass through verbatim: `:floor` sorts by price, `:nitro`\nsorts by throughput, and `@preset/my-team-preset` references an account preset.\nSee OpenRouter's [provider routing](https://openrouter.ai/docs/guides/routing/provider-selection)\nand [presets](https://openrouter.ai/docs/guides/features/presets) documentation.\nCodewhale does not fetch per-vendor endpoint prices or availability; pinned\nusage reports a routing-dependent unknown cost instead of a catalog estimate.\n\n### Custom OpenAI-Compatible Gateways\n\nFor a single third-party service that implements the OpenAI Chat Completions\nAPI, the simplest setup is the built-in `openai` provider name pointed at the\ngateway:\n\n```toml\nprovider = \"openai\"\ndefault_text_model = \"your-model-id\"\n\n[providers.openai]\napi_key = \"YOUR_OPENAI_COMPATIBLE_API_KEY\"\nbase_url = \"https://your-gateway.example/v1\"\n```\n\nPut the endpoint under `[providers.openai]`, not the legacy top-level\n`base_url`, so the OpenAI-compatible provider receives it. `default_text_model`\nis the model ID sent to the gateway; `[providers.openai].model` can be used as\nthe OpenAI-provider-specific override.\n\nIf you keep several OpenAI-compatible gateways, or need a stable name for an\nAgentProfile provider pin, define a user-named custom provider table:\n\n```toml\nprovider = \"lm-studio\"\n\n[providers.lm-studio]\nkind = \"openai-compatible\"\nbase_url = \"http://127.0.0.1:1234/v1\"\napi_key = \"lm-studio\"\nmodel = \"qwen-2.5-7b\"\n```\n\nCustom provider names may be selected with `provider = \"<name>\"`,\n`--provider <name>`, or an AgentProfile `provider = \"<name>\"` when the matching\n`[providers.<name>]` table exists.\n\nA custom table sends Chat Completions requests by default. Add\n`wire = \"responses\"` to send OpenAI Responses requests to\n`<base_url>/responses` instead, or `wire = \"anthropic\"` for Anthropic Messages:\n\n```toml\n[providers.my-gateway]\nkind = \"openai-compatible\"\nbase_url = \"https://gateway.example/v1\"\napi_key_env = \"MY_GATEWAY_API_KEY\"\nmodel = \"gpt-5.6\"\nwire = \"responses\"\n```\n\nStepFun has a first-class provider entry, so keep Coding Plan credentials and\nbase URL scoped to `[providers.stepfun]`:\n\n```toml\nprovider = \"stepfun\"\n\n[providers.stepfun]\napi_key = \"YOUR_STEPFUN_API_KEY\"\nbase_url = \"https://api.stepfun.ai/step_plan/v1\"\nmodel = \"step-3.7-flash\"\n```\n\n`/provider` setup asks which StepFun billing route the key belongs to —\npay-as-you-go (`https://api.stepfun.ai/v1`) or a Step Plan subscription\n(`https://api.stepfun.ai/step_plan/v1`) — and validates the key against the\nendpoint you pick before saving it. The answer is written to\n`[providers.stepfun].base_url` and nowhere else. If that key already holds a\nbase URL Codewhale does not recognize as one of those two routes, the question\nis skipped and your value is left untouched.\n\nAlibaba Bailian / Model Studio DashScope Qwen routes use the same OpenAI\nprovider shape:\n\n```toml\nprovider = \"openai\"\n\n[providers.openai]\napi_key = \"YOUR_DASHSCOPE_API_KEY\"\nbase_url = \"https://dashscope-intl.aliyuncs.com/compatible-mode/v1\"\nmodel = \"qwen-plus\"\ncontext_window = 1000000\n```\n\nUse the regional DashScope `compatible-mode/v1` base URL that matches the\nregion of your API key. Codewhale keeps `qwen-plus` scoped to the `openai`\nprovider route and does not infer a different provider from the model prefix.\nThe same rule applies to all provider-prefixed model strings: a prefix such as\n`deepseek-ai/...` or `deepseek/...` is a provider-owned wire ID under the\nselected provider, not an automatic switch to the DeepSeek provider.\nSet `context_window` to the gateway/model's real total context window when it\ndiffers from Codewhale's static model metadata. See\n[Context length (context window)](#context-length-context-window) for the full\nresolution order and for how to check which value is in effect.\n\nIf the gateway accepts `POST /chat/completions` but rejects\n`/v1/chat/completions`, set a provider-local `path_suffix`:\n\n```toml\n[providers.openai]\nbase_url = \"https://your-gateway.example/v1\"\npath_suffix = \"/chat/completions\"\n```\n\nThe suffix applies only to chat-completion requests. Model listing and\nDeepSeek beta paths keep their built-in routing so a generic gateway override\ndoes not accidentally rewrite `/models` or `/beta/completions`.\n\nFor private gateways with broken or intercepted certificates, use\n`SSL_CERT_FILE` with a trusted CA bundle. The legacy provider-table key\n`insecure_skip_tls_verify = true` is still parsed so `codewhale doctor` can\nreport stale configs, but provider clients reject it instead of disabling TLS\ncertificate verification.\n\nLocal HTTP endpoints such as Ollama, SGLang, and vLLM are allowed by default\nwhen they use localhost or loopback addresses. For a non-local `http://`\ngateway, launch with `DEEPSEEK_ALLOW_INSECURE_HTTP=1` only on a trusted network:\n\n```bash\nDEEPSEEK_ALLOW_INSECURE_HTTP=1 codewhale\n```\n\nThird-party OpenAI-compatible gateways that need extra request headers can set\n`http_headers = { \"X-Model-Provider-Id\" = \"your-model-provider\" }` at the top\nlevel or under a provider table such as `[providers.deepseek]`. When configured,\ncodewhale sends those custom headers on model API requests. The equivalent\nenvironment override is `DEEPSEEK_HTTP_HEADERS`, using comma-separated\n`name=value` pairs such as\n`X-Model-Provider-Id=your-model-provider,X-Gateway-Route=dev`. `Authorization`\nand `Content-Type` are managed by the client and are not overridden by this\nsetting.\n\n### Vision Model\n\nCodewhale's chat provider and `image_analyze` tool are configured separately.\nThe main chat path remains the selected text/tool provider; image analysis runs\nthrough `[vision_model]` when the `vision_model` feature is enabled.\n\nXiaomi's current image-understanding docs include `mimo-v2.5` for image input.\nTo use MiMo for `image_analyze`, configure the vision model explicitly:\n\n```toml\n[features]\nvision_model = true\n\n[vision_model]\nmodel = \"mimo-v2.5\"\napi_key = \"YOUR_XIAOMI_KEY\"\nbase_url = \"https://api.xiaomimimo.com/v1\"\n```\n\nThe example above uses Xiaomi MiMo's pay-as-you-go OpenAI-compatible endpoint.\nIf you are using a Token Plan key (`tp-...`) for `[vision_model]`, you must set\n`base_url` explicitly because this generic OpenAI-compatible block does not\nauto-select MiMo endpoints. Use\n`https://token-plan-sgp.xiaomimimo.com/v1` for Singapore accounts,\n`https://token-plan-cn.xiaomimimo.com/v1` for China-region accounts, or\n`https://token-plan-ams.xiaomimimo.com/v1` for Europe/Amsterdam accounts.\n\n### Auto Model Routing (`[auto]`, `[auto.router]`)\n\nWith `model = \"auto\"`, each turn runs on your **declared default model** unless\nyou have opted into something else. Auto never guesses a cheaper or stronger\nmodel from how a request is worded; the old keyword-and-length heuristic was\nremoved (`auto_route_declared_fallback` in `crates/engine/src/model_routing.rs`).\nTwo optional layers change that:\n\n- an `[auto.router]` classifier, which you write down, that picks a model per\n turn; and\n- `[auto] cost_saving`, which prefers the active provider's fast sibling.\n\nWith neither set, Auto is local and free: the turn uses the default model and\nno classifier call is made.\n\n**There is no default classifier.** With `[auto.router]` unset, no classifier\ncall happens, whatever keys you hold. Holding a DeepSeek key used to elect\n`deepseek-v4-flash` automatically. That was removed because it spent tokens on\na route the user never chose and privileged one provider over the rest\n(`AutoRouterConfig` in `crates/tui/src/config.rs`). Electing a network\nclassifier is now something you write down.\n\nPoint the classifier at any configured provider with `[auto.router]`:\n\n```toml\n[auto.router]\nprovider = \"zai\"\nmodel = \"glm-5-turbo\"\nthinking = \"off\" # optional; defaults to off\ntimeout_secs = 4 # optional; default 4, 0 = default, capped at 300\n```\n\nA classifier call happens only when `[auto.router]` names both `provider` and\n`model` *and* that provider has a key:\n`router_available = router_configured && has_api_key_for(...)` in\n`ModelInventory::from_config` (`crates/engine/src/model_inventory.rs`). If either\ncondition fails, or the classifier call errors or times out, the local\nfallback decides: the default model, or the fast sibling under `cost_saving`.\nThe turn's route receipt (Turn Inspector, Ctrl+Alt+O or `/turn inspect`, \"Model\nroute + tokens/cost\") records which path was taken, and a\nrouter you configured that cannot run or fails (missing key, HTTP error,\ntimeout, invalid answer) is shown as `Auto router: failing — …` rather than\nsilently ignored.\n\n#### Set up model routing\n\n`/router` (also `/model router`) opens one Router setup view with these\npresets. Each writes only `[auto.router]`; none is ever chosen for you.\n\n| Preset | What it writes | Cost and privacy |\n| --- | --- | --- |\n| `/router jev` | Jev, TypeSafe's decision model, over OpenRouter (`typesafe/jev-1.13`) or TypeSafe direct, whichever key you have | About $0.00002 per turn ($0.042 per million input tokens, output free). Your latest request and up to six recent context lines go to OpenRouter → TypeSafe (or TypeSafe). |\n| `/router fast` | The active provider's runnable fast tier with thinking off | Your existing key; the classifier sees the same request text. |\n| `/router off` | Removes `[auto.router]` | No router call; Auto turns use the default model, or the fast tier while `[auto] cost_saving = true` (Off leaves that setting alone). |\n| `/router custom` | Nothing; prints the TOML to edit | — |\n\nChoosing Jev or Fast makes **one test call** with a fixed sample request and\nshows the tier it picked, the probabilities and confidence, the latency and the\nprovider-reported cost. `Enter` (or `/router save <preset>`) then saves through\nthe normal config writer; `Esc` discards it. TypeSafe paused new signups on\n2026-09-22, so OpenRouter is the default route for new users. A TypeSafe key is\nread from `TYPESAFE_API_KEY`, the `typesafe` secret-store entry, or\n`[providers.typesafe] api_key` / `api_key_env`.\n\n#### Decision routers (`kind = \"decision\"`)\n\nA decision router asks a non-generative decision model typed Choice questions\nfor the active provider's `fast` and `strong` tiers and thinking level, then\nreads calibrated probabilities. No prose is parsed.\n\n```toml\n[auto.router]\nkind = \"decision\" # default \"chat\"\nprovider = \"openrouter\" # or \"typesafe\"\nmodel = \"typesafe/jev-1.13\" # \"~typesafe/jev-latest\" also works; TypeSafe direct: \"jev-latest\"\ntimeout_secs = 2\nmin_confidence = 0.5 # default 0.5, clamped to 0..1\n```\n\n- The router is called only when the active provider has a runnable strong/fast\n pair; otherwise there is no call and no spend.\n- An answer below `min_confidence` takes the local fallback. Under\n `[auto] cost_saving`, a `strong` answer also needs a probability of at least\n 0.75, or the turn stays on the fast tier.\n- An unknown `kind`, or a decision `provider` other than `openrouter` /\n `typesafe`, leaves the router unconfigured and shown as failing.\n- `[auto.router] thinking` is ignored for decision routers. Both routes settle tokens through\n the originating session's routed-usage ledger. TypeSafe is a named Custom route\n with unknown billing; its price is never borrowed from the active chat provider.\n Provider-reported cost remains verbatim on the decision receipt.\n- OpenRouter uses `POST /api/alpha/decisions`; TypeSafe uses `POST /v1/systemone`.\n The shared client validates Choice, Noul and Score against the offered questions\n and bounds responses to 256 KiB. Malformed answers fail open with usage retained.\n\n#### Shadow Decision Gate (experimental, off by default)\n\nThe Superfast Decision Gate asks a decision model three typed questions about\nthe latest user message — does it need a tool, can it be answered from the\nconversation, and what is its intent — and logs a conservative recommendation.\nIt is shadow-only: it never changes routing, never skips or delays the model\ncall, and fails open on any error, timeout or malformed answer. It uses the same\nSystem One client as the decision router above; there is no separate HTTP\nclient. It is configured from the environment and reads it when a turn starts:\n\n```sh\nSUPERFAST_ENABLED=1 # off unless set\nSUPERFAST_PROVIDER=typesafe # or openrouter; required when enabled\nSUPERFAST_BASE_URL=http://localhost:8000/v1 # optional TypeSafe-route base, e.g. self-hosted\nSUPERFAST_MODEL=jev-latest # default jev-latest / ~typesafe/jev-latest\nSUPERFAST_TIMEOUT_MS=150 # 1..=10000, default 150\n```\n\n- Enabling the gate never picks an endpoint by itself: without\n `SUPERFAST_PROVIDER` nothing is sent and a warning is logged.\n- The key comes from the same place the decision router reads it. The TypeSafe\n route always authenticates, so a self-hosted server that ignores auth still\n needs a placeholder `TYPESAFE_API_KEY`.\n- Only the latest user message is sent, truncated to 4,000 characters and\n redacted of configured secrets. The log (target `superfast`) carries the\n route, failure class and latency, never prompt text.\n- The gate retains the originating turn's accounting owner and cancellation.\n Its tokens settle through the shared ledger; missing usage or a cancelled/timed\n out request after dispatch creates an explicit coverage gap. Unknown TypeSafe\n pricing is recorded as unpriced rather than free.\n- Provider-reported cost survives rejected answers and late responses in bounded\n receipts on the originating turn or session. These are diagnostic evidence;\n unknown pricing never becomes an authoritative dollar total. Incomplete token\n counters record a coverage gap while preserving the reported raw evidence.\n\nThe wire contracts are documented in [TypeSafe's OpenAPI schema](https://api.typesafe.ai/openapi.json)\nand [OpenRouter's Decisions examples](https://openrouter.ai/blog/insights/what-is-jev/).\n\nThe Decision Gate concept and reference implementation are by Andrea Bruno,\nreleased under CC BY 4.0:\n[harness-superfast](https://github.com/Andrea-Bruno/harness-superfast).\n\nTwo `[auto]` keys shape routing (`AutoConfig` in `crates/tui/src/config.rs`):\n\n```toml\n[auto]\ncost_saving = false # default false\ncross_provider = false # default false\n```\n\n- **`cost_saving`** (default `false`). Without a classifier, Auto pins the\n active provider's validated fast sibling instead of the default model. A\n provider with no runnable fast sibling stays on the default. With a\n classifier, the classifier is told to prefer the fast tier for routine or\n ambiguous work and to pick the strong tier only for clearly agentic,\n multi-step, architecture, security or debugging work. Cost-saving never\n switches provider just to save money.\n- **`cross_provider`** (default `false`). Auto stays on the provider the session\n is configured to use. The classifier is only shown that provider's models,\n and the fallback never leaves it. Setting `cross_provider = true` lets the\n classifier choose among every runnable provider. There is no interactive\n toggle for `cross_provider`; it has to be set in config. A decision router\n always chooses within the active provider.\n\nTo bootstrap MCP and skills directories at their resolved paths, run `codewhale setup`.\nTo only scaffold MCP, run `codewhale mcp init`.\n\nNote: `setup`, `doctor`, `mcp`, `features`, `sessions`, `resume`/`fork`, `exec`,\n`review`, and `eval` are all available from the installed `codewhale` command.\nThe consolidated dispatcher also provides `auth`, `config`, `model`, `thread`, `sandbox`,\n`app-server`, `mcp-server`, `completions`, `login`/`logout`, `account`,\n`metrics`, `update`, `lane`, `workflow`, and `web`. Plain prompts enter the\nin-process TUI runtime. Release installers expose the same bytes as `codew`.\n\n### Startup Update Checks\n\nBy default, the TUI starts a background check for the latest stable Codewhale\nrelease and shows a short toast only when a newer release is available and the\nofficial release assets are complete. The check never blocks startup, never\nblocks a turn, and fails silently when offline.\n\nDisable the startup check entirely for air-gapped, corporate-proxy, or managed\ndesktop environments:\n\n```toml\n[update]\ncheck_for_updates = false\n```\n\n#### Automatic updates\n\n`mode` decides what happens when a newer release is found (release-binary\ninstalls only; package-manager and desktop-app copies always just notify):\n\n```toml\n[update]\nmode = \"auto\" # auto (default): download in the background, apply at next start\n # notify: tell me only; off: no checks\n```\n\n`codewhale config set update.mode notify` writes it. `CODEWHALE_UPDATE_MODE`\noverrides it, and an organization policy file overrides both; see\n[INSTALL.md](INSTALL.md#automatic-updates) for the full flow, signing, the\npolicy file and `codewhale update --rollback`.\n\n#### Throttling\n\nThe answer is cached in `~/.codewhale/update-check.json` and reused for\n`check_interval_hours` (default `6`). Only the *network request* is throttled —\nthe notice still appears on every launch while an update is outstanding. Set `0`\nto check on every launch.\n\n```toml\n[update]\ncheck_interval_hours = 6\n```\n\nA failed check keeps the last answer (so the notice stays) and retries about an\nhour later rather than on every launch, which keeps offline and rate-limited\nmachines quiet.\n\n#### Automatic suppression\n\nChecks are skipped, without contacting the network, when any of these is set to\na non-falsey value:\n\n| Variable | Why |\n| --- | --- |\n| `CODEWHALE_NO_UPDATE_CHECK` | Explicit opt-out. |\n| `NO_UPDATE_NOTIFIER` | The cross-CLI convention, honored for compatibility. |\n| `CI`, `CONTINUOUS_INTEGRATION`, `GITHUB_ACTIONS`, `GITLAB_CI`, `BUILDKITE`, `CIRCLECI`, `JENKINS_URL`, `TEAMCITY_VERSION`, `TF_BUILD` | Automated build; nobody is at the terminal. |\n\nValues of `\"\"`, `0`, `false`, `no`, and `off` do not count as set, so a\n`CI=false` export does not disable checks for ordinary users.\n\n#### Which update command is offered\n\nCodewhale never installs anything on its own — it only tells you an update\nexists. The command it names depends on how the running binary was installed,\ndetected from its path:\n\n| Install | Command offered |\n| --- | --- |\n| GitHub release binary (including Termux) | `codewhale update` |\n| npm (`node_modules` on the path) | `npm install -g codewhale@latest` |\n| Homebrew (`Cellar` / `linuxbrew` prefix) | `brew upgrade codewhale` |\n| `cargo install` (`~/.cargo/bin`) | `cargo install codewhale-cli --locked --force` |\n\nFor package-managed installs the notice also warns against `codewhale update`:\nreplacing a binary Homebrew or npm owns leaves the manager describing a version\nthat is no longer on disk, and the next upgrade silently reverts you.\n\nOverride the detection with `CODEWHALE_INSTALL_METHOD=npm|homebrew|cargo|binary`\nif you relocated the binary somewhere the path heuristics cannot read.\n\nTo redirect the startup check, set `update_uri` to an internal endpoint that\nreturns GitHub-compatible latest-release JSON. Minimal mirror metadata with a\n`tag_name` field is accepted; if `assets` are present, Codewhale requires the\nsame uploaded asset set as the official release before showing the toast.\n\n```toml\n[update]\ncheck_for_updates = true\nupdate_uri = \"https://internal.mirror.example/codewhale/releases/latest\"\n```\n\nWhen `update_uri` is not set, startup checks honor release mirror environment\nvariables such as `CODEWHALE_RELEASE_BASE_URL` before falling back to the\nofficial GitHub API endpoint. If a configured `update_uri` cannot be fetched or\nparsed and a release mirror env var is set, the TUI falls back to that mirror\ninstead of failing startup.\n\n#### What's new after an update\n\nThe first time Codewhale starts after its version goes up, it says so once and\nlinks that version's release notes on codewhale.net:\n\n```text\nUpdated to 0.10.2. Got 12 features, 31 fixes and 8 other changes.\nRelease notes: https://codewhale.net/en/changelog/0.10.2\n```\n\nThe counts come from the changelog compiled into the binary. Entries under\nAdded or New count as features; Fixed, Security and Latest fixes count as\nfixes; Contributors, Thanks, Community and Known limits or issues are not\ncounted; every other section counts as other changes. The link uses your UI\nlanguage where the site has it (`zh` for Chinese) and English otherwise.\n\nThe TUI shows the notice on the startup card and moves it into the transcript\nwhen the card leaves (at once when the session opens from `--resume`,\n`--continue` or a prompt), adding `/change <version>` for reading the notes in\nplace. The `doctor`, `models`,\n`sessions`, `receipts`, `setup`, `init`, `install`, `features`,\n`integrations`, `mcp`, `config`, `model`, `metrics`, `providers`, `account`,\n`login`, `logout` and `update` commands print it to stderr instead, when\nstdout and stderr are both terminals and no JSON output is requested. `exec`,\nservers, JSON output and non-terminal runs never show it; the next start that\ncan show it does.\n\nNothing is shown on a fresh install or after a downgrade. The version that\nlast started is recorded in `~/.codewhale/last-launch.json`. The notice reads\nonly local files, so it is independent of `check_for_updates`. To turn it off:\n\n```toml\n[update]\nshow_whats_new = false\n```\n\n## Workshop output budgets\n\nBy default an oversized tool result uses bounded spillover: a result\nunder the byte threshold stays inline, a larger one gets a head/tail\npreview plus a session artifact the model can read back. There is no\nsynthesis sub-agent and no per-call `raw = true` escape.\n\n`[workshop] large_output_threshold_tokens` and\n`[workshop.per_tool_thresholds]` (exact model-visible tool names such as\n`bash`) only take effect when the process opts in to adaptive evidence\nrouting with `CODEWHALE_ADAPTIVE_OUTPUT_ROUTING=1`; they set where a result\nstops being inline and becomes handle-only evidence.\n\nTwo optional byte ceilings (#5367) apply either way. They raise the\nmodel-visible floor and never lower it:\n\n- `read_result_max_bytes` — cap for a single `read` / `read_file`\n result. Absent keeps the compile-time defaults (100000 bytes for\n `read`, which has no line cap; 16KiB / 500 lines for `read_file`).\n For `read` this is the middle of a three-layer budget: the model's\n own per-call `max_bytes` (hard maximum 500000) raises the budget for\n one call, this setting raises the floor for the whole process, and\n either way 2MiB is the absolute ceiling. Highest wins; neither layer\n can lower a budget the other granted.\n- `tool_result_max_bytes` — cap for a generic tool result after\n spillover. Absent keeps the 12K-character compact floor (48K on\n windows ≥500K tokens). Hard cap is 2MiB.\n\n## Context length (context window)\n\nAlso called context size, context limit, max context, or window. This is the\ntotal token window Codewhale budgets against, and it drives the header/footer\ncontext percent, the auto-compaction trigger, context-pressure checks, and the\nrequest output cap. If Codewhale compacts at 128K on a model you know serves a\n1M window, this is the setting to change (#5134).\n\n**See what is in effect, and where the value came from.** Every one of these\nprints the resolved window *and* its source:\n\n- `/status` — a `Context window:` row with the percent and token counts, and a\n `Window source:` row naming the provenance and the exact key that overrides\n it.\n- `/config` → Provider — `Context window` (your override, or `(not set)`) and\n `Effective context window` (`1048576 tokens · configured`). Typing\n `context length` in the `/config` filter jumps straight to them.\n- `/context report` — `Window: 1048576 tokens (12.4% used, ...; source: configured)`.\n- `/context json` — machine-readable `context_window_tokens` and\n `context_window_source`.\n\n**Change it** with the provider-table key `context_window`:\n\n```toml\n[providers.moonshot]\ncontext_window = 1048576\n```\n\nor from the CLI:\n\n```bash\ncodewhale config set providers.moonshot.context_window 1048576\ncodewhale config unset providers.moonshot.context_window # back to automatic\n```\n\nUse the table for the provider you are actually on (`providers.openai`,\n`providers.deepseek`, `providers.moonshot`, …); `/status` names it for you. The\nvalue is a positive token count for the route's *total* window.\n\nWhen one gateway fronts models with heterogeneous windows, scope the override\nto an exact wire model id with `[providers.<name>.model_context_windows]`:\n\n```toml\n[providers.command_code]\ncontext_window = 204800\n\n[providers.command_code.model_context_windows]\n\"MiniMaxAI/MiniMax-M2.5\" = 204800\n\"google/gemini-3.1-flash-lite\" = 1000000\n```\n\nKeys are the exact wire model ids the route sends (dotted and `org/model`\nspellings both work as TOML keys when quoted); each value must be a positive\ntoken count. A matching entry beats the provider-level `context_window` for\nthat model only — every other model on the provider still resolves against\n`context_window` and the rungs below. From the CLI:\n\n```bash\ncodewhale config set 'providers.command_code.model_context_windows.\"MiniMaxAI/MiniMax-M2.5\"' 204800\ncodewhale config unset 'providers.command_code.model_context_windows.\"MiniMaxAI/MiniMax-M2.5\"'\n```\n\n### How the effective window is resolved\n\nFirst match wins, and the source label each surface prints is exactly this\nrung:\n\n1. `configured (per-model)` — a `[providers.<name>.model_context_windows]`\n entry keyed by the route's exact wire model id. A hard override for that\n model only; it never rewrites another model's window.\n2. `configured` — `[providers.<name>] context_window` in `config.toml`. A hard\n override for every model on the provider: nothing below it can raise or\n lower the result. Read-time aliases:\n `contextWindow`, `context_window_tokens`, `contextWindowTokens`,\n `context_length`, `contextLength`.\n3. `provider-reported` — route-scoped 1M metadata a provider actually reported\n for the Kimi Code `k3` route, when it was observed within the last 24 hours.\n4. `static Kimi Code safe floor` — 262,144 tokens for Kimi Code memberships,\n because 1M access is plan-gated (Allegretto and above).\n5. `catalog` — the bundled route catalog (hand-curated offerings first, then\n the bundled Models.dev rows). The official `openai-codex` roster supplies\n account-specific model choices, not context-window metadata. Refresh it\n with `codewhale models --update --provider openai-codex`; Codewhale does\n not read `$CODEX_HOME` for this route.\n6. `model-name hint` — an `_Nk` suffix parsed from the model name itself\n (`qwen3-32b-256k` → 256,000), vendor-agnostic. A naming convention the\n serving engine may not honor is not a fact about the route, so this rung\n sits *below* the catalog: any catalog row for the same id beats it (#5441).\n7. `fallback` — the static per-provider capability table: 200,000 for\n Anthropic-wire routes, 128,000 for `openai-codex`, 8,192 for Ollama,\n otherwise Codewhale's static per-model metadata, and finally 128,000 when\n the model is unknown.\n\n### What \"(unverified)\" means\n\nThe `model-name hint` and `fallback` rungs still drive real budgets — the\ncompaction trigger, the context meter, and the output reservation all use the\nnumber — but they are guesses, not capabilities anyone checked. Every surface\nthat renders one of these windows appends `(unverified)` to its source label\n(the status line, the context-pressure message, `/status`, `/config`, and the\nmodel picker chip), so a window you did not configure and no provider reported\ncan never read as a verified limit (#5239, #5441). The `context_window` and\n`model_context_windows` provider-table keys above are the fix: a configured\nwindow is a hard override and renders as `configured` (or\n`configured (per-model)`) with no marker.\n\nOutput ceilings follow the same rule (#5440): an Anthropic-family model the\ncatalog does not describe keeps the 64K Messages floor as its clamp, labeled\n`unverified` (or an \"assumed floor\") instead of `documented`. The official\nChatGPT plan preview does not accept `max_output_tokens`; Codewhale sends no\noutput-token cap on that route. A local request budget is not an upstream\nlimit or a guarantee about the completed response length.\n\nThere is no environment variable for the context window; the provider-table\n`context_window` and per-model `model_context_windows` keys are the user\nknobs. They are the right ones to set when a gateway or self-hosted runtime\nserves a window Codewhale's catalog does not model — per-model when only some\nof a provider's routes differ, provider-wide when they all do. Codewhale will\nnot invent a window it cannot justify — it falls back to a conservative value,\nlabels it `fallback`, and marks it `(unverified)` at every surface that shows\nit.\n\n### Adjacent knobs\n\n- `auto_compact_threshold_percent` (settings.toml; also accepted as\n `auto_compact_threshold`; `10`–`100`, default `80`): the share of the full\n route context window at which auto-compaction fires, clamped so it can never\n cross the spendable input ceiling after output reservation and headroom.\n Editable from `/config`. Raising the window without touching this raises the\n absolute compaction point along with it.\n- `auto_compact` (settings.toml, on/off): turns automatic compaction off\n entirely; `/compact` and Ctrl+L stay available.\n- `[compaction] summary_instructions` and\n `[compaction] retained_user_message_tokens` (config.toml): standing\n summarizer instructions and the verbatim user-message retention budget. See\n the `compaction.*` entry in the config-key reference below.\n- `CODEWHALE_MAX_OUTPUT_TOKENS` (environment variable; legacy alias\n `DEEPSEEK_MAX_OUTPUT_TOKENS`): overrides the requested output cap. Without an\n override, Codewhale starts at the safe `65536` request cap and intersects it\n with any smaller documented model or route ceiling; a catalog `max_output`\n such as DeepSeek V4's 384K remains a capability ceiling, not the amount every\n response requests. Explicit overrides are preserved within the resolved\n route context window and any route output ceiling, and preflight/emergency\n budgeting reserves the same effective value that can reach the wire. A\n separately documented route input ceiling also clamps preflight and\n compaction even when the total context window is larger. A blank canonical\n variable falls through to a nonblank legacy value; a nonblank invalid or\n zero canonical value is authoritative and falls back to the safe automatic\n default instead of activating a stale legacy setting. There is no\n `max_output_tokens` key in `config.toml`.\n\nBefore compaction replaces conversation history, Codewhale durably saves the\noriginal messages to the session's `artifacts/context-transfer-<id>.json` and,\nwhen a model summary is produced, its handoff to the matching `.md` file.\nThese use the existing session artifact store and persistence redaction.\nA failed write aborts compaction without replacing context. Pruning-only passes\nsave the original messages without making an extra model call. The model can\nread them back through `context_archive_search` (ASCII case-insensitive\nsubstring search returning short snippets and an archive id) and\n`context_archive_get` (paged read of one message by id); the handoff note names\nboth so the model knows the archive exists. Pressure metadata\nshows estimated input tokens and the configured trigger; it is an estimate, not\nan exact promise about a provider's remaining context.\n\nCompaction history is available through `codewhale metrics` (or `--json`) and\n`audit.log` in the Codewhale home. Completed passes record their trigger,\nsummary/pruning path, message and estimated-token counts, effective threshold,\nand summarizer token usage. Automatic refusals are recorded once per turn with\ntheir reason. These are local diagnostics, not provider invoice totals; earlier\nartifacts are not retroactively counted. A text-mode `exec` that attempts\ncompaction saves its owning session at turn completion so its recovery artifacts\nremain discoverable. A killed process may leave artifacts without that final\nsession snapshot; the audit writer reports I/O failures instead of inventing data.\n\nSee [Settings File](#settings-file-persistent-ui-preferences) for the\ncompaction settings and [Token Quantities and\nDrivers](#token-quantities-and-drivers) for what each displayed token number\nactually measures.\n\n## Profiles\n\nYou can define multiple profiles in the same file:\n\n```toml\ndefault_text_model = \"deepseek-flash\"\n\n[providers.deepseek]\napi_key = \"PERSONAL_KEY\"\n\n[profiles.work.providers.deepseek]\napi_key = \"WORK_KEY\"\nbase_url = \"https://api.deepseek.com/beta\"\n\n[profiles.nvidia-nim]\nprovider = \"nvidia-nim\"\ndefault_text_model = \"deepseek-ai/deepseek-v4-pro\"\n\n[profiles.nvidia-nim.providers.nvidia_nim]\napi_key = \"NVIDIA_KEY\"\nbase_url = \"https://integrate.api.nvidia.com/v1\"\n\n[profiles.fireworks]\nprovider = \"fireworks\"\ndefault_text_model = \"accounts/fireworks/models/deepseek-v4-pro\"\n\n[profiles.siliconflow]\nprovider = \"siliconflow\"\ndefault_text_model = \"deepseek-ai/DeepSeek-V4-Pro\"\n\n[profiles.siliconflow.providers.siliconflow]\nbase_url = \"https://api.siliconflow.com/v1\"\n\n[profiles.openai-compatible]\nprovider = \"openai\"\n\n[profiles.openai-compatible.providers.openai]\nbase_url = \"https://openai-compatible.example/v4\"\nmodel = \"glm-5\"\n\n[profiles.atlascloud]\nprovider = \"atlascloud\"\n\n[profiles.atlascloud.providers.atlascloud]\nbase_url = \"https://api.atlascloud.ai/v1\"\nmodel = \"deepseek-ai/deepseek-v4-flash\"\n\n[profiles.sglang]\nprovider = \"sglang\"\n\n[profiles.sglang.providers.sglang]\nbase_url = \"http://localhost:30000/v1\"\nmodel = \"deepseek-ai/DeepSeek-V4-Pro\"\n\n[profiles.vllm]\nprovider = \"vllm\"\n\n[profiles.vllm.providers.vllm]\nbase_url = \"http://localhost:8000/v1\"\nmodel = \"deepseek-ai/DeepSeek-V4-Pro\"\n\n[profiles.ollama]\nprovider = \"ollama\"\n\n[profiles.ollama.providers.ollama]\nbase_url = \"http://localhost:11434/v1\"\nmodel = \"qwen2.5-coder:7b\" # an installed chat-capable tag\n\n[profiles.ollama-cloud]\nprovider = \"ollama-cloud\"\n\n[profiles.ollama-cloud.providers.ollama_cloud]\nbase_url = \"https://ollama.com/v1\"\nmodel = \"gpt-oss:120b\"\n```\n\nSelect a profile with:\n\n- CLI: `codewhale --profile work`\n- Env: `DEEPSEEK_PROFILE=work`\n\nIf a profile is selected but missing, codewhale exits with an error listing available profiles.\n\nFor local Ollama, omit `providers.ollama.model` to select a tag whose native\ncatalog confirms completion support. `/provider ollama`, exec, and Runtime API\ndefault-model admission refresh that catalog; embedding-only catalogs have no\nautomatic chat default. An explicit model keeps its exact tag.\n\nA loaded model's `/api/ps` context observation is usable for 60 seconds.\n`/api/show` reports the trained maximum, which does not establish the served\nwindow. An unloaded model or an expired observation uses the unverified 8192-token\nfallback; explicit context-window configuration still takes precedence.\n\n## Legacy top-level `base_url` and `api_key`\n\nOlder releases kept DeepSeek's endpoint and key at the top of `config.toml`,\nand each reader decided for itself which other routes inherited them. They\nnow live in provider tables only. An older file keeps working unchanged:\nevery load reads the top-level keys as if they were already in their table,\nby one rule, per file and per `[profiles.<name>]`:\n\n1. `provider = \"custom\"` with no `[providers.custom]` table: the endpoint, key\n and a copy of the model become `[providers.custom]`.\n2. An endpoint on another vendor's official host (for example\n `integrate.api.nvidia.com`, `xiaomimimo.com`, `openrouter.ai`, or the\n ChatGPT Codex endpoint) belongs to that vendor's table, so a DeepSeek route\n never sends requests there.\n3. Anything else belongs to `[providers.deepseek]`, which DeepSeek-CN also\n reads for its endpoint and key.\n\nThe key goes with DeepSeek (or the literal custom route). It follows the\nendpoint to another vendor only when the same table sets `provider` to that\nvendor, the host is that vendor's own, and the vendor's table has no key. When\nno `provider` is set, a NIM host still selects `nvidia-nim` and\n`api.deepseeki.com` still selects `deepseek-cn`. A `[vision_model]` without a\nkey of its own keeps using the top-level key. A profile's top-level\n`base_url` now overrides the base file's `[providers.deepseek] base_url`\n(before, the base table silently won).\n\nWhen a top-level value and its table disagree, the table's `base_url` and the\ntop-level `api_key` are used, which is what the runtime sent before.\n\nLoading never rewrites the file. The next save Codewhale makes (for example\n`codewhale config set`, `/config ... --save`, or `auth set`) moves the keys,\nkeeps comments, writes a one-time credential-free copy of the old file to\n`config.toml.pre-migrate.bak`, and prints one line saying what moved. A\ndisagreeing pair is never resolved in the file on its own; both values stay\nuntil you choose, and `codewhale config doctor` / `codewhale doctor` report\nit (sources only, never values). `codewhale config get|set|unset base_url`\n(and `api_key`) address the active provider's table.\n\nTo tidy the file yourself:\n\n```bash\ncodewhale config migrate --dry-run # show what would move\ncodewhale config migrate # move it (backup first)\ncodewhale config migrate --prefer top-level # resolve a conflict: keep the top-level value\ncodewhale config migrate --prefer table # resolve a conflict: keep the table value\n```\n\n## Environment Variables\n\nMost runtime environment variables override config values. API-key variables are\nfallbacks after saved config and keyring credentials.\n\nThe three user-facing slots — provider, model, base URL — expose `CODEWHALE_*`\naliases. When both forms are set the `CODEWHALE_*` value wins; the\n`DEEPSEEK_*` form is kept for older shells:\n\n- `CODEWHALE_PROVIDER` (preferred) / `DEEPSEEK_PROVIDER` (legacy alias) —\n `deepseek|deepseek-anthropic|nvidia-nim|openai|atlascloud|wanjie-ark|volcengine|openrouter|xiaomi-mimo|novita|fireworks|siliconflow|arcee|siliconflow-CN|moonshot|sglang|vllm|ollama|ollama-cloud|huggingface|modelscope|together|qianfan|openai-codex|anthropic|openmodel|zai|stepfun|minimax|deepinfra|mistral`\n- `CODEWHALE_MODEL` (preferred) / `DEEPSEEK_MODEL` (legacy alias) — default model for the active provider\n- `CODEWHALE_BASE_URL` (preferred) / `DEEPSEEK_BASE_URL` (legacy alias) — base URL for the active provider\n\n`CODEWHALE_BASE_URL` applies to the **active** route only. A request pinned to\nanother provider — a subagent or fleet child, a routed tool, the per-turn\nauto-router, a picker preview — resolves its endpoint from that provider's own\n`[providers.<table>]`, then its provider-scoped variable (`MOONSHOT_BASE_URL`,\n`OPENAI_BASE_URL`, …), then that provider's default. It never inherits the\nactive session's host, and a custom route with no configured `base_url` fails\nclosed on a loopback placeholder rather than borrowing another provider's\nendpoint. The environment writes the value into the active identity's own\ntable: DeepSeek-CN falls back to `[providers.deepseek]` for its endpoint and\nkey, but never to a DeepSeek value the environment addressed to DeepSeek alone.\nA managed-config overlay that supplies or reselects the effective\nroute's endpoint takes the generic override away from every route.\n\nRemaining variables:\n\n- `CODEWHALE_LOCAL_PROXY_FALLBACK` (`0`) — keeps reqwest's build-time proxy snapshot; the loopback fallback is on by default. Read once when the first client is built.\n- `DEEPSEEK_API_KEY`\n- `DEEPSEEK_ANTHROPIC_BASE_URL`\n- `DEEPSEEK_HTTP_HEADERS` (custom model request headers, comma-separated `name=value` pairs)\n- `DEEPSEEK_DEFAULT_TEXT_MODEL` (extra legacy alias of `DEEPSEEK_MODEL`)\n- `DEEPSEEK_STREAM_IDLE_TIMEOUT_SECS` (stream idle timeout in seconds; default `900`, clamped to `1..=3600`)\n- `DEEPSEEK_STREAM_OPEN_TIMEOUT_SECS` (connection setup + response-header wait in seconds; default `45`, clamped to `5..=300`; distinct from the per-chunk idle timeout; `stream.open_timeout_secs` (legacy `tui.stream_open_timeout_secs` fallback) wins when positive)\n- `CODEWHALE_CACHE_MAXIMAL` (`1`/`true`/`on`/`yes`) — cache-maximal context mode (#528). When on, the Repo Working Set block materializes the **full current contents** of the top active files into the system prompt each turn (deterministic order, byte-bounded), instead of only listing their paths. The block stays byte-stable while those files are unchanged so DeepSeek's KV prefix cache keeps hitting; editing a file cache-misses from its block onward. Off by default (path list only). Byte caps default to 24 KB per file / 96 KB total.\n- `NVIDIA_API_KEY` or `NVIDIA_NIM_API_KEY` (when provider is `nvidia-nim`)\n- `NVIDIA_NIM_BASE_URL`, `NIM_BASE_URL`, or `NVIDIA_BASE_URL`\n- `NVIDIA_NIM_MODEL`\n- `OPENAI_API_KEY`\n- `OPENAI_BASE_URL`\n- `OPENAI_MODEL`\n- `ATLASCLOUD_API_KEY`\n- `ATLASCLOUD_BASE_URL`\n- `ATLASCLOUD_MODEL`\n- `WANJIE_ARK_API_KEY`, `WANJIE_API_KEY`, or `WANJIE_MAAS_API_KEY`\n- `WANJIE_ARK_BASE_URL`, `WANJIE_BASE_URL`, or `WANJIE_MAAS_BASE_URL`\n- `WANJIE_ARK_MODEL`, `WANJIE_MODEL`, or `WANJIE_MAAS_MODEL`\n- `VOLCENGINE_API_KEY`, `VOLCENGINE_ARK_API_KEY`, or `ARK_API_KEY`\n- `VOLCENGINE_BASE_URL`, `VOLCENGINE_ARK_BASE_URL`, or `ARK_BASE_URL`\n- `VOLCENGINE_MODEL` or `VOLCENGINE_ARK_MODEL`\n- `OPENROUTER_API_KEY`\n- `OPENROUTER_BASE_URL`\n- `OPENROUTER_MODEL`\n- `XIAOMI_MIMO_TOKEN_PLAN_API_KEY`, `MIMO_TOKEN_PLAN_API_KEY`, `XIAOMI_MIMO_API_KEY`, `XIAOMI_API_KEY`, or `MIMO_API_KEY`\n- `XIAOMI_MIMO_BASE_URL` or `MIMO_BASE_URL`\n- `XIAOMI_MIMO_MODEL` or `MIMO_MODEL`\n- `XIAOMI_MIMO_MODE` or `MIMO_MODE` (`token-plan-sgp`, `token-plan-cn`,\n `token-plan-ams`, or `pay-as-you-go`)\n- `NOVITA_API_KEY`\n- `NOVITA_BASE_URL`\n- `NOVITA_MODEL`\n- `FIREWORKS_API_KEY`\n- `FIREWORKS_BASE_URL`\n- `FIREWORKS_MODEL`\n- `HUGGINGFACE_API_KEY` or `HF_TOKEN` (`HF_TOKEN` is a fallback alias accepted when provider is `huggingface`)\n- `MODELSCOPE_API_KEY`\n- `HUGGINGFACE_BASE_URL` or `HF_BASE_URL`\n- `HUGGINGFACE_MODEL` or `HF_MODEL`\n- `SILICONFLOW_API_KEY`\n- `SILICONFLOW_BASE_URL`\n- `SILICONFLOW_MODEL`\n- `ARCEE_API_KEY`\n- `ARCEE_BASE_URL`\n- `ARCEE_MODEL`\n- `TOGETHER_API_KEY`\n- `TOGETHER_BASE_URL`\n- `TOGETHER_MODEL`\n- `QIANFAN_API_KEY` or `BAIDU_QIANFAN_API_KEY`\n- `QIANFAN_BASE_URL` or `BAIDU_QIANFAN_BASE_URL`\n- `QIANFAN_MODEL` or `BAIDU_QIANFAN_MODEL`\n- `OPENAI_CODEX_ACCESS_TOKEN` or `CODEX_ACCESS_TOKEN` (legacy process tokens; ignored by the official ChatGPT plan route)\n- `OPENAI_CODEX_BASE_URL` or `CODEX_BASE_URL`\n- `OPENAI_CODEX_MODEL` or `CODEX_MODEL`\n- `OPENAI_CODEX_ACCOUNT_ID` or `CODEX_ACCOUNT_ID` (legacy identity hints; ignored by the official ChatGPT plan route)\n- `CODEWHALE_CHATGPT_NEW_ACCOUNT=1` explicitly replaces the single selected ChatGPT registration after a validated browser sign-in\n- `ANTHROPIC_API_KEY`\n- `ANTHROPIC_BASE_URL`\n- `ANTHROPIC_MODEL`\n- `ZAI_API_KEY` or `Z_AI_API_KEY`\n- `ZAI_BASE_URL` or `Z_AI_BASE_URL`\n- `ZAI_MODEL` or `Z_AI_MODEL`\n- `STEPFUN_API_KEY` or `STEP_API_KEY`\n- `STEPFUN_BASE_URL` or `STEP_BASE_URL`\n- `STEPFUN_MODEL` or `STEP_MODEL`\n- `MINIMAX_API_KEY`\n- `MINIMAX_BASE_URL`\n- `MINIMAX_MODEL`\n- `DEEPINFRA_API_KEY` or `DEEPINFRA_TOKEN`\n- `DEEPINFRA_BASE_URL`\n- `DEEPINFRA_MODEL`\n- `MISTRAL_API_KEY`\n- `MISTRAL_BASE_URL`\n- `MISTRAL_MODEL`\n- `MOONSHOT_API_KEY` or `KIMI_API_KEY`\n- `MOONSHOT_BASE_URL` or `KIMI_BASE_URL`\n- `MOONSHOT_MODEL`, `KIMI_MODEL_NAME`, or `KIMI_MODEL`\n- `SGLANG_BASE_URL`\n- `SGLANG_MODEL`\n- `SGLANG_API_KEY` (optional; many localhost SGLang servers do not require auth)\n- `VLLM_BASE_URL`\n- `VLLM_MODEL`\n- `VLLM_API_KEY` (optional; many localhost vLLM servers do not require auth)\n- `OLLAMA_BASE_URL`\n- `OLLAMA_MODEL`\n- `OLLAMA_API_KEY` (optional; many localhost Ollama servers do not require auth)\n- `OLLAMA_CLOUD_BASE_URL`\n- `OLLAMA_CLOUD_MODEL`\n- `OLLAMA_CLOUD_API_KEY` (preferred Cloud key; `OLLAMA_API_KEY` is the official fallback)\nFor every product-level `CODEWHALE_*` variable below, the matching legacy\n`DEEPSEEK_*` name is still read as a compatibility fallback; when both are set,\nthe `CODEWHALE_*` value wins.\n\n- `CODEWHALE_TIMING=1` emits local JSON phase durations on stderr for startup, model preparation, tool planning, approval waits and postprocessing. Nested intervals overlap. The records contain only phase, process ID, end time and elapsed microseconds; stream-json stdout is unchanged.\n- `CODEWHALE_LOG_LEVEL` or `RUST_LOG` (`info`/`debug`/`trace` enables lightweight verbose logs)\n- `CODEWHALE_SKILLS_DIR`\n- `CODEWHALE_MCP_CONFIG`\n- `CODEWHALE_NOTES_PATH`\n- `CODEWHALE_MEMORY` (`1|on|true|yes|y|enabled` turns user memory on)\n- `CODEWHALE_MEMORY_PATH`\n- `CODEWHALE_TELEMETRY` / `DEEPSEEK_TELEMETRY` (legacy alias) — anonymous usage\n counting is on by default in the current 0.9.12 source, with a disclosure\n naming Codewhale and PostHog and an easy durable opt-out. Prior explicit\n declines remain off. Accepts `0|1|true|false|yes|no|on|off|enabled|\n disabled`. An explicit \"off\" is a **floor**: it beats `--telemetry true` and\n `telemetry = true` in config, and a value this list cannot read also resolves\n to off, because a typo in a kill switch must never resolve to \"on\". See\n [`TELEMETRY.md`](TELEMETRY.md).\n- `CODEWHALE_TELEMETRY_ENDPOINT` / `DEEPSEEK_TELEMETRY_ENDPOINT` (legacy alias)\n — `https://`, or plain `http://` only for loopback. Overrides the config file.\n Unset selects the shipped default,\n `https://telemetry.codewhale.net/v1/telemetry`; setting it to the **empty\n string** routes batches to a local dry-run file and contacts nobody. Either\n way it only decides where a session sends — it cannot override an opt-out.\n- `CODEWHALE_ALLOW_SHELL` (`1`/`true` enables)\n- `CODEWHALE_APPROVAL_POLICY` (`on-request|untrusted|never`)\n- `CODEWHALE_SANDBOX_MODE` (`read-only|workspace-write|danger-full-access|external-sandbox`)\n- `CODEWHALE_NO_NEW_PRIVS` (`0`/`false`/`no`/`off`/`disabled` opts out) — Linux only. The\n TUI process sets the kernel's irreversible no-new-privileges flag at startup\n as defense-in-depth, which blocks `sudo`/`su`/setuid helpers for Codewhale's\n whole process tree. The flag is already skipped when the startup sandbox\n mode resolves to `danger-full-access` (#5723), so this variable is the\n explicit override in the remaining cases: set it to a falsey value before\n launching if you administer through Codewhale as a wheel-group user under a\n narrower posture and need escalation to work (#5413), or set a truthy value\n to force the flag on even under `danger-full-access`. Unset, the posture\n decides; the other startup hardening (no ptrace, no core dumps) always\n stays on.\n- `CODEWHALE_MANAGED_CONFIG_PATH`\n- `CODEWHALE_REQUIREMENTS_PATH`\n- `CODEWHALE_MAX_SUBAGENTS` (clamped to `1..=128`)\n- `CODEWHALE_TASKS_DIR` (runtime task queue/artifact storage, default\n `~/.codewhale/tasks`, with legacy `~/.deepseek/tasks` fallback when only the\n legacy directory exists)\n- `CODEWHALE_RUNTIME_DIR` (override the runtime thread store root). Interactive\n sessions default to `$CODEWHALE_HOME/sessions/<session-id>/runtime` so each\n Codewhale process owns its own store (#5630). The store is single-owner: a\n second process on the **same** root fails at startup. Set this variable to\n share one store across processes, or when the runtime API server should use a\n stable non-session path. Unset, the API/server path remains\n `$CODEWHALE_HOME/tasks/runtime`. Legacy alias: `DEEPSEEK_RUNTIME_DIR`.\n- `CODEWHALE_ALLOW_INSECURE_HTTP` (`1`/`true` allows non-local `http://` base URLs; default is reject)\n- `CODEWHALE_FORCE_HTTP1` (`1|true|yes|on` pins the HTTP client to HTTP/1.1, disabling HTTP/2; useful on Windows or behind proxies that mishandle long-lived H2 streams)\n- `CODEWHALE_HOME` (override the base data directory; defaults to `~/.codewhale`).\n If you previously exported `DEEPSEEK_HOME`, rename it to `CODEWHALE_HOME`;\n the old env var is not used for new Codewhale state paths.\n- `CODEWHALE_RELEASE_BASE_URL` (release asset mirror used by `codewhale update`\n and by TUI startup update checks when `[update].update_uri` is not set, or as\n a fallback when that configured URI cannot be fetched)\n- `CODEWHALE_AUTOMATIONS_DIR` (override the automations storage directory; uses\n `~/.codewhale/automations` by default, with legacy `~/.deepseek/automations`\n fallback when only the legacy directory exists)\n- `NO_ANIMATIONS` (`1|true|yes|on` forces `low_motion = true` and\n `fancy_animations = false` at startup, regardless of the saved\n settings; see [`docs/ACCESSIBILITY.md`](./ACCESSIBILITY.md)).\n- `SSL_CERT_FILE` — corporate-proxy / TLS-inspecting MITM users\n point this at a PEM bundle (or single DER cert) and the cert(s)\n get added alongside the platform's system trust store. Failures\n log a warning and continue — the existing system roots still\n apply.\n\n### Instruction sources (`instructions = [...]`, #454)\n\nAdd a list of additional system-prompt sources that get\nconcatenated, in declared order, alongside the auto-loaded\n`AGENTS.md`:\n\n```toml\ninstructions = [\n \"./AGENTS.md\",\n \"~/.codewhale/global.md\",\n \"~/team/agents-shared.md\",\n]\n```\n\nRules:\n\n- Paths run through `expand_path` so `~` and env vars work.\n- These entries are **global-scope** instruction authority: they are charged\n to the global budget together with the global `AGENTS.md`/`instructions.md`\n fallbacks and `~/.claude/CLAUDE.md`.\n- A file keeps at most its first 100 KiB. A file larger than\n `[instruction_budget] file_max_bytes` (8 MiB by default) is skipped with a\n notice. Missing and empty files are skipped with a notice so a stale entry\n doesn't fail the launch.\n- Only user-owned config, profiles, and managed config may set this array.\n Project config (`<workspace>/.codewhale/config.toml`, or legacy\n `<workspace>/.deepseek/config.toml`) ignores `instructions` so a cloned repo\n cannot choose arbitrary local files to place into the prompt.\n\n### Instruction budget (`[instruction_budget]`, #6526)\n\nStanding instruction files are assembled into two independent scopes and\nbudgeted in **estimated tokens** — the same conservative estimator `/context`\nuses, never a `bytes/4` shortcut, which underestimates Cyrillic by 30–50%.\n\n- **global** — `~/.codewhale/AGENTS.md` (or the `.agents`/`.deepseek`\n fallbacks), the global `instructions.md` fallbacks, `~/.claude/CLAUDE.md`\n as a last resort when `claude_fallback` is on, and the\n `instructions = [...]` array.\n- **project** — the repository-root → workspace `AGENTS.md` chain, co-read\n foreign files, and the `.codewhale/rules`/`.claude/rules` block.\n\nEach scope is capped at `min(absolute_tokens, percent × usable_window)`. The\nusable window is the route window minus the output reservation the request\nwill actually carry and a safety reserve of 10% of the window (minimum 2,048\ntokens). A route whose context window is unknown has no usable window: the\npercentage ceilings and the guard are reported as `unknown` and only the\nabsolute caps apply.\n\n```toml\n[instruction_budget]\nglobal_budget_tokens = 100000\nproject_budget_tokens = 100000\nglobal_budget_percent = 25\nproject_budget_percent = 25\nstatic_prefix_guard_percent = 80\nfile_max_bytes = 8388608\npolicy = \"trim\"\nclaude_fallback = false\n```\n\nWith a 64K (65,536-token) output reservation:\n\n| window | usable | per scope | both scopes | guard 80% |\n| --- | --- | --- | --- | --- |\n| 128,000 | 49,664 | 12,416 | 24,832 | 39,731 |\n| 1,000,000 | 834,464 | 100,000 (absolute cap) | 200,000 | 667,571 |\n\nBehaviour:\n\n- **Nothing is trimmed while a source is within its scope budget.**\n- A scope over budget drops whole files from the broadest end first — the\n root-most ancestor goes first, the workspace file last — and a single file\n that alone exceeds the remaining budget keeps its **head** and drops its\n tail with a marker carrying bytes and estimated tokens.\n- Every instruction, rules, and configured file retains at most its first\n 100 KiB of text, ending in a marker that names the bytes kept and dropped.\n- Rules are charged to the project scope after the instruction chain and keep\n a head-first trim.\n- `file_max_bytes` is a hard ceiling that can only be lowered from its 8 MiB\n default: a larger file is **skipped**, never trimmed, and the read is\n bounded on the open file, so a FIFO or a growing file cannot exhaust memory.\n The same applies to a whitespace-only file and to files beyond the\n 50-per-directory rules cap.\n- `policy = \"trim\"` (default) trims and reports. `policy = \"refuse\"` stops the\n request before dispatch when a scope budget, the rules budget, or the\n 100 KiB retention would trim, or when the request prefix exceeds the guard;\n skips stay skips, because one bad file must not make the project unopenable.\n- `static_prefix_guard_percent` caps the request prefix: every system block\n plus the tool schemas the request carries. When it trips, the two scopes are\n narrowed largest first until the prefix fits, only the installed instruction\n block is replaced, and the change is recorded as a declared prefix change.\n If the prefix still exceeds the guard afterwards, the residual excess is\n reported rather than looped on. The guard must leave at least 10 points\n above the two scope percents. Prompts owned by a host (child agents,\n isolated chat, RLM) are measured and can be refused, but are never trimmed.\n- `claude_fallback = true` (opt in; default `false`): a directory with no\n `AGENTS.md` (or codewhale `instructions.md`) uses its `CLAUDE.md`. A skipped\n `AGENTS.md` counts as absent; a file already loaded from a wider scope (a\n symlink) is a reported duplicate, not a fallback trigger. Other tools'\n instruction files are never imported unless you opt in here or through\n `project_instruction_imports`.\n- Every loss — trimmed, skipped, refused — reaches the model as one\n runtime-provenance message whenever the list changes, and appears per source\n in `/context report`. `codewhale doctor`'s report is a preview built from the\n current files and config; it does not claim a request passed the tool-schema\n guard.\n- The table is **user config only**, like `instructions`: project config is\n ignored and a cloned repository cannot shrink the global budget or force\n `refuse`. An invalid table (unknown key, zero budget, percent outside\n 1–100, guard without room left, `file_max_bytes` outside 1–8388608) fails\n the launch.\n\n### Hooks\n\nHooks are a **TUI runtime feature**. They fire from the interactive TUI and\nthe engine turn loop it drives; `codewhale exec`, the CLI subcommands, the\napp-server / ACP surfaces, and the `workflow` tool do not fire them.\n\n[`docs/HOOKS.md`](HOOKS.md) is the authoritative reference for all eleven hook\nevents — their firing points, environment variables, stdin payloads, timeout\nand background semantics, and which three of them can steer Codewhale. The\nsections below cover the configuration surface and the steering contracts in\nmore depth.\n\nTwo contract points worth reading there before writing a hook:\n\n- `background = true` means **submitted and never awaited**. The hook still\n gets the documented stdin payload and the same timeout, but it has no exit\n code and cannot steer.\n- A condition that references context its event never carries (an `exit_code`\n condition outside `tool_call_after` / `on_error`, a `mode` condition on\n `shell_env`, a tool condition on a non-tool event) is **rejected at load**,\n logged, and shown in `/hooks list`. It does not silently never match.\n Rejection is per entry, so a broken hook never drops another one that merely\n shares its `name` or is likewise unnamed.\n\n### `/hooks` listing\n\nRun `/hooks` (or `/hooks list`) inside the TUI to see every\nconfigured lifecycle hook grouped by event, including each\nhook's name, command preview, effective timeout, and condition. When\n`[hooks].default_timeout_secs` is set it replaces every per-hook\n`timeout_secs`, and the listing shows that effective value and names the\noverride rather than echoing the per-hook number. A\n`default_timeout_secs = 0` is rejected at load — it would expire every hook\nin the config immediately — so the override is ignored, per-hook\n`timeout_secs` applies, the listing shows that per-hook value with no\noverride provenance, and the rejection appears under `configuration\nproblems`. The\n`[hooks].enabled` flag's state is shown at the top so it's\nobvious when hooks are globally suppressed, and any entry rejected\nat load is listed under `configuration problems` with the reason.\nHooks are configured under `[[hooks.hooks]]` entries — see\n[`docs/HOOKS.md`](HOOKS.md) for the full schema.\n\n### Mutable `message_submit` hooks\n\n`message_submit` hooks run before a submitted message is added to\nhistory or sent to the model. Unlike observer-only lifecycle hooks,\nnon-background `message_submit` hooks can replace or block the\nsubmitted text.\n\n```toml\n[[hooks.hooks]]\nevent = \"message_submit\"\ncommand = \"~/.codewhale/hooks/inject-context.sh\"\ntimeout_secs = 2\ncontinue_on_error = true\n```\n\nThe hook receives JSON on stdin:\n\n```json\n{\n \"event\": \"message_submit\",\n \"text\": \"original user text\",\n \"text_bytes\": 18,\n \"text_original_bytes\": 18,\n \"text_truncated\": false,\n \"session_id\": \"sess_12345678\",\n \"workspace\": \"/path/to/workspace\",\n \"mode\": \"agent\",\n \"model\": \"deepseek-chat\",\n \"total_tokens\": 1234\n}\n```\n\nThe entire serialized document is capped at 32 KiB. Codewhale retains the\nlargest UTF-8-safe `text` prefix that fits after JSON escaping and bounded\nmetadata, and the three `text_*` fields make truncation explicit. Immediate\nmessages, restored queue entries, merged steers, and prior-hook replacements\nall cross this same serialization boundary.\n\nIf the hook exits `0` and prints JSON with a non-empty string `text` field,\nthat value replaces the submitted text:\n\n```json\n{ \"text\": \"replacement user text\" }\n```\n\nExit `0` with empty stdout, or stdout JSON without `text`, leaves\nthe current text unchanged. A JSON `text` field must not be empty;\n`{\"text\":\"\"}` is treated as invalid stdout and ignored. Exit `2`\nblocks the submission before the turn starts; a structured `reason` field can\nprovide the bounded, redacted status message shown in the TUI. Raw stdout,\nstderr, and process-error text are not copied into denial receipts.\nOther non-zero exits follow the hook's `continue_on_error` setting.\nTimeouts and spawn failures are also surfaced as transient TUI status\nmessages when `continue_on_error = true` lets submission continue.\n\nMultiple `message_submit` hooks run in config order, and each hook\nreceives the text produced by the previous hook. Hooks marked\n`background = true` are observer-only and cannot transform or block\nthe message — they still receive the same stdin payload and the same\nenvironment, they are simply never awaited. Existing environment\nvariables remain available.\n`shell_env` hooks keep their existing `KEY=VALUE` stdout contract;\nJSON stdout contracts exist for `message_submit` (above) and\n`tool_call_before` (below).\n\n### `tool_call_before` decision hooks\n\n`tool_call_before` hooks run before each tool call executes. In\naddition to the legacy hard deny (exit code `2`, which always wins\nregardless of stdout), a foreground hook may print a JSON decision on\nstdout with exit code `0`:\n\n```json\n{\n \"decision\": \"allow\" | \"deny\" | \"ask\",\n \"reason\": \"human-readable explanation (used for deny)\",\n \"updatedInput\": { \"command\": \"ls -la\" },\n \"additionalContext\": \"text appended to the tool result for the model\"\n}\n```\n\nAll fields are optional. Empty stdout, non-JSON stdout, and JSON\nwithout a `decision` field behave exactly as before (allow). An\nunrecognized `decision` string logs a fixed warning without echoing the\nuntrusted value and is treated as allow.\n\n- `deny` blocks the tool; the model receives a permission-denied tool\n result containing `reason`.\n- `ask` forces the interactive approval prompt in Ask and Auto-Review even for\n tools that would otherwise auto-run. Full Access does not open tool-approval\n prompts, so hook `ask` does not downgrade that posture.\n- `updatedInput` must be a JSON object; it replaces the tool input\n before execution. When several hooks supply it, the last hook wins.\n- `additionalContext` is appended to the tool result sent back to the\n model as `[hook context] ...`. Multiple hooks' contexts are\n concatenated.\n\nWhen multiple hooks match, precedence is deny > ask > allow. Hooks\nmarked `background = true` cannot steer tool calls — they are\nsubmitted and never awaited, so they have no verdict to contribute.\n\nA foreground hook that produced no verdict at all — it hit its timeout, the\nprocess could not be started, or a strict process exited non-zero without an\nexplicit JSON decision — is not treated as permission. If\n*that* hook is configured with `continue_on_error = false`, the outcome\ndenies the tool call and the denial names the hook and a bounded reason.\nStrictness is read off the hooks that actually matched this call, so a\nstrict gate scoped to another tool cannot deny it, and a lenient hook's\ntimeout does not deny merely because a strict hook exists elsewhere in\nconfig. Under the default `continue_on_error = true` the outcome is\nlogged and the call proceeds.\n\n`reason` and `additionalContext` are capped (2 000 characters per field,\n8 000 for the concatenated context of one call) and stripped of control\ncharacters before they reach the TUI or the model.\n\nExample deny hook:\n\n```toml\n[[hooks.hooks]]\nevent = \"tool_call_before\"\ncommand = '''echo '{\"decision\":\"deny\",\"reason\":\"blocked by project policy\"}' '''\ncondition = { type = \"tool_name\", name = \"exec_shell\" }\n```\n\nExample ask hook (force approval for every MCP tool):\n\n```toml\n[[hooks.hooks]]\nevent = \"tool_call_before\"\ncommand = '''echo '{\"decision\":\"ask\"}' '''\ncondition = { type = \"tool_name\", name = \"mcp__*\" }\n```\n\nExample input rewrite:\n\n```toml\n[[hooks.hooks]]\nevent = \"tool_call_before\"\ncommand = \"~/.codewhale/hooks/clamp-shell-timeout.sh\"\ncondition = { type = \"tool_name\", name = \"exec_shell\" }\n```\n\nwhere the script reads the hook context, then prints\n`{\"updatedInput\": {...}}` with the adjusted arguments.\n\n`tool_name` conditions support `*` globs: `mcp__*` matches every MCP\ntool (e.g. `mcp__github__create_issue`) but not built-ins like\n`read_file`; exact names keep matching exactly. Other regex\nmetacharacters in the pattern are matched literally.\n\n### Project-local hooks\n\nRepositories can ship policy in `<workspace>/.codewhale/hooks.toml`,\nusing the same shape as the `[hooks]` table (top-level fields plus\n`[[hooks]]` entries). Project hooks are executable shell\nconfiguration, so Codewhale only loads them after the workspace has\nbeen trusted in user-owned config through the trust prompt or a\n`[projects.\"<workspace>\"] trust_level = \"trusted\"` entry. Session\n`/trust on` mode does not enable repo-supplied hooks by itself, and\nrepo-local legacy markers such as `.deepseek/trusted` do not enable\nproject hooks. Once trusted, project hooks are appended after global\nhooks from `config.toml`, so they run last and, for `updatedInput`,\nwin ties. A malformed trusted project file logs a warning and startup\nfalls back to global hooks only.\n\n```toml\n# .codewhale/hooks.toml\n[[hooks]]\nevent = \"tool_call_before\"\ncommand = '''echo '{\"decision\":\"deny\",\"reason\":\"no shell in this repo\"}' '''\ncondition = { type = \"tool_name\", name = \"exec_shell\" }\n```\n\n### Turn-end observer hooks\n\n`turn_end` hooks observe the end of each model turn after post-turn\nstate, usage totals, cost accounting, notifications, receipts, and\nqueue recovery have been updated. They receive JSON on stdin and are\nobserver-only: stdout is ignored, failures are logged as warnings, and\nthe hook cannot block user input, mutate the transcript, or change the\nnext queued follow-up.\n\nObserver-only UI events share one 32-entry queue and two persistent workers;\nthe terminal loop uses non-blocking submission and does not create a thread per\nevent. A full queue or unavailable dispatcher drops that observer event and is\nkept as an event-specific error toast, independent of ordinary agent/turn\nstatus text.\n\n```toml\n[[hooks.hooks]]\nevent = \"turn_end\"\ncommand = \"~/.codewhale/hooks/turn-audit.sh\"\ntimeout_secs = 2\ncontinue_on_error = true\n```\n\nThe payload includes common hook metadata plus post-turn accounting:\n\n```json\n{\n \"event\": \"turn_end\",\n \"session_id\": \"sess_12345678\",\n \"workspace\": \"/path/to/workspace\",\n \"mode\": \"agent\",\n \"created_at\": \"2026-07-12T10:30:00+00:00\",\n \"model_backed\": true,\n \"provider\": \"deepseek\",\n \"model\": \"deepseek-chat\",\n \"billing_surface\": null,\n \"turn_id\": \"turn_12345678\",\n \"status\": \"completed\",\n \"error\": null,\n \"duration_ms\": 1834,\n \"usage\": {\n \"input_tokens\": 1200,\n \"output_tokens\": 180,\n \"prompt_cache_hit_tokens\": 900,\n \"prompt_cache_miss_tokens\": 300,\n \"prompt_cache_write_tokens\": 0,\n \"reasoning_tokens\": null,\n \"reasoning_replay_tokens\": null\n },\n \"totals\": {\n \"session_tokens\": 1380,\n \"conversation_tokens\": 1380,\n \"input_tokens\": 1200,\n \"output_tokens\": 180\n },\n \"tool_count\": 2,\n \"queued_message_count\": 1,\n \"stop_hook_active\": false\n}\n```\n\n`created_at` anchors time-window pricing; `provider` and `model` identify the\neffective route used for model-backed turns. `billing_surface` is an optional,\nnon-secret classification derived from the endpoint that actually served the\nturn. Recognized StepFun routes emit `stepfun-payg` or `stepfun-plan`; the raw\nbase URL is never written to hook or runtime records. Runtime `TurnRecord`\nexports call the same field `effective_billing_surface`, which `scorecard`\naccepts as an alias. This keeps subscription quota separate from token-priced\nusage. Unrecognized and custom endpoints remain `null` and unpriced.\n\nShell-only lifecycle completions set `model_backed` to `false` and may report a\n`null` provider; offline scorecards exclude those records from model token and\ncost totals. Completion-only shell, manual-compaction, and purge events that do\nnot have a matching `TurnStarted` retain the observer notification with a\nsynthetic `lifecycle_<uuid>` turn id and the time the completion was observed.\n\nFor `interrupted` or `failed` turns, `status` reflects that terminal\nstate and `error` carries the engine error string when one is available.\n`stop_hook_active` is reserved for future re-entry protection and is\ncurrently always `false`.\n\n### Sub-agent lifecycle hooks\n\n`subagent_spawn` and `subagent_complete` hooks observe sub-agent lifecycle\nevents. They receive bounded JSON metadata on stdin and are observer-only:\nhook failures are logged as warnings and do not block sub-agent scheduling,\nchange prompts, or change results. For these observer events,\n`continue_on_error` has no effect: later matching hooks still run even when an\nearlier hook exits non-zero.\n\n```toml\n[[hooks.hooks]]\nevent = \"subagent_complete\"\ncommand = \"~/.codewhale/hooks/subagent-audit.sh\"\ntimeout_secs = 2\ncontinue_on_error = true\n```\n\n`subagent_spawn` receives:\n\n```json\n{\n \"event\": \"subagent_spawn\",\n \"agent_id\": \"agent_12345678\",\n \"session_id\": \"sess_12345678\",\n \"workspace\": \"/path/to/workspace\",\n \"mode\": \"agent\",\n \"model\": \"deepseek-chat\",\n \"total_tokens\": 1234,\n \"prompt_preview\": \"bounded prompt preview\",\n \"prompt_truncated\": false\n}\n```\n\n`subagent_complete` receives the same common fields plus terminal metadata:\n\n```json\n{\n \"event\": \"subagent_complete\",\n \"agent_id\": \"agent_12345678\",\n \"session_id\": \"sess_12345678\",\n \"workspace\": \"/path/to/workspace\",\n \"mode\": \"agent\",\n \"model\": \"deepseek-chat\",\n \"total_tokens\": 1234,\n \"status\": \"completed\",\n \"result_preview\": \"bounded result preview\",\n \"result_truncated\": false\n}\n```\n\nPreviews are capped before delivery so lifecycle hooks do not receive full\nsub-agent prompts, transcripts, or unbounded results. Use the transcript handle\nreturned by `agent` when full sub-agent details are needed.\n\n### Running-turn input\n\nComposer shortcuts keep the same role throughout a session:\n\n- **Enter** sends when idle and queues a next-turn follow-up while busy. The\n behavior does not change before versus after the provider's first token.\n- With an empty composer and queued follow-ups visible, **Enter** sends the\n oldest queued follow-up into the active turn now.\n- **Ctrl+Enter** (or **Cmd+Enter** when the terminal forwards it) explicitly\n steers the active turn. It sends normally when idle.\n- By default, **Shift+Enter**, **Alt+Enter**, and **Ctrl+J** insert a newline.\n- Set `composer_multiline_mode = true` to make **Enter** insert a newline and\n **Alt+Enter** send instead. **Shift+Enter** also sends when the terminal\n reports it distinctly; legacy terminals may report it as plain **Enter**.\n **Ctrl+J** still inserts a newline, and supported **Ctrl+Enter** /\n **Cmd+Enter** behavior is unchanged.\n- **Ctrl+G** and **Ctrl+S** only stash drafts; they never send or steer.\n\n### Composer stash (`/stash`, Ctrl+G / Ctrl+S)\n\nPress **Ctrl+G** in the composer to park the current draft to\n`~/.codewhale/composer_stash.jsonl`. `/stash list` shows parked\ndrafts with one-line previews and timestamps; `/stash pop`\nrestores the most recently parked draft (LIFO); `/stash clear`\nwipes the file. Capped at 200 entries; multiline drafts round-trip intact.\n**Ctrl+S** remains an alias in terminals that forward it; Cursor and VS Code\nreserve Ctrl+S for Save, so Ctrl+G is the portable default.\n\n## Settings File (Persistent UI Preferences)\n\ncodewhale also stores user preferences in:\n\n- `~/.codewhale/settings.toml` on new installs\n- `~/.deepseek/settings.toml` or the legacy platform config-dir\n `deepseek/settings.toml` when an existing settings file is present\n\nNotable settings include `auto_compact`, which uses a model-aware default-on\npolicy for known context windows up to the 1M-token V4 class. Automatic\ncompaction runs before the active model limit and carries the compacted summary\nforward into the next request. The trigger defaults to\n`auto_compact_threshold_percent = 80`. Users who prefer manual continuity can\npersist `auto_compact = false`; manual `/compact` / Ctrl+L remains available.\nYou can inspect or update these from the TUI with `/settings` and `/config`\n(interactive editor).\n\nCommon settings keys:\n\n- `theme` (`system`, `terminal`, `underwater`, `underwater-retro`,\n `shoreline`, `shoreline-light`, `dark`, `light`, `grayscale`,\n `catppuccin-mocha`, `tokyo-night`, `dracula`, `gruvbox-dark`, `claude`,\n `matrix`, `solarized-light`, `uwu`; default `underwater`): `underwater` is\n the dark navy fresh-install default, `shoreline` is its warm charcoal\n alternative and `shoreline-light` the light variant, `system` follows terminal\n background detection, `dark`/`light` use the Codewhale Whale pair,\n `terminal` inherits the host terminal, `grayscale` is the low-opinion\n black/white theme, and the named community presets apply across the TUI.\n Aliases such as `whale`, `mono`, `black-white`, `tokyonight`, and `gruvbox`\n are accepted. In Whale, cobalt blue owns action/focus, seafoam owns live\n work, Signal Gold owns human decisions and the whale, coral owns warnings,\n rose owns danger, violet owns Operate, and green remains completed/verified.\n Text labels, markers, and motion policy carry the same states when color is\n unavailable; color is never the only cue.\n User-authored overlays live only at `~/.codewhale/themes/<name>.json` (or\n `$CODEWHALE_HOME/themes/<name>.json`) and are selected with\n `/theme custom:<name>`. The filename is a bounded slug, symlinks and files\n over 64 KiB are refused, colors must be `#RRGGBB`, and unknown fields fail\n validation. `/theme schema` prints the embedded JSON Schema and `/theme path`\n shows the exact directory. An overlay names one compiled `base` theme and\n changes only listed semantic colors; it cannot include or read another file.\n Open `/theme` to browse valid overlays, preview them live, and keep the\n active `custom:<name>` selector when the picker is opened without moving.\n- `auto_compact` (on/off, model-aware default on for known context windows\n unless explicitly configured)\n- `auto_compact_threshold_percent` (10-100, default `80`): pre-send\n auto-compaction threshold used only when `auto_compact` is enabled.\n- `paste_burst_detection` (on/off, default on): fallback rapid-key paste\n detection for terminals that do not emit bracketed-paste events. This is\n independent of terminal bracketed-paste mode.\n- `work_surface_placement` (`bottom`, `top`, `left`, `right`, or `off`;\n default `bottom`): places the workbar — Tasks / To-do / Workers — under the\n composer (the default bottom workbar), above the transcript, in a side\n workbar, or hides it entirely (`off`). Side choices fall back to the top\n layout on narrow terminals without changing the saved preference. Set it\n live with `/config work_surface_placement right --save` (or `left` / `top` /\n `bottom` / `off`).\n- `rail_panel` (`tasks`, `agents`, `background`, `files`, `notepad`,\n `context`, `git`, `price`; default `tasks`, alias key `rail`): which panel\n the workbar shows. Panel selection is orthogonal to placement. `tasks` is\n the full live work list (to-dos, then sub-agents); `agents` narrows to the\n sub-agent rows; `background` lists background shells and automations;\n `files` lists touched files; `notepad` shows the workspace notes; `context`\n is a read-only session-facts list; `git` shows branch status; `price`\n shows cost. In every panel except `context`, rows are selectable and\n clickable and open their detail surface. `Alt+!`/`Alt+@`/`Alt+#`/`Alt+$`\n switch panels live.\n- `work_surface_top_height` (2–16) and `work_surface_side_width` (26–80):\n ceilings for the top strip's height and the side workbar's width. Both are\n normally persisted by dragging the divider rather than edited by hand; the\n strip still auto-fits its content below the ceiling.\n- `focus_texture` (`off`, `scrim`, or `grain`; default `off`): focus-context\n texture for modal views. `scrim` dims the already-rendered background\n outside the focused modal toward the theme surface; `grain` sprinkles\n sparse dots over blank cells there. The texture is static (no time\n component, so it is unaffected by `low_motion`), never writes over a cell\n that carries text, and preserves the 4.5:1 body-text contrast floor\n wherever both colors are resolvable. It is skipped entirely on frames\n below the ambient-life minimum size and when the focused modal already\n covers 90% or more of the frame. Set it live with\n `/config focus_texture scrim --save`.\n- `mention_menu_limit` (integer, default `128`): maximum number of\n `@`-mention popup candidates retained before the composer renders the\n visible window. The visible rows still depend on terminal height.\n- `mention_walk_depth` (integer, default `10`): maximum workspace depth for\n `@`-mention completion walks. Set to `0` for unlimited depth in deeply\n nested workspaces; keep the default in very large repos unless needed.\n- `mention_menu_behavior` (`fuzzy`, `browser`; default `fuzzy`): controls how\n `@`-mention completions are populated. `fuzzy` searches the workspace and\n applies mention frecency. `browser` lists only the immediate children of the\n currently typed directory segment in deterministic alphabetical order.\n- `show_thinking` (on/off)\n- `thinking_default_expanded` (on/off, default off): renders thinking blocks\n expanded initially when `show_thinking` is enabled. Space still toggles the\n selected block, and it decides only the blocks you have not touched: a block\n you expanded or collapsed yourself keeps that state if you change this\n setting later. This is useful in SSH/tmux environments where the Space\n binding may be intercepted.\n- `thinking_preview_lines` (integer, default `2`): how many body rows a\n **collapsed** completed thought still shows. `0` is header-only; `10` is\n the older dump. Live streaming preview is unchanged. Expand a block with\n Space, or set `thinking_default_expanded` to open every block.\n- `help_expand_groups` (on/off, default off): start Help/shortcuts with every\n group expanded. Default folds the long tail (Grok-style); type-to-filter\n still unfolds matches.\n- `pin_last_prompt` (on/off, default on): pin the last user prompt at the top\n of the transcript viewport after it scrolls off.\n- `show_tool_details` (on/off)\n- `inline_diffs` (`full`, `summary`, or `off`; default `full`): controls the\n inline presentation of successful structured File mutations. `full` shows a\n bounded red/green diff and semantic statistics, `summary` keeps only the\n statistics, and `off` keeps the calm changed-file outcome. All three retain\n the exact applied change in the selected File receipt's Alt/Option+V detail.\n Failure and cancellation never render a successful diff. Save the choice\n with `/config inline_diffs <mode> --save`.\n- `locale` (`auto`, `en`, `ja`, `zh-Hans`, `zh-Hant`, `pt-BR`, `es-419`, `vi`,\n `ko`; default `auto`): UI chrome locale. `auto` checks `LC_ALL`,\n `LC_MESSAGES`, then `LANG`; unsupported locale selections resolve to English.\n Every shipped pack holds full `en.json` parity, so no string falls back\n to English. The runtime also exposes the resolved locale in the system\n prompt as the fallback natural language for V4 reasoning and replies when the\n latest user message is ambiguous. Clear user language still takes priority;\n Chinese turns should produce Chinese `reasoning_content` and Chinese final\n replies even when the resolved locale is English.\n- `background_color` (`#RRGGBB`, `RRGGBB`, or `default`): optional main TUI\n background color applied to the root, header, transcript, and footer\n surfaces while preserving panel contrast.\n- `cost_currency` (`usd`, `cny`; default `usd`): currency used by the footer,\n context panel, `/cost`, `/tokens`, and long-turn notification summaries. The\n aliases `rmb` and `yuan` normalize to `cny`.\n- `default_mode` (`agent`, `plan`, or `operate`; legacy values are accepted for migration but are not live mode vocabulary)\n- `launch_screen` (legacy, migration-only): this historical `on`/`off` value\n is still accepted when reading an existing settings file, but it no longer\n changes behavior and is omitted from new saves. A fresh interactive launch\n always opens Tideline Startup; only an explicit resume or an explicit\n initial prompt enters the live session directly.\n- `sidebar_focus` (legacy, migration-only): the classic right sidebar this key\n configured was removed in the 0.9.4 rail unification. The key is still read\n once so old settings carry forward, then folds into the live keys:\n `pinned`/`work`/`plan`/`todos` become `rail_panel = \"pinned\"`,\n `agents`/`subagents` become `rail_panel = \"agents\"`, `context`/`session`\n become `rail_panel = \"context\"`, `tasks`/`auto` (the old default) become the\n `tasks` panel, `sessions` enables `sessions_rail`, and `hidden` turns the\n workbar off via `work_surface_placement = \"off\"`. An explicit `rail_panel`\n in the file always wins over the migrated value. Configure the workbar with\n `rail_panel` and `work_surface_placement`, not this key.\n- `sessions_rail` (`on`/`off`; default `off`): show the persistent Sessions\n list in the workbar. Rows list this workspace's recent\n non-archived sessions, newest first, with the active one marked; activating a\n row opens the session picker preselected on it (`/sessions open <id>`), so\n resume keeps its single implementation. Rows are projected from cached\n session metadata — the list never reads a transcript per frame, and never\n contacts a provider.\n- `session_auto_resume` (`on`/`off`; default `off`): reattach to this\n workspace's most recent session when Codewhale starts. Off by default so\n plain `codewhale` keeps starting fresh. `--resume`, `--continue`, and\n `--fresh` always take precedence. When it is on, startup still refuses to\n resume a session that is archived, fails to load, or is recorded against a\n different workspace; each of those falls back to a fresh transcript and says\n which session was skipped and why. It applies to the interactive launch only\n — `codewhale \"<prompt>\"` and `codewhale exec` are never silently prefixed\n with a prior conversation.\n- `max_input_history` (number of submitted input history entries; cleared\n drafts are also kept locally for composer history search). Note the spelling:\n the serde field on disk is `max_input_history`\n (`crates/tui/src/settings.rs:426`, default 100). `max_history` is the key\n name accepted by `/config set` and `settings.set()` (`settings.rs:1388`), not\n a settings.toml key — writing `max_history` into the file is silently\n ignored.\n- `default_model` (model name override)\n\n`/task digest` (alias `/tasks digest`) renders the canonical Work Graph\noperations and four-state To-do list as plain text, running work first. It\nreads the same snapshots as the styled Work surface and owns no parallel\nprogress state.\n\nPlan and Work are the everyday visible modes in the UI; Operate is an explicit\npreview entry while its Workflow control surface is still being built. Switch\nbetween them with `/mode`. For compatibility, older settings files with\n`default_mode = \"normal\"` still load as `agent`.\n\nLocalization scope is tracked in [LOCALIZATION.md](LOCALIZATION.md). The v0.7.6\ncore pack covers high-visibility TUI chrome only; provider/tool schemas,\npersonality prompts, and full documentation remain English unless explicitly\ntranslated later.\n\nReadability semantics:\n\n- Selection uses a unified style across transcript, composer menus, and modals.\n- Footer hints use a dedicated semantic role (`FOOTER_HINT`) so hint text stays readable across themes.\n\n### Token Quantities and Drivers\n\nDeepSeek V4 prefix caching makes token labels matter. These quantities are kept\nseparate:\n\n| Quantity | Meaning | Allowed to drive |\n|---|---|---|\n| Active request input estimate | Conservative estimate of the next request's live system prompt and transcript payload. | Header/footer context percent, auto-compaction trigger, opt-in Flash seam trigger, and emergency overflow preflight. |\n| Reserved response headroom | The effective request cap plus `1024` safety tokens. Normal no-override requests start at `65536`; a smaller route/provider ceiling narrows that value, and an explicit output override raises it only within the resolved route window and output ceiling. The identical cap reaches the wire and drives preflight, except on a route that states an output limit above half its known context window with no explicit output override: there compaction aims to keep a quarter of the window free (at most `65536`, and never more than the request cap), preflight refuses a request only when fewer than `128` output tokens would fit, and every request's `max_tokens` is what the window has left after that request's estimated input and the `1024` safety tokens, never above the request cap. Reasoning effort does not add a second hidden reservation. A separately published route input ceiling independently clamps the spendable input budget. | Emergency overflow budget checks only. |\n| Cumulative API usage | Provider-reported input plus output tokens summed across completed API calls; multi-tool turns may count the same stable prefix more than once. | Session usage and approximate cost telemetry only. |\n| Prompt cache hit/miss | Provider cache telemetry for the most recent call when available. | Cache-hit display and cost estimation only; never compaction or seam triggers. |\n| Context percent | Active request input estimate divided by the model context window. | Display only; it mirrors the active-input basis used by context safeguards. |\n| Cost estimate | Approximate spend from provider usage and configured DeepSeek rates. | Display only. |\n\nFor known context-window models, including 1M-class V4 models, replacement\ncompaction is enabled by default unless the user explicitly configures\n`auto_compact = false`. It fires at the active model's compaction threshold and\nreplaces old history with recent user context followed by one ordinary\ncheckpoint message. The standing system prompt remains unchanged. Unknown model\nids remain opt-in.\n\n### Command Migration Notes\n\nIf you are upgrading from older releases:\n\n- Old: `/codewhale`\n New: `/links` (aliases: `/dashboard`, `/api`)\n- Old: `/set model deepseek-reasoner`\n New: `/config` and edit the `model` row to `deepseek-v4-pro` or `deepseek-flash`\n- Old: visible `Normal` mode or `default_mode = \"normal\"`\n New: use `Agent` / `default_mode = \"agent\"`; legacy `normal` still maps to `agent`\n- Old: discover `/set` in slash UX/help\n New: use `/config` for editing and `/settings` for read-only inspection\n\n## Key Reference\n\n### Kimi Code membership model IDs\n\nThe exact `https://api.kimi.com/coding/v1` endpoint accepts `k3`, `k3-256k`,\n`kimi-for-coding`, and `kimi-for-coding-highspeed`. Use `k3-256k` for a fixed\n262,144-token K3 window; use bare `k3` with `context_window = 1048576` only\nwhen the membership plan includes the 1M entitlement. Both K3 ids use the same\nreasoning contract, and all four membership ids omit generic sampling fields.\n\n### Core keys (used by the TUI/engine)\n\n- `provider` (string, optional): `deepseek` (default), `deepseek-anthropic`, `nvidia-nim`, `openai`, `atlascloud`, `wanjie-ark`, `volcengine`, `openrouter`, `xiaomi-mimo`, `novita`, `fireworks`, `siliconflow`, `arcee`, `siliconflow-CN`, `moonshot`, `sglang`, `vllm`, `ollama`, `ollama-cloud`, `huggingface`, `modelscope`, `together`, `qianfan`, `openai-codex`, `anthropic`, `openmodel`, `zai`, `stepfun`, `minimax`, `deepinfra`, `sakana`, `longcat`, `opencode-go`, `meta`, `mistral`, `telecomjs`, `xai`, `orcarouter`, `modelstudio-token-plan`, `google`, `edenai`, or `custom`. Legacy `deepseek-cn` configs are still accepted as an alias for `deepseek`; DeepSeek uses the same official host [`https://api.deepseek.com`](https://api-docs.deepseek.com/) worldwide. `deepseek-anthropic` targets DeepSeek's Anthropic Messages-compatible endpoint at `https://api.deepseek.com/anthropic` using `DEEPSEEK_API_KEY`; `nvidia-nim` targets NVIDIA's NIM-hosted DeepSeek endpoints through `https://integrate.api.nvidia.com/v1`; `openai` targets a generic OpenAI-compatible endpoint, defaulting to `https://api.openai.com/v1`; `atlascloud` targets AtlasCloud's OpenAI-compatible endpoint at `https://api.atlascloud.ai/v1`; `wanjie-ark` targets Wanjie Ark's OpenAI-compatible endpoint at `https://maas-openapi.wanjiedata.com/api/v1`; `volcengine` targets Volcengine Ark's OpenAI-compatible coding endpoint at `https://ark.cn-beijing.volces.com/api/coding/v3`; `openrouter` targets `https://openrouter.ai/api/v1`; `xiaomi-mimo` targets Xiaomi MiMo's OpenAI-compatible endpoint, using `https://token-plan-sgp.xiaomimimo.com/v1` by default for Token Plan keys (`tp-...`) and `https://api.xiaomimimo.com/v1` for pay-as-you-go keys. For Token Plan accounts outside the Singapore default, set `base_url` explicitly or use `mode = \"token-plan-cn\"` for China and `mode = \"token-plan-ams\"` for Europe/Amsterdam; `novita` targets `https://api.novita.ai/openai/v1`; `fireworks` targets `https://api.fireworks.ai/inference/v1`; `siliconflow` targets SiliconFlow, defaulting to `https://api.siliconflow.com/v1`; `arcee` targets Arcee AI's OpenAI-compatible endpoint at `https://api.arcee.ai/api/v1`; `siliconflow-CN` targets the SiliconFlow China regional endpoint through `[providers.siliconflow_cn]`; `moonshot` targets Moonshot/Kimi, defaulting to `https://api.moonshot.ai/v1`; `sglang` targets a self-hosted OpenAI-compatible endpoint, defaulting to `http://localhost:30000/v1`; `vllm` targets a self-hosted vLLM OpenAI-compatible endpoint, defaulting to `http://localhost:8000/v1`; `ollama` targets Ollama's OpenAI-compatible endpoint, defaulting to `http://localhost:11434/v1`; `huggingface` targets Hugging Face Inference Providers at `https://router.huggingface.co/v1`; `modelscope` targets ModelScope's OpenAI-compatible inference API at `https://api-inference.modelscope.cn/v1`; `together` targets Together AI at `https://api.together.xyz/v1`; `qianfan` targets Baidu Qianfan at `https://api.baiduqianfan.ai/v1`; `openai-codex` targets official ChatGPT plan inference with Codewhale-owned OAuth at `https://api.openai.com/v1`; `anthropic` targets Claude's native Messages API; `openmodel` targets OpenModel's Anthropic-compatible Messages API at `https://api.openmodel.ai`; `zai` targets Z.ai at `https://api.z.ai/api/coding/paas/v4`; `stepfun` targets StepFun at `https://api.stepfun.ai/v1`; `minimax` targets MiniMax at `https://api.minimax.io/v1`; `deepinfra` targets DeepInfra at `https://api.deepinfra.com/v1/openai`; `sakana` targets Sakana AI Fugu at `https://api.sakana.ai/v1`; `longcat` targets Meituan LongCat at `https://api.longcat.chat/openai/v1`; `opencode-go` targets the subscription-backed OpenCode Go model-aware route (Chat Completions, Responses, or Messages according to the documented model) at `https://opencode.ai/zen/go/v1`; `meta` targets Meta Model API; `mistral` targets Mistral AI's OpenAI-compatible endpoint at `https://api.mistral.ai/v1`; `telecomjs` targets TelecomJS TokenHub at `https://aigw.telecomjs.com/v1`; and `xai` targets xAI's API-key or OAuth route.\n- `opencode-zen` (string provider value): selects the model-aware OpenCode Zen gateway through `[providers.opencode_zen]`. The default base URL is `https://opencode.ai/zen/v1`, the default model is `gpt-5.6`, and credentials come from `api_key`, `OPENCODE_ZEN_API_KEY`, or fallback `OPENCODE_API_KEY`—never ChatGPT/Codex OAuth. `OPENCODE_ZEN_BASE_URL` and `OPENCODE_ZEN_MODEL` are accepted. The selected model's wire comes from the curated Zen snapshot, then from the AI SDK package its Models.dev `opencode` row names (refresh with `codewhale models --update`): GPT, Grok, and Muse Spark use Responses; Claude and most Qwen rows use Anthropic Messages; DeepSeek, MiniMax, GLM, Kimi, `qwen3.8-max`, and the free rows use Chat Completions. Gemini, Models.dev rows marked `deprecated`, and models no loaded catalog lists fail closed because Codewhale has no proven supported wire contract for them. See the exact current model groups in [`PROVIDERS.md`](PROVIDERS.md#opencode-zen-protocol-catalog).\n- `minimax-anthropic` (string provider value): selects MiniMax's Anthropic-compatible Messages route through `[providers.minimax_anthropic]`. The default Base URL is `https://api.minimax.io/anthropic`; set `https://api.minimaxi.com/anthropic` for China. Keep the `/anthropic` suffix because Codewhale appends `/v1/messages`. The route uses `MINIMAX_API_KEY` and defaults to `MiniMax-M3`; `MiniMax-M2.7` is also registered. Official M3 input modalities are text, image, and video, with adaptive or disabled thinking. M2.7 is text-only and always keeps thinking enabled.\n- `api_key` (string, required for hosted providers): must be non-empty for DeepSeek/hosted providers (or set the provider API key env var). Self-hosted SGLang, vLLM, and local `ollama` can omit it. `ollama-cloud` requires a key saved for that provider or supplied by `OLLAMA_CLOUD_API_KEY`, then `OLLAMA_API_KEY`.\n- `auth_mode` (string, optional provider-table key): selects a provider-specific authentication contract. Kimi Code membership uses `auth_mode = \"api_key\"` (or omit the field), a key created in the [Kimi Code console](https://www.kimi.com/code/console), `base_url = \"https://api.kimi.com/coding/v1\"`, and bare `model = \"k3\"` for K3. Codewhale gives that route a safe 262,144-token baseline; set `context_window = 1048576` only when the Kimi Code plan includes 1M access (Allegretto and above). `k3[1m]` is a Claude Code-only convention, not an API model ID, and Codewhale rejects it instead of silently changing the wire model or assuming an entitlement. `model = \"kimi-for-coding\"` remains the valid K2.7 compatibility route available to all Kimi Code members. Legacy `auth_mode = \"kimi_oauth\"` fails closed with API-key guidance and never probes, reads, refreshes, or rewrites `kimi_cli`/`kimi_code_cli` credential files. First-class OAuth requires Codewhale's own vendor-registered client identity and remains tracked in #4417.\n- `base_url` (string, optional, `[providers.<name>]` key; see [Legacy top-level `base_url` and `api_key`](#legacy-top-level-base_url-and-api_key) for the older top-level spelling): defaults to `https://api.deepseek.com/beta` for DeepSeek's OpenAI-compatible Chat Completions API, including legacy `provider = \"deepseek-cn\"` configs. Other defaults are `https://api.deepseek.com/anthropic` for `deepseek-anthropic`, `https://integrate.api.nvidia.com/v1` for `nvidia-nim`, `https://api.openai.com/v1` for `openai`, `https://api.atlascloud.ai/v1` for `atlascloud`, `https://maas-openapi.wanjiedata.com/api/v1` for `wanjie-ark`, `https://ark.cn-beijing.volces.com/api/coding/v3` for `volcengine`, `https://openrouter.ai/api/v1` for `openrouter`, `https://token-plan-sgp.xiaomimimo.com/v1` for `xiaomi-mimo` when the API key starts with `tp-...` and `https://api.xiaomimimo.com/v1` otherwise, `https://api.novita.ai/openai/v1` for `novita`, `https://api.fireworks.ai/inference/v1` for `fireworks`, `https://api.siliconflow.com/v1` for `siliconflow`, `https://api.siliconflow.cn/v1` for `siliconflow-CN`, `https://api.arcee.ai/api/v1` for `arcee`, `https://api.moonshot.ai/v1` for `moonshot`, `https://api.minimax.io/v1` for `minimax`, `https://api.openmodel.ai` for `openmodel`, `https://api.z.ai/api/coding/paas/v4` for `zai`, `https://api.stepfun.ai/v1` for `stepfun`, `https://api.deepinfra.com/v1/openai` for `deepinfra`, `https://api.sakana.ai/v1` for `sakana`, `https://router.huggingface.co/v1` for `huggingface`, `https://api-inference.modelscope.cn/v1` for `modelscope`, `https://api.together.xyz/v1` for `together`, `https://api.baiduqianfan.ai/v1` for `qianfan`, `https://api.openai.com/v1` for `openai-codex`, `https://api.anthropic.com` for `anthropic`, `https://api.mistral.ai/v1` for `mistral`, `http://localhost:30000/v1` for `sglang`, `http://localhost:8000/v1` for `vllm`, `http://localhost:11434/v1` for `ollama`, and `https://ollama.com/v1` for `ollama-cloud`. Set `base_url = \"https://token-plan-cn.xiaomimimo.com/v1\"` for China-region Xiaomi MiMo Token Plan accounts or `base_url = \"https://token-plan-ams.xiaomimimo.com/v1\"` for Europe/Amsterdam accounts. Mistral-specific reasoning fields and polymorphic replay are enabled only on the documented first-party HTTPS `/v1` hosts; a custom Mistral base URL keeps generic Chat semantics. Set `https://api.deepseek.com` or `https://api.deepseek.com/v1` explicitly to opt out of DeepSeek beta features.\n- `ollama-cloud` route: select `provider = \"ollama-cloud\"`, configure `[providers.ollama_cloud]` when overriding the default `https://ollama.com/v1` / `gpt-oss:120b` tuple, and save a key from [Ollama account settings](https://ollama.com/settings/keys) with `codewhale auth set --provider ollama-cloud`. Ambient precedence is `OLLAMA_CLOUD_API_KEY`, then `OLLAMA_API_KEY`; arbitrary Ollama model IDs pass through unchanged.\n- Legacy Ollama Cloud migration: a released `provider = \"ollama\"` config whose normalized `[providers.ollama].base_url` is exactly `https://ollama.com/v1` is upgraded to the `ollama-cloud` runtime identity in memory. Only that exact tuple may read its old `ollama` provider table and secret slot. The config and secrets are never rewritten, and neighboring paths, HTTP downgrades, lookalike hosts, or an explicit `ollama-cloud` selection never consume the fallback.\n- `telecomjs` base URL and catalog: `[providers.telecomjs]` defaults to `https://aigw.telecomjs.com/v1`; `TELECOMJS_BASE_URL` overrides it. With `TELECOMJS_API_KEY`, `/models` refreshes a key-scoped catalog without mixing rows into another provider.\n- `edenai` gateway: select `provider = \"edenai\"`; `[providers.edenai]` defaults to `https://api.edenai.run/v3` and `deepseek/deepseek-v4-pro`. `EDENAI_API_KEY`, `EDENAI_BASE_URL`, and `EDENAI_MODEL` are accepted. Use `EDENAI_BASE_URL = \"https://api.eu.edenai.run/v3\"` for Eden AI's documented EU endpoint; the default `deepseek/deepseek-v4-pro` is only listed on the global catalog, so pair the EU endpoint with an EU-listed model such as `qwen/deepseek-v4-pro` via `EDENAI_MODEL` or `model`. The provider refreshes Eden AI's `/models` catalog, but leaves model-specific reasoning controls untouched because the gateway spans multiple model families.\n- `codewhale` (Codewhale API): select `provider = \"codewhale\"`; `[providers.codewhale]` defaults to `https://api.codewhale.net/v1` and `deepseek/deepseek-v4-pro`. The credential is a Codewhale account API key (`cwc_key_…`) with the `models:infer` scope, read from `CODEWHALE_API_KEY` or the `codewhale` secret-store slot; `codewhale account api-keys create --name <name> --use` mints one and saves it locally. `CODEWHALE_API_BASE` overrides the origin and must be HTTPS except on loopback. The model catalog is the account's own authenticated `GET /v1/models`: ids are `provider/model` and each row states its wire (`chat-completions` → `/v1/chat/completions`, `anthropic-messages` → `/v1/messages`, `responses` → `/v1/responses`). Connect the underlying provider keys with `codewhale account keys set <provider>`.\n- `concentrate` gateway: select `provider = \"concentrate\"`; `[providers.concentrate]` defaults to `https://api.concentrate.ai/v1` and `deepseek-v4-pro` over the OpenAI Responses wire. `CONCENTRATE_API_KEY`, `CONCENTRATE_BASE_URL`, and `CONCENTRATE_MODEL` are accepted. Model ids pass through verbatim (`gpt-5.6-sol`, `openai/gpt-5.6-sol`, or `concentrate/auto` for the gateway router). BYOK only; see [PROVIDERS.md](PROVIDERS.md#concentrate-notes).\n- `mistral` model and reasoning contract: `[providers.mistral]` defaults to `mistral-code-latest`; `MISTRAL_MODEL` overrides it and the generic `CODEWHALE_MODEL` override wins when both are set. The current picker also lists `mistral-medium-latest`, `mistral-small-latest`, and `mistral-large-latest`. On exact first-party HTTPS `/v1` routes, Medium and Small accept only `reasoning_effort = \"none\" | \"high\"` and replay polymorphic thinking blocks. Deprecated native Magistral IDs may still be configured explicitly, remain always-reasoning, and never receive the adjustable effort field.\n- `context_window` (integer, optional provider-table key): override the total context window for the active `[providers.<name>]` route when an OpenAI-compatible gateway, hosted model alias, or self-hosted runtime has a different limit than Codewhale's static model table. For example, `[providers.openai] context_window = 1000000` lets an OpenAI-compatible DashScope/Qwen route budget against a 1M-token window instead of the conservative fallback. For Kimi Code K3, keep `model = \"k3\"` and set `[providers.moonshot] context_window = 1048576` only when the membership plan includes 1M access; otherwise omit it to retain the 262,144-token safe baseline. The value must be greater than 0 and affects prompt context notes, compaction thresholds, context-pressure checks, and request output caps. Full resolution order, and how to see which rung produced the current window: [Context length (context window)](#context-length-context-window).\n- `path_suffix` (string, optional provider-table key): override the chat-completions path for OpenAI-compatible gateways that do not serve `/v1/chat/completions`. For example, `[providers.openai] path_suffix = \"/chat/completions\"` sends chat requests to the unversioned base URL plus `/chat/completions`; `models` and `beta/*` requests keep their normal routing.\n- `reasoning_stream_style` (string, optional provider-table key): override how streaming reasoning is separated from answer text for the active provider route. Use `separate_field` for `reasoning_content` / `reasoning` deltas, `inline_tags` for gateways that stream `<think>...</think>` inside `delta.content`, or `none` to render incoming content exactly as answer text. When unset, every Chat Completions route uses `separate_field` (Mistral's first-party route uses its typed thinking blocks); set `none` only for a gateway that streams its answer inside `reasoning_content`.\n- `[providers.<name>.auth]` (table, optional): provider-scoped auth source metadata. `source = \"command\"` stores a command argv plus optional `timeout_ms`; `source = \"secret\"` stores a `secret_id`. This slice lets provider readiness, `/provider`, and doctor JSON report the auth source class without exposing command argv output or secret values; executing commands and resolving external secret material is handled by the follow-up resolver work.\n- `insecure_skip_tls_verify` (bool, optional provider-table key): legacy compatibility key, disabled by default. When true on the active provider table, provider clients reject the configuration instead of skipping TLS certificate verification. Use `SSL_CERT_FILE` for corporate or private CA bundles; `codewhale doctor` reports stale uses of this setting.\n- `default_text_model` (string, optional): defaults to `deepseek-flash` for DeepSeek and `deepseek-anthropic`, `gpt-5.6` for OpenAI, `grok-4.6` for xAI, `deepseek-ai/deepseek-v4-pro` for NVIDIA NIM, `deepseek-ai/deepseek-v4-flash` for AtlasCloud, `deepseek-reasoner` for Wanjie Ark, `DeepSeek-V4-Pro` for Volcengine Ark, `deepseek/deepseek-v4-pro` for OpenRouter and Novita, `mimo-v2.6-pro` for Xiaomi MiMo, `accounts/fireworks/models/deepseek-v4-pro` for Fireworks, `deepseek-ai/DeepSeek-V4-Pro` for SiliconFlow and DeepInfra, `trinity-large-thinking` for Arcee AI, `kimi-k2.7-code` for Moonshot, `MiniMax-M3` for MiniMax, `GLM-5.3` for Z.ai, `step-3.7-flash` for StepFun, `ernie-4.0-turbo-8k` for Qianfan, `fugu` for Sakana AI, `deepseek-ai/DeepSeek-V4-Pro` for SGLang/vLLM, no fixed id for local Ollama (Codewhale adopts a chat-capable tag from the live local catalog — coder or tool-capable tags first, then the largest context — and never an embedding or reranker model), and `gpt-oss:120b` for Ollama Cloud. Hugging Face and Together AI both default to `deepseek-ai/DeepSeek-V4-Pro`; `openai-codex` defaults to `gpt-6.1-sol`; `anthropic` defaults to `claude-sonnet-5-5`; `openmodel` defaults to `deepseek-v4-flash`. Current public DeepSeek IDs include `deepseek-v4-pro` and `deepseek-flash` (V4.1 Flash, shipped as the unversioned id), both with 1M context windows, 384K max output, and thinking mode enabled by default. DeepSeek's live pricing/model page now labels the Pro backend `DeepSeek-V4-Pro-0813`; the callable API ID remains `deepseek-v4-pro`, so Codewhale does not send the backend label or the Claude Code-specific `deepseek-v4-pro[1m]` selector. DeepSeek retired `deepseek-chat` and `deepseek-reasoner` on July 24, 2026; direct first-party routes migrate both to `deepseek-flash`, with omitted reasoning settings preserving their former non-thinking (`off`) and thinking (`high`) intent. DeepSeek has also retired the `deepseek-v4-flash` and `deepseek-v4-flash-vision-exp` models: it still accepts both names, serves them with V4.1 Flash, and bills them at the `deepseek-flash` price, which Codewhale applies to turns from 2026-09-10 04:00 UTC. The `flash`, `deepseek-v4flash`, `flash-vision`, and `deepseek-v4flashvisionexp` shorthands select `deepseek-flash`. Explicit `reasoning_effort` wins, and provider-owned ids on Wanjie Ark, aggregators, self-hosted runtimes, and custom endpoints are not globally rewritten. SiliconFlow retains its own mapping: `deepseek-reasoner` and `deepseek-r1` select its Pro model while `deepseek-chat` and `deepseek-v3` select Flash. Provider-specific mappings translate `deepseek-v4-pro` / `deepseek-v4-flash` to each provider's model ID where supported. OpenRouter also recognizes recent large IDs such as `arcee-ai/trinity-large-thinking`, `minimax/minimax-m3`, `minimax/minimax-m2.7`, `xiaomi/mimo-v2.5-pro`, `qwen/qwen3.6-flash`, `qwen/qwen3.6-35b-a3b`, `qwen/qwen3.6-max-preview`, `qwen/qwen3.6-27b`, `qwen/qwen3.6-plus`, `qwen/qwen3.7-max`, `google/gemma-4-31b-it`, `moonshotai/kimi-k2.7-code`, `moonshotai/kimi-k2.6`, `nvidia/nemotron-3-nano-omni-30b-a3b-reasoning:free`, and `nvidia/nemotron-3-ultra-550b-a55b`; direct Arcee uses bare IDs such as `trinity-large-thinking` and `trinity-large-preview`; direct Moonshot recognizes `kimi-k3`, `kimi-k2.7-code`, and `kimi-k2.6`. The exact Kimi Code endpoint recognizes bare `k3` for K3 and `kimi-for-coding` for K2.7; those membership IDs are distinct from the direct Moonshot IDs and are never rewritten across routes. Direct MiniMax recognizes `MiniMax-M3` and the documented M2.x chat model IDs; direct Z.ai recognizes `GLM-5.3` (the default), `GLM-5.2`, `GLM-5.1`, and `GLM-5-Turbo`, and OpenRouter recognizes the matching `z-ai/glm-5.1`, `z-ai/glm-5.2`, `z-ai/glm-5.3`, and `z-ai/glm-5-turbo` IDs — `GLM-5.3` has been live on the Z.ai Coding Plan since 2026-08-13; it inherits its catalog metadata from `GLM-5.2` until Z.ai publishes distinct 5.3 numbers and carries no price, and an explicit `GLM-5.2` selection keeps its own id; direct Sakana recognizes `fugu` and `fugu-ultra-20260615`; direct Xiaomi MiMo recognizes chat IDs `mimo-v2.6-pro`, `mimo-v2.6-flash`, and `mimo-v2.5-pro-ultraspeed` (the retired `mimo-v2.5-pro` and `mimo-v2.5` are sent as `mimo-v2.6-pro` and `mimo-v2.6-flash`), while TTS IDs are selected through `codewhale speech` / `tts`. Generic `openai`, `atlascloud`, `wanjie-ark`, `xiaomi-mimo`, `arcee`, `moonshot`, `minimax`, `openmodel`, `zai`, `stepfun`, `qianfan`, `sakana`, local Ollama, and Ollama Cloud model IDs are passed through unchanged after known aliases are normalized. OpenRouter and SiliconFlow provider configs with a custom `base_url` also preserve explicit model values, which lets OpenAI-compatible gateways accept bare model IDs. Use `/models` or `codewhale models` to discover live IDs from your configured endpoint. `CODEWHALE_MODEL` overrides this for a single process; `DEEPSEEK_MODEL` is the legacy alias.\n- TelecomJS uses `deepseek-v4-pro` only as a conservative pre-refresh fallback. Once its key-scoped `/models` catalog is available, the picker uses those live rows; Codewhale omits unsupported reasoning request fields on this route.\n- `reasoning_effort` (string, optional): `auto`, `off`, `minimal`, `low`, `medium`, `high`, `xhigh`, `ultra` (`ultracode` is an alias of `ultra`), or `max`; defaults to the configured UI tier. DeepSeek Platform receives top-level `thinking` / `reasoning_effort` fields. Ollama Cloud's OpenAI-compatible Chat Completions route preserves its documented `none` / `low` / `medium` / `high` / `max` ladder (`off` is sent as `none`; `xhigh` and `ultracode` normalize to `max`). Direct xAI `grok-4.7` and `grok-4.6` on exact `https://api.x.ai/v1` receive top-level `reasoning_effort = \"low\" | \"medium\" | \"high\" | \"xhigh\"` (the ladder comes from the bundled catalog row; `grok-4.5` maps `xhigh` to `high`, and rows without a documented effort such as `grok-4.3` get no field); Grok reasoning cannot be disabled, so `off` normalizes to `high`, `max`/`ultracode` to `xhigh`, and `auto` leaves the field omitted so xAI's documented default `high` applies. A custom xAI-compatible `base_url` does not inherit that dialect. Direct Moonshot `kimi-k3` on exact `https://api.moonshot.ai/v1` is always-thinking and receives only top-level `reasoning_effort = \"low\" | \"high\" | \"max\"`; `off` normalizes to `low`, and `medium` to `high`. Kimi Code membership `k3` on exact `https://api.kimi.com/coding/v1` instead receives nested `thinking.effort`, and its `off` setting also normalizes to enabled `low`. Normal dispatched `auto` uses Codewhale's auto-reasoning selector and sends a concrete route-normalized tier; only an omitted reasoning setting leaves the provider default in control. Neighboring gateways and model/endpoint combinations retain the generic Moonshot contract. OpenAI Codex normalizes stale `off` to `low` and sends `max` / `ultracode` as Responses `xhigh`. Z.ai receives documented `thinking` controls and treats enabled thinking as the GLM coding high/max lane. NVIDIA NIM receives equivalent settings through `chat_template_kwargs`.\n- `verbosity` (string, optional): `normal` or `concise`. `normal` keeps the\n default conversational prompt. `concise` appends a prompt discipline block\n for direct, low-chatter output; CLI noninteractive commands (`exec` and\n `eval`) default to `concise` unless config/env/CLI overrides it.\n Override per process with `CODEWHALE_VERBOSITY` or the legacy\n `DEEPSEEK_VERBOSITY` alias.\n- `telemetry` (bool, optional): anonymous usage counting, **`true` by default\n in the current 0.9.12 source**. Notice version `5` names Codewhale and PostHog\n and describes the opt-out policy; no acceptance is invented for a default\n user. Existing explicit declines remain off. An explicit `false` here is the\n durable *opt-out*: it deletes the random install id, truncates buffered and\n dry-run events, and leaves a tombstone reasserted while the setting is false.\n It is a floor: `--telemetry true` and `CODEWHALE_TELEMETRY=1` lose to it.\n Use `/settings` or `codewhale config set telemetry true` to turn counting\n back on explicitly for new sessions through the existing privacy transition. `CODEWHALE_TELEMETRY`\n (legacy alias `DEEPSEEK_TELEMETRY`) and `--telemetry false` provide a run-scoped\n kill switch that stops collection and delivery without erasing the owner's\n state. A repo-local `.codewhale/config.toml` cannot set this preference.\n `codewhale config telemetry` shows the disclosure; `codewhale config get\n telemetry` reports preference and privacy status. Full schema and opt-out\n behavior: [`TELEMETRY.md`](TELEMETRY.md).\n- `telemetry_endpoint` (string, optional): where batches are POSTed. Leaving it\n unset selects the shipped default,\n **`https://telemetry.codewhale.net/v1/telemetry`** — the first-party ingest\n service described in [`TELEMETRY.md`](TELEMETRY.md), whose source is in\n `telemetry-ingest/`. This key decides only *where* a permitted session sends;\n it cannot override an opt-out. Setting it\n to the **empty string** is how you stay enabled and contact nobody: each batch\n is then written to `$CODEWHALE_HOME/telemetry/dryrun.jsonl` and no HTTP client\n is constructed at all, so you can read exactly what would have been sent. Any\n other value replaces the default outright. `https://` is required; plain\n `http://` is accepted only for loopback hosts, and there is no environment\n variable that overrides that refusal. A rejected endpoint turns telemetry off\n for the run rather than falling back to plaintext or to the default. Override\n per process with `CODEWHALE_TELEMETRY_ENDPOINT` (legacy alias\n `DEEPSEEK_TELEMETRY_ENDPOINT`), where an empty value means the same \"contact\n nobody\". A repo-local `.codewhale/config.toml` cannot set it.\n- `allow_shell` (bool, optional): in interactive TUI Agent sessions, omitting\n this keeps shell tools available with approval prompts; setting it to `false`\n hides shell tools. Headless, durable-task, and other noninteractive profiles\n keep the conservative omitted-field default and require `allow_shell = true`\n to expose shell. Plan mode always hides shell; Full Access enables shell and\n auto-approval.\n- `approval_policy` (string, optional): `on-request`, `untrusted`, or `never`. Runtime `approval_mode` editing in `/config` also accepts `on-request` and `untrusted` aliases.\n- `[approval] default_selection` (string, optional): which option an approval\n card highlights when it first appears — `deny` (default) or `allow_once`.\n `deny` means a reflexive Enter on a card you have not read refuses the call.\n The highlighted row is tagged `(Enter)`; press `y` to allow once.\n Set `allow_once` to restore the pre-v0.9.6 Enter-to-approve muscle memory\n (#5293). It moves the highlight only: which calls are prompted for is still\n `approval_policy` plus the rules in `permissions.toml`.\n\n ```toml\n [approval]\n default_selection = \"allow_once\"\n ```\n- `[approval] timeout_seconds` (integer, optional): bound how long an\n approval may wait — the TUI card and the Runtime API approvals the desktop\n app and web use alike. When the window elapses the call is refused\n (fail-closed) and recorded as a timeout, not as the operator's denial.\n Omitted or `0` waits indefinitely, which is the default everywhere: no\n approval is ever denied on your behalf unless you set this. Values above\n 24h clamp with a warning (#6101).\n\n ```toml\n [approval]\n timeout_seconds = 300\n ```\n- `sandbox_mode` (string, optional): `read-only`, `workspace-write`, `danger-full-access`, `external-sandbox`.\n The value can only narrow the sandbox the permission mode picks (Full\n Access: no wrapper; Ask and Auto-Review: `workspace-write`; Plan:\n `read-only`), so `danger-full-access` and `external-sandbox` take effect\n only in Full Access (`yolo`).\n Platform support is not identical. macOS uses Seatbelt when its runtime\n probe succeeds. Linux uses bubblewrap by default whenever `/usr/bin/bwrap`\n is installed and a probe proves it can confine a child; `prefer_bwrap =\n false` opts out and reports no OS command sandbox. Windows does not currently advertise an OS sandbox; its planned helper contract starts\n with process-tree containment only and must not be described as read-only\n filesystem isolation, workspace-write enforcement, network blocking,\n registry isolation, or AppContainer isolation until those are implemented.\n- The cross-layer relationship between mode admission, hooks, registered tool\n requirements, typed rules, auto-review, repo law, human approval, and the\n execution sandbox is defined in\n [Authorization order](AUTHORIZATION_ORDER.md).\n- **Read deny-list.** Every sandbox posture — `read-only` included — grants\n read access to the whole filesystem; the postures differ in what they may\n *write* and whether they may reach the network. The read deny-list narrows\n that, in two scopes. Codewhale's own file tools (`read`, `grep`,\n `file_search`, `read_media`) refuse every listed path. A shell command\n refuses only the *key material and other-principal* entries — SSH and GPG\n keys, `/etc/shadow`, `/etc/sudoers`, system and login keychains, browser\n profiles, other agents' credential stores, Codewhale's own secret stores,\n runtime daemon sockets, and anything in `sandbox_denied_read_paths`. A\n tool's own auth config is refused to the model but stays readable to the\n tool: `gh` cannot start without `~/.config/gh/hosts.yml`, so denying it to\n the process would disable the CLI without keeping its token out of reach.\n - `sandbox_read_denylist_defaults` (bool, default `true`): apply the built-in\n credential-store set — `~/.ssh`, `~/.gnupg`, cloud credential directories\n (`~/.aws`, `~/.config/gcloud`, `~/.azure`, `~/.kube`, …), `~/.netrc`,\n `~/.npmrc`, `~/.git-credentials`, macOS keychains, browser profiles,\n Codewhale's own secret stores, and `.env` files (but not `.env.example`\n and friends). Ordinary source, `Cargo.toml`, `~/.gitconfig`, `~/.cargo`,\n and `~/.npm` stay readable so builds and tests still work. Set `false` to\n restore the pre-0.9.12 full-disk-read behavior.\n - `sandbox_denied_read_paths` (list of paths): additional denied subpaths.\n `~` expands. These can never be exempted.\n - `sandbox_read_denylist_exempt` (list of paths): subtract a path from the\n *built-in defaults* when a project genuinely needs it. Deny wins over\n allow: this never reopens anything in `sandbox_denied_read_paths`.\n\n Exemption granularity is **whole-rule**, not per-file. An exempt path\n removes a built-in rule only when the rule's own path is at or below it,\n so exempting `~/.ssh/config` does nothing: the `~/.ssh` rule still denies\n it, because `~/.ssh` does not lie within `~/.ssh/config`. To reopen that\n one file you must exempt `~/.ssh` itself — which also reopens the private\n keys next to it. That is the documented tradeoff: there is no shipped way\n to narrow a built-in rule to \"everything except one file\"; copy what you\n need out of the denied tree instead. The one name-shaped rule, `.env`\n files, is exempted by *name* rather than by path: any exempt entry whose\n file name is exactly `.env` — bare `.env`, `~/.env`,\n `some/project/.env` — disables the entire `.env` filename rule, i.e.\n every `.env` and `.env.<name>` on disk rather than one project's.\n (`.env.example` and friends are never denied, so they need no exemption.)\n\n Enforced at two points, with different reach. Codewhale's own in-process\n tools, which the OS sandbox never wraps, refuse every rule — `read_file` /\n `read` / `read_media` for contents, `list_dir` / `file_search` for\n *enumeration* (listing a denied directory, or searching one, is refused just\n as Seatbelt blocks its readdir; a name search rooted above a denied tree\n skips entries inside it). Sandboxed shell commands (Seatbelt last-match-wins\n `deny file-read*` rules; bubblewrap masks each path) refuse only the\n key-material and other-principal rules, so an authenticated CLI can still\n read its own config. A refused read is always an explicit error, never an\n empty result, and the error names the path as the caller spelled it rather\n than a symlink target's real location.\n\n **This is defense-in-depth, not a security boundary.** It does not stop a\n hardlink to a denied file, a secret already copied into the workspace, an\n indirect read (`ssh-agent`, `security find-generic-password`, `aws sts …`),\n reads under `danger-full-access` shell commands, reads by MCP servers or\n other unwrapped child processes, or exfiltration of anything that *was*\n read. Keep least-privilege credentials and short-lived tokens doing the real\n work.\n- `permissions.toml` (sibling file, optional): typed permission rule records\n loaded next to `config.toml`, for example `~/.codewhale/permissions.toml`.\n This active user file is the only permission-rule source today; project\n config overlays do not load a project-local `permissions.toml`. A rule's\n optional `workspace` field is its repository scope, not a second source.\n Manually authored `[[rules]]` entries accept `tool`, optional `command` or\n `path`, optional absolute `workspace`, optional `command_exact = true`, and\n optional `action = \"deny\" | \"ask\" | \"allow\"`; omitted `action` defaults to\n `\"ask\"`. `workspace` limits a rule to that repository, while\n `command_exact = true` changes a command rule from the historical\n arity-aware prefix match to a complete-command match. `deny` blocks matching\n invocations before mode-based\n approval handling, `allow` skips approval for matching invocations, and\n `ask` forces approval only in modes that can prompt. Outside the TUI\n auto-approve path, a matching `ask` rule under `approval_policy = \"never\"`\n is rejected because no prompt can be shown. In Full Access / auto-approval sessions,\n `ask` rules do not downgrade the session into prompting or blocking; explicit\n `deny` rules still block according to the current execution-policy logic.\n\n In a supported approval card, press `S` to allow the request once and append\n exact `action = \"ask\"` rules to this file. For eligible safe requests, choose\n **Always allow this exact rule in this repo** (shortcut `P`) to append an\n `action = \"allow\"` rule with the current absolute `workspace` scope.\n Remembered shell grants set `command_exact = true`, so later commands with\n extra arguments do not inherit the grant. File and patch grants retain the\n exact workspace-relative paths produced by the existing validation path.\n Supported saves are intentionally narrow:\n `exec_shell` stores the exact approved command string; `write_file` and\n `edit_file` store the exact workspace-relative file path; `apply_patch`\n stores one exact workspace-relative `path` rule per validated touched file\n from apply-patch preflight. Existing exec command matching remains\n arity-aware for manually authored prefix rules; approval-card allow grants\n use complete-command matching. File paths are normalized to the same\n workspace-relative form used by runtime matching.\n\n `read_file` rules can still be authored manually when you want future reads\n of a specific path to ask, allow, or deny, but the approval UI does not save\n `read_file` rules. Commands classified as requiring approval or dangerous,\n critical approval cards, and repo-law prompts cannot save allow grants and\n continue to require review.\n\n `/permissions` (or `/permissions list`) is the narrow rule-management\n surface. It lists each numbered rule with the active user-file source, its\n exact effective matcher (tool-wide, command prefix, exact command, or exact\n normalized path), global or repository scope, and whether that scope\n applies in the current workspace. `/config ask-rules` remains a compatibility\n entry to the same list.\n\n Deletion is review-gated: `/permissions remove <number>` only previews the\n selected rule and prints a confirmation command. That command carries an\n opaque token bound to the exact file bytes and rule index; if another writer\n changes `permissions.toml`, confirmation fails instead of deleting a rule\n that moved into the old position. Confirmed removal and approval-card appends\n share the adjacent `permissions.toml.lock`, preserve unrelated TOML comments\n and formatting, and atomically replace the file. The running TUI reloads the\n user ruleset without clearing session-only approvals.\n\n This editor intentionally does not create or rewrite rules, persist deny\n choices from approval cards, expand globs, or create broad\n directory/recursive rules. Author those supported exact/prefix records\n manually when needed.\n- `[[hotbar]]` (array of tables, optional): user-owned 1-8 slot bindings for\n the TUI hotbar. Each entry has `slot`, `action`, and optional `label`.\n The Hotbar is hidden by default (#3807): omitting `hotbar` and setting\n `hotbar = []` both mean no hotbar. `/hotbar on` writes the eight default\n slots (`slash.workflow`, `slash.goal`, `slash.auto`, `mode.plan`,\n `mode.agent`, `mode.operate`, `palette.open`, `sidebar.toggle`) as explicit\n `[[hotbar]]` tables. When one or more `[[hotbar]]` tables are present, that\n list is the whole bar; missing slots stay empty. Invalid slots outside `1..=8` are skipped with a warning, duplicate\n slots use the later entry, and unknown action IDs are kept so the UI can show\n a disabled/unknown cell instead of silently deleting user config. Trusted\n user config, profiles, and managed config replace the whole list; project\n overlays cannot change hotbar bindings. Setup or wizard flows that persist\n hotbar bindings write this same schema to the resolved `~/.codewhale/config.toml`\n path, preserving legacy `~/.deepseek/config.toml` only when that fallback file\n is already the active config.\n\n ```toml\n [[hotbar]]\n slot = 1\n action = \"mode.plan\"\n label = \"Plan\"\n\n [[hotbar]]\n slot = 2\n action = \"session.compact\"\n ```\n- `[auto_review]` (table, optional): tool-call review policy — a deterministic floor plus a model guardian tier.\n This layer sits on top of the existing permission posture; it can hold or block a\n tool call, but it is not an auto-push, auto-merge, or hosted review service.\n Block rules are checked first, then the built-in safety floor, then allow\n rules. In Ask, a safety hold opens approval that no session grant or allow\n rule can answer; in Auto-Review or a non-interactive `never` posture it\n fails closed as a hard block. Full Access skips the built-in floor. The\n safety floor covers publish-like actions, destructive background/headless\n actions, and `curl`/`wget` uploads whose request body carries a credential\n file or a secret-named variable, or reads stdin fed from one of those or\n from `env`/`printenv`, even if an allow rule matches; a shell command,\n shell input or verifier over 64 KiB or 4,096 words whose text contains\n `curl` or `wget` anywhere, or one nested too deeply to read, is held too.\n Outside Full\n Access it also refuses, in every posture, a\n shell command, shell input or verifier containing a hidden Unicode character\n (bidirectional controls, zero-width and other invisible characters, tag\n characters, stray variation selectors, and terminal control characters\n except ASCII control keys in shell input), naming the code point and its\n position.\n\n ```toml\n [[auto_review.allow]]\n id = \"read-only-inspection\"\n action_kind = \"read\"\n reason = \"Read-only inspection is safe to run automatically.\"\n\n [[auto_review.block]]\n id = \"no-release-publish\"\n action_kind = \"publish\"\n reason = \"Release and publish actions require maintainer review.\"\n ```\n\n Rule matchers are exact `tool` and/or `action_kind`. At least one matcher is\n required. `action_kind` accepts the six decision-relevant kinds `read`,\n `write`, `shell`, `external`, `publish`, and `destructive`. Invalid names\n fail config validation instead of silently broadening into another policy\n class. In block rules, the old names remain conservative compatibility\n aliases: `network`, `git`, `mcp_action`, `browser`, and `unknown` map to\n `external`; `secret` maps to `destructive`; and `mcp_read` maps to `read`.\n Retired narrow kinds in allow rules fail validation rather than widening to\n a broader class. The retired `text_contains` matcher likewise fails\n validation instead of silently broadening an old intent-dependent rule.\n Fallback holds in interactive Auto-Review escalate to one stateless guardian\n request. The request contains the exact held call and deterministic\n observations as separate JSON fields. Conversation history, skill\n instructions, attached file contents, and other expanded model context are\n excluded. The guardian does not infer user intent or compute an authorization\n score. It exposes no tools and returns a risk level, allow/deny, and a\n rationale. High or critical risk cannot auto-run even if the model says\n allow. An oversized exact call is denied rather than truncated. Exactly one\n reviewer request is made; incomplete or malformed output, timeout,\n cancellation, provider failure, or an empty rationale all fail closed. The\n deterministic floor is never model-reviewed, and headless adapters use the\n deterministic-only tier. The pinned Codex, Kimi, and DeepSeek source\n boundaries are linked from [Permission Posture](MODES.md#permission-posture).\n Reviewer outcomes emit `tool.auto_review` audit events with\n `gate = \"guardian\"`.\n\n Auto-review decisions emit `tool.auto_review` audit events with\n `gate = \"deterministic\"` when tool\n audit logging is enabled. Future PreToolUse/PostToolUse hooks can add\n observer input around this layer, but the configured auto-review policy is\n evaluated before a tool call is allowed to proceed.\n- `managed_config_path` (string, optional): managed config file loaded after user/env config.\n- `requirements_path` (string, optional): requirements file used to enforce allowed approval/sandbox values.\n- `max_subagents` (int, optional): defaults to `64` and is clamped to `1..=128`.\n- `subagents.*` (optional compatibility table): manual per-role model pins\n for direct and Workflow `agent` starts. An explicit saved profile wins,\n then a manual role pin, then a unique saved role pin. Conflicting tool\n `model` or `model_strength` choices are refused before admission. Unpinned\n roles allow task model/strength choices before inherited defaults.\n `[subagents.roles.<role>] model = \"provider/model\"` folds into the existing override\n map and wins over `[subagents.models]`, then the convenience keys. Structured\n canonical role keys win over legacy aliases. Only this structured syntax\n separates the explicit provider from the model suffix; unknown providers\n fail before admission. Bare structured model ids inherit the active provider.\n Legacy scalar/map values preserve namespaced provider-owned ids unchanged.\n Supported convenience keys are\n `default_model`, `worker_model`, `scout_model`, `planner_model`,\n `reviewer_model`, `custom_model`, `max_concurrent`, `max_admitted`,\n `launch_concurrency`, `token_budget`, `api_timeout_secs`, and\n `heartbeat_timeout_secs`. The v0.9.x keys `explorer_model`, `awaiter_model`,\n and `review_model` remain accepted as aliases. The `[subagents]\n max_concurrent` value overrides\n top-level `max_subagents` and is also clamped to `1..=128`. `[subagents]\n max_admitted` (aliases: `max_total`, `admission_limit`) is the bounded total\n of queued plus running sub-agents; it defaults to `1024`\n (`MAX_SUBAGENT_ADMISSION`, `crates/tui/src/config/subagent_limits.rs:21`,\n applied at `config.rs:6400`) so high-fanout turns can queue and drain while\n runtime launch pressure remains bounded, and is clamped to\n `max_concurrent..=1024`. `[subagents]\n launch_concurrency` sets how many direct children start at once before the\n rest queue for a launch slot; it defaults to the resolved `max_subagents` cap\n and is clamped to `1..=max_subagents` (the deprecated\n `interactive_max_launch` key is accepted as an alias, with the new key\n winning when both are set). `[subagents] token_budget` is an optional\n aggregate token ceiling for each root `agent` run and its descendants; unset\n or `0` preserves unlimited legacy behavior. `[subagents] api_timeout_secs`\n controls the per-step API timeout for sub-agent model calls and is clamped to\n `1..=3600`, with `0` or unset preserving the 600 second default. A timeout\n after possible dispatch interrupts with a preserved checkpoint and unknown\n outcome. Only a transient connection timeout proven to precede dispatch uses\n exponential backoff, up to 5 retries.\n `[subagents] heartbeat_timeout_secs` controls stale running agent cleanup,\n defaults to `300`, and is clamped to `30..=3600` while staying above the\n resolved API timeout. `[subagents.providers.<provider>]` accepts the same\n fanout, depth, budget, and timeout knobs (`enabled`, `max_concurrent`,\n `max_admitted`, `launch_concurrency`, `max_depth`, `token_budget`,\n `api_timeout_secs`, `heartbeat_timeout_secs`) and inherits the global\n `[subagents]` value for any key you omit. Provider keys accept canonical\n names such as `deepseek`, `zai`, `openrouter`, `anthropic`, plus convenience\n aliases such as `glm` for Z.ai and `deepseek_api` for direct DeepSeek:\n\n ```toml\n [subagents]\n max_concurrent = 20\n launch_concurrency = 20\n max_admitted = 200\n max_depth = 6\n\n [subagents.providers.deepseek]\n max_concurrent = 20\n launch_concurrency = 20\n max_admitted = 200\n\n [subagents.providers.glm]\n max_concurrent = 4\n launch_concurrency = 3\n max_admitted = 12\n max_depth = 2\n\n [subagents.providers.openrouter]\n max_concurrent = 5\n launch_concurrency = 3\n max_admitted = 20\n ```\n\n `/config subagents status` prints both global values and the active\n provider's resolved profile so rate-limit tuning is visible in the TUI.\n `[subagents.models]` accepts lower-case Fleet role keys such as `worker`,\n `scout`, `planner`, `reviewer`, `builder`, and `verifier`; legacy type keys\n remain accepted during v0.9.x. Values are validated\n against the active provider at spawn time; direct DeepSeek requires DeepSeek\n IDs, while OpenAI-compatible/custom provider routes pass explicit model IDs\n through to that provider. To route a child to a different provider than the\n parent session, save a Fleet/AgentProfile with explicit `provider` and\n `model` fields (including user-named custom providers such as `lm-studio`)\n and call `agent(profile: \"...\")`; see [SUBAGENTS.md](SUBAGENTS.md).\n- `skills_dir` (string, optional): defaults to `~/.codewhale/skills` (each skill is\n a directory containing `SKILL.md`). After workspace trust, project skills\n take precedence, with `.codewhale/skills` ahead of compatible roots such as\n `.agents/skills`. Flat `./skills` requires explicit opt-in below. The runtime\n also discovers global agentskills.io-compatible `~/.agents/skills` and the\n broader Claude-ecosystem\n `~/.claude/skills`. With the default bundle policy and no `skills_dir` override, first launch installs versioned bundled skills for common\n workflows including skill creation, delegation, MCP/plugin scaffolding,\n documents, presentations, spreadsheets and PDFs. Feishu/Lark is outside the\n default pack. Only\n Codewhale-owned roots (`<workspace>/.codewhale/skills` and\n `~/.codewhale/skills`) are writable install/import targets; compatible harness\n roots stay read-only. Bare `/skills` opens Extensions → Skills (owned-only,\n zero network); `/skills manage` opens the manager. See [SKILLS.md](SKILLS.md)\n for the manager, audit statuses, provenance markers, and mutation rules, and\n [CLAUDE_PLUGIN_COMPAT.md](CLAUDE_PLUGIN_COMPAT.md) for the supported boundary\n between portable `SKILL.md` bundles and Claude Code plugin runtimes.\n- `[skills.bundled].install` (`\"auto\"` default, `\"all\"`, `\"none\"`): controls bundle writes independently of discovery. `auto` keeps deletion-as-opt-out and disables installation when `skills_dir` is set. `all` repairs enabled missing skills into `$CODEWHALE_HOME/skills` (normally `~/.codewhale/skills`) even with a discovery override. `none` writes nothing and leaves the marker untouched. Modified and managed bodies are preserved in every mode.\n- `[skills.bundled].selected` / `disabled` / `explicit_only` (arrays of names, default `[]`): selection limits installation and discovery; empty selects the whole bundle. Disabled wins and reveals lower-root copies. Explicit-only retains user invocation while removing the shipped copy from the model index and model loading. Aliases share canonical policy. These settings affect recognized global bundle copies only and never delete files. Profile fields merge separately; `[]` clears an inherited list. `doctor` warns about unknown/ignored entries and `/skills inspect` reports per-path reasons. See [bundled skills](SKILLS.md#bundled-skills).\n- `[skills].listing_max_description_chars` (integer, default `120`, range `1..=40000`): ambient-index description cap, shared by parent and child prompts and `/skills inspect`.\n- `[skills].listing_budget_fraction` (finite number, default `0.002`, greater than `0` and at most `1`): share of the route window for that same index, estimated at four characters per token and clamped to `2400..=40000` characters. Explicit profile fields override inherited values independently; `/context` uses the same resolution. Full skill loading is unaffected.\n- `[skills].scan_codewhale_only` (bool, default `false`): when `true`, session\n skill discovery ignores cross-tool roots such as `.claude/skills`,\n `.opencode/skills`, `.cursor/skills`, and `~/.agents/skills`. Codewhale still\n scans `<workspace>/.codewhale/skills`, `~/.codewhale/skills`, and any explicit\n `skills_dir` override. The Skills Manager can still toggle a local compatible\n audit scan independently of this runtime knob — see [SKILLS.md](SKILLS.md).\n- `[skills].flat_workspace_root` (bool, default `false`): opt in to the flat\n `<workspace>/skills` compatibility root after workspace trust. Without this\n opt-in it is an audit candidate only; an explicit `skills_dir` remains an\n alternative. `scan_codewhale_only = true` excludes the flat compatibility\n root regardless of this flag, unless it is the explicit `skills_dir`.\n- `[skills].registry_url` / `[skills].max_install_size_bytes` (optional): used by\n `/skills --remote`, `/skills suggest <task>`, `/skills sync`, and `/skill\n install|update`. The default manager open path does not contact the registry.\n- `[verifier].enabled` (bool, default `false`): enables automatic\n claim-of-done verifier preview once that runtime trigger is active. The\n manual `run_verifiers` tool is still available when this is false.\n- `mcp_config_path` (string, optional): defaults to `~/.codewhale/mcp.json`, with\n legacy `~/.deepseek/mcp.json` fallback when the Codewhale path is absent.\n Custom paths must be absolute; a relative value falls back to the user-global\n path so changing the launch directory cannot silently change the MCP pool.\n It is visible in `/config` and can be changed from the TUI. The new path is\n used immediately by `/mcp`; run `/mcp reload` to rebuild the model-visible\n MCP tool pool without restarting the TUI. A headless runtime still needs a\n restart because its `ConfigReload` request does not refresh MCP connections.\n- `notes_path` (string, optional): defaults to `~/.codewhale/notes.txt`, with\n legacy `~/.deepseek/notes.txt` fallback when the Codewhale path is absent, and\n is used by the model-visible `note` tool.\n- `[memory].enabled` (bool, optional): defaults to `false`. When `true`,\n the TUI loads the user memory file into a `<user_memory>` prompt block,\n enables `# foo` quick-capture in the composer, surfaces the `/memory`\n slash command, and registers the `remember` tool. The same toggle is\n available via `DEEPSEEK_MEMORY=on`.\n- `memory_path` (string, optional): anchors the native memory store. The\n configured filename is **not** the file that is written. Under the Native\n backend (the only backend) the store is re-rooted to\n `<parent-of-memory_path>/memory/global/MEMORY.md` — so the default\n `~/.codewhale/memory.md` yields `~/.codewhale/memory/global/MEMORY.md`\n (plus workspace-scoped files and a rebuildable SQLite FTS5 index). See\n [`MEMORY.md`](MEMORY.md) for the full feature surface (`# foo` composer\n prefix, `/memory` slash command, `remember` tool, opt-in toggle).\n- `snapshots.*` (optional): side-git workspace snapshots for file rollback:\n - `[snapshots].enabled` (bool, default `true`)\n - `[snapshots].max_age_days` (int, default `7`)\n - snapshots live under\n `~/.codewhale/snapshots/<project_hash>/<worktree_hash>/.git`, with legacy\n `~/.deepseek/snapshots/...` fallback when only the legacy state exists, and\n never use the workspace's own `.git` directory\n- `context.*` (optional):\n - `[context].project_pack` (bool, default `false`): include a deterministic\n project context pack (a large pretty-printed directory listing) in the\n stable prompt prefix (#4781). Useful for weak tool-calling models; the\n model can rebuild the same information with one `File` call.\n - The removed seam-manager keys (`enabled`, `verbatim_window_turns`,\n `l1_threshold`, `l2_threshold`, `l3_threshold`, `seam_model`) are\n ignored if an older config still carries them; they no longer load.\n- `compaction.*` (optional, config.toml): how a compaction pass behaves once\n it fires. `auto_compact` / `auto_compact_threshold_percent` (settings.toml)\n still decide *when* it fires. Both keys are absent by default, and absent\n means the built-in behaviour, unchanged:\n - `[compaction].summary_instructions` (string, default empty): standing\n operator instructions appended to the summarizer prompt as a clearly\n delimited \"Additional instructions from the operator\" section on **every**\n pass, manual and automatic — the effort-free counterpart to a one-off\n `/compact <focus>`, which still composes after this text. Useful for\n \"always list exact file paths and line numbers\", \"always restate open\n decisions and their trade-offs\", \"always write a TL;DR first\". Truncated\n at 4 000 characters with a warning naming the key; whitespace-only reads\n as unset. The summarizer still runs with no system prompt and no tools —\n this suffix is the only operator-authored input it sees.\n - `[compaction].retained_user_message_tokens` (int, default `20000`, clamped\n to `2000`–`200000`; also accepted as `retained_user_message_max_tokens`):\n token budget for the recent plain user messages kept **verbatim** in the\n replacement history. Raising it keeps more of the user's own earlier\n messages instead of only whatever the lossy summary captured; the\n last-round survival contract still applies on top. The `/compact` receipt\n names the effective budget and whether operator instructions were applied,\n so you can tell the knob took effect.\n- `retry.*` (optional): HTTP retry settings. The following backoff fields govern\n ordinary transport retries; HTTP 429 uses the independent policy below:\n - `[retry].enabled` (bool, default `true`)\n - `[retry].max_retries` (int, default `3`): ordinary transport retries\n - `[retry].initial_delay` (float seconds, default `1.0`)\n - `[retry].max_delay` (float seconds, default `60.0`)\n - `[retry].exponential_base` (float, default `2.0`)\n - `[retry].jitter` (bool, default `true`): randomize ordinary transport backoff\n - `[retry].jitter_factor` (float, default `0.1` = ±10%; clamps to `0.0..=1.0`)\n - `[retry].respect_retry_after` (bool, default `true`): wait for a server\n `Retry-After` preference retained for legacy retry configuration; it does not\n disable HTTP 429 server minima\n\n HTTP 429 recovery has its own validated `[retry.rate_limit]` fields:\n\n - `max_attempts` (integer, default `20`, range `1..=1000`): total attempts,\n including the first\n - `recovery_budget_secs` (integer seconds, default `600`, range `1..=86400`)\n - `initial_delay_secs` (integer seconds, default `5`)\n - `max_delay_secs` (integer seconds, default `60`), with\n `1 <= initial_delay_secs <= max_delay_secs <= 86400`\n\n This policy always honors server minima and uses equal jitter, independently\n of the legacy `initial_delay`, `max_delay`, `exponential_base`, `jitter`,\n `jitter_factor` and `respect_retry_after` fields. `max_retries = 0` does not\n disable 429 recovery; set `retry.rate_limit.max_attempts = 1` to disable only\n automatic 429 retries, or `retry.enabled = false` to disable this HTTP retry\n loop. A safe pre-dispatch HTTP/1.1 fallback inside a stream-opening operation\n remains possible. The one-hour limit on actual provider pauses and automatic sleeps\n still applies. See [HTTP rate-limit retries](rate-limit-retries.md) for the\n budget, server-minimum and active-time boundaries.\n\n `[retry]` schedules HTTP-request retries inside the client. Each physical\n streaming request gets its own `stream.open_timeout_secs` response-header\n wait. Provider backoff and honored `Retry-After` waits happen between\n attempts, outside that timer, so the configured retry count remains\n reachable. Inference retries require an ordinary rate limit or a proven\n transient connection failure before dispatch. Only that connection failure\n can switch once to HTTP/1.1; a header stall cannot. Both inference clients\n disable redirects and reqwest protocol retries. Explicit plan-window\n exhaustion is a terminal quota error. Server failures, uncertain timeouts\n and interrupted streams preserve unknown coverage and stop without\n automatically reissuing inference. Read-only catalog GETs retain bounded\n retries at their actual method owner. The `[stream]` keys below bound safe\n pre-dispatch recovery and stream consumption; legacy `tui.stream_max_*`\n keys remain configuration fallbacks.\n- `[notifications]`: notification delivery, attention, categories and audio share one\n policy. `quiet = true`, `method = \"off\"`, `condition = \"never\"` and disabled\n categories suppress both the banner and Codewhale's selected sound.\n- `notifications.method`: `auto` (default), `osc9`, `kitty`, `ghostty`, `bel`, `off`.\n- `notifications.condition`: `unfocused` (default), `always`, `never`. When absent,\n the legacy `tui.notification_condition` remains the fallback. `always` also\n bypasses the duration threshold; `unfocused` requires two seconds away.\n- `notifications.threshold_secs`: nonnegative integer, default `30`.\n- `notifications.include_summary`: boolean, default `false`.\n- `notifications.sound`: optional `off`, `whale`, `bell`, `beep`, `file`.\n A selected value controls audio across enabled categories. Absent keeps legacy\n `completion_sound` and `event_sound` choices; `off` overrides both.\n- `notifications.sound_file`: custom local WAV path for `sound = \"file\"` or legacy\n `completion_sound = \"file\"`.\n- `notifications.subagent_completion`: `always`, `final-only` (default), `off`.\n Covers all background work that finishes: sub-agents, background shells and\n durable tasks. `final-only` sends one notice naming everything that finished\n once no agent, workflow or durable task is still running; a running shell\n (a dev server, a watcher) never holds it back. `always` sends one per item.\n- `notifications.quiet`: boolean, default `false`.\n- `notifications.events`: six boolean categories, all enabled by default; see below.\n- `notifications.completion_sound`: legacy completion cue, default `off`, with the\n same values as `sound`. Used only when `sound` is absent.\n- `notifications.event_sound`: legacy `enabled` (default `false`), `events`\n (default `[\"turn-complete\", \"approval-needed\"]`), and `quiet` (default `false`).\n `min_interval_ms` (default `2000`) applies to each category's audio in both modes.\n- `tui.alternate_screen` (string, optional, default `auto`): which screen an interactive session starts on. `auto` and `always` start on the TUI-owned alternate screen; `never` starts in inline mode — a ratatui viewport the full height of the terminal with no alternate screen, so the shell's scrollback survives the session and stays scrollable after exit. `/fullscreen` and `/inline` switch it in-process; a switch that the terminal refuses rolls back and says why. Inline mode paints the whole transcript inside its viewport — nothing is written into the host scrollback while the session runs.\n- `tui.mouse_capture` (bool, optional, default `true` on non-Windows terminals and on Windows Terminal/ConEmu/Cmder when the alternate screen is active; `false` on legacy Windows console and inside JetBrains JediTerm — PyCharm/IDEA/CLion/etc. — where mouse-event escapes leak into the input stream as garbled text, see #878 / #898): enable internal mouse scrolling, transcript selection, right-click context actions, and transcript scrollbar dragging. TUI-owned drag selection copies the intersected cells, removes visual wrap-column line breaks from paragraphs, and keeps selection scoped to the transcript pane; the payload is Markdown source by default, see `tui.selection_copy_markdown` below. Set this to `false` or run with `--no-mouse-capture` for raw terminal selection; set it to `true` or run with `--mouse-capture` to opt in anywhere it's defaulted off. On raw terminal selection, especially on legacy Windows console or when mouse capture is disabled, selection may cross the right workbar and include visual wraps because the terminal, not the TUI, owns the selection.\n On Linux, finishing a transcript or composer selection quietly copies text to\n PRIMARY, leaving the regular clipboard unchanged. Middle-click inside the\n composer pastes PRIMARY at the pointer without submitting it. This uses native\n X11 or Wayland data control; compositors must support PRIMARY selection. Over\n SSH without a forwarded graphical display, use your terminal's selection/paste\n gestures or `--no-mouse-capture`. Explicit Copy still uses the regular clipboard.\n\n- `tui.selection_copy_markdown` (bool, optional, default `true`): copy TUI-owned\n transcript selections (drag release, context-menu Copy, and `Cmd+C`/`Ctrl+C`\n on an active selection) as Markdown source instead of rendered text. Every\n intersected cell serializes through the same canonical projection `Ctrl-Y`\n and `/copy` use — user and assistant cells keep their authored Markdown,\n other cells keep their full transcript form — partial intersections round out\n to whole cells, cells join with blank lines, and a toast names the copied\n cell count. Set `false` to copy the rendered text as displayed. Composer\n selections and the Linux PRIMARY auto-copy are unchanged; PRIMARY always\n carries rendered text.\n\n- `tui.stream_chunk_timeout_secs` (int, optional, default `900`): per-SSE-chunk idle timeout for streamed model responses. Slow local or compatible servers can raise this with `/config stream_chunk_timeout_secs <seconds>` (add `--save` to write canonical `stream.chunk_timeout_secs`); `0` maps to the default and explicit values must be `1..=3600`. The legacy `DEEPSEEK_STREAM_IDLE_TIMEOUT_SECS` env var is still honored when this key is omitted.\n- `tui.osc8_links` (bool, optional, default on for macOS/Linux, off for Windows): emit OSC 8 escape sequences around URLs in transcript output so supporting terminals (iTerm2, Terminal.app 13+, Ghostty, Kitty, WezTerm, Alacritty, recent gnome-terminal/konsole) can open them with the terminal's link gesture—usually Cmd-click on macOS and Ctrl-click on Linux/Windows. Terminals without OSC 8 support render the plain label and ignore the escape. The escapes are emitted out-of-band (not inside buffer cells), so column corruption is not a concern; set `false` only for terminals that misrender the OSC 8 terminator itself. Windows legacy consoles default off; opt in with `true`.\n- `tui.max_model_steps` (int, optional, default uncapped): optional model-step ceiling for one ordinary turn. Omission or `0` leaves model steps uncapped; explicit positive values are preserved (`4294967295` is the compatibility value for no limit). Headless `exec` and Fleet workers also have no implicit model-step ceiling; `exec --max-turns N` and positive worker budgets still apply. At exhaustion the turn ends `Failed` with `Maximum model steps reached before completion (limit: N)` after one bounded final-report response when needed. Cumulative wall-clock and per-stream limits remain independent. Active interactive goal turns use `goal.max_steps` instead (also uncapped by default); see the Goal loop section below.\n- `tui.turn_wall_clock_secs` (int, optional, default: no limit): cumulative per-turn wall-clock budget in seconds, measured across every model step of one turn (not per request). Time blocked on a human approval is excluded. Omitted or `0` means no limit; positive values are preserved without an added runtime ceiling. When exhausted the turn stops before authorizing another billable request with a message naming the limit and the key to raise.\n- `tui.stream_max_resumes` (int, optional, default `3`): turn-level recovery after a proven transient connection failure before request dispatch. `0` disables this additional recovery; values clamp to `0..=10`. Missing response headers, empty or partial streams, sleep and network drops never authorize inference replay.\n- `tui.stream_max_transparent_retries` (int, optional, default `2`): retained for configuration compatibility, clamped to `0..=10`. Interrupted streams no longer trigger transparent inference reissues, including before visible content.\n- `tui.stream_max_errors` (int, optional, default `5`): retained for configuration compatibility; `0` selects the default and positive values clamp to `1..=50`. Core ends an uncertain stream on its first read error or error frame regardless of this value.\n- `tui.stream_open_timeout_secs` (int, optional, default `45`): wait for a streaming request's response headers (connection setup included). A header stall ends with an unknown outcome and does not replay over HTTP/1.1. Omitted or `0` falls back to `CODEWHALE_STREAM_OPEN_TIMEOUT_SECS`, then the default; values clamp to `5..=300`.\n- `tui.connect_timeout_secs` (int, optional, default `30`): TCP/TLS connect timeout for the model HTTP client. Omitted or `0` uses the default; values clamp to `1..=300`.\n- `tui.force_http1` (bool, optional, default `false`): pin the model HTTP client to HTTP/1.1, for provider edges or proxies that mishandle long-lived HTTP/2 streams. `CODEWHALE_FORCE_HTTP1=1` does the same; either one pins.\n- `tui.stream_max_content_mb` (int, optional, default `10`) and `tui.stream_max_duration_secs` (int, optional, default `1800`): per-step caps on one stream's accumulated content and wall-clock duration. `0` selects the default; values clamp to `1..=512` MB and `10..=86400` seconds.\n- `transcript.prose_measure` (positive integer, optional, default absent = full width): wrap cap, in columns, for prose cells — user messages, assistant answers, and reasoning/thinking blocks — in the live transcript (#5436). Absent (or `0`) spends the full content width, consistent with tool/status cells and the #5322 wide-frame decision; the former 105-column prose rail is gone. Set a positive whole number (e.g. `prose_measure = 120` under `[transcript]`) to restore a bounded reading measure on ultrawide terminals. Narrow terminals always keep their content width — the cap clamps from above only. Tool, diff, and status cells never inherit this cap. Invalid values (negative or non-integer) are rejected at startup with a `transcript.prose_measure` config error. Resolved once per render pass, so the main transcript cache and the full-screen overlay always agree on the effective width.\n- `hooks` (optional): lifecycle hooks configuration (see `config.example.toml`).\n- `features.*` (optional): feature flag overrides (see below).\n\n### Stream and transport settings\n\n`[stream]` is the canonical table for model-stream policy and its HTTP clients.\n`codewhale config dump` and `codewhale config get stream` show effective values\nfrom the runtime resolver, including environment fallbacks and clamps; this does\nnot persist defaults. Set or unset individual fields with, for example,\n`codewhale config set stream.open_timeout_secs 120` and\n`codewhale config unset stream.open_timeout_secs`. Per-run\n`--set stream.open_timeout_secs=120` uses the same validation.\n\n| `[stream]` key | Default | Accepted behavior | Legacy `[tui]` fallback |\n| --- | --- | --- | --- |\n| `open_timeout_secs` | 45 | Positive values clamp to 5–300; 0 or omission falls through to the environment/default | `stream_open_timeout_secs` |\n| `chunk_timeout_secs` | 900 | 0 selects default; positive values clamp to 1–3600 | `stream_chunk_timeout_secs` |\n| `max_resumes` | 3 | Proven transient pre-dispatch recovery only; 0 disables, maximum 10 | `stream_max_resumes` |\n| `max_transparent_retries` | 2 | Compatibility value; interrupted streams never reissue inference | `stream_max_transparent_retries` |\n| `max_stream_errors` | 5 | Compatibility value; Core ends on the first uncertain stream error | `stream_max_errors` |\n| `max_duration_secs` | 1800 | Per-stream wall clock; 0 selects default, positive values clamp to 10–86400 | `stream_max_duration_secs` |\n| `max_content_mb` | 10 | Per-stream content; 0 selects default, positive values clamp to 1–512 MiB | `stream_max_content_mb` |\n| `connect_timeout_secs` | 30 | TCP/TLS setup; 0 selects default, positive values clamp to 1–300 | `connect_timeout_secs` |\n| `force_http1` | false | Boolean; a truthy environment pin always enables HTTP/1.1 | `force_http1` |\n| `tcp_keepalive_secs` | 30 | Idle time before TCP keepalive probes; 0 disables, positive values clamp to 1–3600 | none |\n| `http2_keep_alive_interval_secs` | 15 | PING interval on active HTTP/2 connections; 0 disables, positive values clamp to 1–3600 | none |\n| `http2_keep_alive_timeout_secs` | 20 | PING acknowledgement deadline; 0 selects default, positive values clamp to 1–3600 | none |\n\nFor each field, an explicit canonical value wins over the legacy `[tui]` field,\nincluding zero or false; an absent canonical field preserves the legacy value.\nThe existing environment precedence is unchanged: a positive configured header\nwait wins over `CODEWHALE_STREAM_OPEN_TIMEOUT_SECS` (then its `DEEPSEEK_` alias);\nzero falls through to those variables. An omitted chunk timeout uses\n`CODEWHALE_STREAM_IDLE_TIMEOUT_SECS` (then its `DEEPSEEK_` alias), whereas an\nexplicit zero uses 900. `CODEWHALE_FORCE_HTTP1` (legacy `DEEPSEEK_FORCE_HTTP1`)\nis ORed with the selected config flag, so a false config flag cannot defeat a\ntruthy environment pin. Profile overlays merge canonical fields independently.\n\nHTTP transport values apply to all newly constructed model, catalog and HTTP/1\nfallback clients. They do not rebuild an active client, govern MCP/other network\nservices, or send HTTP/2 PINGs on idle pooled connections. HTTP/2 knobs have no\neffect when HTTP/1.1 is pinned. The operating system controls TCP probe details;\nthese settings do not disable certificate validation. `[retry]` still owns the\nHTTP-request backoff schedule independently of stream-level budgets.\n\n### Workspace notes\n\n`/note` manages a simple notes file in the current workspace at\n`.codewhale/notes.md` (legacy `.deepseek/notes.md` is the fallback path when\nno `.codewhale/notes.md` exists yet).\nExisting `/note <text>` usage still appends a note.\nThe management forms are:\n\n| Command | Action |\n|---|---|\n| `/note <text>` | Append a note (legacy shorthand) |\n| `/note add <text>` | Append a note explicitly |\n| `/note list` | List notes with temporary 1-based numbers |\n| `/note show <n>` | Show the full note at number `n` |\n| `/note edit <n> <text>` | Replace note `n` with new text |\n| `/note remove <n>` | Delete note `n`; `rm` and `delete` are aliases |\n| `/note clear` | Empty the workspace notes file |\n| `/note path` | Show the resolved workspace notes path |\n\nThe numbers shown by `/note list` are not stored in the file; they are derived\nfrom the current order each time notes are read. This keeps the file format\ncompatible with the existing `---`-separated notes.\n\n### User memory\n\nUser memory is split across one top-level path setting and one opt-in\ntoggle table:\n\n```toml\n# Anchors the store only — actual writes go to\n# ~/.codewhale/memory/global/MEMORY.md (see MEMORY.md).\nmemory_path = \"~/.codewhale/memory.md\"\n\n[memory]\nenabled = true\n```\n\nNotes:\n\n- `memory_path` stays at the top level beside `notes_path` and\n `skills_dir`; it is not nested under `[memory]`.\n- The configured path is an **anchor**: its parent directory gains\n `memory/global/MEMORY.md`, workspace-scoped files, and `index.db`.\n Pointing `memory_path` at the native layout path itself would double-nest\n (`…/memory/global/memory/global/MEMORY.md`); keep the legacy-style\n anchor filename.\n- `DEEPSEEK_MEMORY_PATH` overrides the anchor path from the environment.\n- `DEEPSEEK_MEMORY=on` (also `1`, `true`, `yes`, `y`, or `enabled`)\n flips the feature on without editing `config.toml`.\n- The feature is inert when disabled: no file is injected, `# foo`\n falls through to normal message submission, and the model does not\n see the `remember` tool.\n- See [`MEMORY.md`](MEMORY.md) for examples and the full `/memory`\n command surface.\n\n### Goal loop (`[goal]`)\n\nOperate-mode goals run to their completion gate with no default token, time, or\ncontinuation ceiling (#5052). Token/time budgets, when explicitly supplied,\nare telemetry only and do not stop a goal. Users who want a circuit breaker can\nopt into one:\n\n```toml\n[goal]\n# Optional safety backstop on automatic goal continuation passes.\n# Default: 0 (unlimited). Set a positive value to opt into a ceiling.\nmax_continuations = 100\n\n# Optional cancellable quiet period between successful turns. This is useful\n# for coordinator goals that should poll on a cadence instead of keeping one\n# provider turn open. Default: 0 (continue immediately).\ncontinuation_delay_seconds = 300\n\n# Optional per-turn model-step ceiling while a goal is active (#6512).\n# Default: uncapped (0 or absent). Positive values are preserved.\n# This bounds each provider turn, never the number of continuation passes.\nmax_steps = 0\n\n# Optional independent judges (#6013). Default: false (the model's own\n# update_goal call is final). When true, one adversarial critique of the claim\n# runs before `update_goal` completes (complete_judge) or blocks (blocked_judge)\n# the goal. Each judged call is billed as auxiliary usage.\ncomplete_judge = false\nblocked_judge = false\n```\n\nThe effective delay is capped at 86,400 seconds (24 hours); use an automation\nfor schedules that are less frequent than once per day.\n\nWhen an explicit backstop fires, the goal pauses with a status message naming\n`[goal] max_continuations` and a warning is logged; resume the goal after\ninspecting progress, or raise/disable the backstop.\n\n`[goal] max_steps` governs one engine turn at a time. Like ordinary interactive\nturns, omitted or `0` leaves model steps uncapped. Explicit per-invocation\nceilings — `exec --max-turns N`, child-worker caps — always win over it. At exhaustion\nit gets one bounded final report and the turn classifies as budget-exhausted. An\nunfinished goal then pauses with the BudgetLimit reason instead of re-arming\nanother goal turn — resume it explicitly after reviewing the report. Wall-clock\nand stream protections are separate and still apply.\n\nWith `[goal] complete_judge = true`, a refuted completion claim keeps the goal\nactive and records the judge's findings as critical gaps. A missing,\nunparseable, uncertain or failed verdict never completes the goal; it records\none fixed gap, so a persistent judge outage ends in the existing no-progress\npause rather than a silent pass. With `[goal] blocked_judge = true`, a refuted\nblocker keeps the goal active, and an unverifiable one hands the goal back as a\nyield that your next message resumes. The judge reads only the objective, the\nevidence and the verification receipt, not the transcript or the diff. The\noperator `POST /v1/threads/{id}/goal/complete|block` routes and `/goal`\ncommands stay ungated.\n\nThe delay starts only after a successful turn while an explicitly created goal\nis still active. `/goal pause`, `/goal done`, `/goal blocked`, `/goal clear`,\nEsc, or Ctrl+C cancels a pending continuation before another provider request\nstarts. Failed turns and policy/route failures never schedule another turn.\nOnly the numeric cadence is stored in config; no prompt, credential, or secret\nis persisted for the loop.\n\n### Reasoning-only recovery (`[reasoning_only]`)\n\nWhen a model finishes with only reasoning and no answer or tool call, Codewhale\ncan request an answer again. A clean stop permits two retries by default:\nthe first keeps the request unchanged, and later retries carry a request-scoped\nnudge. The default nudge is `Continue: give your answer, or make the next tool call.`\n\n```toml\n[reasoning_only]\nmax_reprompts = 2\non_output_limit = false\n```\n\nSet `on_output_limit = true` to also retry `length`/`max_tokens` stops that\nproduced no answer or tool call, including providers that did not stream their\nhidden reasoning. This option is off by default. Output-limit retries carry the\nnudge immediately and share the same `max_reprompts` allowance. Set\n`max_reprompts = 0` to disable both reasoning-only and output-limit recovery;\ntransport and empty-stop recovery have their own existing limits.\n\nThis applies to ordinary interactive, exec and Runtime API engines and Fleet\nworkers. Workers use the captured parent configuration. RLM keeps its separate\ncompletion/FINAL/REPL stop protocol and does not use this option.\n\nEvery retry is a fresh request and may repeat the model's reasoning. It does not\ncarry hidden reasoning forward, raise output limits or bypass route, context,\nturn, cancellation or tool-admission controls. Each request can incur provider\ncost. Existing output-limit overrides still apply, and request preparation may\nreduce or refuse an allowance when input grows. Restart existing engines after\nchanging these settings.\n\nThe nudge appears in one outbound request without joining conversation history\nor later resumed model context. Retry status retains its stable protocol prefix\nand localizes its human text. Its receipt is visible in the transcript; Fleet\nalso stores an owner-bound prepared-request receipt in its existing private\nartifact before dispatch. A prepared receipt does not claim a completed model\ncall. Existing version-1 artifacts remain readable.\n\nTo customize or disable the nudge:\n\n```toml\n[reasoning_only]\nreprompt_message = \"Give your answer, or make the next tool call.\"\n```\n\nAn empty `reprompt_message = \"\"` disables the nudge, including on output-limit\nretries; the retry still counts against `max_reprompts` and uses the unchanged\nrequest. Configuration with recovery enabled does not guarantee the model will\ncomplete a long proof within its allowance.\n\n### Notifications\n\nNotification controls are available in the existing `/config` settings view,\nfrom terminal commands, and through the CLI. All write the same `config.toml`\nkeys. Terminal changes apply immediately; add `--save` to keep them. CLI writes\napply when the next process loads its configuration.\n\n```sh\ncodewhale config set notifications.sound whale\ncodewhale config set notifications.events.approval-needed false\ncodewhale config set notifications.quiet true\ncodewhale config get notifications\ncodewhale config unset notifications.quiet\n```\n\n```text\n/config notifications sound whale --save\n/config notifications condition unfocused --save\n/config notifications quiet true\n/config notifications status\n```\n\nNested CLI edits preserve TOML types, unrelated keys and comments. Unset removes\nonly the selected leaf. `notifications.sound legacy` in the TUI, or CLI unset of\n`notifications.sound`, restores previous sound choices. Invalid values are\nrejected before file or session changes. In an active TUI profile, saved edits\nupdate its notification table when it owns one; otherwise they update the\ninherited root table. The settings detail keeps saved and current values distinct.\n\n```toml\n[notifications]\nmethod = \"auto\" # auto | osc9 | kitty | ghostty | bel | off\ncondition = \"unfocused\" # unfocused | always | never\nthreshold_secs = 30\ninclude_summary = false\nsound = \"whale\" # optional; sound is opt-in, not enabled by default\nquiet = false\n\n[notifications.events]\nturn-complete = true\nsubagent-terminal = true\napproval-needed = true\ninput-needed = true\nelevation-needed = true\nmodel-notify = true\n```\n\n`quiet = true` mutes every category without changing saved choices. `method =\n\"off\"` also stops both banner and selected audio. Disabling a category stops its\nsound. Attention and duration gates apply before any sink runs. Title animation\ncompletion is silent; only the authorized notification event can request audio.\nSuccessful turn completion is a notification category; failed/cancelled turns do\nnot create a success notification.\n\n`auto` chooses a recognized terminal protocol or the existing macOS native\nfallback; unknown terminals remain unsupported and never invent a bell. `bel`\nis an audio-only transport: one selected cue is dispatched, without a second\ntransport bell. With `sound = \"off\"`, that transport is silent. `osc9`, `kitty`\nand `ghostty` use their terminal notification protocols; tmux passthrough is\npreserved. Terminal/OS notification preferences still govern display, attribution\nand any sound the host itself adds.\n\nBy default the terminal must stay unfocused for two seconds. `condition =\n\"always\"` allows foreground notifications and bypasses the duration threshold;\n`\"never\"` suppresses all delivery. The canonical condition takes precedence over\nlegacy `[tui].notification_condition`.\n\nThe bundled `whale` is a 1.55-second original whale-inspired cue, with no\nthird-party recording. It remains an opt-in candidate pending listening approval.\nWAV playback uses a background worker: macOS `/usr/bin/afplay`, Linux `aplay`\nfrom [ALSA utilities](https://github.com/alsa-project/alsa-utils), or Windows\n`PlaySoundW`. A missing player/file or unsupported platform has no fallback bell.\nOnly one WAV plays at a time. A worker-start receipt is a dispatch attempt, not\nproof of audible playback or OS acceptance. The existing macOS `osascript`\nbanner retains Script Editor attribution; this Core change does not provide a\nbranded native Apps banner.\n\n#### Previous sound settings\n\nWhen `notifications.sound` is absent, completion uses a non-off\n`completion_sound` selection, and other events use the existing event allow-list.\nThese are compatibility inputs to the same audio decision, not separate playback\npaths. When the global sound is selected, it takes precedence over the previous\ncompletion/event choices. The default remains silent unless a legacy sound or\nexplicit `bel` transport was already selected.\n\n```toml\n[notifications.event_sound]\nenabled = false\nevents = [\"turn-complete\", \"approval-needed\"]\nmin_interval_ms = 2000\nquiet = false\n```\n\nLegacy event cues use one bell for completion, subagent completion, input and\nmodel notices; approval/elevation cues use two. The per-category repeat interval\nsurvives settings refreshes. Old unknown event names are ignored on load; new\nCLI/TUI edits require names from the six categories above.\n\nLocal approval/input/elevation prompts and error receipts remain available when\nexternal notifications are muted. Action prompts use the selected UI language\nand retire when that request settles. Repeated live notices keep their first\nexpiry; a later routine update does not hide an unresolved warning at completion.\nOptional plugin suggestion toasts and contextual tips share one session guidance\nbudget. `/config contextual_tips off` hides those toasts while preserving\nrequired notices and explicit plugin review requests.\n\n#### What a notification can contain\n\nA desktop notification is a glance surface: on macOS it can appear on the\nlock screen, and on every platform it is visible to anyone near the machine.\nCodewhale therefore builds notifications from a typed payload with a fixed\nper-event disclosure policy rather than from whatever text was on hand:\n\n| Event | Shown | Never shown |\n|---|---|---|\n| Turn complete | status line (+ elapsed/cost when `include_summary`), preview of the assistant's reply | — |\n| Sub-agent finished | status line, agent id, preview of the child's summary line | — |\n| Approval needed | the tool name | the tool description, the command, the arguments |\n| Input needed | \"Answer the question in the terminal to continue\" | the question |\n| Sandbox elevation needed | the tool name and the denial reason | the command |\n| `notify` tool | model-supplied title and body | — |\n\nEvery field is capped (80 characters for the status line, 120 for the\nidentifier, 200 for the preview), stripped of control bytes and escape\nsequences, and passed through a redactor that replaces credential-shaped\nstrings with `[redacted]`, reduces absolute local paths to `…/basename`,\nand replaces raw tool JSON with `[details hidden]`. The redactor is\ndeliberately over-eager: an unbroken 40-character run has no word\nstructure, so it is redacted even when it is not a secret.\n\n#### macOS: why the banner says \"Script Editor\"\n\nOn macOS terminals that provide no notification escape of their own —\nApple Terminal, the VS Code and JetBrains embedded terminals, plain tmux\nwithout `LC_TERMINAL` — `method = \"auto\"` falls back to `osascript`'s\n`display notification`. That command posts on behalf of the *bundled*\nhost process, and `/usr/bin/osascript` is unbundled, so macOS attributes\nthe banner to `com.apple.ScriptEditor2`. That attribution supplies the\nScript Editor icon and owns the System Settings → Notifications entry\n(alert style, previews, Do Not Disturb). `display notification` has no\nicon parameter, so this cannot be fixed from the notification code; it\nneeds Codewhale to ship a real `.app` bundle. Tracked in\n[#4834](https://github.com/codewhale-hq/CodeWhale/issues/4834). In the meantime,\niTerm2, WezTerm, Ghostty, and kitty are matched first and use their own\nnotification protocols, and `method = \"osc9\"` / `\"bel\"` / `\"off\"` opt out\nof the `osascript` path explicitly.\n\n## Automations in the terminal\n\nOpen `/automation`, then choose **New automation** (`n`) or **Edit automation**\n(`e`). The form edits the name, multiline prompt, schedule, model, workspace,\nand enabled status. `Tab` moves between fields; `Enter` inserts a newline in\nthe prompt. Schedule presets include daily, weekly, hourly, once, and a custom\nRRULE. Time fields accept `HH:MM`; their arrow controls change the time by\n15 minutes. Weekly day buttons and model search support mouse and keyboard.\n\nThe next-run preview uses the scheduler's local time zone, shown with its UTC\noffset. An enabled automation can run after **Save** (`Ctrl+S`); a paused one\nhas no scheduled next run. **Cancel** (`Esc`) discards the draft. Editing keeps\nexisting permission settings, custom schedules, and additional workspace\nentries unless the corresponding supported field is explicitly changed.\n\nSchedules are evaluated in the machine's local time zone against the wall\nclock. A wall time that does not exist on a spring-forward day is skipped and\nan ambiguous fall-back time fires once. Occurrences missed while Codewhale was\nclosed, asleep, or still running the previous occurrence are coalesced: the\nnext start runs one catch-up occurrence and then continues from the next\nfuture slot, never replaying every missed slot. An occurrence never starts\nwhile an earlier run of the same automation is still queued or running. Each\nrun is recorded durably with its status, timing and error; a run that needs a\ntool approval has no operator to ask and fails once the approval wait expires.\n\nChoosing a concrete model pins both the model and its exact configured\nprovider, including named custom routes. Later changes to the active provider\ndo not move that automation's pin. The default-model choice and legacy\ndefinitions without a provider pin keep the runtime's existing default\nbehavior. New automation records use schema v2 and tasks use v3 so older\nruntimes reject records whose provider pins they cannot preserve.\n\n## Lifecycle Outbox (`[lifecycle_outbox]`)\n\nThe lifecycle outbox is an opt-in, machine-readable stream of session,\nturn, and sub-agent lifecycle events. With a path configured, Codewhale\nappends one JSON line per event to that file — for interactive TUI\nsessions *and* headless `codewhale exec` runs — so a supervisor\n(terminal multiplexer wrapper, automation harness, alerting setup) can\nreact to what happened without scraping the screen or installing per-hook\nshell commands. Unset or empty `path` = the feature is **off** and\nbehavior is unchanged.\n\n```toml\n[lifecycle_outbox]\npath = \"~/.codewhale/notifications/outbox.jsonl\" # unset/empty = OFF\nwebhook_url = \"\" # optional; POSTs events as JSON when set\nwebhook_token = \"\" # optional bearer token for webhook_url\n```\n\n### Events emitted\n\n| Event | Kind | Fired at |\n|---|---|---|\n| `turn_start` | `turn.started` | a new turn begins (TUI TurnStarted; `exec` at message dispatch) |\n| `turn_end` | `turn.completed` / `turn.failed` / `turn.interrupted` | turn completion, kind projected from the turn status |\n| `turn_stalled` | `turn.stalled` | the stall watchdog recovers a wedged turn |\n| `subagent_spawn` | `subagent.spawned` | a sub-agent is spawned |\n| `subagent_complete` | `subagent.completed` | a sub-agent reaches a terminal state |\n| `session_start` | `session.started` | interactive session start |\n| `session_end` | `session.ended` | interactive session end |\n\n### File contract\n\nEach line is a `RuntimeEventEnvelope`:\n\n```json\n{\"schema_version\": 1, \"seq\": 3, \"event\": \"turn_start\", \"kind\": \"turn.started\",\n \"thread_id\": \"…\", \"turn_id\": \"…\", \"item_id\": null, \"timestamp\": \"…\",\n \"created_at\": \"…\", \"payload\": {…}}\n```\n\n- `seq` is monotonic per outbox file and recovers from the last written\n line when a new process opens the file.\n- Lines are written one complete JSON line per append, serialized by an\n internal writer task and flushed before the next event; concurrent\n sessions writing the same path do not interleave bytes mid-line, but\n separate processes each continue from their own recovered `seq`, so seqs\n can repeat across processes sharing one file — prefer one file per\n process for strict uniqueness.\n- Parent directories are created lazily on the first event.\n- Payloads are constructed from bounded, pre-redacted fields only — never\n raw tool arguments, environment, or full transcript text. Free-form\n fields (error messages, previews) are capped at the notification limits\n (80 headline / 120 detail / 200 preview characters) and stripped of\n control bytes.\n\n### Webhook delivery\n\nWith `webhook_url` set, every event is additionally POSTed as\n`{\"at\": \"<ISO 8601 timestamp>\", \"event\": {…}}` with\n`Authorization: Bearer <webhook_token>` when a token is configured.\nDelivery is best-effort: failures are logged and dropped, never retried\ninto the agent loop, and a failing webhook never blocks the local file\nappend.\n\n## Control Socket (`[control_socket]`)\n\nPer-session control surface for supervised operation: with the feature\nenabled, the interactive TUI binds one unix domain socket per *running*\nsession at `<sessions-dir>/<session-id>/control.sock` (mode `0600`;\n`<sessions-dir>` is the same directory the session store uses, typically\n`~/.codewhale/sessions`). The socket is removed with the session's\nartifact directory, and a stale socket left by a crashed process is taken\nover by the next launch. Unset or `enabled = false` = the feature is\n**off** (the default) and behavior is unchanged. Unix-only; on other\nplatforms the key parses but no socket is bound.\n\n```toml\n[control_socket]\nenabled = false # default: OFF\n```\n\nThe socket speaks newline-framed JSON-RPC, one request per connection:\nwrite one request line, read one response line, close.\n\n```json\n{\"id\":\"1\",\"method\":\"message\",\"params\":{\"text\":\"hello\"}}\n{\"id\":\"2\",\"method\":\"interrupt\",\"params\":{}}\n{\"id\":\"3\",\"method\":\"status\",\"params\":{}}\n```\n\n- `message` — delivers `text` as a structured user message through the\n ordinary composer dispatch path; dispatched immediately when idle,\n queued when a turn is in flight (the response's `delivery` field says\n which).\n- `interrupt` — the Esc-shaped cancel of the active turn; `cancelled`\n reports whether active work was in flight.\n- `status` — answers `turn_state` (`idle` / `in_progress` / `waiting`) and\n `goal` (`objective`, `status`, `paused`).\n\nSuccess responses echo the request id with a `type`-tagged result;\nfailures carry `error.code` (`invalid_request`, `command_error`,\n`timeout`, `server_unavailable`). Requests are bounded at 1 MiB per line\nand a handler that does not answer within 5 s is reported as `timeout`.\n\n## Tool Catalog\n\nCodewhale loads a small core native tool catalog by default and leaves less\ncommon native tools discoverable through ToolSearch. To keep specific native\ntools loaded on every request, add them to `[tools].always_load`:\n\n```toml\n[tools]\nalways_load = [\"Git\", \"notify\"]\n```\n\n### Script tools and overrides\n\nScripts in `~/.codewhale/tools/` (or `[tools].plugin_dir`) that start with a\n`# name:` header become model-visible tools, and `/plugin tools` lists them.\nThe script reads the tool's JSON input on stdin and writes a JSON\n`ToolResult` (`{\"content\": \"...\", \"success\": true}`) on stdout.\n\n```sh\n#!/usr/bin/env sh\n# name: word_count\n# description: Count words in the given text\n# schema: {\"type\":\"object\",\"properties\":{\"text\":{\"type\":\"string\"}}}\n# approval: required\n```\n\n`# approval:` takes `suggest` (the default) or `required`; either way the\ntool follows the session's approval setting. A script cannot approve itself:\n`approval: auto` is no longer supported, so such a script gets the default,\nand the runtime log (`~/.codewhale/logs/`) and `/plugin tools` name it.\n\nA script cannot replace a built-in tool either. A script whose `# name:` is\nalready registered is not loaded. `[tools.overrides]` may disable a built-in,\nor add a script or command tool under a name of its own:\n\n```toml\n[tools.overrides]\n\"Web\" = { type = \"disabled\" } # turn a built-in off\n\"audited_shell\" = { type = \"script\", path = \"audit-shell.sh\" } # a new tool\n\"Bash\" = { type = \"script\", path = \"audit-shell.sh\" } # refused: Bash is built in\n```\n\nA `script` or `command` override keyed by a built-in is refused, and the\nbuilt-in stays active. A status line names the key once per session, and the\nruntime log records it. To route a\nbuilt-in through your own wrapper, disable the built-in and register the\nwrapper under a new name. An override keyed by a drop-in script's name still\nreplaces that script. Relative `path` values resolve against the plugin\ndirectory.\n\n### `request_user_input` limits\n\n`request_user_input` asks the user a short batch of multiple-choice questions.\nBoth ceilings are configurable (#5949): raise `user_input_max_questions` when a\nresearch or planning workflow legitimately needs more clarifications, lower it\nwhen interactive triage should stay terse.\n\n```toml\n[tools]\nuser_input_max_questions = 6 # default 6, clamped to 1..=10\nuser_input_max_options = 4 # default 4, clamped to 2..=10\n```\n\nThe effective values are applied in three places at once: the tool's JSON\nschema (`minItems` / `maxItems`), its model-visible description, and the\npayload validator. A rejected payload names the key to raise, so the model can\neither resize the batch or tell the user which setting to change.\n\n### User-input wait timeout\n\nQuestions from `request_user_input` wait until answered or canceled by default\n(#6003). An omitted setting or `0` leaves the wait unbounded; a positive value\ncancels the question when that many seconds pass, capped at 86,400 (24 hours).\nHeadless `exec` runs have no\nresponder, so `request_user_input` is withheld there by default:\nthe model reports the tool absent and finishes instead of stalling.\n\n```toml\n[tools]\nuser_input_timeout_seconds = 300 # opt into 5 minutes; omitted or 0 waits indefinitely; maximum 86400\n```\n\nThis key governs question waits only. Approvals have their own clock,\n`[approval] timeout_seconds`, which waits indefinitely unless you set it.\nWall-clock and stream protections elsewhere are unaffected.\n\n## Feature Flags\n\nFeature flags live under the `[features]` table and are merged across profiles.\nDefaults are enabled for built-in tooling, so you only need to set entries you\nwant to force on or off.\n\n```toml\n[features]\nshell_tool = true\nsubagents = true\nweb_search = true # enables deferred Web; the flag name is retained for config compatibility\napply_patch = true\nmcp = true\nexec_policy = true\ncode_mode = true # execute_tools composes MCP/plugin/native calls; false defers it behind tool_search\nverify_tool = true # agent-callable `verify` self-critique; false removes it from the tool catalog\nvision_model = false # true routes image analysis to the [vision_model] model (see above)\nextension_host = false # experimental: run reviewed plugins' native code (see EXTENSIONS.md)\n```\n\n`code_mode` is on by default: `execute_tools` is advertised from the first\nrequest and nested calls go through the same permission gate as direct calls\n(see [Tool surface](TOOL_SURFACE.md#code-mode-execute_tools)). Set\n`code_mode = false` to defer it behind `tool_search` again.\n\n`extension_host` is experimental and off by default. Turning it on lets reviewed\nplugins run their `native` TypeScript/JavaScript tool and slash-command code in\na separate Node (or, opt-in, Bun) process; toggling it in either direction\nchanges the plugin activation policy, so every plugin is reviewed again after a\nrestart. Its `[extension_host]` table (`runtime`, `node`, `bun`) and what the\nhost's sandbox does on each platform are in\n[Writing an extension tool](EXTENSIONS.md).\n\n`[extension_host] mcp_backend` (user config only) chooses the MCP client\nindependently of the `extension_host` flag: `\"host\"` (default) uses the official\nMCP SDK behind the Rust broker; `\"rust\"` keeps the previous Rust client for one\nrelease. A host refusal never falls back to Rust. See\n[MCP connection lifecycle](MCP.md#connection-lifecycle).\n\nEvery flag has a row in [`docs/features.toml`](features.toml), the feature\nregistry; a test fails when a flag and its row disagree.\n\nYou can also override features for a single run:\n\n- `codewhale --enable web_search`\n- `codewhale --disable subagents`\n\nUse `codewhale features list` to inspect known flags and their effective state.\nThe native `/config` view also includes a read-only **Experimental** section\nfor experimental feature flags. It shows each flag's effective enabled/disabled\nstate and whether that state comes from the default or a configured override.\nChange feature flags in `[features]` or with `--enable` / `--disable`; the\n`/config` section is an audit surface, not a stability promise. Goal and\nWorkflow preview rows may appear there as reserved entries until those workflows\ngraduate behind real gated flags.\n\n## Web Search Provider\n\n`web_search` uses keyless Firecrawl by default. Runtime failure or an exhausted\nkeyless quota degrades visibly through DuckDuckGo and Bing. China deployments\ncan explicitly select Baidu, Metaso, Volcengine, or a trusted SearXNG endpoint;\nCodewhale does not guess geography from locale or model provider.\n\nConfigured API providers are attempted first. Runtime failure or an empty\nresult visibly degrades through DuckDuckGo and then Bing; the structured search\nreceipt records every hop. Missing configuration and network-policy denials\nfail closed without sending the query to another provider.\n\n**Provider-native search.** On routes whose provider offers its own web-search\ntool (OpenAI, xAI, Anthropic, DeepSeek, Kimi and others), that search can run\nahead of the configured provider. It is a separate model call on the active\nroute. `[search] native` decides the order:\n\n- unset (default): native search leads only when no search provider is\n configured; a provider chosen in `[search] provider`,\n `CODEWHALE_SEARCH_PROVIDER`, or a Tavily key wins;\n- `native = true`: native search leads even when a provider is pinned;\n- `native = false`: native search is never used.\n\nNative citations are link-checked before they are returned. Each cited URL is\nrequested through the same guarded fetch as `Web` fetch (SSRF, DNS and redirect\nchecks, `[network]` policy): a `HEAD`, then a one-byte ranged `GET` when `HEAD`\nis refused, at most four at a time and five seconds each. The request uses the\naddress as it will appear in `results`, without userinfo, fragment or\ncredential-like query parameters; an address whose query string repeats the\nsearch query is not requested and is removed. Only sources that\nanswer with a 2xx status stay in `results`; the receipt lists each removed one\nas `citation_unverified` with its reason. When no citation passes, or when the\nquery asks for `recency` (no native search tool can enforce a time window), the\nsearch falls through to the configured provider. A citation whose host the\n`[network]` policy denies is removed. A citation whose host the policy would\nask you about is not fetched: it stays in `results` and the receipt records it\nas `citation_unchecked` until you allow the host.\n\nThe provider's prose answer is returned whole in a separate\n`unverified_synthesis` field: it is the provider model's summary, and neither\nits claims nor its links are checked. Oversized tool output spills to a session\nartifact the model can page back.\n\n**Recency and locale.** `recency` and `locale` are forwarded where the\nbackend's API takes them, and the search receipt reports each as honored or\nignored:\n\n| Backend | Recency | Locale |\n| --- | --- | --- |\n| Firecrawl | `tbs=qdr:d/w/m/y` | `country` from the region (`de-DE` → `DE`); a bare language is ignored |\n| Tavily | `time_range` | not sent (Tavily takes country names) |\n| SearXNG | `time_range` | `language`, as given |\n| Serply | not sent (undocumented) | `hl` language, `gl` country |\n| DeepSeek | not sent | not sent (`localization` takes an IP address) |\n| Provider-native | declined; the search falls through to the configured provider | not sent |\n\nRecency is rounded up to the backend's nearest window (day, week, month,\nyear), so `recency = 10` searches the last month. Other backends ignore both\nknobs and say so in the receipt.\n\nFor a private/internal search service that serves DuckDuckGo-compatible HTML,\nkeep `provider = \"duckduckgo\"` and set `base_url`; Codewhale appends the `q`\nquery parameter to that endpoint and applies network policy to its host.\nCustom endpoints do not fall back to public Bing. `CODEWHALE_SEARCH_BASE_URL`\ncan override this per process; `DEEPSEEK_SEARCH_BASE_URL` remains accepted as\nthe legacy alias.\n\n**SearXNG** ([docs](https://docs.searxng.org/dev/search_api.html)) uses the\nconfigured instance's JSON API. Set `provider = \"searxng\"` and\n`base_url = \"https://your-searxng.example\"`; Codewhale calls\n`/search?q=...&format=json`. Codewhale does not use a public SearXNG instance\nby default because public instances often disable JSON output or rate-limit API\ntraffic.\n\nSelf-host it as a separate process (Docker is fine); Codewhale never bundles or\nmanages the search engine itself:\n\n- Enable JSON on the instance (`settings.yml`, `search.formats` must include\n `json`) and restart it. An HTML-only instance answers the API with HTTP 403;\n Codewhale reports that as a JSON/API-access problem on the SearXNG hop rather\n than silently returning no results.\n- Bind it to loopback, or to a host and port your network policy allows. The\n instance keeps its own engine list, limiter, and limits.\n- Point Codewhale at it with `[search] provider = \"searxng\"` and `base_url`\n (required; either the root URL or the `/search` endpoint). No instance ships\n as a default, and none is discovered automatically.\n- `codewhale doctor --probe-search` sends a transport-only `HEAD` to that\n origin — no `q=`, no credentials, no redirects, no audit receipt — so a green\n probe proves reachability and network-policy admission, not that JSON is on.\n\nConfirm the JSON API itself before assuming a Codewhale bug:\n\n```sh\ncurl -sS \"$BASE/search?q=codewhale&format=json\" | jq '.results[0] | {title,url,score}'\n```\n\nCodewhale ranks the returned rows by `score`, highest first, and applies\n`max_results` to that ranking; rows an instance reports without a usable score\nkeep their original relative order.\n\n**Metaso** ([metaso.cn](https://metaso.cn)) requires a user-supplied key. Set\n`METASO_API_KEY` or `[search] api_key`; Codewhale does not ship a shared key.\n\n**Firecrawl** ([docs](https://docs.firecrawl.dev/sdks/cli)) searches Firecrawl\nCloud without a key using its bounded per-IP daily quota. Set\n`FIRECRAWL_API_KEY` or `[search] api_key` for authenticated limits. Codewhale\nsends no `Authorization` header in keyless mode.\n\n**Baidu** uses Baidu AI Search at\n`https://qianfan.baidubce.com/v2/ai_search/web_search`. Set\n`BAIDU_SEARCH_API_KEY` or `[search] api_key`. This is a search-tool backend\nonly; it does not add a Baidu model provider.\n\n**Sofya** ([sofya.co](https://sofya.co)) returns full extracted page content\nrather than snippets. Set `[search] api_key` to your `ay_live_...` key, or the\n`SOFYA_API_KEY` env var. This is a search-tool backend only; it does not add a\nSofya model provider.\n\n**Serply** ([serply.io](https://serply.io)) returns Google organic results with\ntitle, URL, and snippet. Set `[search] api_key` to your Serply key, or the\n`SERPLY_API_KEY` env var. This is a search-tool backend only; it does not add a\nSerply model provider.\n\n**DeepSeek** ([docs](https://api-docs.deepseek.com/guides/search)) uses\n`POST https://api.deepseek.com/v1/tools/search`. Set `provider = \"deepseek\"`\n(`deep-seek` and `deepseek-search` are also accepted) and supply `[search] api_key`\nor the `DEEPSEEK_API_KEY` env var, in that order. The saved DeepSeek model-provider\nkey is not reused by this search backend. Queries are truncated to 128 Unicode\ncharacters. The backend clamps counts to 1–50; the shared search tool currently\ncaps requests at 10 results. Query operators such as `site:`, `lang:`,\n`after:`, and `before:` are passed through; the `recency` and `locale` knobs are\nignored. Results include title, URL, and snippet, falling back to summary when a\nsnippet is empty.\n\n**Tavily** ([tavily.com](https://tavily.com)) is selected automatically when a\nTavily key is present and no provider is pinned: `TAVILY_API_KEY` set, or\n`[search] api_key` / `CODEWHALE_SEARCH_API_KEY` in the `tvly-` family. Doctor\nreports that as `source: tavily key`. Autodetect is runtime-only — Codewhale\nnever writes `[search] provider` for it, and `TAVILY_API_KEY` is never merged\ninto `[search] api_key`. An explicit `[search] provider` or\n`CODEWHALE_SEARCH_PROVIDER` always wins, so `provider = \"firecrawl\"` keeps\nFirecrawl even with a Tavily key in the environment. Pinned `tavily` accepts\nany non-empty `[search] api_key` and is configured by that key or\n`TAVILY_API_KEY`; with both empty it fails closed.\n\n```toml\n[search]\nprovider = \"firecrawl\" # also duckduckgo | bing | tavily | bocha | metaso | searxng | baidu | volcengine | sofya | serply | deepseek\n# base_url = \"https://search.example/\" # optional with provider = \"duckduckgo\"; required with \"searxng\"\n# api_key = \"YOUR_KEY\" # optional for firecrawl; required by the other API providers\n# native = false # provider-native search: unset = only when no provider is configured\n```\n\n## Local Media Attachments\n\nUse `@path/to/file` in the composer to add local text file or directory context\nto the next message. Use `/attach <path>` for local image/video media paths, or\n`Ctrl+V` to attach an image from a local clipboard or an explicitly forwarded\nX11/Wayland clipboard. SSH terminal paste without a forwarded graphical display\nis text-only; use the local terminal's paste command (`Cmd+V` on macOS or\n`Ctrl+Shift+V` on Linux/Windows), and use `/attach <path>` for remote image\nfiles. OpenSSH loopback X11 displays are detected automatically. For an\nexplicitly forwarded Wayland or non-loopback X11 display, set\n`CODEWHALE_SSH_CLIPBOARD=graphical`; set it to `terminal` to force terminal\ntransfer instead of an ambient remote display. DeepSeek's public Chat\nCompletions API currently accepts text message\ncontent, so media attachments are sent as explicit local path references instead\nof native image/video payloads.\nAttachment rows appear above the composer before submit; move to the start of\nthe composer, press `↑` to select an attachment row, then press `Backspace` or\n`Delete` to remove it without editing the sample text by hand.\n\n## Managed Configuration and Requirements\n\ncodewhale supports a policy layering model:\n\n1. user config + profile + env overrides\n2. managed config (if present)\n3. requirements validation (if present)\n\nBy default on Unix:\n- managed config: `/etc/deepseek/managed_config.toml`\n- requirements: `/etc/deepseek/requirements.toml`\n\nRequirements file shape:\n\n```toml\nallowed_approval_policies = [\"on-request\", \"untrusted\", \"never\"]\nallowed_sandbox_modes = [\"read-only\", \"workspace-write\"]\n```\n\nIf configured values violate requirements, startup fails with a descriptive error.\n\n## Notes On `codewhale doctor`\n\n`codewhale doctor` follows the same config resolution rules as the rest of the\nTUI. That means `--config`, `CODEWHALE_CONFIG_PATH`, and the legacy\n`DEEPSEEK_CONFIG_PATH` are respected, and MCP/skills\nchecks use the resolved `mcp_config_path` / `skills_dir` (including env overrides).\n\nTo bootstrap missing MCP/skills paths, run `codewhale setup --all`. You can\nalso run `codewhale setup --skills --local` to create the owned project\n`.codewhale/skills` directory. Project skills load after workspace trust.\n\nBoth plain `codewhale doctor` and `codewhale doctor --json` are structural and\noffline by default. They do not check the release service, hosted provider\nAPIs, local provider endpoints, or MCP processes, and they do not load a\nworkspace credential `.env`. Use `--check-updates`,\n`--probe-api`, `--probe-local`, or `--probe-mcp` to opt into the corresponding\nlive boundary; `--probe-local` may start a desktop-managed service such as\nOllama. Only the explicit API/local probe paths may load workspace credential\n`.env` values. Live flags conflict with `--json`, so machine-readable doctor\noutput is always offline. Top-level keys include `version`, `paths`, `secret_backend`,\n`config_path`, `config_present`, `workspace`, `api_key.source`,\n`api_key.availability`, `base_url`,\n`default_text_model`, `mcp`, `skills`, `tools`, `plugins`, `sandbox`,\n`platform`, `api_connectivity`, and `capability`. CI consumers should rely on\n`api_key.source` (`config_declared`/`env_declared`/`external_auth_declared`/\n`secret_store_unprobed`/`secret_store_unavailable`/`oauth_unprobed`/\n`external_consent`/`none`/`local_runtime`/`unknown`) and\n`api_key.availability`\n(`present`/`not_required`/`not_probed`/`unavailable`/`unknown`) rather than parsing the\nhuman-readable `doctor` text. Source is declaration metadata, not proof that a\ncredential exists or works. Only a non-empty, non-sentinel literal config value\nis structurally `present`; no-auth and local routes are `not_required`. Environment,\nexternal-auth, OAuth, consent, and secret-store declarations remain `not_probed`\nand cannot make structural Setup or Fleet readiness true. A secret-store sentinel\non a named/custom endpoint that is prohibited from using the shared store is\n`secret_store_unavailable`/`unavailable`, while `unknown` remains reserved for\nthe absence of a supported structural conclusion. Exact and whitespace-wrapped\nlegacy sentinels are never treated as literal credentials. The structural\nloader still honors safe environment routing/model/policy fields, but it never\nmaterializes environment HTTP headers, sandbox API keys, or search API keys;\nonly an explicit API/local probe switches to the normal credential-loading\npath. An opted-in update check also emits only typed generic failures: untrusted\nrelease metadata and transport errors are not echoed.\n\nIf configuration loading or validation fails, `doctor --json` returns nonzero\nand prints a bounded JSON error envelope with\n`status = \"error\"` and `error.kind = \"config_validation\"`. It does not emit a\nnormal route or capability report—or the underlying possibly sensitive error—\nfor an invalid configuration.\n\nMCP entries are configuration diagnostics unless an explicit MCP command is\nrun. `mcp.probe_scope` is `configuration`, `mcp.live_health_checked` is false,\nand each server separates `checks.configuration` / `checks.command` from\n`checks.process_reachable`, `checks.protocol_initialized`, and\n`checks.backend_tool_health`. The latter three remain `not_checked` in doctor\noutput. Run `codewhale mcp validate` to explicitly start enabled servers and\nverify protocol initialization/discovery; backend health still requires an\nappropriate explicit tool call. Doctor reports only safe structural MCP fields:\nURL userinfo/path/query/fragment and raw command arguments, environment values,\nheader values, and token material are omitted. Provider URLs follow the same\nrule and expose only `scheme://host[:explicit-port]`.\n\nThe `capability` key contains per-provider capability info derived from\nstatic knowledge (release docs, API guides) rather than live API probes.\nTop-level sub-keys: `resolved_provider`, `resolved_model`, `context_window`,\n`max_output`, `thinking_supported`, `cache_telemetry_supported`,\nand `request_payload_mode`.\n\nUse `capability.context_window` and `capability.max_output` for model-limit\nchecks in CI scripts; do not treat `capability.max_output` as the per-turn\nrequest budget. Use `capability.thinking_supported` to decide whether to\nconfigure reasoning effort.\n\n## Setup status, clean, and extension dirs\n\n`codewhale setup` accepts a few flags beyond the existing `--mcp`,\n`--skills`, `--local`, `--all`, and `--force`:\n\n- `--status` — print a compact one-screen status (api key, base URL, model,\n MCP/skills/tools/plugins counts, sandbox, `.env` presence). Read-only and\n network-free; safe to run in CI. If `.env` is missing and `.env.example` is\n present in the workspace, the status output points at `cp .env.example .env`.\n- `--tools` — scaffold `~/.codewhale/tools/` with a `README.md` describing the\n self-describing frontmatter convention (`# name:` / `# description:` /\n `# usage:`) and an `example.sh` that follows it. The directory is\n intentionally not auto-loaded; wire individual scripts into the agent via\n MCP, hooks, or skills.\n- `--plugins` — scaffold `~/.codewhale/plugins/` with a `README.md` and an\n `example/plugin.toml` plus a namespaced example Skill. Bundles are discovered\n read-only, untrusted, and disabled; review them through `/plugin` before\n enabling. v0.9.1 activates only declared Skills and MCP servers. See\n [PLUGIN_BUNDLES.md](PLUGIN_BUNDLES.md).\n- `--all` now scaffolds MCP + skills + tools + plugins together.\n- `--clean` — list `~/.codewhale/sessions/checkpoints/latest.json` and\n `offline_queue.json` if they exist. Legacy\n `~/.deepseek/sessions/checkpoints/` files are not scanned automatically; set\n `CODEWHALE_HOME=~/.deepseek` for a one-off legacy cleanup. Pass `--force` to\n actually remove matched files. This never touches real session history or the\n task queue.\n\n`--status` and `--clean` are mutually exclusive with the scaffold flags.\n\n## Why the engine strips XML/`[TOOL_CALL]` text\n\ncodewhale sends and receives tool calls only over the API tool channel\n(structured `tool_use` / `tool_call` items). The streaming loop in\n`crates/engine/src/core/engine.rs` recognizes a fixed set of fake-wrapper start\nmarkers — `[TOOL_CALL]`, `<codewhale:tool_call`, `<tool_call`, `<invoke `,\n`<function_calls>` — and scrubs them from visible assistant text without ever\nturning them into structured tool calls. When a wrapper is stripped, the loop\nemits one compact `status` notice per turn so the user can see why their\nvisible text shrank. Treat any change that re-enables text-based tool\nexecution as a regression; the protocol-recovery tests in\n`crates/tui/tests/integration/protocol_recovery.rs` lock the contract.\n\n## Model-bound redaction (`[redaction] model_bound`)\n\nCodewhale masks credential-looking values in tool output **before it is sent\nto an upstream model** — the \"model boundary\". A file read by a tool can\ncontain a configured API key, a bare provider token, or a credential-shaped\nopaque string, and the model must not see those bytes. This backstop is\nseparate from the display/export scrubbers: it decides what the model itself\ncan quote back, and it is deliberately conservative (`CredentialShaped`\npolicy, see `crates/config/src/persistence.rs`), so ordinary code and config\nstay byte-exact while keys, JWTs, bearer tokens, PEM blocks, and long opaque\nruns are masked.\n\nTurning that masking **off** is a security decision, so it is not a plain\nboolean:\n\n```toml\n[redaction]\nmodel_bound = \"disabled\" # \"enabled\" (default) | \"disabled\"\n```\n\nSetting `\"disabled\"` only records a *request*. It takes effect only when all\nof these are true:\n\n1. You restart the interactive TUI.\n2. The startup gate appears and you press `1`/`Y` on its first stage\n (\"confirm and disable\"). This only advances to a second, final-confirmation\n stage - the gate repeats the red warning and asks \"are you really sure?\".\n3. On that second stage you press `1`/`Y` again. The gate is rendered with the\n same explicit-key discipline as workspace trust - `Enter` never confirms by\n reflex, and `2`/`U` on the second stage steps back.\n4. Only that second confirmation persists a receipt to\n `~/.codewhale/redaction-state.json` (next to `config.toml`) and rebuilds\n the engine with masking off for the rest of this launch and future ones.\n\nThe receipt is bound to the config it was made against and is valid only\nwhile that config still requests `\"disabled\"`. Setting `model_bound` back\nto `\"enabled\"` - or rewriting `config.toml` in any way after the\nconfirmation - invalidates it, so requesting `\"disabled\"` again later\nasks for a fresh confirmation on the next launch. The receipt checks both the\nconfig contents and modification time; missing, unreadable, or malformed config\nand older receipts without this binding keep masking enabled. Legacy-home\ninstalls store the receipt beside their resolved config file.\n\nUntil a confirmation exists, the effective mode is always `\"enabled\"`:\n\n- Choosing `2`/`U` (\"keep masking on\") leaves the config field untouched, so\n the next launch asks again. Edit the field back to `\"enabled\"` to stop being\n asked.\n- Non-interactive entry points (`codewhale exec`, hooks, automations, headless\n agents) never confirm anything and never apply an unconfirmed request.\n- Routing/classification summaries and durable goal-state text keep their own\n always-on redaction regardless of this switch; the opt-out exists so the\n model can quote file bytes for exact edits, not to relax stored state.\n\nThe config value itself is forgiving: `true`/`false`, `\"on\"`/`\"off\"`, and\n`\"enabled\"`/`\"disabled\"` (any casing) all parse, with `false`/`\"off\"` meaning\n`\"disabled\"`.\n\nA confirmed opt-out still sends your configured API keys to the provider you\nare already talking to. Only use it when the model must read and edit files\nthat contain real credentials.\n\n### Stored sessions\n\nThe masking runs once, when tool output enters the transcript, so saved\nsession files (`~/.codewhale/sessions/*.json` and their checkpoints) and\nRuntime API thread items (their text and tool metadata) do not store a\ncredential-shaped value from tool output. With the confirmed opt-out above,\ntool output is stored as the model saw it.\n\nNot yet masked: large outputs spilled to `~/.codewhale/tool_outputs`, shell\ncompletion evidence artifacts, and tool-call *inputs* (for example a\n`curl -H \"Authorization: Bearer …\"` command line). `doctor` and\n`scrub-secrets` do not check those either.\n\nSessions saved by builds before this change may still hold credentials in\ntheir tool output; nothing rewrites them automatically. `codewhale doctor`\nreports them under **Stored Sessions** (it checks the newest 50 files), and\nthis command finds and masks them across every saved session:\n\n```sh\ncodewhale sessions scrub-secrets # report only\ncodewhale sessions scrub-secrets --apply # rewrite the affected files\n```\n\n`--apply` rewrites each file under the same per-session lock a save takes,\nso it never loses a concurrent save. A session that is still open can write\nits in-memory copy back on its next save, so close open sessions first, and\nrotate any credential that was exposed — masking a stored copy cannot un-leak it.\n"
|
||
}
|
||
]
|
||
},
|
||
{
|
||
"id": "work",
|
||
"slug": "work",
|
||
"sources": [
|
||
{
|
||
"path": "docs/TOOL_SURFACE.md",
|
||
"sha256": "949991cc3ca1b4281507fade514ea5a6fd7dac66706ea1e2859a2ce28b5e30ac",
|
||
"content": "# Tool surface\n\n> 阅读简体中文版:[zh_hans/TOOL_SURFACE.md](zh_hans/TOOL_SURFACE.md)。\n\nThis document describes the current model-facing tool contract. The v0.9.1\ncutover that produced it is recorded in `docs/RUNTIME_SIMPLIFICATION_DESIGN.md`;\nread the workspace version from `Cargo.toml`, not from this line. The registry\nremains larger than the first-turn catalog so\nsaved transcripts can replay and uncommon capabilities can be loaded on demand.\nThe model should learn one canonical name for each common operation.\n\nImplementation sources:\n\n- `crates/engine/src/core/engine/tool_catalog.rs` owns the eager/deferred catalog.\n- `crates/engine/src/tools/registry.rs` registers canonical tools and hidden aliases.\n- `crates/engine/src/tools/{file,file_tool,shell}.rs` own the small foreground\n primitive behavior and schemas; the other native tools remain searchable.\n- `docs/RUNTIME_SIMPLIFICATION_DESIGN.md` records the v0.9.1 cutover and receipt.\n\n## Default-active contract\n\nNew turns start with sixteen eager native names plus synthetic `tool_search`:\n\n1. `read`\n2. `write`\n3. `edit`\n4. `file_search`\n5. `bash`\n6. `agent`\n7. `workflow`\n8. `todo_write`\n9. `create_goal`\n10. `get_goal`\n11. `update_goal`\n12. `task_shell_wait`\n13. `present`\n14. `load_skill`\n15. `context_archive_get`\n16. `context_archive_search`\n17. `tool_search` (synthetic, always active)\n\nThe sixteen native names are `DEFAULT_ACTIVE_NATIVE_TOOLS` in\n`crates/engine/src/core/engine/tool_catalog.rs`, pinned by\n`default_active_contract_keeps_discovery_and_core_tools_eager`. An authority\nboundary may remove `agent` at the maximum child depth, and Plan registers only\nthe foreground shell surface (so no `task_shell_wait`), but route size alone\nmust not change this core vocabulary.\n\nThe direct schemas deliberately stay small:\n\n| Tool | Input | Purpose |\n|---|---|---|\n| `read` | `path`, optional `offset`, optional `limit` | Read a bounded file window with explicit continuation or truncation notices. |\n| `write` | `path`, `content` | Create or replace a file. |\n| `edit` | `path`, `edits` | Apply one or more unambiguous text replacements against one original snapshot. |\n| `file_search` | `query`, optional `path`, `limit`, `extensions`, `exclude` | Find files by name, path fragment or glob and return bounded, ranked matches. Respects ignore files; `exclude` replaces default exclusions. |\n| `bash` | `command`, optional `timeout`, optional `description` | Run one cancellable foreground shell command and return a bounded tail. The optional description is display-only. |\n| `agent` | delegated task and optional scope/context controls | Start or inspect focused child work. |\n| `workflow` | plan/script/source_path plus run controls | Coordinate multi-agent phases with dependencies and completion checks. |\n| `todo_write` | complete replacement list of `{content, status}` items | Keep optional, agent-owned progress notes for genuinely multi-step work. |\n| `create_goal` | objective plus optional budget | Start the session goal the turn works toward. |\n| `get_goal` | none | Read the active goal and its progress. |\n| `update_goal` | terminal status | Mark the goal complete or blocked. |\n| `task_shell_wait` | `task_id`, optional `wait`, `timeout_ms`, `cancel`, `gate` | Poll or stop a background shell task. Completion still arrives as a runtime event, so use it for early output, true barriers, or gate evidence. |\n| `present` | `files`, optional `note` | Declare 1–32 existing workspace files as final deliverables with a read-only SHA-256 handoff receipt; nothing is copied or moved. |\n| `load_skill` | optional `name`, `query` | Load a named skill's `SKILL.md` body and companion-file list; omit `name` or use `name=\"list\"` to list eligible skills, or use `query` to search names and descriptions. |\n| `tool_search` | `query`, optional matching controls | Discover policy-allowed deferred tools and add selected schemas to this conversation's toolbox. |\n\nMode is an authority decision, not a synonym system. Plan, Work, and Operate\nuse the same primitive identities. Plan centrally refuses `write`, `edit`, and\n`bash`; Work and Operate still pass those calls through approval, sandbox,\ntrusted-path, repository-law, and managed-policy gates. Full Access changes\nordinary approval behavior but does not bypass hard safety or repository law.\n\n`update_plan` remains registered only for saved-artifact compatibility and is\nnot model-visible. `tasks`, `Git`, `Run`, `Web`, `remember`, and other\nspecialized capabilities are searchable rather than first-turn ceremony.\n\nOn the TUI's default (calm) shell card the Bash description is the headline,\nshown as written apart from stripped terminal escapes, collapsed whitespace and\na length bound, and the command sits on its own `$` row directly under it in\nevery state and at every width, including automatic runs. A card without a\ndescription leads with the `$` command instead. The labeled card\n(`show_tool_details = true` with `calm_mode = false`) keeps the command in its\nheader and the description as a muted `note:` row. The description never\nreplaces the command and is excluded from execution, exact approval keys and\nAuto-Review input. The original description remains in the recorded tool input\nand exports. Approval prompts, `/jobs`, the activity inspector, ACP and other\nRuntime clients keep showing the command; this change adds no description row\nto those surfaces.\n\n## Local code execution\n\n`code_execution` (Python) and `js_execution` (Node.js) use the same\npermission-aware local launcher as workspace task gates and test runs. Ordinary\napproval retains the effective session policy. To retry a denied exact call\nwith wider permissions, supply `sandbox_permissions` (`workspace-write` or\n`danger-full-access`) and a nonempty `justification`; Ask mode requires explicit\none-shot user approval. The approved policy affects only that call.\n\nBoth tools read source from stdin, return stdout/stderr/return_code, and keep\nthe existing timeout and process-tree cleanup. They are not persistent REPLs;\ntracebacks refer to stdin rather than a temporary script path. The separate\nRLM/REPL kernel is not changed by this launcher integration.\n\nPlatform/backend limits of the shared launcher still apply: no available local\nwrapper means workspace-write remains unenforced, while read-only is refused.\nAn external sandbox session cannot use these local interpreter tools and must\nuse its shell execution path. See [sandbox limits](SANDBOX.md).\n\n## Deferred and dynamic tools\n\n`Web` is conditional and deferred. It is discoverable through `tool_search`\nonly when the active policy and runtime backend permit it. Read-only\nchildren retain its read-only search/fetch evidence path; read-only authority\ndoes not mean \"unable to research.\"\n\nThe durable `github`, `automation`, and `rlm` action families are also deferred\nby default. `rlm` owns `open`, `eval`, `configure`, and `close` actions for a\npersistent local Python session (a subprocess with a scrubbed environment, not\nan OS sandbox). Inline ```` ```repl ```` fences in a reply run in the same kind of\nkernel only when `code_execution` is on the turn's surface (never in Plan mode),\nonly when the fence opens its own line, and only after `code_execution`'s\napproval under the session posture. Feature-gated native tools may be added to\nthe active or deferred catalog only when their implementation and host\ndependencies are available.\n\nMCP tools are dynamic. Successfully connected servers register names such as\n`mcp_<server>_<tool>` from `~/.codewhale/mcp.json`; a failed or disabled server\nmust not be presented as available. MCP and plugin tools are normally deferred. When the composition VM is eager and the turn admits exactly one compact named batch schema, `execute_tools` also accepts `{batch:{tool,arguments}}` instead of JavaScript. The VM forwards the data through the same nested gate, bounds and receipts; the server tool stays deferred unless explicitly requested. The current server routes and turn permissions narrow the shortcut’s member names; its nested dispatch checks the offered members after hooks. The server supplies a bounded guide to their arguments. Other tools stay searchable; `[tools].always_load` still makes individual schemas eager.\n\nFor an unstarted configured server, an MCP-focused `tool_search` first\nperforms bounded discovery under the current turn's server/tool ceiling.\nIt searches real server-provided schemas after connection, rather than\ninventing `mcp_*` definitions. A search inside `execute_tools` describes\nthose schemas without activating them; a direct search uses the existing\nbounded activation cache. Ordinary unrelated searches leave optional servers\nunstarted. The CLI's standalone MCP inspection commands own a separate pool.\n\n### Code mode (`execute_tools`)\n\nFor a known sequence, prefer a tool’s own batch operation. Use `execute_tools` when the next call depends on an intermediate result, and return the verified outcome. Search unknown names and schemas before acting. An unexpected state, refusal, uncertain delivery or direct-only decision ends that sequence.\n\n`execute_tools` is engine-injected, alongside the synthetic interpreter tools.\nIt runs a JavaScript program whose only host surface is\n`await tools.call(name, args)`, and it is the default way to compose several\ntool calls — MCP and plugin tools included — without round-tripping every\nintermediate result through the conversation. It is hidden from Plan mode and\nrefused under a worker authority envelope.\n\n- **One gate.** In a session turn every nested call is sent back to the turn\n loop and planned exactly like a direct call: deny/allow lists, preparation\n (MCP `readOnlyHint`/`destructiveHint`), `tool_call_before` hooks, ask-rules,\n Auto-Review, repository law, and the Computer Use consent refusal. MCP calls\n run through the session MCP pool. Approving the program grants nothing, so\n `execute_tools` itself is auto-approved. If the permission posture changes\n while a program runs, its remaining nested calls are refused (an approved\n call survives only an equal or broader posture, as for a direct call) and\n the model retries them under the new posture.\n- **Approvals suspend the program.** A nested call that needs approval raises\n the normal approval card (named `execute_tools program call: ...`) and the\n program waits; allow resumes it, deny fails only that nested call as an\n exception the program can catch. Time spent waiting on a decision does not\n count against the program's run deadline, which is the turn's remaining\n wall clock.\n- **Receipts.** The result lists every nested call with its decision (`auto`,\n `approved`, `denied`, `refused`) and status (`ok`, `failed`, `refused`,\n `in_flight`). A program that hits its deadline still returns the receipt;\n `in_flight` calls were cancelled and may have partially run. Each nested\n result is `{content, metadata, truncated}`; an oversized result keeps that\n shape, and `truncated` names the original size and the spillover file with\n the full output.\n- **Discovery without re-pinning.** Inside a program,\n `tools.call('tool_search', {query})` returns matching deferred tools with\n their input schemas and does not activate them, so the request's tool array\n and the session-pinned prefix do not change.\n- **Stays direct:** `agent`, `workflow`, `request_user_input`, nested\n `execute_tools`, interactive shells, sandbox escalation, Computer Use\n consent and scripts, and MCP sign-in (`mcp_<server>_authenticate`).\n\nCode mode is on by default (`[features] code_mode = true`), which makes\n`execute_tools` eager from the first request; direct tools and `tool_search`\nstay available either way. Set `code_mode = false` (or run with\n`--disable code_mode`) to go back to deferring `execute_tools` behind\n`tool_search`. The flag is session configuration, so the prompt prefix stays\nstable within a session.\n\nSub-agents get `execute_tools` on the same terms, unless their role is limited\nto the read-only evidence tools or an explicit tool list leaves it out. A\nchild's nested call is planned by the child's own gate, so its grant and its\napproval path apply as for its direct calls, and it is admitted at execution\nagainst the child's write-scope claim and live peers. A child's program also\nstops at the child's tool timeout (time waiting on approvals is not counted)\nor its work deadline, and still returns the receipt. A program in which no\nnested call ran does not count as progress for the child's permission-denial\nguard, but the guard does not yet count a refused nested call as a denial\neither; only the child's step and time budgets end a loop of denied calls\nretried inside programs. Without an engine turn\n(a direct unit call), a program keeps the conservative profile: read-only,\nauto-approved native calls only, no MCP.\n\n### Conversation toolbox cache\n\nA successful search activation is remembered by name for the current\nconversation. Its soft targets are eight deferred names and 16 KiB of added\nprovider-wire schemas, measured after the route's production transformations\nagainst the eager-tool baseline. An adapter that cannot measure the wire reports\nunknown bytes; internal `Tool` serialization is not substituted. To make room it evicts tools that were never called first\n(oldest activation first), then the least recently called; a tool called in the\ncurrent or previous turn is never evicted. When such tools already fill the\ncache, a search still activates its best match over the bound and holds the\nrest; a later activation restores the bound once those tools go unused. Every\nentry is revalidated against the current catalog and policy before it is\nadvertised again. A session sync clears it. The cache cannot resurrect a\nremoved, denied, or newly-eager tool.\n\nWhen a search's activation pushes a previously activated tool out of the\ncache, that search's result lists it under `evicted_tool_references`, so the\nmodel is told the tool is no longer callable rather than finding it missing\nfrom the next request. A held match is listed under\n`unavailable_tool_references`. Both carry `tool_search_input`, an exact regex\nquery for that one name that loads it again. Only tools that actually left the\nactive set are listed; a tool that became eager stays callable and is not\nreported. Each search also returns the current `active_deferred_tools` snapshot\nand wire-byte measurement in `toolbox`; a new admission explicitly supersedes\nolder eviction or unavailable notices for the same name. Other declared toolbox\nchanges, including revalidation and direct first-use activation, append one\ncurrent deferred-toolbox snapshot before the next model request. That snapshot\nsupersedes old availability notices without rewriting earlier history.\n\nEach subagent gets its own policy-filtered deferred catalog, always-present\n`tool_search`, and bounded activation cache. Forked messages and instructions\nremain in context, but the child cache starts empty and discovers tools locally;\nneither forked context nor a cache can become a discovery allowlist. A child can\nstill search every tool its own authority permits, including Web search/fetch\nfor read-only research roles.\n\n## Inspect the model-client request tool payload\n\nRun `/tools` after a model turn to inspect a bounded projection of the exact\ntool field in the latest prepared model-client request. `/tools json` emits the\nsame evidence as bounded machine-readable JSON. Both formats open in a pager;\nthey are not copied into transcript history. `/tool-studio` remains a human-\ncommand compatibility alias; it is not a model tool.\n\nThe snapshot distinguishes an absent tool field from a present empty array. It\nreports the exact model-client tool JSON byte count and SHA-256 digest only when\nmeasurement fits the one-MiB inspection bound; larger payloads stay unavailable.\nProvider adapters may transform, sanitize, or omit those fields while building\na provider-specific wire body, so `/tools` marks provider delivery and the wire\npayload unavailable. The separate per-request receipt described in [CACHE.md](CACHE.md)\nmeasures the production wire schema and joins it with reported provider usage. Capture and rendering are bounded: retained schemas,\ndescriptions, caller lists, catalog rows, turn IDs, and payload measurement all\ncarry explicit truncation, omission, or unavailable receipts. The snapshot stays\nin memory only for the current session and is replaced on each prepared request.\n\nProvider, model, approval, registry provenance, and runtime capability metadata\nare not fields in the request tool schema. `/tools` therefore reports them as\nunavailable instead of joining against mutable state or inferring values. Use\nthe separate route and permission receipts for those facts.\n\n## Modes and permission postures\n\nModes and permission postures are separate controls:\n\n- **Plan** keeps the stable primitive vocabulary but centrally refuses shell\n execution and file mutation.\n- **Work** is ordinary interactive execution.\n- **Operate** uses the same direct-tool authority as Work. Small work stays\n direct; multi-step delegation uses a compact Workflow plan with dependencies,\n bounded scopes, and completion evidence. Fleet manages the same sub-agents\n and roles. One bounded, independent task can use a direct agent; `followup`\n reuses that agent for continued work.\n- **Ask**, **Auto-Review**, and **Full Access** control approval behavior within\n an action-capable mode. They never widen Plan into write or shell access.\n\nSee `docs/MODES.md` for the full mode and posture contract.\n\n## Compatibility names\n\nThe model-facing contract is the lowercase core above. Saved v0.9.x\ntranscripts and protocol clients may still call exact hidden compatibility\nnames such as `File`, `Bash`, and the older single-operation file names. Those\nnames never enter a new model catalog or `tool_search` result.\n\nCompatibility is execution compatibility, not fuzzy aliasing: an exact legacy\ncall must reach the handler for its legacy schema. It must not be rewritten\ninto a small lowercase primitive whose input shape is different. Unknown or\nretired names still fail closed instead of guessing a destination.\n\nSpecialized native families such as `Git`, `Run`, and `Web` are not aliases for\nthe lowercase core. They remain real, policy-filtered deferred tools and are\nloaded through `tool_search` when needed.\n\n## Long-running work\n\n`bash` runs one cancellable foreground command. It does not carry background,\nTTY, wait, interact, or cancel action fields. Stateful process and terminal\ncontrol stays out of the shell schema: `task_shell_wait` is eager for the\nbackground jobs `bash` starts, while interactive terminal control remains\ndeferred behind `tool_search`; neither enlarges the first-turn shell schema.\n\nUse `tasks` when the work itself needs a durable lifecycle, structured gates,\nartifacts, replayable timelines, or a stable task id. Large tool results should\nremain behind bounded handles or artifacts instead of being copied wholesale\ninto the parent transcript.\n\n## Parallel fan-out\n\nThe sub-agent capacity source of truth is\n`crates/tui/src/config/subagent_limits.rs`:\n\n- default configured concurrency: **64**;\n- maximum configured concurrency: **128**;\n- maximum admitted running-plus-queued work: **1024**.\n\nThese are capacity ceilings, not advice to dispatch every available slot. A\nmanager should use the smallest useful fan-out, preserve a single owner for\nfan-in, and verify worker receipts before reporting combined completion.\n\nRLM child-query batching is a different, cheaper cost class. Its\n`sub_query_batch` helper accepts 1–16 one-shot children inside a live `rlm`\nsession; it is not a substitute for tool-carrying `agent` workers.\n\n## Human inspection: `/tools` (`/tool-studio`)\n\n`/tools` renders a **read-only, bounded human projection** of the tool field of\nthe request that was prepared for one `(turn, step)`. It is not a second\nregistry and not an execution surface.\n\n**The seam.** The snapshot is built in `crates/engine/src/core/engine/turn_loop.rs`\nimmediately after `MessageRequest` is constructed, from `request.tools` — the\nsame value the model client is handed. The engine resolves the surrounding\nper-turn data once in `engine.rs` (`ToolSurfaceContext`: flattened registry\nfacts, the MCP pool's own server attribution, the engine-injected catalog names,\nand the resolved model client's receipt) and passes it as plain data, so the\nper-step seam never re-locks the MCP pool or holds a tool object.\n\n**Turn and step identity.** The tool set can differ between steps of a turn, so\neach snapshot is stamped with turn id and step and each seam emits its own. The\nTUI keeps only the latest (`SessionState.last_tool_request_snapshot`). Before\nthe first seam there is no snapshot and `/tools` says so rather than rebuilding\na registry in the UI.\n\nTwo kinds of fact are kept apart:\n\n- **Wire facts** come from the prepared request: name, description, schema,\n `defer_loading` / `strict` / `allowed_callers` / `cache_control`, byte\n accounting, and the catalog digest.\n- **Surface facts** come from the `ToolSurfaceContext`: provenance\n (`builtin` / `plugin` / `mcp` / `synthetic` / `unknown`), MCP server identity,\n declared capabilities, declared approval requirement, and model visibility.\n\nContract:\n\n- **One digest.** `active_tool_catalog_sha256`\n (`crates/engine/src/core/engine/preview.rs`) is the single definition of the\n active-tool-catalog hash. The request manifest publishes it as\n `ToolSurfaceFacts::active_tool_catalog_sha256` and `/tools` reports the same\n value for the same prepared request; neither surface keeps a hash of its own.\n- **Nothing is guessed.** MCP server identity is shown only when the real pool\n attributed that exact model tool name. `McpPool::mcp_model_tool_name` is the\n single definition shared by the model catalog and the human attribution, and\n an ambiguous name (two servers colliding on one model name) resolves to no\n server. Synthetic provenance comes from\n `default_synthetic_catalog_tool_names`, which is asserted against the engine's\n own `is_synthetic_catalog_tool` predicate. A transmitted tool with no registry\n entry reports `capabilities: unknown`, never \"none\".\n- **Provider availability follows the resolved client.** It comes from\n `Engine::tool_surface_provider_receipt`, never from \"a tool registry exists\".\n With no client the receipt is `unavailable` even when the registry is full.\n- **Unknown shrinks, it does not vanish.** `unavailable_for_this_request` always\n contains `provider_wire_payload`: nothing on this path observes what the\n provider adapter finally transmits. It additionally contains `provider` and\n `model` without a resolved client, and `provenance` / `capabilities` /\n `approval` when no surface context was captured.\n- **Absent stays distinct from empty.** A request with no tools field is not a\n request with an empty tools array; an unresolved field is `unknown` with a\n reason, not a default.\n- **Bounded.** Rendering is capped by tool count (32), name, description, schema\n bytes, allowed-caller count, and a payload measurement bound, each with an\n explicit truncation or omission receipt. Registered tools that this request\n does *not* carry are reported as a bounded name list plus an exact count\n rather than expanding the projection.\n- **Inert.** The snapshot lives beside the transcript, never in\n `session.messages`, so it cannot enter a model request or perturb the\n provider's prefix cache. It never executes a tool, never reads credentials,\n never reorders the catalog, and is never registered as a model-callable tool.\n- **Delivery is never claimed.** The capture happens before connection setup, so\n `delivery_status` stays `unknown`.\n\n## Release verification\n\nDo not infer the public surface from handler function names. Verify the model\ncatalog and alias visibility at the exact candidate SHA:\n\n```bash\npython3 scripts/measure-runtime-contract.py\ncargo test -p codewhale-tui --lib --locked core::engine::tests::default_active_contract_keeps_discovery_and_core_tools_eager -- --exact\ncargo test -p codewhale-tui --lib --locked tools::file_tool::tests::primitive_schemas_are_separate_and_small_contract_shaped -- --exact\ncargo test -p codewhale-tui --lib --locked tools::shell::tests::lowercase_bash_schema_is_small_contract -- --exact\ncargo test --locked -p codewhale-tui --lib core::engine::tests::print_mode_tool_catalog_metrics -- --ignored --exact --nocapture\n```\n\nCheck the test names against the source before trusting a green run: `cargo test`\nexits 0 with \"0 passed; N filtered out\" when a filter matches nothing, so a\nmisspelled filter is indistinguishable from a pass. Each `--exact` command\nabove must report `1 passed` (the ignored metrics test reports `1 passed`\nonly because `--ignored` selects it); `0 passed` means the filter matched\nnothing and the check did not run.\n\nThe provider-free receipt must report the thirteen default-active names listed\nabove. A separate repository-wide tool count may include deferred, dynamic,\nfeature-gated, and compatibility-only registrations; it is not the number of\ntools placed in the first-turn model catalog.\n"
|
||
},
|
||
{
|
||
"path": "docs/TOOL_LIFECYCLE.md",
|
||
"sha256": "40829bd3915ad52fdb03deccaf77f2fc827cba7fb8939ca8ae2f2561ebd0fc05",
|
||
"content": "# Historical Tool-Surface Lifecycle Policy (v0.8.53)\n\n> 阅读简体中文版:[zh_hans/TOOL_LIFECYCLE.md](zh_hans/TOOL_LIFECYCLE.md)\n\n**Status:** Historical design record, not current runtime documentation. The\nv0.9.1 canonical action surface and replay-only alias contract are documented in\n[`RUNTIME_SIMPLIFICATION_DESIGN.md`](RUNTIME_SIMPLIFICATION_DESIGN.md) and\n[`TOOL_SURFACE.md`](TOOL_SURFACE.md). No catalog code landed in this old cycle — the code\nwork is **deferred**. This document is the umbrella policy for GitHub **#2681**,\nwith **#2682** and **#2683** as concrete instances of the planned diet. It\ndescribes *what will be done* and the invariants any future diet PR must hold.\n\n**Scope of related open work (do not contradict):**\n- PR **#2684** — subagent role vocabulary, lifecycle signals, eval ergonomics.\n Legacy subagent-name cleanup + guardrail tests in this policy rebase on #2684.\n- PR **#2685** — git-history active + RLM/field errors.\n\n**What actually happened, so you can read the rest as history:** the \"hidden\ncompatibility\" plan below was *not* what shipped in full. `exec_wait` and\n`exec_interact` were removed rather than kept dispatchable. The work-progress\nsurface is different: `todo_write` is the sole model-visible name, while\n`work_update`, `TodoWrite`, `todo`, `checklist_write`, and `checklist_update`\nstay registered as hidden-compatibility aliases of `TodoWriteTool` so old\ntranscripts replay; `checklist_add`, `checklist_list`, `todo_add`,\n`todo_update`, and `todo_list` are not registered and are not callable.\n`tts`/`speech` remain dispatchable. See [`TOOL_SURFACE.md`](TOOL_SURFACE.md)\nfor the shipped contract. Read §4 and §8 as a rejected catalog-diet proposal,\nnot as a guarantee that every listed alias is gone.\n\n**All file:line citations here were correct at v0.8.52/0.8.53 and are now\nexpired.** They have not been rewritten, because renumbering a historical\nrecord makes it look current. Symbols this document cites that no longer exist\nat all include `ARCEE_FIRST_TURN_NATIVE_TOOLS` and `apply_provider_tool_policy`\n(both removed by `1bfcced43c`, \"fix(engine): remove Arcee tool catalog\nexception\"), and the planned `HIDDEN_COMPATIBILITY_TOOLS` / `DEPRECATED_ALIASES`\nsets, which were never written. Resolve any symbol here against the tree before\nacting on it.\n\n---\n\n## 1. Purpose and the weaker-model problem\n\nCodewhale ships a large native tool surface. The first-turn *active* partition\nof that surface is what every model sees before it has run a single\n`tool_search_*` call. Today that active set contains several **near-duplicate\ntools** that map to the *same* implementation under different names:\n\n- `exec_wait` and `exec_shell_wait` are both `ShellWaitTool`\n (`crates/engine/src/tools/registry.rs:526,529`).\n- `exec_interact` and `exec_shell_interact` are both `ShellInteractTool`\n (`registry.rs:527,530`).\n- `tts` and `speech` are both `SpeechTool`\n (`registry.rs:787-792`, both deferred).\n- `todo_write` is the single model-visible `TodoWriteTool` surface;\n `todo_write`, `TodoWrite`, `todo`, `checklist_write`, and\n `checklist_update` are hidden compat aliases of it.\n\nFor a strong model, redundant names are harmless noise. For **weaker / smaller\nmodels** (the Arcee Trinity lane, `deepseek-v4-flash` child executors, and any\nnon-thinking executor), every additional near-duplicate in the visible set is a\nreal cost:\n\n- It widens the choice space with options that do *nothing distinct*, increasing\n wrong-tool selection and oscillation between synonyms.\n- It spends scarce first-turn catalog budget (Section 5) on zero-information\n entries.\n- It dilutes the \"one name = one thing\" contract that lets a small model reason\n about the surface at all.\n\nThe lifecycle policy exists to **shrink and discipline the model-visible\nsurface** without ever breaking the ability to replay an old transcript that\nreferenced a now-retired name.\n\n### Canonical work-tracking surface for v0.9.1\n\nThe model-visible progress surface is a single tool: `todo_write` (#4132).\nAgents and Fleet workers use it for concrete To-do / Work progress under the\nactive runtime thread or durable task.\n\n`task_*` and the Fleet/Workflow ledger remain the durable lifecycle owners.\nChecklist metadata is the model-visible projection of progress:\n`task_updates.checklist` carries the current items, completion percentage, and\nin-progress item.\n\n**The To-do is the only Work surface.** `update_plan` is conversational\nreasoning — strategy, context, and route notes for complex initiatives. It is\nnot a progress surface, must not duplicate To-do items, and plan-only state is\nnever rendered as a To-do snapshot.\n\n**No request re-states the list.** The model learns what is on the To-do the way\nit learns anything else: from its own `todo_write` call, whose input is the\nwhole list, and the short receipt it returned (counts and the item in\nprogress). Both are ordinary persisted history. Nothing is appended to a parent turn-loop\nstep or a sub-agent step. The complete list stays visible in the UI, which is a\ndifferent surface from the request.\n\n`crates/engine/src/todo_snapshot.rs` renders the one bounded body — hard-bounded in\nboth item count and characters, in-progress item preserved preferentially, any\nelision marked — for the three seams that show a snapshot *once*, because a\nperson asked: the `<codewhale:fork_state>` block a newly forked agent is handed,\n`/relay`, and the in-transcript agent card.\n\nTwo properties of that renderer are load-bearing:\n\n- **Authority.** The snapshot is read from the `WorkRuntime` graph projection\n when a runtime owns that list, because `todo_write` stages there and only\n publishes into the legacy `SharedTodoList` view later. Sessions with no\n attached runtime read the list directly.\n- **Per-agent isolation.** Every agent reads *its own* list (`#4810`), so a\n worker sees its own progress and never a parent's or sibling's. The parent's\n list reaches a forked child only as the immutable `<codewhale:fork_state>`\n To-do section, resolved at the spawn seam so a same-turn `todo_write` is\n included.\n\nThe renderer bounds and frames the snapshot; it does not vet To-do content. It\nguarantees that item text cannot close the wrapper early, cannot forge the line\nformat with control characters, and cannot exceed the item/character bounds —\nnot that arbitrary item text is safe to follow as instructions.\n\nThe registered hidden compatibility aliases of `todo_write` are `work_update`,\n`TodoWrite`, `todo`, `checklist_write`, and `checklist_update`\n(`ToolRegistryBuilder::with_todo_tool`). They remain dispatchable against the\nsame To-do state so old transcripts replay without data loss, but they are not\nadvertised to the model catalog (`TodoWriteTool::model_visible` is true only\nfor the canonical name). `checklist_add`, `checklist_list`, `todo_add`,\n`todo_update`, and `todo_list` are not registered and are not callable.\n\n---\n\n## 2. The five lifecycle states\n\nEvery native tool name occupies exactly one lifecycle state.\n\n| State | Meaning | Visible on first turn? | In `tool_search_*`? | Executes if called? | When used |\n|---|---|---|---|---|---|\n| **active** | Canonical, in the first-turn catalog head | **Yes** | n/a (already active) | Yes | The tool a model should reach for by default |\n| **deferred** | Registered + discoverable, hydrated on demand | No | **Yes** | Yes | Real, useful tools that don't earn a first-turn slot |\n| **hidden-compatibility** | Registered + dispatchable, but removed from active **and** from search | No | **No** | **Yes — identical behavior, silent** | Old synonym kept only so old transcripts replay; no model should newly discover it |\n| **deprecated** | Like hidden-compat, but execution **appends a replacement notice to result metadata** | No | **No** | **Yes — works, plus a \"use X instead\" notice** | A retired name we actively steer callers off of, still safe to replay |\n| **removed** | Not registered at all | No | No | **No — hard error** | Only after `planned_removal_version`, once replay support is formally dropped |\n\n### hidden-compatibility vs deprecated — be precise\n\nBoth states are **invisible** (not active, not in tool search) and both remain\n**dispatchable** (calling them still works). The *only* difference is the\ncaller-facing signal:\n\n- **hidden-compatibility:** completely silent. The tool behaves byte-for-byte\n like its canonical twin. We use this when there is *no behavioral or naming\n lesson to teach* — the name was a pure alias and we simply don't want models\n re-learning it. (Example: `exec_wait` is literally `exec_shell_wait`.)\n- **deprecated:** behaves identically *and succeeds*, but the tool result's\n **metadata** carries an appended notice like\n `\"deprecated: use <replacement> instead\"`. The notice goes **only in the\n result metadata returned for that call** — never in the cached tool catalog\n prefix (see Section 8). We use this when there is a canonical replacement we\n want the caller (and any human reading the transcript) nudged toward.\n\nNeither state ever changes the *behavior* of the call. Replay always works.\n\n---\n\n## 3. Representation in code\n\nThe lifecycle is represented as **const name-sets plus an alias/manifest table**\nin `crates/engine/src/core/engine/tool_catalog.rs`, alongside the existing\n`DEFAULT_ACTIVE_NATIVE_TOOLS` (`tool_catalog.rs:37-64`) and\n`ARCEE_FIRST_TURN_NATIVE_TOOLS` (`tool_catalog.rs:106-115`).\n\n### 3a. Name-sets and the manifest (sketch)\n\n```rust\n// crates/engine/src/core/engine/tool_catalog.rs (planned)\n\n/// Tools removed from the active set AND from tool-search, but still\n/// registered and dispatchable with byte-identical behavior. Silent.\npub(super) const HIDDEN_COMPATIBILITY_TOOLS: &[&str] = &[\n \"exec_wait\", // == exec_shell_wait (ShellWaitTool)\n \"exec_interact\", // == exec_shell_interact (ShellInteractTool)\n \"tts\", // == speech (SpeechTool)\n \"work_update\", // == todo_write (TodoWriteTool)\n \"TodoWrite\", // == todo_write (TodoWriteTool)\n \"todo\", // == todo_write (TodoWriteTool)\n \"checklist_write\", // == todo_write (TodoWriteTool)\n \"checklist_update\", // == todo_write (TodoWriteTool)\n];\n\n/// Deprecated aliases: invisible + dispatchable, with a replacement notice\n/// appended to RESULT METADATA only (never the cached prefix).\npub(super) struct DeprecatedAlias {\n pub name: &'static str,\n pub replacement: &'static str,\n pub note: &'static str,\n}\n\npub(super) const DEPRECATED_ALIASES: &[DeprecatedAlias] = &[\n // Empty in the #4132 work-surface cutover: the legacy names above are\n // silent hidden-compatibility aliases of todo_write for transcript replay.\n];\n\n#[inline]\npub(super) fn is_hidden_or_deprecated(name: &str) -> bool {\n HIDDEN_COMPATIBILITY_TOOLS.contains(&name)\n || DEPRECATED_ALIASES.iter().any(|d| d.name == name)\n}\n```\n\n### 3b. The two filter points\n\n1. **Catalog / tool-search exclusion (tool_catalog.rs).**\n Deferral is decided by `should_default_defer_tool` (`tool_catalog.rs:66-82`),\n and the active set is the head built by `build_model_tool_catalog`\n (`tool_catalog.rs:178-196`). Hidden-compat and deprecated tools must be\n forced *out of the active head* and *out of the tool-search-discoverable\n pool*. Concretely, the deferral predicate gains a short-circuit so these\n names are never active, and the tool-search index builder skips any name for\n which `is_hidden_or_deprecated(name)` is true. Arcee's narrowed first-turn\n path (`apply_provider_tool_policy`, `tool_catalog.rs:134-149`) already\n excludes them by construction since they aren't in\n `ARCEE_FIRST_TURN_NATIVE_TOOLS`.\n\n2. **Result-notice append (tool_routing.rs).**\n Dispatch already routes by tool name in\n `crates/tui/src/tui/tool_routing.rs` (e.g. the wait/interact unification at\n `tool_routing.rs:1139-1140`). After a successful dispatch, if the called name\n is in `DEPRECATED_ALIASES`, the router appends the matching `note` to the\n **result metadata only**. Hidden-compat names append nothing.\n\n### 3c. Why name-sets, not a per-`ToolSpec` enum field\n\nA per-`ToolSpec` `lifecycle: Lifecycle` field was rejected for three reasons:\n\n- **Prefix-cache safety.** The tool catalog array is part of DeepSeek's\n immutable KV prefix (`tool_catalog.rs:169-177`). A per-spec field invites\n serializing lifecycle state *into* each tool's schema, which is exactly the\n kind of head mutation that forces a full re-prefill. Name-sets live entirely\n in the catalog-build logic and never touch the emitted tool JSON.\n- **Single source of truth + diffability.** The diet for a release is one small,\n reviewable edit to two or three const arrays in one file, instead of scattered\n field flips across many tool modules.\n- **Registration stays orthogonal.** Tools remain registered exactly as today\n (e.g. `with_shell_tools`, `registry.rs:523-531`). Lifecycle is a *catalog\n policy* layered on top of registration, not a property baked into the tool.\n\n---\n\n## 4. Deprecation manifest (the #2681 acceptance-criteria table)\n\nThis was the proposed manifest. Columns are the #2681 AC columns. No entry was\n\"removed\" in 0.8.53; replay was to be supported for everything listed.\n\n> **Superseded in part.** `exec_wait` and `exec_interact` were removed\n> (`shell_surface_exposes_lowercase_bash_and_hides_legacy_handler` in\n> `crates/engine/src/tools/registry/tests.rs`); they are not callable. The\n> work-progress rows are not uniformly removed. `todo_write` is the canonical\n> model-visible `TodoWriteTool`; `ToolRegistryBuilder::with_todo_tool` also\n> registers hidden replay aliases `work_update`, `TodoWrite`, `todo`,\n> `checklist_write`, and `checklist_update`. `checklist_add`, `checklist_list`,\n> `todo_add`, `todo_update`, and `todo_list` are not registered (engine tests\n> assert they \"must not be callable\"). The `\"must no longer be callable\"`\n> assertion in `registry/tests.rs` (`rlm_is_the_only_registered_session_surface`)\n> applies to the old `rlm_*` session names, not to checklist/todo. `tts`\n> remains dispatchable via `with_speech_tools`. The `replay_supported = Yes`\n> column is true for `tts` and for the registered todo/checklist aliases; false\n> for `exec_wait`/`exec_interact` and for the unregistered checklist/todo names.\n\n| Alias | Replacement (canonical) | Lifecycle state | first_deprecated_version | planned_removal_version | replay_supported |\n|---|---|---|---|---|---|\n| `exec_wait` | `exec_shell_wait` | hidden-compatibility | 0.8.53 | TBD (≥ 0.9.x) | Yes |\n| `exec_interact` | `exec_shell_interact` | hidden-compatibility | 0.8.53 | TBD (≥ 0.9.x) | Yes |\n| `tts` | `speech` | hidden-compatibility | 0.8.53 | TBD (≥ 0.9.x) | Yes |\n| `checklist_write` | `todo_write` | hidden-compatibility | 0.9.0 | TBD (≥ 0.9.x) | Yes |\n| `checklist_add` | `todo_write` | hidden-compatibility | 0.9.0 | TBD (≥ 0.9.x) | Yes |\n| `checklist_update` | `todo_write` | hidden-compatibility | 0.9.0 | TBD (≥ 0.9.x) | Yes |\n| `checklist_list` | `todo_write` | hidden-compatibility | 0.9.0 | TBD (≥ 0.9.x) | Yes |\n| `todo_write` | `todo_write` | hidden-compatibility | 0.8.53 | TBD (≥ 0.9.x) | Yes |\n| `todo_add` | `todo_write` | hidden-compatibility | 0.8.53 | TBD (≥ 0.9.x) | Yes |\n| `todo_update` | `todo_write` | hidden-compatibility | 0.8.53 | TBD (≥ 0.9.x) | Yes |\n| `todo_list` | `todo_write` | hidden-compatibility | 0.8.53 | TBD (≥ 0.9.x) | Yes |\n\nThe `todo_*` aliases first entered hidden compatibility in v0.8.53. v0.9.0\nchanges their canonical replacement to `todo_write`; it does not reset their\nfirst-deprecated version.\n\n**Legacy subagent names — removed, no manifest entry needed.**\nThe model-visible subagent surface is only `agent`. The old lifecycle names and\nthe experimental tool-agent lane were removed rather than kept as hidden\ncompatibility tools.\n\n`planned_removal_version` is intentionally `TBD`: a name only moves to **removed**\nonce we formally drop replay for transcripts old enough to contain it, which is a\nseparate, deliberate decision per name.\n\n---\n\n## 5. Active-catalog budget (per mode, per provider)\n\nThe active set is the first-turn cost. Do not duplicate the exact\n`DEFAULT_ACTIVE_NATIVE_TOOLS` count here: adjacent PRs in the v0.8.53 batch may\nadd or remove active tools, and the source of truth is always\n`tool_catalog.rs`. This document defines the diet policy and invariants, not a\nsecond catalog snapshot.\n\n### Per provider\n\n| Provider | First-turn active source | Budget policy |\n|---|---|---|\n| Default (DeepSeek et al.) | `DEFAULT_ACTIVE_NATIVE_TOOLS` | Remove duplicate aliases from the active head when their canonical twins stay active; any net growth needs an explicit budget decision. |\n| Arcee (Trinity) | `ARCEE_FIRST_TURN_NATIVE_TOOLS` | Provider-specific read-only WAF workaround; unchanged by the default diet unless explicitly reviewed. |\n\nThe default diet removes `exec_wait` and `exec_interact` from the active head\n(they become hidden-compat; their canonical twins `exec_shell_wait` /\n`exec_shell_interact` stay). `tts` and the legacy `todo_*` aliases remain out\nof the active set. The canonical `todo_write` tool became eager in v0.9.6 as an\nexplicit budget decision so ordinary progress tracking never requires a\ndiscovery turn.\n\n### Per mode (Plan / Agent / YOLO)\n\nThe native active head is the **same set across modes** by design — mode does not\nadd or remove native tools from `DEFAULT_ACTIVE_NATIVE_TOOLS`\n(`should_default_defer_tool` ignores `_mode` for native tools,\n`tool_catalog.rs:66-68`). Mode affects **MCP** deferral instead:\n`apply_mcp_tool_deferral` keeps MCP tools deferred unless `mode == Yolo`\n(`tool_catalog.rs:162-167`).\n\n| Mode | Native active budget | MCP tools active? |\n|---|---|---|\n| Plan | same native head | No (deferred) |\n| Agent | same native head | No (deferred) |\n| YOLO | same native head | Yes (a known, intentional widening) |\n\n**Budget rule:** the native active head must stay byte-identical across Plan ↔\nAgent ↔ YOLO (Section 8). Any growth of the head requires retiring something\nelse or an explicit budget bump in this doc.\n\n---\n\n## 6. The canonical-surface rule\n\n> **Every model-visible (active or deferred-discoverable) tool must have one\n> clear niche. If a tool is superseded, it gets a named replacement and moves to\n> hidden-compatibility or deprecated — it does not stay visible.**\n\n### Canonical vs compatibility summary for the confusing clusters\n\n| Cluster | Canonical (keep visible) | Compatibility / retired | Notes |\n|---|---|---|---|\n| **Shell wait** | `exec_shell_wait` | `exec_wait` → hidden-compat | Same `ShellWaitTool` (`registry.rs:526,529`); router already unifies (`tool_routing.rs:1139`) |\n| **Shell interact** | `exec_shell_interact` | `exec_interact` → hidden-compat | Same `ShellInteractTool` (`registry.rs:527,530`) |\n| **Work progress / checklist / todo** | `todo_write` | registered hidden-compat: `work_update`, `TodoWrite`, `todo`, `checklist_write`, `checklist_update`; not registered: `checklist_add`/`list`, `todo_add`/`update`/`list` | Same `TodoWriteTool`; registered aliases replay old transcripts only |\n| **Speech / tts** | `speech` | `tts` → hidden-compat | Same `SpeechTool` (`registry.rs:787-792`) |\n| **Subagent lifecycle** | `agent` | old lifecycle names and tool-agent lane removed | Single async launcher. (The \"child agents are leaf workers\" note here did not ship — see §7.) |\n| **Edit family** | `apply_patch`, `edit_file`, `write_file`, `fim_edit` | none — **all distinct niches** | NOT touched (per #2681 non-goals); doc-only canonical guidance |\n| **Search family** | `grep_files` (content), `file_search` (filename), `project_map` (structure) | none — **distinct niches** | NOT touched; no FTS5/BM25/semantic index exists today |\n\n**Non-goals (explicitly NOT diet targets in this cycle, per #2681):**\n`apply_patch` / `edit_file` / `write_file` / `fim_edit`;\n`grep_files` / `file_search` / `project_map`;\n`fetch_url` / `web.run` / `web_search`;\n`task_shell_*`; `handle_read` / `retrieve_tool_result`. These have distinct\nniches and receive **canonical guidance only** — no lifecycle change.\n\nThe RLM surface (`rlm_open` / `rlm_eval` / `rlm_configure` / `rlm_close` /\n`rlm_session_objects`, `crates/engine/src/tools/rlm.rs`) is likewise out of scope;\n`handle_read` retrieves var handles, and `finalize` / `FINAL` is an in-kernel\nPython function, **not a tool** — so there is nothing to retire there.\n\n---\n\n## 7. Subagent cutover decision: one visible launcher\n\nThe old lifecycle trio and tool-agent lane are removed, not hidden compatibility\ntools.\n\n**Decision: expose only `agent`.**\n\n- `agent` starts one focused background child and returns the agent id plus\n transcript handle.\n- Child results arrive as completion events. The parent should keep working\n instead of polling a lifecycle tool.\n- Child tool catalogs exclude the removed subagent *lifecycle* tools.\n (**Not as shipped:** this bullet originally continued \"so children are leaf\n workers and cannot recursively summon more agents.\" That is not what landed.\n Children receive `agent` and can recurse to the configured depth — see\n `with_full_agent_surface_options` and `can_spawn_child` in\n `tools/subagent/mod.rs`, and [`SUBAGENTS.md`](SUBAGENTS.md).)\n- Detailed inspection goes through `handle_read` on the returned transcript\n handle.\n\nThis is a lifecycle simplification, not a provider gate.\n\n---\n\n## 8. Prefix-cache safety + replay guarantee\n\n### Prefix-cache rules every diet PR MUST follow\n\nThe tools array is part of DeepSeek's immutable KV prefix. The catalog-head\nbyte-stability invariant (`tool_catalog.rs:169-196`) is binding:\n\n1. **Never mutate the active head non-deterministically.** The first-turn active\n block must be **byte-identical run-to-run** and across Plan ↔ Agent ↔ YOLO.\n2. **A diet is a one-time deterministic edit.** Removing a name from\n `DEFAULT_ACTIVE_NATIVE_TOOLS` shifts the head exactly once; after that it must\n be stable. Land such edits as their own focused change.\n3. **Notices live in result metadata, never the prefix.** Deprecated replacement\n notes are appended at dispatch time in `tool_routing.rs` to the *call result*\n only. **Nothing** about hidden/deprecated state may be serialized into a tool\n schema, description, or the catalog array.\n4. **Preserve ordering and partitioning.** `build_model_tool_catalog` sorts each\n partition by name and keeps built-ins as a contiguous prefix ahead of MCP\n tools (`tool_catalog.rs:186-194`). Diet edits must not break this.\n5. **Hidden/deprecated tools are excluded *before* the head is built**, so their\n removal is the only head change — they do not appear in the prefix at all.\n\n### Old-transcript replay guarantee (not adopted)\n\nThe blanket guarantee below was proposed, not shipped as written. `exec_wait`\nand `exec_interact` were removed and fail as unknown tools. Work-progress\nreplay is narrower: `todo_write` plus the hidden aliases registered by\n`with_todo_tool` (`work_update`, `TodoWrite`, `todo`, `checklist_write`,\n`checklist_update`) still dispatch; `checklist_add`, `checklist_list`,\n`todo_add`, `todo_update`, and `todo_list` do not. `tts` remains\ndispatchable. `apply_patch` is a separate replay-only edit alias; see\n[`TOOL_SURFACE.md`](TOOL_SURFACE.md).\n\n> For every name in the deprecation manifest with `replay_supported = Yes`, the\n> tool stays **registered and dispatchable with identical behavior**. Replaying\n> an old transcript that calls `exec_wait`, `exec_interact`, `tts`, or any\n> `todo_*` produces the same result it always did. Deprecated names additionally\n> attach a result-metadata notice; hidden-compat names are silent. A name is only\n> ever made non-dispatchable (**removed**) after a deliberate, per-name decision\n> to drop replay support at `planned_removal_version`.\n\n---\n\n## 9. Required tests\n\nAny diet PR (and the umbrella #2681 work) must add/keep:\n\n1. **Duplicate-active-alias guard.** A test asserting that no name in\n `HIDDEN_COMPATIBILITY_TOOLS` or `DEPRECATED_ALIASES` appears in\n `DEFAULT_ACTIVE_NATIVE_TOOLS` or `ARCEE_FIRST_TURN_NATIVE_TOOLS`, and that no\n two active entries resolve to the same underlying tool implementation.\n\n2. **Tool-search exclusion test.** Assert that hidden-compat and deprecated names\n are absent from the tool-search-discoverable pool while remaining present in\n the registry (dispatchable).\n\n3. **Replay / dispatch tests.** For each manifest name, calling it still\n executes and returns the same result as its canonical twin. Deprecated names\n additionally assert the replacement note is present **in result metadata** and\n absent from the catalog/prefix. Hidden-compat names assert **no** added\n notice.\n\n4. **Golden active-block byte test.** A snapshot test pinning the byte\n serialization of the first-turn active tool block, asserting it is identical\n across Plan / Agent / YOLO (native head) and stable run-to-run — enforcing the\n `tool_catalog.rs:169-196` invariant. The golden updates **only** as a\n reviewed, deliberate one-time edit when the diet lands.\n\n5. **Subagent guardrail test.** Assert only `agent` is registered as a\n model-visible subagent tool and that hidden/legacy names from\n `subagent/mod.rs` are not advertised.\n\n6. **Leaf-worker test.** Assert subagent tool catalogs exclude `agent` and\n retired legacy lifecycle names.\n"
|
||
}
|
||
]
|
||
},
|
||
{
|
||
"id": "pet",
|
||
"slug": "pet",
|
||
"sources": [
|
||
{
|
||
"path": "docs/PET_MODE.md",
|
||
"sha256": "33b6c3dae527ca950b9dab442741e198bf3f1bd78691da929aedff2628732cab",
|
||
"content": "# Pet mode\n\n> 阅读简体中文版:[zh_hans/PET_MODE.md](zh_hans/PET_MODE.md)\n\nPet mode turns the terminal you already use into a glanceable status view. A\nwhale fills the main view and shows what the session is doing: idle, working,\nwaiting for you, done, or failed. The message box stays at the bottom, so you\ncan keep typing. Replies and agents are one key away.\n\nEach terminal runs its own pet mode for its own session. With several terminals\nopen, you can look across them and see which one needs you, which is still\nworking, and which has finished.\n\n```text\nCodewhale ● working\n (the whale)\n ●\nF5 replies & agents · /pet off standard view\nCtrl+] view: whale\n╭──────────────────────────────────────────────────────────╮\n│❯ Type a follow-up… │\n╰──────────────────────────────────────────────────────────╯\n ● auto Shift+Tab to change work (Tab) 2 agents\n```\n\n## Turn it on\n\n```text\n/pet on # pet mode becomes the main view\n/pet off # back to the standard view\n/pet # toggle\n/pet status # on or off, whether the habitat is open, and companion identity\n```\n\n`/pet on` and `/pet off` are saved as the `pet_mode` setting (also in the\nsettings editor), so a new session starts the way you left it. Pet mode never\nstarts a turn, calls a model, or changes your draft, mode, or approvals; it only\npresents the session you are already running.\n\n## What the whale tells you\n\nThe first line of the view and the mark under the whale always carry the state\nas a mark and a word, so it reads without colour or motion:\n\n| State | Mark and word | Meaning |\n| --- | --- | --- |\n| Ready | `○ idle` | Nothing running. |\n| Working | `● working` | A turn is running, including tools and verification. |\n| Needs you | waiting or approval phase | The session is waiting for an approval or an answer. Motion is reduced while it waits. |\n| Done | `✓ Done` | The last turn completed. |\n| Failed | `✕ failed` | The last turn ended in failure. |\n\nThe status bar underneath keeps its usual live readouts, such as the agent\ncount (`2 agents`) and the work bar. Transient notices, such as a retry\n(`Retry attempt: transport 1/3; network error; waiting 0.94s`), show on the\nsecond line.\n\n## Views\n\n`Ctrl+]` switches the pet to its next view while the pet main view is showing.\nTerminals with the kitty keyboard protocol can also send `Ctrl+Tab` for the\nnext view and `Ctrl+Shift+Tab` for the previous view. Terminal.app uses those\nTab chords to switch terminal tabs; use `Ctrl+]` there. The\nfooter's second line names the current view, `Ctrl+] view: whale`, and\nclicking it also moves to the next view. `/pet avatar <key>` jumps straight to\none.\n\n| View | What it draws | When its requirement is missing |\n| --- | --- | --- |\n| `whale` | The companion's live dot whale. | Shows `cove` (or `braille`) until the companion is reachable (`codewhale pet serve`) and this terminal's session is its source (`/pet source`). |\n| `cove` | The local half-block whale, without the translucent cove scenery. | Needs colour: `braille` is shown under `NO_COLOR`, a 16-colour terminal, or an unknown background. `COLORTERM=truecolor` enables true colour; `CODEWHALE_APPEARANCE=dark` or `light` selects a known background. |\n| `braille` | The local whale in coloured Braille dots. | Shows only the state words under `CODEWHALE_ASCII_SAFE` or when the art area is smaller than 16x8 cells. |\n| `pixels` | The companion's whale as a kitty-graphics image. | Needs kitty graphics, synchronized output, a known cell size in pixels, and `CODEWHALE_PET_GRAPHICS` not set to `braille`; otherwise shows `particles`. Terminal.app has no kitty graphics. |\n| `particles` | The companion's Braille particle grid. | Shows the resting whale and `offline` while the companion is unreachable. |\n| `whale-girl` | The bundled sprite avatar. | Needs colour, as `cove` does; otherwise shows a one-ink ASCII form. |\n| Installed avatar packs | Each pack, in catalog order. | Shows `particles` while the pack's atlas loads. |\n\nThe order is `whale → cove → braille → pixels → particles → whale-girl →`\ninstalled packs, then back to `whale`. A view that cannot draw is never\nskipped: it shows its fallback, and the line under the title says what is\nmissing, for example `pixels needs kitty graphics · showing particles`.\n\n## Replies and agents on demand\n\nThe main view stays quiet. Press `F5`, or run `/pet inspect`, to open the\ninspector over it:\n\n- the whale and an **Agents** list on top, with each agent's state and usage;\n- a **Reply** pane below, which streams the current answer and shows terminal\n errors from the same session.\n\n| Key | Effect |\n| --- | --- |\n| `F5` or `/pet inspect` | Open replies and agents. |\n| `Ctrl+]` / `Ctrl+Tab`, `Ctrl+Shift+Tab` | Next or previous pet view (main view only). The Tab chords require a terminal that sends them; use `Ctrl+]` in Terminal.app. |\n| `F6` | Turn the companion's sound on or off. |\n| `F8` | Open the appearance studio in your browser. |\n| `F9` | Open the native Codewhale Pet window (macOS). |\n| `Esc` | Back to the message box. Your draft, the transcript, and any running turn are untouched. |\n| `Tab` | Switch between the Reply and Agents panes. |\n| `Up` / `Down`, `PgUp` / `PgDn`, `Home` / `End` | Scroll the focused pane. |\n| `Enter` (Agents pane) | Open the selected agent's transcript. |\n| `c` | Copy the last finished reply, through the same path as `/copy`. |\n\n`F5`, `F6`, `F8` and `F9` work on the main view and in the inspector. On a Mac\nkeyboard the top row sends media keys unless Fn is held, so F-key hints\nthroughout the TUI, including help, read `fn+F5`, `fn+F8` and so on; press\nFn with the key. ASCII-safe hints keep that prefix. A successful `F8` or `F9` says\n`habitat open`; a failed launch shows its own reason.\n\n## Several terminals, one pet each\n\nPet mode is per terminal. Start Codewhale in two terminals and run `/pet on` in\neach: each whale follows its own session, so one can show `✓ Done` while the\nother shows `● working`. Nothing is mixed between sessions.\n\nBehind the scenes, all terminals on a machine share one local companion\n(`codewhale pet serve`, started automatically when a view needs it). The\ncompanion keeps the pet's identity, animation clock, saved recording, optional\nsound, and the appearance and window views. It follows one session at a time,\ncalled the source:\n\n- The first saved terminal session to attach becomes the source.\n- Later terminals are views of the same companion. They still show their own\n state word and mark, but the companion's finer activity detail inside the\n whale body (reading, editing, searching and so on) appears only in the\n terminal whose session is the source.\n- Run `/pet source` in a terminal to make its session the source.\n\nThe companion exits after ten minutes with no view, producer, or audio client.\nThe next view starts it again.\n\n## Desktop and app-server sessions\n\nDesktop and HTTP app-server turns can feed the same companion. The Runtime host\nattaches only when the companion is already running; it never starts one. Start\n`codewhale pet serve` or open a pet view first. A new turn can follow an idle\ncompanion, but the owner refuses automatic selection while the followed session\nhas a live producer. An active TUI keeps its source.\n\nOnly turn, response, operation, agent and waiting metadata crosses this link.\nPrompts, replies, reasoning, tool names and arguments remain in the Runtime.\nThe connection lasts for 12 seconds after completion, then its lease expires.\nAn event gap clears the observed turn and releases the connection after the same\ninterval unless a new turn starts.\nThis presentation data does not enter model requests or affect the KV cache.\n`serve --acp` and TUI background-task threads do not feed this producer.\n\n## Commands\n\n| Command | Effect |\n| --- | --- |\n| `/pet`, `/pet on`, `/pet off` | Toggle, enable, or disable pet mode for this terminal. |\n| `/pet inspect` | Open replies and agents (same as `F5`). |\n| `/pet status` | Show on or off, habitat open or closed, the companion's identity and tick, the source session, and measured output counters. |\n| `/pet source` | Make this terminal's session the companion's source. Needs a saved session. |\n| `/pet appearance` | Open the appearance studio (palette, brightness, dot size, glow) in your browser, on loopback. Same as `F8`. |\n| `/pet window` | Open the native Codewhale Pet window, if the macOS app is installed. Same as `F9`. If it is not installed, Codewhale says to build or install it. |\n| `/pet sound on`, `/pet sound off` | Turn the companion's sound on or off. Same toggle as `F6`. |\n| `/pet avatar [key]` | List the views, or choose one: `whale`, `cove`, `braille`, `pixels`, `particles`, `whale-girl`, and any avatar packs you have installed. |\n| `/pet action <name>`, `/pet view <name>`, `/pet action live` | Preview a named pose or view of the chosen avatar, or return to live. |\n| `/pet export` | Save a replay of the pet's recording to this saved session's artifacts. |\n\nAppearance settings are shared by every view of the companion and never change\nthe session.\n\n## Sound\n\nSound is off by default. `/pet sound on` (or `F6`) asks the companion for its\nsingle audio output, played through FFmpeg's `ffplay`. The status reads\n`Sound on`, `Sound off`, or `Sound paused` when another view holds the output.\nIf `ffplay` or the audio device is unavailable, Codewhale says so and sound\nstays off.\n\n## Fallbacks\n\n- **Small terminals.** The whale art is drawn only when it has room; otherwise\n the mark, word, hint line, and message box remain.\n- **`NO_COLOR`.** Colour is left to your terminal; the marks and words still\n carry the state.\n- **`CODEWHALE_ASCII_SAFE=1`.** Marks, borders, and the pointer use ASCII\n (`o idle`, `|`, `>`), and the animated art is replaced by the mark and word.\n- **Reduced motion.** Follows the shell's motion setting and shows a still pose.\n- **`CODEWHALE_PET_GRAPHICS=braille`.** Keeps the `pixels` view on text: it\n shows `particles` and says why.\n- **No companion.** If the companion cannot be reached, the view says so and the\n state word and mark continue to work, because they come from your session.\n `/pet on`, `F8`, `F9` and the companion commands such as `/pet appearance`\n start it again.\n\n## Privacy\n\nThe companion is presentation only. It has no agent, prompt, provider, or\nexecution API.\n\n- What reaches it from a session is a short allowlist of event kinds, ids, and\n outcomes (for example \"a read started\", \"turn completed\"). Prompt text,\n reasoning, tool arguments, results, and paths are not sent, and nothing about\n the session's model context changes.\n- It listens only on `127.0.0.1`. Every request needs a random token kept in a\n private (`0600`) file in `~/.codewhale/pet-shared`; browser access also checks\n `Host` and `Origin`. Set `CODEWHALE_PET_HOME` to keep it elsewhere.\n- Nothing is uploaded. Your recording and saved world stay on your machine.\n\n`CODEWHALE_PET_PORT` chooses the starting loopback port (default 4633).\n\n## Known limitations\n\n- **No \"reply ready\" cue yet.** When a turn finishes, the whale shows `✓ Done`\n and the footer hint stays the same static line. There is no separate\n notification, badge, or sound that says a reply is waiting. Glance at the\n state word, or press `F5` to read the reply.\n- **Agent rows can lag.** While agents run, the Agents list in the inspector is\n refreshed when the turn ends, not on every change. The agent count in the\n status bar and the work bar are live; treat the inspector's rows as a\n snapshot until the turn finishes.\n- **One source at a time.** The companion's activity detail follows one session.\n Other terminals show their own state word and mark, not the detail.\n- **`/pet window` is macOS only** and needs the Codewhale Pet app.\n- **Sound needs `ffplay`** and one terminal holds the output at a time.\n- **The chosen view is not saved.** It lasts for this run of Codewhale,\n including session switches; a restart begins on `whale`.\n- **No view cycling inside the inspector.** Press `Esc` first; `Ctrl+]` cycles\n only on the main view.\n- **`Ctrl+]` cannot open the work bar** while the pet main view is showing. Use\n `/pet off` first.\n- **The browser studio and the macOS window are not views in the ring**, because\n each press would start a process. Use `F8` and `F9`.\n- **No sixel or iTerm2 images.** The `pixels` view draws only through kitty\n graphics.\n\nSee also [Keybindings](KEYBINDINGS.md), [Modes](MODES.md), and the design notes\nin [`design/DOT_WHALE_PET.md`](design/DOT_WHALE_PET.md) and\n[`../pet/SHARED.md`](../pet/SHARED.md).\n"
|
||
}
|
||
]
|
||
},
|
||
{
|
||
"id": "subagents",
|
||
"slug": "subagents",
|
||
"sources": [
|
||
{
|
||
"path": "docs/SUBAGENTS.md",
|
||
"sha256": "8fdaef0ecf37457e4b6e3a0d84f64ed21963ef7c6c8a17bd2fb5efb3d7004acd",
|
||
"content": "# Fleet and sub-agents\n\n> 阅读简体中文版:[zh_hans/SUBAGENTS.md](zh_hans/SUBAGENTS.md)\n\nFleet manages saved models and role assignments for these same sub-agents.\nUse `agent` for an individual assignment and `workflow` for phases with\ndependencies and completion checks. See [Workflow authoring](WORKFLOW_AUTHORING.md)\nfor plans that use the Fleet model shortlist.\n\nFleet roles are the user-facing vocabulary for delegated work: a parent\nlaunches a focused `general`, `explore`, `planner`, `reviewer`, `implement`,\n`test`, or `advisor` through `agent` and gets back an `agent_id`, declared\ndeliverables, and effective limits while the worker runs. The default receipt is\ncompact; request addressed detail when you need the transcript handle or ledger.\nThe internal runtime type is `FleetRole` (formerly\n`SubAgentType`); the older role spellings (`worker`, `scout`, `plan`,\n`review`, `builder`, `verifier`, `consultant`, `oracle`, …) remain accepted only as a persisted/deserialize\ncompatibility adapter during v0.9.x. New prompts and config should use fleet\nnames.\n\nChild assignments run through the same `Engine::run_turn` planner, executor,\nSession and approval inbox as ordinary turns. The private captured ChildGrant\nnarrows tool actions, shell, workspace, network and descendant depth at the final\nexecution boundary. The existing SubAgentManager still owns launch slots,\ncoordination, worker checkpoints and terminal delivery; it does not run another\nmodel/tool loop. Work uses the full explicit owner limit. At the limit the host\npreserves a partial-result receipt without an extra model reporting request.\n\nTransient provider failures use Core's dispatch and retry boundaries. An exhausted\nbudget or interrupted request preserves the recorded checkpoint and a continuation\nhandle. Each actually dispatched attempt carries its original session, route and\nsource identity: provider usage is billed once, then projected to the worker\nledger. A successful response without usage and an unknown request outcome retain\ndistinct coverage gaps; neither is priced as zero. For work that must survive\nprocess restarts, sleep or remote execution, use Fleet or a Workflow-backed run.\n\nSub-agents inherit the parent's permitted tool registry, including `agent`\ncoordination. Spawning obeys one absolute depth ceiling: the root is depth 0,\nits child is depth 1, and a child at `max_spawn_depth` cannot spawn again.\nThe default has no product depth cap. Explicit operator, role and saved-profile\nlimits still narrow descendant authority; model requests cannot choose caps. Recovery and transcript\nforking retain the source's position and bounds; they do not buy another\ngeneration. The removed `agent_open`/`agent_eval`/`agent_close` lifecycle tools\nare absent from every registry.\n\nHealthy children continue after an ordinary parent response. Their completion\nreturns through the existing Engine inbox and can wake the parent for another\nnormal turn. Explicit interruption or cancellation remains authoritative.\n`detached: true` additionally opts a subtree out of parent-turn cancellation;\nit does not remove child budgets or the headless host's deadline.\n\nThis doc covers roles and individual worker controls. Use `workflow` to coordinate\nmultiple assignments through the same worker runtime; see the per-role prompts in\n`crates/engine/src/tools/subagent/mod.rs` (`*_AGENT_INTRO`) and the in-line\ntool description.\n\n## Role taxonomy\n\nThe `type` field on `agent` selects a fleet posture for the child\n(`agent_type` is accepted as a compatibility alias). Each role is a distinct\nstance toward the work — not just a different label.\n\n## Maintainer posture\n\nSub-agents help Codewhale move faster, but the parent agent still owns the\nmaintainer decision. Use children to gather evidence, review patches, and run\nverification while keeping the community posture in\n[`AGENT_ETHOS.md`](AGENT_ETHOS.md): issues are open intake, PR gates are\nreview-load controls, and harvested work needs clear contributor credit.\n\nWhen a child reviews community work, the parent should still inspect the PR\ndiff, linked issues, tests, and CI before merging, harvesting, closing, or\ndeferring it. A sub-agent's result is a working set, not a substitute for\nstewardship.\n\n| Role | Stance | Writes? | Network? | Shell posture | Typical use |\n|---------------|----------------------------------------|---------|----------|---------------|----------------------------------------------|\n| `general` | flexible; do whatever the parent says | yes | yes | yes | the default; multi-step tasks |\n| `explore` | read-only; map the relevant code fast | no | yes | bounded inspection | \"find every call site of `Foo`; check the PR with gh\" |\n| `planner` | analyse and produce a strategy | no | yes | read-only probes | \"design the migration; don't execute\" |\n| `reviewer` | read-and-grade with severity scores | no | yes | bounded inspection | \"audit this PR for bugs\" |\n| `implement` | land a specific change with min edit | yes | yes | yes | \"rewrite `bar.rs::Foo::bar` to do X\" |\n| `test` | run tests / validation, report outcome | no | yes | bounded verification (no writes) | \"verify the diff with the bounded test checks; report PASS/FAIL\" |\n| `advisor` | short-lived, high-reasoning counsel | no | yes | none | \"what are we missing in this design?\" |\n| `custom` | explicit narrow tool allowlist | inherits | inherits | inherits | hand-picked tools on the parent's posture |\n\nA role's default is what the role *intends*, and the parent's effective\nposture is always the ceiling (a child never widens beyond its parent).\nRead-only roles withhold **workspace writes** by intent; nothing else is\ntaken away by default — every role keeps network reads, and `custom`\ninherits the parent's write/network/shell posture and is narrowed only by\nits explicit tool list or the spawning call. The focused worker's header\nstates the effective posture (`scout · read-only · network · read-only\nshell`) from the runtime's own permission snapshot.\n\n**Delegation moves work, never authority.** A read-only parent may delegate\nto `implement`, but the child's effective write, network, shell, and tool\npermissions remain within the parent's live posture. Inspection roles can use\nthe classified read-only shell surface and, where native enforcement is\navailable, the explicit read-only analysis mode described below. A different\nrole name or `read_only` flag cannot grant a shell tool the caller lacks.\nThe clamp (`ChildAuthority::clamp` in `fleet/exact.rs`) intersects every field\nwith the narrower side. Deny lists are unioned, so\n`inherit_disallowed_tools: false` cannot drop any operator or ancestor denial.\nResuming a saved worker intersects its saved posture with the current caller's\nposture again. This containment is pinned by\n`a_read_only_parents_delegation_never_widens_authority` in\n`crates/engine/src/fleet/exact.rs` tests.\n\nInside the process, the resolved authority is one object —\n`ChildGrant` in `crates/agent-contract/src/worker_profile.rs`: `files`\n(none/read/write), `shell` (none/inspect/verify/full), `network`, `desktop`\n(never granted to a child), the named tool `surface`, the caller's explicit\n`scope`, and remaining `spawn` depth. A role is a preset over that object\n(`ChildGrant::for_role`); `ChildGrant::resolve` intersects it with the\nparent-derived profile. The child's tool catalog, its dispatch refusals, and\nits capability envelope all read the same fields — a tool that is visible is\ncallable, and a tool that is denied never appears.\n\nThe session's **permission posture** applies inside every child exactly as\nit applies to the parent turn: under Auto-Review the same deterministic\nfloor and one-shot model guardian decide a worker's held calls (never a\nprompt; an unavailable guardian denies, fail closed); under Ask a held call\nthe role cannot delegate is raised as an approval prompt in the parent's\nUI and the worker waits visibly (`waiting for user`), or is denied with the\nreason on hosts that cannot prompt; Full Access applies\nexactly as it does to the parent turn, so a call the person granted is not\nstranded by the Auto-Review floor. Each decision nobody was prompted for is a\none-line note in that worker's transcript (visible when it is focused) and\nan audit-log record. See `docs/MODES.md`.\n\nEach role's full system prompt lives in\n`crates/engine/src/tools/subagent/mod.rs` (search for\n`*_AGENT_INTRO`). The prompt prefix loads automatically when the\nchild agent boots; the parent's assignment prompt becomes the first\nturn's user message.\n\n## Context forking\n\n`agent` starts fresh by default: the child gets its role prompt plus the\ntask you pass. Use `fork_context: true` when the child should continue from\nthe parent's current request prefix instead. (`fork_context` is not in the\nadvertised schema — it stays parse-accepted for compat callers, and\nauto-forking for read-only roles continues unchanged.) In fork mode the runtime keeps the\nparent prefill/prompt prefix byte-identical where available, appends a\nstructured state snapshot, then adds the sub-agent role instructions and task\nat the tail. That preserves DeepSeek prefix-cache reuse while giving the child\nthe context needed for continuation, review, summarization, or compaction work.\n\nUse fresh sessions for independent exploration. Use forked sessions when the\ntask depends on decisions, files, todos, or plan state already in the parent\ntranscript.\n\nForked state shows the parent's To-do snapshot — the sole Work surface, written\nby `todo_write`. The child's `<codewhale:fork_state>` block carries the bounded\nbody rendered by `crates/engine/src/todo_snapshot.rs`, so a fork continues from the\nparent's real progress position rather than a paraphrase. That To-do section is\nresolved when the spawn happens, so a `todo_write` earlier in the same parent\nturn is included.\n\n**The list is shown once, at that spawn, and never re-sent.** No sub-agent\nrequest re-states a To-do list, and neither does a parent request. Each agent\nkeeps its own private list (#4810); what it knows about that list comes from its\nown `todo_write` calls (each sends the whole list) and the short receipts they\nreturned, which are ordinary messages in its own transcript. A worker therefore cannot read or write a parent's or a\nsibling's list, and a forked child cannot mutate the snapshot it was handed or\nkeep reading later parent changes.\n\nThat same private list is what the child's in-transcript card shows. A\ndelegate card renders a bounded projection of **its own** agent's To-do — the\nsettled/total count, the in-progress item always included, up to three rows, and an\nexplicit `… +N more` when the bound elides the rest — built by\n`card_todo_projection` from the same snapshot, priority order, and sanitizer the\nmodel-facing body uses. A card only ever consumes an envelope whose `agent_id`\nmatches it, so a parent's list never appears under a child and no sibling's list\nappears under another. An agent that has stated no work shows no To-do rows at\nall rather than a placeholder task, and a terminal card keeps the last snapshot\nits agent actually published. Fanout cards stay a dot grid and do not show child\nTo-do: with many workers behind one card there is no truthful place to hang a\nsingle list. A child To-do appears only when the runtime already represents\nthat child as its own delegate card.\n\nThe durable Runtime ledger (projected through fleet task status) still owns\nlifecycle state. `update_plan` is no\nlonger reachable by a model: `model_visible()` returns `false`\n(`crates/engine/src/tools/plan.rs:408-413`), so it is filtered out of the API tool\nlist and never appears to a child. It survives only to replay older transcripts.\nStrategy that used to go there now goes in the response body, and lifecycle\nstate goes in `todo_write`.\n\n## Worktree isolation\n\nFor parallel edit lanes, launch the child with `worktree: true`. Codewhale\ncreates a fresh git worktree and branch for that child, runs the child from the\nisolated checkout, and reports the resulting workspace/branch in the returned\nsession projection and worker record. By default the branch is\n`codex/agent-<name>-<id>` and the checkout lives beside the parent repo under\n`.codewhale-worktrees/`, so the parent checkout stays clean.\n\nIsolation is not write authority. A prompt-only start with no role/profile or\nwrite declaration remains read-only, and read-only roles need no write scope.\nExplicitly selected write-capable roles such as `general` and `implement`\ninherit the parent's write ceiling and default to the workspace\n(`write_roots: [\".\"]`) unless narrowed. Prefer explicit, disjoint `exact_files`\nor `write_roots` for parallel work; `coordination_contracts` can reserve named\nshared contracts. If only `deliverables` supplies a writer's scope, those files\nbecome the exact-file scope.\n\n`write_authority` is optional typed narrowing: `read_only` admits no write\nscope, `workspace_write` uses the shared checkout, and `worktree_write`\nrequires actual worktree isolation. Incompatible role/scope declarations fail\nbefore admission. Active overlapping shared claims fail before mutation; a\nreal isolated worktree may proceed in parallel. A `custom` role requires\nexplicit write-capable authority to claim writes; otherwise it starts\nread-only.\n\nRead-only is not file-only. A normally write-capable agent narrowed with\n`write_authority: \"read_only\"` keeps the existing classifier-bounded inspection\nshell when its parent permits it: Git history/status, search, and allowed `gh`\nlog reads, not arbitrary commands or test programs. Workflow read-only steps\nlikewise use the effective role's tools instead of imposing a second File-only\nlist; a `test` role retains its bounded verification interface. Explicit tool\nallowlists, `deny_all_tools`, parent denials, network limits, and mutation checks\nstill apply. A parent without shell access cannot delegate it.\n\nOptional fields:\n\n- `worktree_branch`: exact branch to create.\n- `worktree_base`: git ref to branch from; defaults to `HEAD`.\n- `worktree_path`: exact checkout path. Relative and absolute paths must stay\n under the default sibling `.codewhale-worktrees/<repo>/` root (symlinks are\n resolved before the check). Any worktree request keeps the approval card,\n even for a read-only role.\n\n`cwd` may be combined with `worktree`: the requested directory becomes the\ndiscovery anchor the repo root (and the new checkout) is resolved from\n(`prepare_child_workspace`). Without `worktree`, `cwd` remains the manual\nescape hatch for an already-created directory inside the parent workspace.\n\n### File deliverables and edit claims\n\nPut required files in `deliverables`; keep the human outcome in\n`expected_artifact`. For example, call `agent` with:\n\n```json\n{\n \"action\": \"start\",\n \"type\": \"implement\",\n \"prompt\": \"Summarize the local routing evidence in reports/routing.md.\",\n \"exact_files\": [\"reports/routing.md\"],\n \"deliverables\": [\"reports/routing.md\"],\n \"expected_artifact\": \"A concise report with source references and open gaps\"\n}\n```\n\nAt most 16 repo-relative file paths are accepted. Absolute paths, traversal,\nrepository metadata paths, and symlink traversal are refused. Completion checks\neach file against the admitted scope and reports its path, status, and byte\ncount where available. The terminal statuses are `present`, `missing`, `empty`,\n`not_file`, `out_of_scope`, `invalid_path`, and `unreadable`.\n`present` means a nonempty regular file exists; it does not prove the report is\ncorrect or that tests passed.\n\nA missing or invalid required file sets `verification.status` to\n`deliverable_missing` with the individual verdicts. Successful file checks can\nproduce `deliverables_present`; they do not turn a child self-report into an\nindependent quality gate. The completion notice includes the actual verdicts,\nincluding when a worker fails or exhausts a budget.\n\nEdit claims are checked separately against the spawn-time git HEAD and dirty\nfile contents. Explicit changed-file declarations can produce\n`claim_mismatch` when a claimed file did not change, or when a successful\nbounded write receipt changed a file the child did not declare. A peer's\nchange inside a worker's broad scope is not enough to attribute that write to\nthe worker. `path:LINE` and `path:LINE-LINE` evidence citations, including\nsentence punctuation and Markdown links, never count as edit claims.\n\n### Read-only shell commands\n\nScout, reviewer and planner agents, agents narrowed with\n`write_authority: \"read_only\"`, and durable Fleet workers with a read-only\nshell grant all judge `bash` calls by the same read-only grammar:\n\n- inspection programs: `ls`, `pwd`, `cat`, `head`, `tail`, `wc`, `which`,\n `stat`, `file`, `du`, `df`, `grep`, `rg`, `fd`, `find` without `-exec` or\n `-delete`, and `sed -n <range>p`;\n- `git status`, `log`, `diff`, `show`, `ls-files`, `blame` and `grep`,\n optionally after `-C <dir>` or `--no-pager`;\n- the text filters `sort`, `uniq`, `cut`, `tr` and `comm`, and literal\n `echo`/`printf`;\n- with a network grant, `gh` issue/pr/release/repo/run/workflow view or list\n reads.\n\n`npm view` is refused even with a network grant: npm configuration can select\nexecutable helpers, so package metadata reads are outside this bounded grammar.\n\nAdmitted commands can be joined with `|`, `&&`, `||` and `;`, for example\n`git diff HEAD && echo '=== FILES ===' && ls -la`. A leading `cd <dir> &&`\nsets the working directory, and that directory must be inside the workspace.\nThe only redirects are `2>/dev/null`, `>/dev/null` and `2>&1`. Quoted text is\ndata, so `rg 'a && b' src` is one search. Other redirects, `$` or backtick\nexpansion, subshells, backgrounding, inline environment assignments, and any\nother program (such as `python`, `awk`, `jq` or `cargo`) are refused. Options\nand path operands are still checked, and each `gh` or `npm` read needs the\nnetwork grant wherever it appears in the command.\n\nA refused command comes back to the agent as an error result that names the\nrule, for example\n`[shell.readonly.command] program: `touch` is not a read-only inspection command`,\nfollowed by what the agent can do instead. The agent keeps working, and\nrepeated refusals without progress end it as failed rather than completed.\n\n### Reading beside a writer\n\nRead-only tools and classifier-approved shell reads can run while a peer owns\na shared write claim. For arbitrary analysis code, call `bash` with explicit\n`read_only: true`:\n\n```json\n{\n \"action\": \"run\",\n \"read_only\": true,\n \"command\": \"python3 -c \\\"import sqlite3; db = sqlite3.connect('file:cache/index.db?mode=ro', uri=True); print(db.execute('SELECT name FROM sqlite_schema').fetchall())\\\"\"\n}\n```\n\nThis mode requires native filesystem read-only isolation. Outbound network\nfollows the caller's own shell and is on only when the session runs in Full\nAccess, its sandbox allows network, its `[network]` policy does not default to\ndeny, and the agent keeps its network grant; in Ask or Auto-Review, where a\nnetworked command needs an approval this mode never asks for, it stays offline.\nWith network on, `curl` and HTTPS clients work. `read_only` blocks local file\nwrites, not requests that change remote services. On macOS the mode then reaches\nhosts over IPv4, the system DNS resolver, and a local proxy named in\n`HTTP_PROXY`, `HTTPS_PROXY` or `ALL_PROXY`, but not IPv6, another port on this\nmachine's own addresses, or a local socket. On Linux it shares the host network\nnamespace, including loopback and abstract unix sockets, as a networked\nworkspace-write shell does. A networked run records `sandbox_network: true` in\nits tool-result metadata. It accepts only foreground `run` with `command`,\noptional `cwd`, and `timeout_ms`. Background or interactive modes, stdin,\nsandbox escalation, and external execution backends are incompatible. If native\nenforcement is absent or cannot be prepared, the call refuses before executing\nthe command; the flag never falls back to trusting a promise that the code only\nreads. Existing role, tool, and ancestor policy restrictions still apply.\n\nFor a write refusal outside your own scope, `agent(action=\"claim\", ...)` can\nadd permitted paths to your claim. It cannot take a live peer's claim. Wait for\nthat peer, choose disjoint bounded writes, or use a separate worktree for code\nthat needs writes. `action=\"release\"` only clears claims whose owners are no\nlonger live; it is not a way to unlock another running worker's files.\n\n## Delegation briefs\n\nThe parent should pass a compact brief instead of a loose paragraph. Use the\nstructured `dependencies` and `acceptance` arrays for bounded prerequisite facts\nand observable checks; keep the focused objective in `prompt`. Do not copy raw\nparent reasoning or an unbounded transcript.\n\nDefault shape for the brief — a plain sentence beats the template when the\ndelegation is trivial:\n\n```\nQUESTION:\nSCOPE:\nALREADY_KNOWN:\nEFFORT: quick | medium | thorough\nSTOP_CONDITION:\nOUTPUT: VERDICT, EVIDENCE, GAPS, NEXT\n```\n\n`scout` briefs request read-only investigation with enough evidence to answer\nthe question reliably. Let the task determine the depth of investigation.\nDo not repeat `ALREADY_KNOWN` work unless evidence contradicts it. Builder and\nrepair-style briefs should use checkpoints before scope expansion or after\nrepeated failures, then adapt and continue the remaining authorized work.\n\nGood delegation prompt examples:\n\n```text\nQUESTION: Does PR #N introduce release-risk behavior around provider routing?\nSCOPE: PR #N diff, linked issue, provider routing tests, docs/PROVIDERS.md.\nALREADY_KNOWN: Branch is <release-branch>; workspace version is <live-version>.\nEFFORT: medium\nSTOP_CONDITION: Return once you have either one BLOCKER/MAJOR issue or enough evidence for no MAJOR+ issues.\nOUTPUT: VERDICT, EVIDENCE with file:line refs or PR refs, GAPS, NEXT.\n```\n\n```text\nQUESTION: Where is the child-agent prompt assembled?\nSCOPE: crates/engine/src/prompts*, crates/engine/src/tools/subagent/*.\nALREADY_KNOWN: The model-facing launcher is only `agent`; do not look for removed lifecycle tools.\nEFFORT: quick\nSTOP_CONDITION: Stop after identifying the prompt source files and the function that wraps assignment text.\nOUTPUT: VERDICT, EVIDENCE, GAPS, NEXT.\n```\n\n```text\nQUESTION: Is the focused prompt/subagent test filter valid, and what fails if not?\nSCOPE: cargo test -p codewhale-tui --lib --locked prompt; subagent filter if needed.\nALREADY_KNOWN: Do not fix failures; capture exact command, exit code, and first relevant assertion.\nEFFORT: medium\nSTOP_CONDITION: Stop after one clean PASS or one reproducible failing assertion with command evidence.\nOUTPUT: VERDICT, EVIDENCE, GAPS, NEXT.\n```\n\n### When to pick which role\n\n- **`general`** — when the task is \"do this whole thing\", not \"go\n look\", \"design\", or \"verify\". This is the right default; reach for\n a more specific role only when the posture matters.\n- **`explore`** — when the parent needs evidence before deciding what\n to do next. Scouts are cheap and fast; open 2–3 in parallel\n for independent regions.\n They should orient first: confirm the project root, read relevant\n `AGENTS.md`/`README.md` guidance in unfamiliar trees, search only the\n likely scope, and return `path:line-range` evidence instead of a narrative\n tour. The role name to use is `explore`.\n- **`planner`** — when the parent has an objective but no executable\n decomposition. Planners write artifacts (`todo_write` items,\n strategy in the response body) but don't carry them out.\n- **`reviewer`** — when there's already a change and the parent wants\n it graded. Reviewers run under read-only posture, so the runtime\n refuses patch attempts — describe the fix in the finding and the\n parent dispatches a builder when the verdict is \"fix it\".\n- **`implement`** — when the change is already specified and just\n needs to land. Builders stay tightly scoped: minimum edit, no\n drive-by refactoring, run a quick verification before handing back.\n- **`test`** — when the parent needs an authoritative pass/fail\n on the test suite or other validation. The verifier posture never\n writes — the runtime refuses fix attempts — so capture the failing\n assertion + stack and put fix candidates under RISKS for the parent\n to dispatch. Shell is clamped to the bounded built-in verification\n surface: Run tests/verifiers (pass `cwd` when the checks live in a\n subdirectory), Git fetch for remote refs, Git merge_tree for merge\n results. The write ceiling is read-only and unbounded shell forms\n are refused (#5186). A refused probe is reported to the parent,\n never worked around (#6298).\n- **`advisor`** — when the operator wants a high-leverage second opinion\n before cheaper execution continues. Consultants read enough to ground a\n recommendation; their grant carries no writes and no shell, so the\n runtime refuses both. `oracle` and\n `consultant` remain accepted only when loading older requests or persisted\n records; new prompts, receipts, and UI use `advisor`.\n- **`custom`** — only when the parent needs to constrain the tool\n set explicitly. Pass the allowlist via the `allowed_tools` field\n on legacy/internal sub-agent records; the model-facing `agent` tool keeps the\n public schema intentionally small.\n\n### Aliases\n\nThe model can spell each role multiple ways:\n\n| Canonical | Aliases |\n|---------------|------------------------------------------------------------------|\n| `general` | `worker`, `default`, `general-purpose`, `general_purpose` |\n| `explore` | `scout`, `explorer`, `exploration` |\n| `planner` | `plan`, `planning`, `awaiter` |\n| `reviewer` | `review`, `code-review`, `code_review` |\n| `implement` | `builder`, `implementer`, `implementation` |\n| `test` | `verifier`, `verify`, `verification`, `validator`, `tester` |\n| `advisor` | `consultant`, `oracle` (compatibility input only) |\n| `custom` | (none; explicit `allowed_tools` array required) |\n\nAll matching is case-insensitive. Unknown values produce a typed\nerror listing the accepted set, so the model can self-correct on\nthe next turn.\n\n## Concurrency cap\n\nUp to **64** sub-agents run concurrently by default (`DEFAULT_MAX_SUBAGENTS`),\nconfigurable via `[subagents].max_concurrent` in `~/.codewhale/config.toml` up to\nthe hard ceiling of **128** (`MAX_SUBAGENTS`). The session admits a bounded\nqueue of up to **1024** running plus queued sub-agents by default\n(`MAX_SUBAGENT_ADMISSION`, `crates/tui/src/config/subagent_limits.rs:21`), so a turn can\nrequest broad fan-out and let the manager drain it without creating an\nunbounded population.\n\nBy default every admitted child may start immediately — there is no artificial\nthrottle beyond the rate-limit governor described below. Request the fan-out the work actually needs and let the runtime\nqueue and drain it; the caps above are enforcement, not a reason to\npre-refuse valid work. If you want gentler fan-out, lower `[subagents].launch_concurrency`\n(how many direct children start at once); children beyond that limit **queue**\nfor a launch slot rather than bursting. `launch_concurrency` defaults to the\nresolved `max_subagents` cap. (The pre-v0.8.61 `interactive_max_launch` key is\nstill accepted as a deprecated alias; the new key wins when both are set.)\n\nHigh-fanout Workflows can tune that bounded population with `[subagents]\nmax_admitted` (aliases: `max_total`, `admission_limit`). That total ceiling\ncounts both **running** and **queued** agents, while `launch_concurrency` keeps\ninstantaneous execution bounded. Completed / failed / cancelled records persist\nfor inspection but don't occupy an admission slot. Agents that lost their\n`task_handle` (e.g. across a process restart) also don't count against the cap.\n\n### Rate-limit governor\n\nThe one automatic throttle is the rate-limit governor. It watches provider\nrate limits (HTTP 429) across a 60-second window. After repeated limits it\nshrinks the number of launch slots; under a sustained burst it pauses new\nlaunches entirely. Steady successes add slots back one at a time. It never\ninterrupts an agent that is already running, and quota exhaustion is not\ntreated as a throttle.\n\nWhile the governor is holding launches back, it says so in two places:\n\n- a queued agent's row gives the reason, for example\n `launch slots throttled to 4/8 after 2 provider rate limit(s) in the last 60s`\n or `launches paused after 4 provider rate limit(s) in the last 60s`, and the\n time its owner-configured wall budget ends, if one exists;\n- `GET /v1/agent-runs` returns a `governor` object next to `runs`, with\n `launch_slots`, `max_launch_slots`, `paused`, `recent_rate_limits`, and a\n `status` line while launches are held back. It describes launches made by the\n runtime serving the request (Fleet runs).\n\nKnown limitation: the `/subagents` register header does not show the governor\nline yet; the TUI receives agent lists from the Engine without governor state.\n\nProvider profiles let one config stay aggressive for direct API routes while\nkeeping subscription or aggregator routes gentle. Every key under\n`[subagents.providers.<provider>]` inherits from `[subagents]` when omitted.\nProvider keys accept canonical names such as `deepseek`, `zai`, `openrouter`,\nand aliases such as `glm` for Z.ai:\n\n```toml\n[subagents]\n# Global fallback for providers without a profile.\nmax_concurrent = 20\nlaunch_concurrency = 20\nmax_admitted = 200\n# Operator-selected Runtime delegation depth. Omit to leave it uncapped.\n# This explicit value restricts descendants to six levels.\nmax_depth = 6\n# Omitted or zero model-step budget is unbounded. Set a positive value only\n# when an operator deliberately wants a per-child cap.\ndefault_max_steps = 0\ndefault_wall_time_secs = 0\n\n[subagents.providers.deepseek]\n# Direct API key with room to fan out.\nmax_concurrent = 20\nlaunch_concurrency = 20\nmax_admitted = 200\n\n[subagents.providers.glm]\n# Z.ai / GLM subscription-style route: keep pressure tight.\nmax_concurrent = 4\nlaunch_concurrency = 3\nmax_admitted = 12\nmax_depth = 2\napi_timeout_secs = 180\nheartbeat_timeout_secs = 240\n\n[subagents.providers.openrouter]\nmax_concurrent = 5\nlaunch_concurrency = 3\nmax_admitted = 20\n\n[subagents.providers.anthropic]\nmax_concurrent = 3\nlaunch_concurrency = 2\nmax_admitted = 12\n```\n\nUse `/config subagents status` to see both the global values and the active\nprovider's resolved fanout, depth, and timeout profile.\n\n## Advertised agent-tool fields\n\nThe model-facing `agent` schema exposes these controls:\n\n| Purpose | Fields |\n| --- | --- |\n| Launch and route | `action`, `prompt`, `type`, `profile`, `name`, `model`, `model_strength`, `thinking` |\n| Scope and outputs | `worktree`, `cwd`, `write_authority`, `write_roots`, `exact_files`, `coordination_contracts`, `deliverables`, `expected_artifact` |\n| Coordinate and recover | `agent_id`, `agent_ids`, `all_parked`, `message`, `until`, `detached`, `resume_from` |\n| Inspect | `detail`, `offset`, `limit` |\n\n`start` requires `prompt`. `message` requires a target and message;\n`followup` requires a message and exactly one target form: `agent_id`/`name`,\n`agent_ids`, or `all_parked: true`. `peek`, `interrupt`, and `cancel` require a\ntarget. `claim` requires scope entries. These action requirements are validated\nbefore execution.\n\n`agent(action=\"roster\")` reports each built-in role's resolved provider, model,\nreasoning effort, known route limits and capability provenance. It uses the\nsame resolver as execution. An explicit saved profile wins first, followed by\na manual role pin in the current configuration, then a unique saved member\npinning that semantic role. Conflicting task `model` or `model_strength` choices\nfail before admission. For an unpinned role, per-task `model` precedes\n`model_strength`, then inherited role defaults and the session route.\nWhen a Pod is selected, the `models` rows list its exact routes in saved order.\nUse a listed `provider/model` selector for a task on an unpinned role; the session\nmodel remains allowed. Off-list choices fail with the allowed routes, and a bare\nmodel shared by multiple providers requires an exact selector. Without selected\nmodels, current-provider overrides and `model_strength` retain their behavior;\nforeign-provider requests fail. These choices do not change child authority.\n\nThe `profiles` rows expose saved members from the existing selected Fleet or\ntrusted config/personal/workspace/plugin layers, with bounded identities and the\nsame route/cost evidence. `profile=\"bug-hunter\"` loads that member's instructions,\nrole, provider/model pin and depth limit. Conflicting type or model requests are\nrefused; explicit `thinking` overrides the saved tier. Missing providers, revoked\nplugin authority and disabled project profiles fail before child admission.\nDiscovery never creates a profile or enrolls a model. These identity choices use\nthe existing child lifecycle; a saved profile alone does not create a continuing\nBot conversation or a computer lease.\n\nCost classes describe current uncached text input/output rates, not the total\nprice of a future task. Missing or routing-dependent prices remain unknown;\nsubscription/local routes are labelled not money metered. Discovery makes no\nprovider request and reports reachability as unverified.\n\n**Parse-accepted but unadvertised (compat).** Other inputs remain accepted\nfor saved transcripts, ACP/MCP clients, fleet execution data, and\ninternal/operator compatibility. Runtime validates and intersects them with\nlive policy:\n\n- delegation compatibility: `max_depth`, `maxDepth`, or `max_spawn_depth`;\n model-facing calls ignore these values and inherit depth from the operator\n and selected profile. Exact Fleet authority still supplies its validated\n depth ceiling.\n- workspace/isolation: `workspace_policy`, `fork_context`,\n `worktree_path`, `worktree_branch`, `worktree_base`\n- spawn contract: `deliberate`, `dependencies`, `acceptance`, `allowed_tools`\n- lifecycle extras: `timeout_secs` (wait), `reason` (interrupt),\n `include_archived` (status)\n\nCompatibility input is not a way to widen inherited authority or remove a\nfinite budget.\n\n## Child limits and completion\n\nChildren have no default model-turn or wall-clock limit. They continue until\ncompletion, a need for user input, cancellation, or an explicit owner limit.\nElapsed time and step counts do not establish whether useful work is finished.\nThe runtime does not inject percentage-based instructions to stop exploring or\nwrap up early.\n\nThe model-facing `agent` tool does not advertise `max_steps`, `wall_time_secs`,\nor `max_output_tokens`. Valid legacy inputs still parse, but do not constrain\nmodel-launched children. A launch receipt lists `ignored_limit_fields` when\nthose inputs are supplied; `effective_limits` shows what actually applies.\nModels should specify the task, write scope, deliverables and acceptance criteria,\nnot guess how many turns or minutes completion will require.\n\nAn operator can deliberately set `[subagents] default_max_steps` or\n`default_wall_time_secs`. Omission or zero means no limit. Positive owner\nlimits and role, parent and saved-run bounds remain authoritative; a child\ncannot remove them. There is no additional 2,000-turn or 24-hour child ceiling.\nContinuations preserve an existing run's remaining steps and absolute deadline.\n\nConcurrency limits queue work rather than giving it a guessed completion budget.\nWithout an owner deadline, a queued child waits for a slot or cancellation. With\nan explicit wall budget, its queue wait is bounded by that duration; if no slot\nopens it reports `never started` with zero work. After launch its work clock\nrestarts within any inherited absolute deadline, which is persisted for\ncontinuation. The queued row reports the wait reason and any actual deadline.\n\nFor example:\n\n```json\n{\n \"action\": \"start\",\n \"type\": \"reviewer\",\n \"prompt\": \"Review the parser diff and report concrete regressions with file and line evidence.\"\n}\n```\n\n### Usage and partial results\n\nToken usage is tracked, never used as a run-wide kill budget. Legacy\n`token_budget` input is ignored. Provider-reported usage is the accounting\nsource; missing usage remains unknown. Context compaction lets a child\ncontinue as its conversation grows. Provider context windows and output limits\nstill apply to individual requests, and incomplete tool arguments are never\nexecuted.\n\nWhen an explicit owner step or time limit ends a run, the existing hand-back\npath preserves partial work and attempts one tools-disabled report within the\nremaining allowance. Its fixed input/output allowance is at most 8,192 tokens,\nwith at most 1,024 output tokens. A step limit of at least two reserves one turn;\na wall limit reserves at most 10 seconds for reporting. These reserves do not\ncreate a limit on an otherwise unbounded run.\n\nThe outcome remains `BudgetExhausted` rather than successful completion, even\nwhen a useful partial report is available. If reporting cannot run, the receipt\nretains recorded work and explains why. Cancellation remains available during\nwork and queue waits. Unknown provider usage is never reported as zero.\n\n## Per-role models (#3018)\n\nChildren can run on a different model than the parent. Structured role pins,\nthe legacy model map, and convenience keys feed one override map. Structured\n`[subagents.roles.<role>]` entries win over `[subagents.models]`, which wins over\nthe convenience keys. Keys are case-insensitive; within the structured table,\na canonical role key wins over its legacy alias:\n\n```toml\n[subagents]\ndefault_model = \"deepseek-v4-flash\" # fallback for every role\nworker_model = \"deepseek-v4-pro\" # worker\nscout_model = \"deepseek-v4-flash\" # scout\nplanner_model = \"deepseek-v4-flash\" # planner\nreviewer_model = \"deepseek-v4-pro\" # reviewer\ncustom_model = \"deepseek-v4-pro\" # custom\n\n[subagents.models]\n# Free-form role → model map; any role alias accepted by agent works.\nbuilder = \"deepseek-v4-pro\"\n\n[subagents.roles.reviewer]\nmodel = \"deepseek/deepseek-v4-pro\"\n```\n\nThese are manual pins for direct and Workflow `agent` starts. A task may restate\nthe same model or exact provider/model pair, but cannot change the pin with\n`model` or `model_strength`. An explicit saved profile takes precedence over a\nmanual role pin. A type-only start also selects a unique saved role pin when\nthere is no manual override; ambiguous saved roles fail instead of choosing one.\nDurable Fleet runs retain their selected member's frozen route.\n\nA structured role pin may list approved replacement routes:\n\n```toml\n[subagents.roles.reviewer]\nmodel = \"xai/grok-4.6\"\nreplacements = [\"deepseek/deepseek-v4-pro\"]\n```\n\nWhen the pinned route refuses the agent's **first** request (exhausted quota,\nrejected credentials or authorization, or an unavailable model), the agent\nretries that same request on the next listed route, keeping its role,\npermissions, tools, scope and budgets. Listing a route authorizes sending the\nagent's task to that provider, so each entry must name `provider/model`; at\nmost three are allowed and each is tried once. The route receipt records the\neffective route, `route_source = \"role.replacement\"`, and a note with the\noriginal route, the reason and the attempt. Replacement never happens after\nthe agent has run a tool, never for content-policy, context-length or\ninvalid-request errors, never for Codewhale's own permission denials, and never\nfor exact Fleet members or task-level `model` choices, which stay exact.\n\nStructured role pins accept `provider/model`, preserving the configured provider's\nexact identity and the complete model suffix. Unknown providers, empty pairs,\nand cross-provider `auto` choices fail before admission. A bare structured model\ninherits the session provider. For a namespaced model, qualify it explicitly,\nfor example `openrouter/deepseek/deepseek-v4-pro`. Legacy scalar and\n`[subagents.models]` values keep their full provider-owned id, including slashes;\nthey do not change providers.\n\nThe v0.9.x convenience keys `explorer_model`, `awaiter_model`, and\n`review_model` remain accepted as deprecated aliases so existing config files\ndo not break.\n\nModel ids may be **any model the active provider accepts** — validation is\nprovider-aware and happens at spawn time, not load time. On the official\nDeepSeek API only DeepSeek ids are accepted; every other provider passes the\nid through to the provider API, which is the authority. A non-DeepSeek\nexample:\n\n```toml\nprovider = \"moonshot\"\nmodel = \"kimi-k2.7-code\"\n\n[subagents]\nworker_model = \"kimi-k2.6\"\n```\n\nModel ids are validated the same way when applied to a child route; an invalid\nid on the official DeepSeek API fails the spawn with the accepted-id list\ninstead of an opaque provider 400.\n\nWith `/model auto`, sub-agent routing is provider-aware too: providers with a\nknown big/cheap pair (DeepSeek, and the hosted DeepSeek routes on NVIDIA NIM,\nOpenRouter, Novita, SiliconFlow, SGLang, vLLM) route between that pair;\nproviders without a known cheap tier (e.g. Ollama, Moonshot) skip the\nnetwork router and keep children on the session model.\n\n## Per-profile provider routes (#3965)\n\n`[subagents.models]` changes the child model within the active provider. A slash\nin that legacy input does not grant another provider. To pin a different provider,\nuse a structured `[subagents.roles.<role>]` declaration as above, or use a\nfleet/AgentProfile and select it with `profile` or its unique saved role.\nThe profile's explicit `provider` +\n`model` fields win over the parent session route; omitting `provider` preserves\nthe existing inherit behavior.\n\nExample: keep the parent session on DeepSeek, but run a formatter child on a\nlocal LM Studio OpenAI-compatible endpoint:\n\n```toml\n# ~/.codewhale/config.toml or workspace config\nprovider = \"deepseek\"\n\n[providers.deepseek]\napi_key = \"YOUR_DEEPSEEK_KEY\"\n\n[providers.lm-studio]\nkind = \"openai-compatible\"\nbase_url = \"http://127.0.0.1:1234/v1\"\napi_key = \"lm-studio\"\nmodel = \"qwen-2.5-7b\"\n```\n\n```toml\n# .codewhale/agents/local-formatter.toml\nid = \"local-formatter\"\nrole_hint = \"formatter\"\nprovider = \"lm-studio\"\nmodel = \"qwen-2.5-7b\"\nreasoning_effort = \"off\"\n\n[instructions]\ntext = \"Use small, local edits. Keep formatting changes mechanical.\"\n```\n\nThen call `agent(profile: \"local-formatter\", prompt: \"...\")`. In-process\nchildren build a client for `lm-studio`; fleet workers forward\n`--provider lm-studio` to `codewhale exec`, which resolves the same\n`[providers.lm-studio]` table. Unknown or unconfigured provider ids fail the\nspawn rather than silently falling back to the parent provider.\n\n## Per-step API timeout (#1806, #1808)\n\nEach sub-agent step wraps its provider streaming call in a\nper-step timeout so a single stuck request can't pin the parent's\ncompletion wakeup channel indefinitely. The default is `600` seconds.\nA timeout after possible dispatch interrupts the step with a preserved\ncheckpoint and unknown request outcome. Only a transient connection timeout\nproven to precede dispatch uses the existing backoff, up to 5 retries. Ordinary\nrate limits retain their bounded retry policy; stream interruptions do not\nautomatically reissue inference.\nLong-thinking children that legitimately exceed that, for example\nheavy plan or review work behind `agent`, can extend the timeout in\n`~/.codewhale/config.toml`:\n\n```toml\n[subagents]\napi_timeout_secs = 900 # 15 minutes; clamped to 1..=3600\n```\n\nValues are clamped to `1..=3600`. `0` and `unset` keep the `600`\nsecond default.\n\n## Stale-agent heartbeat (#2614)\n\nRunning agents also track manager-visible progress. If a child stops emitting\nprogress for the heartbeat window, the manager auto-cancels it, releases its\nsub-agent slot, and keeps the cancelled record inspectable through the returned\ntranscript handle and persisted worker record. The default is 5 minutes\n(resolved to at least 30 seconds above `api_timeout_secs`, so 630 seconds\nwith the 600-second default API timeout):\n\n```toml\n[subagents]\nheartbeat_timeout_secs = 300 # clamped to 30..=3600\n```\n\nThe effective heartbeat is kept at least 30 seconds above\n`api_timeout_secs`, so a configured long model request is not cancelled before\nits own request timeout can fire.\n\n## Lifecycle\n\nEach opened session produces a record that progresses through:\n\n```\nPending → Running → (Completed | Failed(reason) | Cancelled | Interrupted(reason) | BudgetExhausted)\n```\n\nAn explicit interrupt, exhausted provider retries, or recovery of an orphaned\nrunning record can leave an `Interrupted` worker with a checkpoint. Inspect\n`needs_continuation` and the recorded reason; use `followup` for continuable\nwork. `BudgetExhausted` includes the specific token, step, or wall-time cause;\ncontinuation cannot replenish an exhausted allowance.\n\n`wait` observes workers. A timeout returns current outcomes and never parks,\ncancels, or resumes them. `until: \"completion\"` returns when one child settles;\n`until: \"all\"` joins the workers running when that call starts;\n`until: \"activity\"` can return on progress. A later spawn is not silently added to an\nearlier join.\n\nAn ordinary parent response leaves healthy children running. The same Engine\nturn loop consumes their completion notices and can continue the parent.\nHeadless `codewhale exec` defers a successful final receipt until its existing\nEngine reports no live children and no queued child completions. Its original\nwall-clock deadline still bounds that settlement, including autonomous parent\nturns. Cancellation, deadline exhaustion, a fatal event, or a lost Engine\nchannel stops settlement and returns the appropriate interrupted or failed\nreceipt with recorded partial usage. It does not report successful child\ncompletion merely because the parent's first response ended.\n\n### Session boundaries (#405)\n\nEach `SubAgentManager` instance assigns itself a fresh `session_boot_id` on\nconstruction. Every new session stamps the agent with that id; the workspace\nstate file records it for restart recovery.\n\nWork-bar/status projections focus on current-session agents by default.\nPrior-session agents that are not still running are treated as archived records\nso the model does not mistake stale work for live work. This is a\n*prior-session* rule only: agents that finished in the CURRENT session keep\ntheir work-bar rows for the rest of the session (quiet completion), and their\ndetails still open from those rows.\n\nRecords that loaded from a pre-#405 persisted state file (no\n`session_boot_id` field) classify as prior-session because the\nmanager can't match them to the current boot.\n\n## Run receipts, follow-up, and takeover\n\nEach compatibility sub-agent has a persisted worker record in\n`.codewhale/state/subagents.v1.json`. The record is the current run-ledger\nslice for sub-agent lanes until those lanes are backed directly by the fleet\nledger: it stores `run_id`, objective, role/model,\nworkspace/branch, lifecycle events, artifact refs, follow-up target, takeover\ntarget, usage provenance, and verification provenance.\n\nThe normal parent flow is to keep working and consume the completion event.\nDefault start and status receipts are compact; full snapshots and worker\nrecords are diagnostic detail, not repeated in every response.\n\n### Continue an existing worker\n\n`message` queues a note without waking the child. `followup` wakes a running\nchild or resumes a continuable checkpoint:\n\n```json\n{\"action\":\"followup\",\"agent_id\":\"child-previous-id\",\"message\":\"Continue the assignment using the recorded evidence.\"}\n```\n\nUse the returned `agent_id` for subsequent waits and messages. The receipt's\n`from` and `to` identify the original target and its current continuation.\nThe original receipt is retained. Retrying through an old ID follows the\npersisted continuation chain and does not create a duplicate worker. If the\ncurrent successor is running, the follow-up is delivered there; if it has\nalready settled and cannot continue, the response says no message was\ndelivered. Duplicate workers are prevented, but repeated messages to a running\nworker are still repeated messages.\n\nFor a batch, choose exactly one target form:\n\n```json\n{\"action\":\"followup\",\"agent_ids\":[\"child-a\",\"child-b\"],\"message\":\"Continue the remaining checks.\"}\n```\n\n```json\n{\"action\":\"followup\",\"all_parked\":true,\"message\":\"Continue the parked assignments.\"}\n```\n\nExplicit batches accept up to 32 distinct IDs. `all_parked` selects parked\nchildren you control and refuses more than 32 so you can choose explicit\nbatches. Bulk responses return separate `results` and `errors`; a failing\ntarget does not roll back a successful continuation. Parent/descendant control\nchecks apply to both the addressed record and its current successor.\n\nUse `start` with `resume_from` only to create a separate worker from a settled\nchild's transcript, for example to assign a new review. Each such start is a\nnew worker. Missing, running, or cross-workspace sources are refused; the\nsource's authority and budget bounds still apply. This is distinct from\ncontinuing parked work with `followup`.\n\n### Compact status and full transcript retrieval\n\nUnscoped `agent(action=\"status\")` returns a session-scoped page bounded to\n8 KiB. `offset` and `limit` page the roster; the default and maximum limit is\n20. Follow `next_offset`, since the byte bound can return fewer rows than\nrequested. The model-facing roster wire uses one stable `columns` header and\nan array of values per entry in `agents`; pair each row with the header instead\nof reading it as an object. `null` means absent or unreported, and a measured\nzero stays numeric `0`. The header is present even on an empty page.\nRows include worker and parent IDs, current depth, state,\nelapsed time, own token total, recent activity, pending input, and continuation\nlineage (`resumed_from` / `resumed_as`). Verification includes the verdict,\nnonempty deliverable counts and a short warning when needed. Names, steps,\nroutes, effective limits (including maximum depth), and token breakdowns remain\navailable in the unchanged object projection when addressing one `agent_id`.\nAggregate usage counts each worker's own reported tokens once and reports its\ncoverage. Completion receipts additionally report measured descendant usage,\ndeduplicate continuation lineage, and distinguish unknown usage from zero. A worker's\n`has_unreported_usage` and the descendant/subtree `unreported_usage_workers`\ncounts identify missing responses even when later responses provide a measured subtotal.\n\nRequest one worker's detail when investigating a failure:\n\n```json\n{\"action\":\"status\",\"agent_id\":\"child-a\",\"detail\":true,\"offset\":0,\"limit\":20}\n```\n\nAddressed `peek` also accepts `detail: true`. Detail remains bounded to 32 KiB;\nmessage/event archives and deliverable verdicts are paged, and omission fields\nidentify truncated detail. Use the returned typed `transcript_handle` with\n`handle_read` for the complete retained transcript. The handle's lookup\ncoordinates are preserved even when diagnostic prose is omitted. Unscoped\n`detail: true` does not expand the entire roster into transcripts.\n\nArtifacts are symbolic refs. Treat `result_summary` as a child self-report and\ninspect the specific `verification.status` and its evidence before relying on\nit. `usage.status` remains `unknown` until provider usage is reported, then\nbecomes `reported` or `budget_exhausted` for a spent token scope. Neither a\nfile's `present` verdict nor a completed lifecycle state proves a test gate.\n\n## Output contract\n\nNon-scout sub-agents end with five Markdown headings, in this order:\n\n```\n### SUMMARY one paragraph; what you did and what happened\n### EVIDENCE path:line-range citations and key findings; one bullet each\n### CHANGES files modified, with one-line descriptions; \"None.\" if read-only\n### RISKS what could go wrong / what the parent should double-check\n### BLOCKERS what stopped you; \"None.\" if you finished cleanly\n```\n\nUse `### HEADING` lines, with `EVIDENCE` before `CHANGES`. List edited\nrepo-relative file paths under `### CHANGES`; blank lines before the bullets\nare allowed. Begin each bullet with its file path, followed by a description;\nquote paths containing spaces or literal trailing punctuation. The verifier\nalso accepts older explicit `CHANGES:`,\n`Changed files:`, and `Files changed:` declarations. Evidence citations and\npaths under `RISKS` are not declarations of edits. The five-heading prompt\ncontract is `SUBAGENT_OUTPUT_FORMAT` in\n`crates/engine/src/prompts/text.rs`. `prompt_documents_structured_subagent_briefs`\nin `crates/engine/src/prompts.rs` asserts every heading against it.\n\nScouts are the carve-out (#5189 F5): they end with `### SUMMARY` and\n`### EVIDENCE` only (`SUBAGENT_SCOUT_OUTPUT_FORMAT` in\n`crates/engine/src/prompts/text.rs`). `FleetRole::system_prompt` in\n`crates/engine/src/tools/subagent/mod.rs` injects the scout contract for\n`FleetRole::Scout` and the five-heading contract for every other role. A\nsubagent test pins that scouts contain `## Output contract (scout)` and do\nnot contain `### BLOCKERS`.\n\nThe parent reads `EVIDENCE` as a working set for the next turn, so\nscouts and reviewers should be precise here.\n\n## Memory and the `remember` tool (#489)\n\nSub-agents share the parent's native memory store when memory is enabled\n(`[memory] enabled = true` or `DEEPSEEK_MEMORY=on`). They can\nappend durable notes via the `remember` tool — handy for a\nscout that discovers a project convention worth carrying across\nsessions, or a verifier that learns \"this test is flaky\".\n\n`remember` takes a `scope` of `global` or `workspace`\n(`crates/engine/src/tools/remember.rs`, schema enum plus scope handling) and\nwrites through `NativeMemoryStore` to `~/.codewhale/memory/global/MEMORY.md`\nor `~/.codewhale/memory/workspace/<id>/MEMORY.md`. Writes do not go through\nthe standard write-approval flow. The legacy single-file `memory.md` path was\nremoved in v0.9.4; see `docs/MEMORY.md` for the full layout.\n\n## Implementation notes\n\n- Source: `crates/engine/src/tools/subagent/mod.rs`.\n- Persisted state: `<workspace>/.codewhale/state/subagents.v1.json`. Schema\n version `1` (forward-compatible — new optional fields use\n `#[serde(default)]`).\n- Settled records normally expire after `COMPLETED_AGENT_RETENTION`\n (default 1h), with a normal retained-record target of 256. Running /\n starting / waiting workers and the continuation identities and budget\n lineage needed by live work are preserved. Cleanup cannot discard an old\n ID while its continuation is still active or erase usage history needed\n to enforce an active scope.\n- `SubAgentRuntime::background_runtime()` starts from `child_runtime()` but\n replaces the turn-scoped child token with a fresh cancellation token, so\n parent turn cancellation does not stop detached background sessions.\n- The `is_running` check ignores agents whose `task_handle` is\n `None`; this avoids counting persisted-but-detached records\n toward the concurrency cap (#509).\n- `SharedSubAgentManager` is `Arc<RwLock<...>>` — read paths use\n read locks so `/agents` and the workbar projection don't block\n the main loop during multi-agent fan-out (#510).\n\nPersonal profiles use the same format at\n`$CODEWHALE_HOME/agents/<id>.toml` (normally `~/.codewhale/agents/`). For example:\n\n```toml\n# ~/.codewhale/agents/reasoner.toml\nbase_role = \"explore\"\nprovider = \"openrouter\"\nmodel = \"qwen/qwen3.7-plus\"\nreasoning_effort = \"high\"\n\n[permissions]\nallow_shell = false\ntrust = false\n```\n\nSelect it with `agent(action: \"start\", profile: \"reasoner\", prompt: \"...\")`.\nThe provider must also be configured in `config.toml`. The receipt names the\nresolved profile, its personal/project origin, provider/model and effective\nreasoning effort. Effort is normalized to the selected model's supported tiers;\nan explicit `thinking` request overrides the saved preference.\n\n`allow_shell` and `trust` belong under `[permissions]`, not at the top level.\nA profile cannot grant `allow_shell = true`, `trust = true`, or disable approval.\nUse the appropriate `base_role` for the task; the parent session's live policy\nremains the authority ceiling. These profile fields are not a way to grant\nadditional access.\n\nA malformed, unreadable or duplicate profile now causes an explicit selection\nerror, including when its name matches a built-in role. It never silently\nsubstitutes a lower roster layer. Repair the file and retry; profiles are reloaded\nfor each launch. `agent(action: \"roster\")` reports affected profile identities and\npaths in `profile_load_issues` without exposing parser excerpts. Other valid\nprofiles remain available, and a valid project override still wins over a broken\npersonal definition. Fleet run creation performs the same check before storing\na run or launching workers.\n"
|
||
}
|
||
]
|
||
},
|
||
{
|
||
"id": "fleet",
|
||
"slug": "fleet",
|
||
"sources": [
|
||
{
|
||
"path": "docs/FLEET.md",
|
||
"sha256": "10a8c50860305920357042c95fb1c471696a68cc8fd219e571157563e8494654",
|
||
"content": "# Agent fleet\n\n> Baca terjemahan bahasa Indonesia: [id/FLEET.md](id/FLEET.md)\n\nAgent fleet is the local-first roster and member-selection layer for durable\nmulti-worker runs. It does not execute or authorize work. After fleet resolves\nwho should participate, the delegated coordinator launches a headless\n`codewhale exec` run and the Runtime tracks it durably. See\n[AGENT_RUNTIME.md](AGENT_RUNTIME.md) for how sub-agents, `exec`, and\nfleet-backed workers converge on one runtime. In product language, a user may\nstill \"open a sub-agent\"; in architecture language, durable nested work uses a\nfleet member identity with delegated runtime execution.\n\n## Naming and compatibility boundary\n\n**Fleet** is the public product noun. The durable ledger, saved rosters, config\ntables, and `--fleet` flag share that name:\n\n| Surface | Canonical |\n| --- | --- |\n| CLI | `codewhale fleet …` |\n| Slash command | `/fleet …` |\n\nThese shared names are load-bearing wherever changing them would break\nexisting workspaces, receipts, or scripts:\n\n- the durable ledger `.codewhale/fleet.jsonl` and the log directories\n `.codewhale/fleet/` and `.codewhale/fleet-host/`;\n- saved rosters `fleets/<name>.toml` and their `schema = \"fleet\"` header, under\n `$CODEWHALE_HOME/` or the workspace's `.codewhale/` (checked-in rosters at the\n workspace root's `fleets/` are still read);\n- the `[fleet]` config table (inline `[fleets.*]` tables were removed in 0.9.14; named fleets live in `fleets/<name>.toml` files);\n- the `codewhale workflow run --fleet <name>` flag;\n- wire, receipt, and control-plane operation ids such as `fleet.status`.\n\nThe rest of this document uses fleet as the public product noun and retains\nthese literal paths, keys, flags, and ids.\n\nUse a fleet roster rather than anonymous short-lived `agent` fanout whenever a\ndelegated run needs stable member identities across retries, sleep/restart,\nremote execution, receipts, or a ledgered audit trail. The initial CLI surface\nis:\n\nFor a guided start-to-monitor walkthrough that combines fleet task specs with\nWorkflow authoring, see [fleet + Workflow Tutorial](FLEET_WORKFLOW_TUTORIAL.md).\n\n```sh\ncodewhale fleet init\ncodewhale fleet run tasks.json --check # validate only; nothing is created or launched\ncodewhale fleet run tasks.json --max-workers 4\ncodewhale fleet status\ncodewhale fleet inspect <worker-id>\ncodewhale fleet logs <worker-id>\ncodewhale fleet artifacts <worker-id>\ncodewhale fleet interrupt <worker-id>\ncodewhale fleet restart <worker-id>\ncodewhale fleet resume <run-id>\ncodewhale fleet stop --all\n```\n\n`codewhale fleet resume <run-id>` is the restart-recovery verb: it replays the\nledger, reconciles any in-flight lease whose worker stopped heartbeating\n(retrying within the task's budget, else failing and escalating per the alert\npolicy), and prints the post-resume status. It launches no new work and is\nidempotent, so it is safe to run after a manager exit, laptop sleep, or runtime\nrestart.\n\nCoordinator state for fleet-backed runs is stored under the workspace in\n`.codewhale/fleet.jsonl`. Worker logs and adapter logs are stored under\n`.codewhale/fleet/` and `.codewhale/fleet-host/`.\n\n## Public contract: identity, membership, and selection\n\n**Fleet** = the user's model inventory: who is in the roster and which member is selected.\n\nA public fleet identity consists only of:\n\n- a stable member id and an optional user-facing name;\n- a semantic role, such as `explore`, `implement`, or `reviewer` (the legacy\n spellings `worker`, `scout`, `builder`, `verifier`, `consultant`, and `oracle`\n are still accepted on input and map to `general`, `explore`, `implement`,\n `test`, and `advisor`);\n- an exact provider/model identity, or an explicit inherited route;\n- visible roster state or origin.\n\nProject or workspace trust, filesystem and network reach, secret access,\napproval mode, sandboxing, tool authorization, and every other form of runtime\nauthority are separate delegated-coordination and Runtime policy inputs. They\nare never fleet identity fields, and they never select or reroute a fleet\nmember. The Runtime applies and clamps those policies only after member\nselection; if the selected member cannot run inside the effective envelope,\nlaunch fails closed instead of choosing somebody else.\n\nNatural-language member selection is deterministic. A caller may name:\n\n- an exact member id, optionally as `member:<id>` or `id:<id>`;\n- a unique user-facing member name, optionally as `name:<name>`;\n- a unique semantic role, for example `explore` or `role:explore`;\n- an exact pinned model id, for example `deepseek-v4-flash`, or its offline\n display name, for example `DeepSeek V4 Flash`; or\n- an exact `route:<provider>/<model>`.\n\nAn unqualified exact member id wins. Every other match succeeds only when it\nidentifies one distinct roster member. Multiple matches produce an ambiguity\nerror that names the candidates and asks for `member:<id>`; Codewhale never\npicks whichever match happened to be listed first. Users do not need to know\nan internal role label such as `explore`: a unique member name, display model, or\nexact model id is equally valid. Saved v2 fleets store that optional human name\nas `display_name` (the input alias `name` is also accepted); it must be one\ntrimmed printable line of at most 80 characters.\n\n### Your fleet as models\n\nThe same fleet file answers a third question: **which models has this person\nput in their fleet?** Every exact `provider` + `model` pin in the selected\nfleet — the operator route, each pinned member, and each explicitly marked\nshortlist row — is a fleet model. Executable member rows supply the roles it\nfills; a shortlist row has no role. All remain in the same fleet file. Legacy\nmembers that omit `role` still use their id as the role identity.\nShortlist rows carry only model choices; reasoning, instructions, and capability\nrequirements belong on executable role members and are refused on shortlist rows.\n\n- `/fleet models` prints the fleet: `provider/model · roles · price · context ·\n tools`, facts read from the model catalog. With no selected fleet the line\n reads \"Your fleet is the session model only\".\n- `/fleet add <provider> <model> [role…]` adds a model (one member row per\n role, or one `shortlist = true` row for a role-less add). The provider must be one you configured\n and, when the catalog knows the provider, must serve that exact id.\n A role member asked to run the fleet's own operator route inherits it\n instead of pinning — the role follows when the operator moves; a pin on\n any other route is the deliberate opt-out. Files that already pin the\n operator route are read as inheritance.\n With no fleet selected, a user-global fleet named `My fleet` is created and\n selected first. `/fleet remove <provider> <model>` drops every row that pins\n the route; the operator route is changed with `/fleet save`, not removed.\n- In `/model`, `⇧F` adds or removes an explicit shortlist row for that exact\n route. It preserves saved role pins and the operator route;\n fleet models are listed first, labelled `fleet · <roles>`, ahead of your\n own `⇧P` pins and the provider lists. `/models` prints the fleet before the\n provider's list.\n\nThe operator model reads this list when it assigns sub-agents (design\n`MODEL-ROUTING-CATALOG-20260901.md` §10, slice F2).\nThe model-facing roster resolves roles through the same route admission code\nas a start. Explicit profiles and manual role pins remain authoritative; a\nunique saved role pin also applies to a start naming only its role. Task model\nchoices are available for unpinned roles, constrained to the selected models\nplus the session route. Shortlist model rows disclose their own exact route\nindependently of any role pin.\n\n### Interactive and persistent status\n\n`/fleet status` and `codewhale fleet status` are the **same** command on two\nsurfaces. Both read the durable `.codewhale/fleet.jsonl` ledger for the\nworkspace, through one shared control-plane contract, and both report the same\nverb id (`fleet.status`), read-vs-write authority, persistence scope, and\nreceipt. When the workspace has no ledger they say so with a typed reason\n(`no_fleet_ledger`) instead of rendering an empty-looking \"all clear\" — and\nneither creates the ledger as a side effect of reading it.\n\nThe current interactive session's sub-agents are a **different set**, and now\nhave their own name:\n\n- `/fleet workers` (or `/subagents`, or Tab / `w` from the `/fleet` roster)\n shows sub-agents attached to the current TUI session. It does not read the persistent ledger.\n- `/fleet list|status|interrupt|resume` and `codewhale fleet\n list|status|interrupt|resume` act on the durable ledger.\n- `codewhale fleet restart <worker-id>` is CLI-only: it re-leases the task and\n then drives the manager loop to completion. `/fleet restart` does not\n silently do a smaller thing — it reports `surface_not_supported` and names\n the CLI command.\n\nThe contract behind this — descriptors, availability reasons, exact-identity\ntargets, receipts, typed unknowns, and bounds — is documented in\n[`docs/COMMAND_CONTROL_PLANE.md`](COMMAND_CONTROL_PLANE.md).\n\n## Authoring agent profiles (`/fleet setup`)\n\nAgents: the durable artifact is the profile TOML described below; the\nkey-by-key walkthrough is the human interactive path.\n\n`/fleet setup` (also `/fleet setup edit` / `new`) opens an in-TUI wizard for\nauthoring a reusable agent-team profile. Bare `/fleet` and the\n`roster`/`roles`/`profiles`/`party` aliases open the selected fleet's member\nroster. `/fleet saved` opens the named saved-fleet picker. `/fleet workers` opens the\ncurrent-session worker view; `/subagents` is a\ncompatibility shortcut for that view. For durable run history, use\n`/fleet status` or the shell command `codewhale fleet status` described above —\nthey are the same command.\n\nThe wizard is progressive: you make one focused choice at a time — a **role**,\nthen a **model** (`inherit`, or a concrete model from *any configured\nprovider*, not only the one the parent session is currently using), then\n**where the profile lives**, and finally a **review** of the member identity\nand route. When the review also previews thinking, tools, approvals, or another\nexecution control, those rows summarize separate Runtime policy; they do not\nbecome fleet identity or member selectors. The header shows \"Saves to: …\" on\nevery step — the choice you still have to make, or the exact resolved file\nonce you have made it. Nothing is written until you activate the save control\non the review step.\n\nThe **Destination** step is a focused two-option list:\n\n- **This project** writes `<workspace>/.codewhale/agents/<role>.toml`. It\n applies to this project only and takes precedence over a Personal profile\n with the same id. When project profiles are disabled for the session\n (`--no-project-config`) or the workspace folder is unavailable, the option is\n shown disabled with that reason; the wizard never falls back to Personal on\n its own.\n- **Personal** writes `$CODEWHALE_HOME/agents/<role>.toml` and is available in\n every project on this machine, except where a project has its own profile\n with the same id.\n\nFor the highlighted option the step shows the exact file, whether saving would\ncreate a new file or **replace an existing one**, and the precedence\nconsequence for the roster. The review step repeats those facts under\n\"Saves to\" and names the final action by its effect — **Save to this\nproject**, **Save as Personal profile**, or **Replace …**. Replacing an\nexisting file asks for a second confirmation on the save control. Reopening a\nsaved member from `/fleet` starts from what is on disk: its member identity,\nroute, and save scope. Thinking (`inherit`, `off`, `low`, `medium`, `high`,\n`max`, or `auto`) is adjusted on the review step, but remains a route\nexecution setting rather than part of the member's fleet identity.\n\nProfile scope controls where a role definition is reusable; it does not widen\nthe authority of a running operation and is not a project-trust setting. To\ncoordinate several nearby repositories, start Codewhale from their shared\nparent directory so that parent is the workspace. Project/workspace trust,\nexternal paths, filesystem and network reach, secrets, approvals, sandboxing,\nand tool authorization come from delegated-coordination and Runtime policy.\nFor nested delegation, Runtime intersects the requested child posture with the\nlive parent. For standalone `codewhale fleet` execution, Runtime instead uses\nthe bounded tool-authority envelope minted from the task's explicit write\nscope together with live config, sandbox, and platform enforcement. Neither\npath reads authority from the profile's storage scope or identity selector.\nA worker whose envelope grants read-only shell access runs the same read-only\ncommand grammar as an in-session read-only agent, including pipelines, chains\nand a leading `cd` (see \"Read-only shell commands\" in `docs/SUBAGENTS.md`);\nAdmitted `gh` reads also need the envelope's network grant. `npm view` remains\noutside the read-only grammar because npm configuration can select executable\nhelpers; a network grant does not authorize those helpers.\n\nPicking a concrete model pins its provider explicitly: the saved profile records both\n`model` and `provider` fields, so the route it names doesn't depend on\nwhichever provider happens to be active when the profile is later loaded.\nPressing **Enter** (\"start\") on the review step previews the exact starter\nprofile TOML inline on that same screen; nothing is written until you save it.\nThe `provider` field may be a built-in provider id such as `openrouter` or a\nuser-named OpenAI-compatible provider configured under `[providers.<name>]`\nsuch as `lm-studio`; the launch path preserves that id and fails closed if the\nprovider is not configured.\n\nProfiles are also how the model-facing `agent` tool selects a route: a child\neither runs as a `profile` (whose saved route and thinking tier it uses\nexactly) or inherits the operator's model. Per-task `model`, `model_strength`,\nand `thinking` remain advertised for unpinned roles; saved profile and manual\nrole pins refuse overrides. See docs/SUBAGENTS.md for the advertised field\nlist and the parse-accepted compat list.\n\nWhen a provider is configured, the review step also offers model-assisted\ndrafting behind an explicit preview-before-save gate:\n\n- Press **`m`** to have your first configured model draft the profile. The\n draft arrives sanitized and bounded. Separately, the Runtime keeps its\n conservative execution floor (no shell or trust escalation and approval\n required) regardless of what the model proposes.\n- **Drafting is not saving.** The exact rendered TOML preview renders\n inline on the review step (not in a separate scrollable viewer), so nothing\n is saved until you press **`g`** or **Enter** to save (or press `m` again\n to redraft). Saving writes the profile to the project or personal scope\n shown in the preview.\n\n## Naming: Modes, Workflow, and fleet\n\nThese names describe different layers, not competing systems. Plan and Act are\nthe everyday work modes. Operate accepts ordinary messages and keeps the\nparent's normal tool surface under the same approval, sandbox, shell, ask-rule,\nand repository protections as Act. It prefers background fleet workers for\nindependent, parallel, isolated, or long-running work, but does not require a\nworker for every executable step. Workflow is an optional orchestration overlay\nfor work that needs ordering, gates, shared budgets, replay, or deterministic\nfan-in.\n\nThe short public vocabulary is:\n\n- **Fleet** is the durable roster and deterministic member-selection surface.\n It records member ids and names, semantic roles, provider/model identities,\n and roster state. Fleet is also the name used by storage and wire formats.\n- **Workflow** = what order the work follows: phases, gates, budgets, replay,\n and fan-in.\n- **Lane** = one running Workflow instance and its live progress.\n- **Runtime** = where, how, and with what authority selected work executes.\n Runtime owns the local or remote process, provider route, project/workspace\n trust, filesystem, network, secrets, approvals, sandbox, tools, and API\n boundary.\n\n- **Workflow** is the repeatable plan and user-facing orchestration\n overlay: a script/IR that decides which phases and agents run next, keeps\n intermediate results out of the main conversation, and can be inspected or\n rerun. A Workflow run should have a visible progress view and a clear active\n header state instead of feeling like a hidden background task.\n- **Fleet** is the durable roster and deterministic member-selection surface:\n member ids and names, semantic roles, pinned or inherited provider/model\n identities, and roster state. The delegated\n coordinator and Runtime own launch concurrency, leases, heartbeats, logs,\n receipts, tools, sandboxing, approvals, and authority.\n- **High fan-out** is a behavior of a Workflow run, not a separate system:\n when a phase needs many workers at once, Workflow dispatches them as a\n fleet-backed run (durable workers, receipts, goal re-dispatch) rather than\n reviving prompt-only sub-agent fanout.\n- **Fan-in is explicit:** when the user needs one combined result, an owner\n aggregates, verifies, and synthesizes the worker receipts. Independent tasks\n may finish separately; dispatch is never presented as completion.\n\nUI guidance: keep the main transcript calm. A Workflow run should appear as a\ncompact progress card plus workbar rows (the strip under the composer, or\na side workbar) with phase names, worker counts, receipts, and nested\nindentation for child workers. Use the whale mark sparingly as an active\nheader/status signal; avoid repeating emoji-heavy rows for every worker.\n\n## Saved fleets and the Reasoning Router\n\nA selected v2 fleet freezes each selected member's id, semantic role, provider,\nand model identity into the durable run before a Workflow starts. Save the\nfleet as `fleets/<name>.toml` under the workspace's `.codewhale/` (where the\nfleet editor saves folder fleets) or under `$CODEWHALE_HOME`; a checked-in\n`fleets/<name>.toml` at the workspace root is also read.\nModels cannot replace those identity or route assignments at runtime:\n\n```toml\nschema = \"fleet\"\nschema_revision = 2\nname = \"release\"\n\n[operator]\nprovider = \"deepseek\"\nmodel = \"deepseek-v4-pro\"\n\n[[members]]\nid = \"implementer\"\ndisplay_name = \"Release Builder\"\nrole = \"implement\"\nprovider = \"zai\"\nmodel = \"glm-5.2\"\n\n[[members]]\nid = \"advice\"\nrole = \"advisor\"\nprovider = \"openai\"\nmodel = \"gpt-5.6\"\n```\n\nThe workflow crate's older `schema = \"exact\"`, revision 1 files are migration\ninput only. Do not author revision-1 files; the selected roster and setup UI\nread and write only `schema = \"fleet\"`, revision 2.\n\n`workflow(fleet: \"release\")` runs a saved Fleet without selecting it. At\nWorkflow start, a member with no pin takes the Fleet's `[operator]` route or,\nwithout one, the session route and reasoning tier; that frozen route is what\nruns and what receipts name, and editing the file mid-run changes only the next\nWorkflow. If a saved Fleet and an older exact/legacy file share a name, the\nWorkflow refuses to guess; qualify the saved one as `user/<name>` or\n`folder/<name>`. Members with `instructions` or `requires` cannot run in a\nWorkflow yet.\n\nReasoning is a separate route-execution decision, not fleet identity. The\noptional Reasoning Router is a reusable Runtime service, not a fleet member.\nSave one profile at `routers/<name>.toml` in any search root and reference it\nfrom any number of fleets:\n\n```toml\nname = \"luna-low\"\nschema = \"reasoning_router\"\nschema_revision = 1\nprovider = \"openai\"\nmodel = \"gpt-5.6-luna\"\ncall_reasoning = \"low\"\n```\n\nAt runtime it may choose only the reasoning tier for an already-frozen worker\nroute. It cannot change the member, provider, model, or semantic role. The\nRouter call itself is capped at `off` or `low`; more expensive values are\nrejected. A manually selected worker reasoning tier makes no Router call. Route\nand reasoning receipts name the worker model and, when used, the Router's exact\nprovider/model so the operator can see which model did which job. If the same\nbare Router or fleet name exists in more than one root, qualify it as\n`codewhale_home/<name>`, `workspace/<name>` (the workspace's `.codewhale/`), or\n`workspace_root/<name>` (the workspace root) instead of relying on shadowing.\n\nCompatibility schemas may serialize `reasoning`, `permissions`, tool hints, or\nother execution settings beside a member. Those values are not fleet identity,\nmember selectors, or active authority. A valid legacy `schema = \"exact\"` roster snapshot\nretains its old `permissions` bytes only while verifying and replaying that\nsnapshot's recorded content hash; a fresh capture emits the authority-free\nmember shape. New-run validation rejects legacy roster\n`security_policy` and worker `trust_level` fields; configure execution authority\nthrough Runtime policy. The delegated coordinator resolves and durably freezes\nthe member first. Runtime then applies either the delegating parent's effective\nceiling or, for standalone fleet CLI work, Runtime execution configuration plus\nlive sandbox/platform enforcement. That boundary may\nreduce or refuse the selected worker's execution surface, but it must never\nchoose a different member or route. See\n[`docs/MODES.md`](MODES.md), [`docs/SUBAGENTS.md`](SUBAGENTS.md), and\n[`docs/AGENT_RUNTIME.md`](AGENT_RUNTIME.md) for the enforcement contract.\n\nReasoning receipts record the requested tier *and* the tier the provider was\nactually asked for. Those differ whenever a route cannot express the requested\none — Codewhale's route normalizer sends `high` for a requested `low` on most\nroutes, and Z.AI's GLM routes express only thinking on/off — so the receipt\nreports the real request rather than the label that was selected. The value a\ncall actually carries is spelled by that route's own normalizer, not by the tier\nlabel: an OpenAI Codex route is asked for `xhigh`, not `max`, and cannot be\nasked for `off` at all.\n\nA durable fleet CLI receipt keeps the selected profile id in\n`effective_permissions.profile_id`, the resolved semantic role in\n`resolved_route.role`, and the effective Runtime surface in the permission,\nshell, and tool-scope fields. An exact Workflow launch receipt records\n`member_role` separately from an optional Runtime `posture_role`, plus the\nfingerprint of the effective authority envelope checked at the spawn boundary.\nA member named `auditor` can therefore retain that identity while Runtime\nreports a `custom` posture and independently proves the narrower surface it\nenforced.\n\nA Workflow start fails closed on anything decidable locally: an unresolvable\nprovider or model, a missing credential, a client that cannot be built for a\nmember's route, or an `auto` member with no usable Reasoning Router. Per-task\nvalidation that the spawn boundary would refuse anyway — notably a write-capable\nmember with no declared `write_roots`/`exact_files`/`coordination_contracts` —\nis checked before the Router is called, so an invalid task never spends a\nrouting request. If a spawn fails *after* a Router decision, the receipt is\nstill recorded: the tokens were spent, and any cross-provider disclosure already\nhappened.\n\n## Manager-owned Workflow fan-in\n\nWhen parallel work must return one combined answer, prefer a manager-owned\nWorkflow over a flat `agent` fan-out. Default shape:\n\n1. **Cast one manager** (operator or workflow orchestrator).\n2. **Fan out** child tasks through `workflow` (`task()`, `parallel()`,\n `pipeline()`, `phase()`) or a single manager session that owns the children.\n3. **Wait** for child receipts or completion events.\n4. **Aggregate and verify** load-bearing claims before treating them as facts.\n5. **Synthesize** one result the operator can depend on.\n\nRaw `agent` fan-out fits independent work with no combined result. When\nresults must be merged, compared, or verified, route through `workflow` so\nthe manager owns fan-in — that is what the shape above is for, not a ban\non simpler patterns when nothing needs combining.\n\n## Workflow on fleet\n\nThe intended high-capability path is agent-authored. When the main agent\ndecides a task needs more durable coordination than turn-by-turn sub-agent\ncalls, it drafts a Workflow script/IR, presents the run plan according to the\nactive permission mode, and the runtime compiles it into typed fleet work.\n\nfleet remains the sub-agent roster and member-selection surface. It owns member\nidentity, membership, semantic roles, saved provider/model pins or inheritance,\nand roster state. Workflow owns the orchestration plan:\nbranch, sequence, loop, expand, review, and reduce decisions. The delegated\ncoordinator and Runtime own slot admission, launch concurrency, the execution\nledger, and every authority decision. A workflow script receives no direct\nshell, filesystem, network, provider-secret, cancellation, or TUI authority;\nworkers perform real work as `codewhale exec` processes under the effective\nRuntime policy.\n\nDefault Workflow-to-fleet validation is intentionally bounded:\n\n- 1,000 total worker agents per Workflow run;\n- 16 live worker agents at once; larger populations queue (block) on the host's\n per-run concurrency gate until a live slot frees, then route through fleet;\n- Workflow IR structural nesting no deeper than 5;\n- Runtime child delegation defaults to 3 levels and has an opt-in hard ceiling\n of 8, independently of the Workflow document's structural depth;\n- bounded loops only (`max_iterations` required);\n- bounded dynamic expansion only (`max_children` plus a template required).\n\nThese are delegated-coordination population limits, not fleet identity and not\na demand to launch everything at once. A 1,000-agent Workflow should still\ndrain through the configured Runtime worker pool. They are also not model-step\nbudgets: omitted or zero `max_steps` remains unbounded. An explicit positive\n`max_steps` may cap that task, while wall-clock timeouts, cancellation,\nprovider safeguards, heartbeats, and admission controls remain independent.\n\nRecommended model layouts, such as a DeepSeek Pro orchestrator with Flash\nworkers in the first ring and cheaper workers farther out, are presets only.\nEvery slot can inherit the active model or carry an explicit model override.\nInheritance is literal: the model you select in `/model` is the **operator**\n(the pinned first row in `/fleet roster`), and any worker whose task spec and\nroster profile pin no model runs on that session model. Once a selected member\nhas an exact provider/model pin, the Runtime does not silently reroute that\nidentity because a policy input differs; it either runs that route inside the\neffective envelope or fails closed. Route receipts record the requested and\nresolved identity.\n\nThe setup UI should render this as an expanding grid: an orchestrator plus a\nsmall number of visible sub-agent slots, with Right/Enter drilling into a slot's\nnext recursive ring rather than trying to show the whole tree at once.\n\n## Task Spec\n\n`codewhale fleet run` accepts JSON or TOML. `codewhale fleet run <spec> --check`\nruns every validation a real run performs (spec shape, roster members, agent\nprofiles, model routes) and prints the same warnings, then stops: no ledger is\ncreated, no run is written, no worker starts, and nothing is spent. A minimal\nJSON spec:\n\n```json\n{\n \"name\": \"local smoke\",\n \"tasks\": [\n {\n \"id\": \"lint\",\n \"name\": \"Lint\",\n \"instructions\": \"Run the lint check and report failures.\",\n \"expected_artifacts\": [\"log\"]\n }\n ]\n}\n```\n\nWorkers are optional. If omitted, Codewhale creates local worker slots up to\n`--max-workers`.\n\nA spec file takes one of three shapes, chosen by its structure before any\nfield is read:\n\n- a **document** — an object with `tasks` (and optionally `name`, `labels`,\n `workers`, `usage_ceiling`);\n- a **task array** — a bare JSON array of task objects;\n- a **single task** — one task object with `id` / `instructions` at the top\n level (JSON or TOML; a TOML file is never a task array).\n\nArray and single-task files take their run name from the file name. Because\nthe shape is picked first, a malformed spec reports the real problem, for\nexample ``JSON spec document at tasks[1] (id \"review\"): missing field\n`instructions` at line 7 column 5``. The checked-in\n[`docs/examples/fleet-dogfood.toml`](examples/fleet-dogfood.toml) and the\ntutorial's `tasks.json` are parsed by the test suite, so they stay valid.\n\nTask specs are typed in Rust and keep verification data separate from worker\ntranscripts. Only the `worker` member/role reference participates in fleet\nidentity selection. The remaining execution fields are delegated-coordination\nor Runtime inputs applied after the member is resolved. A task can declare:\n\n- `id`, `name`, `description`, `objective`, and `instructions`\n- `worker` role, tool profile, tools, and required capabilities\n- `workspace` root, required files, writable paths, and environment allowlist\n- `input_files`, extra `context`, `budget`, `timeout_seconds`, and `retry_policy`\n- `expected_artifacts`, `scorer`, `tags`, and free-form `metadata`\n\nNone of those execution-policy fields becomes part of a fleet identity or an\nalternate member selector. Omitted or zero `max_steps` means no model-step\nceiling; Codewhale must not synthesize a default step budget. (This is the\nfleet file task-spec convention; model-facing `agent` calls differ — the tool\nparser rejects an explicit zero. See `docs/SUBAGENTS.md`.) Explicit positive\nstep limits, timeouts, cancellation, provider safeguards, heartbeats, and\nadmission control are enforced independently by the delegated coordinator and\nRuntime.\n\nWorkers write bounded artifact files under `.codewhale/fleet/` and ledger only\nthe artifact refs: kind, path, checksum, MIME type, and size. Receipts record\n`pass`, `fail`, `partial`, `skip`, or `timeout`; failed receipts may also mark\nthe source as `transport`, `task`, or `verifier`. `codewhale fleet status`\nsurfaces those failure-source counts separately.\n\nDeterministic built-in scorers are `exit_code`, `file_exists`, `regex_match`,\nand `json_path`. Specs may also declare `command`,\n`code_whale_verifier_prompt`, or `manual`; those record a partial receipt until\nan explicit verifier pass completes.\n\n### Using Role Presets\n\nTasks can reference a semantic role name to select one unique roster member.\nBuilt-in role names (`smoke-runner`, `reviewer`, `builder`, `read-only`) remain\navailable for compatibility, and custom roles may be defined in\n`[fleet.roles]`.\n\n```json\n{\n \"name\": \"smoke check\",\n \"tasks\": [\n {\n \"id\": \"lint\",\n \"name\": \"Lint check\",\n \"instructions\": \"Run lint and report failures.\",\n \"worker\": { \"role\": \"smoke-runner\" },\n \"expected_artifacts\": [\"log\"]\n }\n ]\n}\n```\n\nAfter identity resolution, compatibility role presets may provide tool,\ntimeout, or retry defaults to the delegated coordinator. Those defaults do not\ngrant authority, do not change which member was selected, and remain subject to\nRuntime clamping. A task spec may request its execution settings explicitly:\n\n```json\n{\n \"id\": \"deep-review\",\n \"name\": \"Deep review\",\n \"instructions\": \"Review the entire crate for soundness issues.\",\n \"worker\": {\n \"role\": \"reviewer\",\n \"tools\": [\"cargo\", \"rg\", \"git\"],\n \"capabilities\": [\"rust\"]\n },\n \"input_files\": [\"crates/**/*.rs\"],\n \"budget\": { \"max_tokens\": 32000 },\n \"expected_artifacts\": [\"log\", \"report\"],\n \"scorer\": { \"kind\": \"regex_match\", \"path\": \".codewhale/fleet/report.md\", \"pattern\": \"finding|all clear\" }\n}\n```\n\n### Multi-Task Run Example\n\nA single fleet run can dispatch several independent tasks in parallel:\n\n```json\n{\n \"name\": \"CI gate\",\n \"tasks\": [\n {\n \"id\": \"check\",\n \"name\": \"Compile check\",\n \"instructions\": \"Run cargo check --workspace and report errors.\",\n \"worker\": { \"role\": \"builder\" },\n \"expected_artifacts\": [\"log\"],\n \"scorer\": { \"kind\": \"exit_code\" }\n },\n {\n \"id\": \"clippy\",\n \"name\": \"Clippy lint\",\n \"instructions\": \"Run cargo clippy --workspace and report warnings.\",\n \"worker\": { \"role\": \"reviewer\", \"tools\": [\"cargo\", \"cargo-clippy\"] },\n \"expected_artifacts\": [\"log\"],\n \"scorer\": { \"kind\": \"exit_code\" }\n },\n {\n \"id\": \"security\",\n \"name\": \"Secret audit\",\n \"instructions\": \"Search for plaintext secrets and report any matches.\",\n \"worker\": { \"role\": \"read-only\", \"tools\": [\"rg\"] },\n \"input_files\": [\"crates/**/*.rs\"],\n \"expected_artifacts\": [\"log\", \"report\"],\n \"retry_policy\": { \"max_attempts\": 1 }\n }\n ]\n}\n```\n\n## Alerts\n\nfleet alerting is disabled by default. A caller must supply an enabled alert\nconfig before anything is sent. Routes match typed fleet event classes, not log\nstrings:\n\n- `stale`\n- `restart_exhausted`\n- `needs_human`\n- `budget_exceeded`\n- `verifier_failed`\n- `run_completed`\n\nAdapter config stores environment variable names, not secret values. Send-time\ncode resolves those names from the environment or a future secrets provider.\nLedger records store only audit labels such as `slack`, `webhook`, or\n`pagerduty`; task specs persisted in the ledger redact webhook URLs and routing\nkeys.\n\nExample alert config shape:\n\n```json\n{\n \"enabled\": true,\n \"dry_run\": true,\n \"routes\": [\n {\n \"events\": [\"stale\", \"restart_exhausted\", \"verifier_failed\"],\n \"adapter\": \"ops-slack\"\n },\n {\n \"events\": [\"restart_exhausted\"],\n \"adapter\": \"pager\"\n }\n ],\n \"adapters\": {\n \"ops-slack\": {\n \"kind\": \"slack\",\n \"webhook_env\": \"CODEWHALE_FLEET_SLACK_WEBHOOK\",\n \"channel\": \"#codewhale-fleet\"\n },\n \"pager\": {\n \"kind\": \"pager_duty\",\n \"routing_key_env\": \"CODEWHALE_FLEET_PAGERDUTY_ROUTING_KEY\",\n \"severity\": \"critical\"\n }\n }\n}\n```\n\nUse dry-run to inspect a redacted adapter payload without sending:\n\n```sh\ncodewhale fleet alert-dry-run \\\n --event stale \\\n --run-id fleet-demo \\\n --worker-id fleet-demo-local-1 \\\n --task-id release-triage \\\n --reason \"worker heartbeat stale since 2026-06-13T02:00:00Z\" \\\n --adapter slack\n```\n\nThe payload includes the run id, worker id, task id, status, short reason, and\nsafe inspection commands such as `codewhale fleet status` and\n`codewhale fleet inspect <worker-id>`. Endpoints, webhook secrets, and\nPagerDuty routing keys are shown as `<redacted:env:...>`.\n\n## Status Surfaces\n\n`codewhale fleet status` shows compact counts for queued, running, completed,\npartial, failed, restarted, escalated, cancelled, stale, and verifier/transport\nfailure sources. `inspect` shows the worker state plus the current task\nobjective, role, host, heartbeat, latest event, artifact refs, latest error, and\nalert state. `logs` prints bounded log artifact contents, and `artifacts` lists\nartifact refs without embedding large payloads.\n\nThe Runtime API exposes the same ledger-backed projection behind the existing\nruntime auth middleware:\n\n```text\nGET /v1/fleet/runs\nGET /v1/fleet/runs/{run_id}\nGET /v1/fleet/runs/{run_id}/workers\nGET /v1/fleet/workers/{worker_id}\nPOST /v1/fleet/workers/{worker_id}/interrupt\nPOST /v1/fleet/workers/{worker_id}/restart\nPOST /v1/fleet/runs/{run_id}/stop\n```\n\nAction endpoints call the same manager controls as the CLI and record their\ndecisions in the fleet ledger.\n\n## Manager-Agent Runbook\n\nManager agents should treat fleet operations as typed, ledgered control-plane\nwork. Start with `codewhale fleet status`, then inspect one run or worker with\n`codewhale fleet inspect <worker-id>`, `logs`, and `artifacts`. Use direct\nreads of `.codewhale/fleet.jsonl`, host logs, or remote files only when the\ntyped CLI/API surface cannot provide the required evidence.\n\nClassify the worker before taking action:\n\n- `transient failure`: stale heartbeat, host timeout, interrupted transport,\n retryable provider/network error, or an adapter status that can plausibly\n recover without changing the task.\n- `task failure`: the worker completed but produced an incorrect result,\n domain failure, missing required artifact, or explicit task-level error.\n- `verifier failure`: the worker result exists, but the scorer/verifier failed,\n timed out, or disagrees with the receipt.\n- `needs-human`: missing authority, secret request, destructive operation,\n repeated restart exhaustion, ambiguous product decision, or conflicting\n evidence that the manager cannot resolve from typed artifacts.\n\nChoose one typed action:\n\n- Restart a worker only when the failure is transient, retry budget remains,\n the task is idempotent or retry-safe, and no permission or secret boundary is\n involved: `codewhale fleet restart <worker-id>`.\n- Interrupt or stop only when the current task is unsafe to continue or the\n operator explicitly asks for cancellation: `codewhale fleet interrupt\n <worker-id>` or `codewhale fleet stop --all`.\n- Do not restart pure task failures by default; preserve artifacts and hand the\n receipt to the task owner unless the task spec says retrying can produce new\n evidence.\n- For verifier failures, inspect scorer inputs and artifact refs first. If the\n verifier cannot be corrected through typed fleet actions, escalate for human\n review.\n- For `needs-human`, draft an escalation instead of sending it unless alert\n config explicitly authorizes sending.\n\nSafe Slack or PagerDuty draft:\n\n```text\nCodewhale fleet needs attention\nRun: <run-id>\nWorker: <worker-id>\nTask: <task-id or unknown>\nClassification: <transient failure | task failure | verifier failure | needs-human>\nReason: <one sentence, no secrets>\nLatest typed evidence: codewhale fleet inspect <worker-id>; codewhale fleet artifacts <worker-id>\nSafe log excerpt: <3 lines max or \"see artifact <ref>\">\nRequested decision: <restart approval | verifier review | task owner review | permission decision>\n```\n\nPost-run summaries should include the run id, workers checked, classification,\ntyped action taken or drafted, expected ledger effect, artifact refs reviewed,\nand next owner. Keep summaries bounded; link artifact refs instead of copying\nfull logs or transcripts.\n\nThe bundled `fleet-manager` skill mirrors this runbook for manager agents. It\nis a first-party system skill and should be discoverable through the normal\nskill registry after system skills are installed or refreshed.\n\n## Host Adapters\n\nThe Runtime host-adapter boundary supports local child processes and explicit\nSSH workers. Host choice is Runtime placement on a worker spec, not fleet member\nidentity or a member selector. It does not authenticate the host or grant\naccess. Adapters expose the same operations: start, read status, read bounded\nlogs, interrupt, restart, stop, and cleanup.\n\nLocal workers run as child processes with stdin closed and stdout/stderr written\nto bounded host-adapter logs. They inherit only a small safe base environment\nsuch as `PATH` and explicitly allowlisted variables.\n\nSSH workers run through the system `ssh` client with `BatchMode=yes` and a\nbounded connect timeout. Remote environment variables are sent with OpenSSH\n`SendEnv`; values are not embedded in the local ssh argv or fleet logs.\n\nHost keys must already be trusted: connections use `StrictHostKeyChecking=yes`\nand never accept a new key automatically. An explicit `known_hosts` file limits\ntrust to that file; when it is omitted, OpenSSH uses its normal known-host stores.\nVerify the host key before adding it to either store. The legacy\n`host_key_fingerprint` field is unsupported and is rejected; migrate it to a\nverified `known_hosts` entry. The `identity` file selects the client login key;\nit does not verify the remote host.\n\nExample SSH worker spec:\n\n```json\n{\n \"id\": \"builder-1\",\n \"name\": \"Builder 1\",\n \"host\": {\n \"kind\": \"ssh\",\n \"host\": \"builder.example.com\",\n \"user\": \"codewhale\",\n \"port\": 22,\n \"identity\": \"~/.ssh/codewhale_fleet\",\n \"known_hosts\": \"~/.ssh/codewhale_fleet_known_hosts\",\n \"working_directory\": \"/srv/codewhale/work\",\n \"env_allowlist\": [\"CODEWHALE_PROFILE\"],\n \"codewhale_binary\": \"/usr/local/bin/codewhale\"\n },\n \"capabilities\": [\"local\", \"linux\", \"tests\"],\n \"max_concurrent_tasks\": 1\n}\n```\n\nDefaults are intentionally conservative:\n\n- no hosted control plane or cloud provisioning is enabled;\n- SSH requires an explicit host, working directory, and Codewhale binary path;\n- secret-like environment names such as `TOKEN`, `SECRET`, `PASSWORD`,\n `API_KEY`, and `PRIVATE_KEY` are rejected from adapter allowlists;\n- secrets should remain in Codewhale config providers or remote host config,\n not in task instructions, argv, or fleet logs.\n\n## Runtime policy and authority are not fleet identity\n\nfleet does not define a project/workspace trust level, filesystem or network\nreach, secret access, approval mode, sandbox, tool set, or execution authority.\nThose belong to delegated-coordination and Runtime policy. This separation is\nload-bearing:\n\n- member resolution considers only member id/name, semantic role,\n provider/model identity, and roster state;\n- the selected identity is frozen before any authority policy is evaluated;\n- the Runtime applies the live parent ceiling when one exists; standalone\n fleet CLI launches instead carry an explicit bounded authority envelope,\n and both paths remain subject to live sandbox and platform enforcement;\n- no trust, permission, capability, secret, sandbox, approval, or tool-policy\n value may select another member or silently change its provider/model route;\n and\n- receipts report requested and effective Runtime posture separately from the\n fleet member identity.\n\nOlder persisted configuration and protocol shapes may still contain fields such as\n`security_policy`, `trust_level`, `permissions`, `capability_grants`, secret\nreferences, host authentication, environment allowlists, or tool profiles.\nThey remain deserializable for ledger replay, but new fleet run creation rejects\n`security_policy` and worker `trust_level` rather than pretending they grant\nauthority. Their presence in old data does not make them fleet variables or\ngrants. The active Runtime remains the final authority and fails closed when a\nrequested operation cannot be enforced.\n\nFor current enforcement behavior, use [Modes](MODES.md),\n[Sub-agents](SUBAGENTS.md), [Agent Runtime](AGENT_RUNTIME.md), and the\n[Command Control Plane](COMMAND_CONTROL_PLANE.md). Keep secret values out of\ntask instructions, arguments, logs, and receipts; adapter and Runtime layers\nmust continue to redact or reject them independently of fleet selection.\n\n## Child grants: 0.10.1 scope and the 0.11 rework (#6298)\n\nA child's authority is one grant object, `ChildGrant`\n(`crates/agent-contract/src/worker_profile.rs`). It shipped in v0.10.0 (966ef974e1,\n#5633). It has `files` (none / read / write), `shell` (none / inspect /\nverify / full), `network`, `desktop`, a tool `surface`, the caller's explicit\n`scope`, and `spawn`. Roles are presets over it (`ChildGrant::for_role`), and\n`ChildGrant::resolve` intersects the preset with the parent-derived profile\nand the caller's scope. The child's tool catalog, its dispatch refusals, and\nits capability envelope all read the same grant, so a tool the child can see\nis a tool it can call. `desktop` is in no preset.\n\n`ShellGrant::Verify` also shipped in v0.10.0: the Verifier preset gets the\nbounded built-in verification surface (default workspace checks, pure test\nselection, bounded Git fetch and merge-tree) instead of a shell grammar.\n\n**Other fixes shipped by v0.10.0** (on top of the grant):\n\n- Children never inherit desktop or computer-control tools (b5e48cd31, #6296).\n- A bounded verify surface for Git: `fetch` against a configured remote name\n and a read-only `merge_tree` (b89349286).\n- Refusals name the sanctioned alternative and tell a child to report a\n blocked probe to its parent instead of working around it (23747acea).\n- One reasoning vocabulary (c2bc1244d). Token budgets are tracked but never\n enforced (a7a8bdb33).\n\n**0.10.1 scope.** This release adds no new grant-model code. #6298 is\nre-scoped to the remainder below, which lands in 0.11 as its own slices.\n\n**0.11 remainder** (one slice at a time):\n\n1. **A `verify` shell mode for builds.** `cargo test`/`check` run under an\n explicit, bounded write scope (`target/`, refs), so a verifier can run the\n builds it is handed without a full shell.\n2. **Classified tool families that fail closed.** MCP and desktop tools form a\n labeled family. A child gets that family only when the spawn grants it with\n a reason, and an unclassified tool is not granted.\n3. **Legible grants.** The role picker, roster, and receipts show the effective\n grant, model, and thinking tier in plain words.\n\nRelated work is tracked in #6015, #5633, #6194, and #6232.\n"
|
||
},
|
||
{
|
||
"path": "docs/WORKFLOW_AUTHORING.md",
|
||
"sha256": "8a2433da7a3d025c01de1a98224719ef1351e72a039fcc7c55801243fcd2bd90",
|
||
"content": "# Workflow Authoring\n\n> 阅读简体中文版:[zh_hans/WORKFLOW_AUTHORING.md](zh_hans/WORKFLOW_AUTHORING.md)。\n\n> **Ordinary multi-agent work does not require this file.** In Operate, send\n> normal messages. Small work stays direct; multiple delegated steps use a\n> compact Workflow plan with dependencies, bounded scopes, and completion\n> evidence. Fleet manages the same sub-agents and roles. One bounded,\n> independent task can use a direct agent, with `followup` for continued work.\n> Act/Agent may also use optional soft-auto launch. See\n> [Automatic Workflows](AUTOMATIC_WORKFLOWS.md).\n\nWorkflow has one runtime boundary: authored source lowers to typed\nRust `WorkflowSpec`, Rust validates the IR, and the scheduler/headless worker\nruntime executes leaves. Authoring languages do not get hidden authority to own\nfiles, shell, network, providers, cancellation, or TUI state.\n\nCompatibility launch paths on the `workflow` tool:\n\n| Input | When to use |\n|-------|-------------|\n| `plan` | Structured goal / phases / children (preferred agent path) |\n| `script` | Short inline JS the model owns |\n| `source_path` | Checked-in `.workflow.js` / `.workflow.ts` in the workspace |\n\nUse `agent(action=\"roster\")` to inspect the saved Fleet models and roles before\nassigning children. Native plan children accept `model` for a saved shortlist\nselector, or `role`/`profile` for a saved assignment. Named Exact Fleets keep\ntheir member routes fixed and reject per-step model overrides. Plan children\nalso accept `cwd`, a repository-relative working directory — required in\nmulti-repository workspaces so the child (and worktree isolation) resolves the\nright repository, mirroring `task({cwd})`.\n\nFor a guided walkthrough from fleet task specs to Workflow authoring and\nmonitoring, see [fleet + Workflow Tutorial](FLEET_WORKFLOW_TUTORIAL.md).\n\n\n## Access model\n\nThe Workflow script is a **coordinator only**. It has no filesystem or shell of\nits own. Real work happens in sub-agents the script launches.\n\n| Layer | What it can access |\n|-------|--------------------|\n| Workflow script (JS VM) | Script variables, branching/loops, `task()` / `parallel()` / `pipeline()`, `phase` / `log`, `budget` / `args`. **No** direct FS, shell, network, env, imports, clock, or randomness. |\n| Workflow-spawned sub-agents | Normal tool surface (read/search/edit/write, shell, web, MCP) subject to role posture, allowlists, and parent policy. File edits for write-capable roles auto-accept under Workflow; shell / web / MCP still require parent auto-approve or fail closed. |\n| Parent session | Working directory, configured tools/MCP, permission mode, sandbox/network rules. |\n\n### Scale\n\n- Up to **16 concurrent** live agents in one run (additional spawns wait for a slot).\n- No default cumulative child limit; `[workflow] max_children = 0` leaves total work uncapped.\n- Owners can set a positive `max_children` limit and configure `max_concurrent`.\n- Automatic launch is model-judged on scope; the host enforces configured owner limits and plan validation.\n- Plan the population the work needs and let the host queue and clamp it.\n These ceilings are enforcement, not a reason to pre-shrink a valid plan.\n\nSee the Workflow JS sandbox tests for the fail-closed host surface inventory.\n\n## Language Choice\n\n| Surface | Strength | Tradeoff | Stance |\n|---|---|---|---|\n| YAML / JSON IR | Simple, reviewable, no runtime | Verbose for generated workflows | Keep as interchange/debug format |\n| JavaScript | Familiar object syntax and easy agent generation | Unsafe if executed as a general runtime | First-class authoring through declarative compile-only subset |\n| TypeScript | Best editor/types story for workflow SDK | Needs stripping/typechecking if full TS is supported | Same compile-only subset for now; richer SDK later |\n\nThe default high-capability path is TypeScript/JavaScript authoring, but only as\na compile step. The compiler accepts a JSON-compatible object inside\n`workflow({...})` from `.workflow.js` or `.workflow.ts`, lowers it to\n`WorkflowSpec`, and runs the Rust validation gate. (Starlark authoring was a\nbootstrap reference and has been removed; Workflow authoring is JS-only.)\n\n## Contract\n\nAccepted source shape:\n\n```js\nexport default workflow({\n \"id\": \"issue-audit-js\",\n \"goal\": \"Audit an issue fix with parallel agents\",\n \"nodes\": [\n {\n \"branch\": {\n \"id\": \"parallel-audit\",\n \"children\": [\n { \"agent\": { \"id\": \"code-audit\", \"prompt\": \"Review code\", \"agent_type\": \"review\" } },\n { \"agent\": { \"id\": \"test-audit\", \"prompt\": \"Review tests\", \"agent_type\": \"verifier\" } }\n ]\n }\n },\n { \"reduce\": { \"id\": \"summary\", \"inputs\": [\"code-audit\", \"test-audit\"], \"prompt\": \"Summarize\" } }\n ]\n});\n```\n\nSupported node wrappers: `agent`, `branch`, `sequence`, `reduce`,\n`teacher_review`, `loop_until`, `cond`, and `expand`. Raw `WorkflowNode` JSON IR\nwith `kind` / `spec` also remains valid.\n\nAn `agent` node may declare `\"profile\": \"reviewer\"` to run as a named fleet\nroster profile. The name is trimmed and lowercased at compile time and must be\na single token (no whitespace, quotes, or `=`); the saved roster is resolved at\ndispatch time, and explicit fields on the agent override profile defaults.\n\nThe runtime `task()` surface also accepts `cwd` for an existing repository-\nrelative working directory. This is required when a workflow is launched from\na multi-repository workspace and the child needs shell or file access. `cwd`\nis validated by the host, does not grant mutation authority, and should be\npaired with `worktree: true` when the child needs an isolated checkout.\n\nThe compiler rejects effectful constructs such as `import`, `require`, `fetch`,\n`process`, `Deno`, `Bun`, `child_process`, file reads/writes, `eval`, `async`,\nand `await`. This is intentionally stricter than JavaScript: workflow source is\na familiar declaration format, not a second execution runtime. The denied\neffects are not denied to the run — put them in a child worker, which has\nthe full tool surface, and keep the script to coordination.\n\n## Gates\n\nA gate fires on a role's completion (`on: role_complete`) and blocks the role\nnamed in `blocks_role` until the gate passes. That makes `blocks_role` a\n**different** role from the one being gated — a gate that verifies phase 2's\nwork must block phase 3, never phase 2:\n\n```js\n// Correct: verify the implementer, block the next stage.\ngates: [{\n id: \"verify-fix\",\n gate: \"verify\",\n on: \"role_complete\",\n role: \"implement\",\n blocks_role: \"verify\",\n on_fail: \"escalate\",\n max_retries: 1,\n require_explicit_verdict: true,\n}]\n```\n\nAdmission rejects a gate that blocks its own role, a cycle of role gates,\nand a gate whose explicitly named prerequisite first occurs after its blocked\nrole in a sequential plan. Diagnostics name the contradictory gate and roles\nbefore any child starts. Parallel structure, unknown profile roles and dynamic\nraw JavaScript order are not assigned a total order; runtime gates still enforce the prerequisite at dispatch.\n\nNative singleton and sequence steps use the same settled behavior as parallel\nsteps. Ordinary failures retain partial results and let independent work\ncontinue. A step whose required input failed or is missing is skipped with a\nnamed reason; it never receives an empty substitute and never launches a child.\nRaw JavaScript `task()` remains fail-fast unless the author uses the existing\nsettled combinators. Reducers can explicitly summarize partial inputs; they\nmust disclose missing inputs instead of claiming that blocked work ran.\n\nLet workers finish their assigned outcome. Legacy model task limits are accepted\nfor compatibility but ignored, and token hints are not spend caps. Actual owner\nlimits belong in operator, role/Fleet, parent or saved-runtime configuration.\nPreserve partial findings and exact blockers for a follow-up instead of discarding\ncompleted work. Schema repair cannot invent a deadline from a legacy task field.\n\n## Verification\n\n- `cargo test -p codewhale-workflow --locked javascript`\n\nCurrent example: `workflows/issue_audit.workflow.js`.\n\n## Agent-Written fleet Workflows\n\nThe primary product flow is not \"ask the user to write a script.\" The main\nagent should decide when a task deserves workflow orchestration, draft the\nWorkflow source, show the plan for the current permission mode, and then let\nthe runtime compile and monitor it.\n\nWorkflow owns the plan: phases, branches, loops, reducers, and intermediate\nresults. fleet owns the durable roster, member identity, semantic role, and\nsaved provider/model pins or inheritance. Runtime owns tool posture, launch\nconcurrency, leases, heartbeats, logs, receipts, and resume/stop/restart\ncontrols. In other words, a workflow selects fleet members and monitors their\nRuntime runs; it isn't an executor, because the script has no shell or\nfilesystem of its own — effects live in the workers.\n\nWorkflow-to-Runtime launch validation applies a conservative default shape\nbefore any Workflow IR is lowered to selected workers:\n\n- no default total worker limit; a positive owner-configured `max_children` still applies;\n- up to 16 live worker agents at once; larger populations queue (block) on the\n host's per-run concurrency gate until a live slot frees, then select through\n fleet and execute through Runtime;\n- no default structural nesting cap; positive owner `workflow.max_depth` values apply;\n- no default Runtime delegation depth cap; explicit owner restrictions remain\n independent of Workflow IR shape;\n- loops require `max_iterations`;\n- dynamic `expand` nodes require `max_children` and a template.\n\nThose limits distinguish population from instantaneous launch concurrency. A\nlong-running Workflow can still drain through a smaller Runtime worker\npool. Model selection stays per member: a DeepSeek preset can suggest\n`deepseek-v4-pro` for the orchestrator and `deepseek-v4-flash` for nearby\nworkers, but users and agents may override any slot when the task calls for it.\n\n## Experimental search is a Workflow option\n\nExperimental search generalizes the existing best-of-N recipe without adding a\nnew product mode, scheduler, or sub-agent API. The proposed search spec would\nfreeze the objective, baseline, model request and resolved version, public\nevidence, evaluator hash, hard gates, scoring rule, budgets, write scope,\nrounds, and review-only integration policy before admission; it is a design,\nnot shipped code.\n\nThe current JS starter supports structured generation and read-only review with\n`strategy: \"search\"`. Runtime-owned command gates, hidden evaluation, benchmark\nscoring, and clean-baseline replay are an explicit host seam still to wire; a\ncandidate's self-verdict must never be promoted into evaluator truth. See\n[Workflow Experimental Search](WORKFLOW_EXPERIMENTAL_SEARCH.md).\n"
|
||
}
|
||
]
|
||
},
|
||
{
|
||
"id": "sandbox",
|
||
"slug": "sandbox",
|
||
"sources": [
|
||
{
|
||
"path": "docs/SANDBOX.md",
|
||
"sha256": "84e4604e2f9788bc39ba96b52302a847be8861b59740c7894d37d4b17d577c7f",
|
||
"content": "# Sandbox threat model\n\n> 阅读简体中文版:[zh_hans/SANDBOX.md](zh_hans/SANDBOX.md)。\n\nCodewhale can launch shell commands proposed by a model. Approval policy,\nworkspace-aware tools, and an operating-system command wrapper are separate\ncontrols: an approval is not a sandbox, and selecting `workspace-write` does\nnot prove that the current platform has an OS wrapper available.\n\nThis document describes only behavior wired into the command execution path.\nSee [Authorization order](AUTHORIZATION_ORDER.md) for the policy layers that\nrun before execution reaches this boundary.\n\n## Platform overview\n\n| Mechanism | Platform | Selection | What Codewhale reports |\n|---|---|---|---|\n| Seatbelt (`sandbox-exec`) | macOS | Automatic when the runtime probe succeeds | `macos-seatbelt` |\n| Bubblewrap (`/usr/bin/bwrap`) | Linux | Default when installed and a wrapped probe run succeeds; `prefer_bwrap = false` opts out | `linux-bwrap` |\n| No OS wrapper | Linux without usable bwrap, or opted out | `prefer_bwrap = false`, or bwrap absent/unusable | `none` |\n| No OS wrapper | Windows | Current implementation | `none` |\n| OpenSandbox-compatible service | Any supported host | `sandbox_backend = \"opensandbox\"` | External execution path |\n\nThe repository also contains a future Windows helper contract. It is not\nwired into child-command launch, so Codewhale does not advertise it as an\nactive sandbox. Source-only sandbox code is not evidence that a command was\nrestricted.\n\n## macOS: Seatbelt\n\nCodewhale probes `/usr/bin/sandbox-exec` by running a minimal profile. When the\nprobe succeeds and the selected `SandboxPolicy` requests a sandbox, the child\ncommand is wrapped with a generated Seatbelt profile.\n\nThe profile can provide:\n\n- broad filesystem reads;\n- writes limited by the selected policy, including the workspace and specific\n runtime/cache paths needed by supported tools;\n- network access only when the policy enables it. A `read-only` policy with\n network (the agent `read_only: true` shell mode) gets outbound IPv4 and the\n system DNS resolver only. IPv6 and the host's own addresses, loopback\n included, are denied, except the port of a local proxy named in\n `HTTP_PROXY`, `HTTPS_PROXY` or `ALL_PROXY`, and other local sockets stay\n denied.\n\nIf the probe fails or `sandbox-exec` is unavailable, Codewhale reports no OS\nsandbox and launches the command without a Seatbelt wrapper. It does not set a\nSeatbelt marker on that fallback.\n\n## Linux: default bubblewrap\n\nLinux command sandboxing is on by default whenever bubblewrap works. Set the\ntop-level configuration key to opt out:\n\n```toml\nprefer_bwrap = false\n```\n\nCodewhale selects bubblewrap only when `/usr/bin/bwrap` is a regular\nexecutable file AND a wrapped probe run proves it can create its namespaces\non this host — the exec bit alone lies where user namespaces are restricted\n(e.g. Ubuntu 24.04 with `kernel.apparmor_restrict_unprivileged_userns`),\nwhere every wrapped command would fail rather than run unsandboxed. The\nwrapper derives its mounts and network namespace from the resolved\n`SandboxPolicy`:\n\n```text\n/usr/bin/bwrap \\\n --unshare-all \\\n [--share-net] \\\n --ro-bind / / \\\n --dev /dev \\\n --proc /proc \\\n --tmpfs /tmp \\\n [--dev-bind <device-root> <device-root> ...] \\\n --bind <writable-root> <writable-root> ... \\\n --ro-bind <protected-descendant> <protected-descendant> ... \\\n [--ro-bind <extra-ro-root> <extra-ro-root> ...] \\\n --chdir <cwd> \\\n -- <program> <args>\n```\n\nThe sandbox always gets a private `/dev` (fresh device nodes, so `>/dev/null`\nworks), a private `/proc`, and a tmpfs `/tmp` (#5410). Two optional top-level\nconfig keys extend the mounts: `bwrap_ro_roots` (extra host paths bind-mounted\nread-only, applied last so they can narrow a policy-writable path) and\n`bwrap_dev_roots` (host character/block device nodes bind-mounted read-write;\ndirectories are never honored). Missing paths are skipped silently.\n\nThat gives the child a read-only root view. For `workspace-write`, every safe,\nexisting policy root is mounted read-write: the working directory, configured\nadditional roots, `/tmp` and `TMPDIR` unless excluded, and verified Git\nworktree metadata roots. Existing `.codewhale` and `.deepseek` descendants are\nremounted read-only after their writable parent. Missing paths, non-directory\npaths, and `/` are not promoted to writable mounts.\n\nFor `read-only`, there are no writable binds, so the working directory remains\ninside the read-only root view. `--unshare-all` isolates the network namespace\nby default. Codewhale adds `--share-net` only when the policy's\n`network_access` is true. The agent `read_only: true` shell mode turns it on\nonly in Full Access (see [Subagents](SUBAGENTS.md)); it then shares the host\nnetwork namespace, including loopback and abstract unix sockets, like a\nnetworked `workspace-write` shell. Bubblewrap does not filter either.\n`danger-full-access` and `external-sandbox` bypass the local wrapper entirely.\n\nIf the user opts out, or `/usr/bin/bwrap` is missing, non-executable, or\ncannot actually confine a child, Codewhale reports `none` and launches the\ncommand without a Linux OS wrapper. There is no marker-only fallback to a\ndifferent Linux sandbox.\n\nInstall bubblewrap separately to get enforcement on Linux:\n\n- Ubuntu/Debian: `apt install bubblewrap`\n- Fedora: `dnf install bubblewrap`\n- Arch: `pacman -S bubblewrap`\n\nCodewhale does not vendor bubblewrap.\n\n## Windows: no advertised OS sandbox\n\nThe Windows command path currently reports no OS sandbox. The source tree has\na future helper contract for Job Object process-tree cleanup, but it is not\nwired into selection and must not be described as any of the following:\n\n- read-only filesystem or workspace-write enforcement;\n- network blocking;\n- registry isolation;\n- restricted-token or AppContainer isolation.\n\nWindows host permissions and approval policy still apply, but they are not a\nCodewhale OS command sandbox.\n\n## Linux process hardening is not a command sandbox\n\nAt startup on Linux, Codewhale best-effort applies `PR_SET_DUMPABLE=0`,\n`PR_SET_NO_NEW_PRIVS=1`, and `RLIMIT_CORE=0` to its own process. Each failure is\nlogged and startup continues. These controls reduce process-inspection,\nprivilege-escalation, and core-dump risk; they do not create filesystem or\nnetwork isolation for a child command and are not listed as a sandbox backend.\n\nThe one exception is the startup posture itself: when the startup sandbox mode\nresolves to `danger-full-access` (via `CODEWHALE_SANDBOX_MODE` or the config\nfile's `sandbox_mode` key), `PR_SET_NO_NEW_PRIVS` is skipped so that\n`sudo`/`su`/setuid helpers keep working from the agent shell (#5723) — \"full\naccess\" means it. Every narrower posture keeps the flag as defense-in-depth,\nand `CODEWHALE_NO_NEW_PRIVS` overrides the posture in both directions\n(#5413): a falsey value always skips the flag, a truthy value always sets it.\nThe flag is irreversible for the process tree, so the decision can only be\nmade at launch; per-call sandbox escalation inside a session cannot lift it.\n\n## External OpenSandbox execution\n\nWhen `sandbox_backend = \"opensandbox\"` is configured, shell execution is sent\nto the configured OpenSandbox-compatible HTTP endpoint instead of starting a\nlocal child. Codewhale validates the request/response contract, but isolation\nguarantees belong to the configured service and its operator.\n\n```toml\nsandbox_backend = \"opensandbox\"\nsandbox_url = \"http://localhost:8080\"\nsandbox_api_key = \"YOUR_API_KEY\"\n```\n\n`sandbox_backend = \"none\"` (or omitting the key) keeps local execution.\nUnsupported backend settings refuse shell execution; they never silently select\nlocal execution. Choose a supported backend or explicitly select `none`.\n\n## Policies and fallbacks\n\nThe local `sandbox_mode` values are:\n\n```toml\nsandbox_mode = \"workspace-write\" # read-only | workspace-write | danger-full-access | external-sandbox\n```\n\n- `read-only` and `workspace-write` are enforced by Seatbelt or bubblewrap only\n when that wrapper is selected and available.\n- The configured value can only narrow the sandbox the permission mode\n picks: Full Access (`yolo`) runs without the wrapper, Ask and Auto-Review\n use `workspace-write`, and Plan uses `read-only`. A configured\n `danger-full-access` or `external-sandbox` therefore takes effect only in\n Full Access; it never loosens Ask or Auto-Review.\n- `danger-full-access` deliberately bypasses the local OS wrapper. On Linux it\n also skips the `PR_SET_NO_NEW_PRIVS` process-hardening flag at startup so\n `sudo`/setuid workflows keep running (#5723); see the process-hardening\n section above.\n- `external-sandbox` declares that execution is already externally isolated\n and bypasses a second local wrapper.\n- When no wrapper is selected, the shell command runs without Codewhale OS\n isolation. Approval rules and workspace-aware native file tools remain\n separate controls.\n\nCanonical environment overrides exist for `sandbox_mode` and the external\nbackend:\n\n- `CODEWHALE_SANDBOX_MODE`\n- `CODEWHALE_SANDBOX_BACKEND`\n- `CODEWHALE_SANDBOX_URL`\n- `CODEWHALE_SANDBOX_API_KEY`\n\n`CODEWHALE_PREFER_BWRAP` (legacy alias `DEEPSEEK_PREFER_BWRAP`) overrides the\npreference explicitly; the top-level `prefer_bwrap` config key is the durable\nsetting.\n\n## Diagnostics and failure attribution\n\n`codewhale setup --status`, `codewhale doctor`, `codewhale doctor --json`, and\nthe `diagnostics` tool report the locally available wrapper after applying the\nresolved bubblewrap preference. An individual command can still bypass that\nwrapper when its policy does not request sandboxing. On Linux, merely finding\na sandbox-related syscall or source module does not make `sandbox_available`\ntrue.\n\nDenial attribution is intentionally conservative:\n\n- Seatbelt uses its wrapper-specific denial patterns.\n- Bubblewrap setup errors must be prefixed by `bwrap:`; a read-only-filesystem\n error from the bwrap filesystem view can also identify the boundary.\n- A child command's generic `Permission denied` or `Operation not permitted`\n is not, by itself, proof that Codewhale's sandbox blocked it.\n- Unsandboxed command failures are never labeled sandbox denials.\n\n## Limitations\n\n- Availability is checked before launch; the selected wrapper can still fail\n because of host policy, container restrictions, or a race after the probe.\n- Bubblewrap ignores a configured writable root if it is missing, is not a\n directory, or canonicalizes to `/`; a path can also disappear between policy\n resolution and wrapper launch.\n- Seatbelt profiles are generated at runtime and must be tested against the\n commands they are expected to support.\n- No current local wrapper is advertised on Windows.\n- An external sandbox backend is only as strong as its configured service.\n- No sandbox protects against kernel vulnerabilities or all resource-exhaustion\n and side-channel attacks.\n\n## Implementation references\n\n- `crates/sandbox/src/lib.rs` — truthful selection and public capability markers\n- `crates/sandbox/src/seatbelt.rs` — macOS wrapper and availability probe\n- `crates/sandbox/src/bwrap.rs` — Linux wrapper and functional availability probe\n- `crates/sandbox/src/process_hardening.rs` — Linux parent-process hardening\n- `crates/sandbox/src/backend.rs` — external backend selection\n- `crates/engine/src/tools/diagnostics.rs` — machine-readable diagnostics\n"
|
||
},
|
||
{
|
||
"path": "docs/AUTHORIZATION_ORDER.md",
|
||
"sha256": "218dcab34337314dbf4083086a9451de4bece1f833d5bc33e5b5963d6cdf45bd",
|
||
"content": "# Authorization order\n\n> 阅读简体中文版:[zh_hans/AUTHORIZATION_ORDER.md](zh_hans/AUTHORIZATION_ORDER.md)。\n\nCodewhale combines tool availability, hooks, typed permission rules, approval\nposture, repository policy, and sandboxing. An approval from one layer is not a\nuniversal bypass: a later safety layer can still require review or block the\ncall, and an approval is not an operating-system sandbox grant.\n\nThis page records the order implemented by the interactive engine's\nmodel-requested tool path. Entrypoints that use only part of that path, such as\nthe core runtime's direct tool API, keep the relative order of the layers they\ndo use.\n\n## Model tool-call pipeline\n\nThe interactive engine evaluates a model-requested tool call in this order:\n\n| Order | Layer | Result |\n|---:|---|---|\n| 1 | Effective configuration and posture | User settings, command/runtime overrides, and the project overlay resolve before the turn. A project overlay may tighten `approval_policy`, `sandbox_mode`, or shell availability, but may not loosen them. |\n| 2 | Mode and tool admission | Plan-mode restrictions, input parse errors, per-command tool deny/allow lists, caller restrictions, and missing execution registrations fail before policy rules are considered. A tool present in both command lists is denied. |\n| 3 | Preparation, then `tool_call_before` hooks | Registry preparation is side-effect free. Foreground hooks then fold as `deny > ask > allow`, and a strict matching hook that produces no verdict fails closed. The last `updatedInput` wins; the engine prepares the rewritten input again before later gates inspect it. |\n| 4 | Registered-tool baseline | The prepared tool's `ApprovalRequirement` establishes its ordinary approval need. A hook `ask` is applied after that assignment so the baseline cannot erase it. Non-bypassable registered holds remain forced in postures that can prompt; Full Access auto-approves them instead of opening a contradictory modal. Plan mode also blocks write-capable tools here. |\n| 5 | Typed `permissions.toml` rule | A matching `deny` blocks. A matching `allow` may clear only ordinary registry approval; it cannot clear a hook `ask` or a non-bypassable registered hold. A matching `ask` forces review only in a posture that can prompt. Full Access/auto-approval is not downgraded into a prompt, while an explicit typed `deny` still blocks. |\n| 6 | Auto-review policy and built-in safety floor | Configured block rules run before the built-in floor, then configured allow rules and the deterministic fallback. This layer runs after typed permissions and can add a prompt or block, but cannot remove an earlier hold. The built-in floor refuses a shell command, shell input or verifier containing a hidden Unicode character, and holds publish-like actions, destructive background/headless actions and `curl`/`wget` credential uploads with a prompt no grant or allow rule can answer (a block in Auto-Review and Never). Full Access skips the built-in floor. |\n| 7 | Repository law | Protected path invariants can only add a prompt or block. A repo-law prompt becomes a hard block in Full Access, which has no contradictory approval modal. |\n| 8 | Human approval | A remaining prompt is sent to the approval channel. Denial stops the call. Approval authorizes this planned call; session and persistent choices affect later matching calls but do not erase a later gate from this call. |\n| 9 | Tool authority and execution sandbox | Worker authority envelopes, native tool path checks, and the selected OS or external sandbox still apply during execution. A sandbox denial remains a denial unless the user separately authorizes a supported elevation path. |\n\nThe ordering is intentionally monotonic after the typed permission layer:\nauto-review and repository law can tighten a result, not turn a previous block\nor prompt into an unreviewed execution. There are explicit posture choices\ninside those layers—for example, Full Access does not create an interactive\npublish prompt—but those choices do not let an earlier remembered grant erase\na hold that the layer actually produced.\n\n## Typed permission-rule selection\n\n`permissions.toml` is currently a sibling of the active user `config.toml`.\nThere is no project-local permission-rule source today. An optional `workspace`\nfield scopes one user rule to a repository; it does not create a project\noverlay. `/permissions` reports that source, matcher, scope, and whether the\nscope applies to the current workspace.\n\nThe execution-policy engine evaluates matching rules as follows:\n\n1. Normalize the tool, command, workspace, and any workspace-relative path.\n Unsafe or external paths do not become matchable file rules.\n2. Check denied command prefixes against the whole command and every chained\n segment. These hard prefix denies are merged across rulesets and always win.\n3. Compute a trusted-prefix candidate for an unchained shell command. This is a\n candidate for the approval-mode fallback, not an immediate decision.\n4. Select one matching typed rule by this lexicographic precedence:\n 1. higher source layer: `User > Agent > BuiltinDefault`;\n 2. stronger action inside that layer: `deny > ask > allow`;\n 3. the more specific matcher when layer and action tie.\n5. Apply the selected typed action. `deny` forbids the call, `allow` skips the\n execution-policy approval, and `ask` requires approval. A typed `ask`\n overrides a trusted prefix.\n6. If no typed action decides the result, apply the approval-mode fallback\n using the trusted-prefix candidate.\n\nSource layer is compared before typed action. Consequently, a user-layer typed\n`allow` can override an agent-layer typed `deny`; inside the same layer, `deny`\nstill beats `ask`, which beats `allow`, regardless of file order or matcher\nspecificity. Hard denied prefixes are the exception: they are checked before\ntyped-layer selection and cannot be overridden by a typed allow.\n\nSpecificity is only a tie-breaker after source and action. A constrained\ncommand, exact-command, path, or workspace matcher beats a tool-wide rule with\nthe same action in the same layer. Specificity never lets a narrow allow beat a\nsame-layer deny.\n\nFor chained shell commands, a trusted prefix never approves the whole chain. A\ntyped deny that wins for any individual segment blocks the full invocation.\n\n## Approval posture and missing prompts\n\nThe execution-policy result is combined with the registered-tool baseline; the\ntwo should not be interpreted independently.\n\n- Ask and Auto-Review may surface tool safety approvals. Model-authored user\n questions (`request_user_input`) are a separate channel and reach the user in\n every interactive posture, Auto-Review included. Only headless `exec` runs,\n which have no responder, withhold the tool. Runtime threads (app\n conversations, background tasks and automations) keep it: a question parks\n the turn until someone answers or cancels it through the app or runtime API,\n or until a positive `tools.user_input_timeout_seconds` expires. An omitted or\n zero timeout waits indefinitely, the same way an Ask-posture approval parks.\n- Full Access and YOLO-compatible auto-approval paths do not let a typed `ask`\n downgrade the session into prompting. Non-bypassable registered holds\n auto-approve in Full Access. Typed deny, catastrophic background/headless\n safety holds, and repository law still fail closed where their respective\n layers apply.\n- With `approval_policy = \"never\"`, a matching typed `ask` is forbidden because\n the required prompt cannot be shown.\n\nRuntime adapters may transport an approval decision differently from the\ninteractive modal. That transport and any continuation protocol are separate\nfrom this ordering contract; see [Runtime API](RUNTIME_API.md).\n\n## Project overlays\n\nThe project config overlay at `<workspace>/.codewhale/config.toml` is not a\npermission-rule layer. It can only move approval and sandbox posture toward\nmore restrictive values:\n\n- approval: `auto` → `on-request`/`untrusted` → `never`;\n- sandbox: `danger-full-access` → `workspace-write` → `read-only`;\n- shell availability: `true` may become `false`, never the reverse.\n\nProject config cannot add credentials, hooks, provider authority, or a\nproject-local `permissions.toml`. See\n[Configuration](CONFIGURATION.md#per-project-overlay-485) for the complete\noverlay allow-list.\n\n## Regression coverage\n\nThe contract is exercised by tests at the layers that own each decision:\n\n- `authorization_order_contract_matches_documented_precedence` covers hard\n prefix denial and the typed `layer → action → specificity → approval-mode`\n sequence through the public execution-policy API.\n- `hook_fold_deny_wins_over_ask_and_allow` covers foreground hook folding.\n- `non_bypassable_registered_tools_auto_approve_in_full_access` covers\n registered holds.\n- `full_access_permission_allow_cannot_bypass_background_catastrophic_floor`\n and `full_access_permission_allow_cannot_bypass_repo_law` cover later safety\n layers overriding a remembered allow.\n- `project_merge_only_tightens_approval_and_sandbox_policy` covers project\n overlay monotonicity.\n\nFocused commands:\n\n```bash\ncargo test -p codewhale-execpolicy --test authorization_order --locked\ncargo test -p codewhale-tui --lib --locked full_access_permission_allow_cannot_bypass\ncargo test -p codewhale-config --locked project_merge_only_tightens_approval_and_sandbox_policy\n```\n\n## Related references\n\n- [Configuration](CONFIGURATION.md) — rule schema, `/permissions`, hooks, and\n project overlays\n- [Modes](MODES.md) — Plan/Work/Operate and permission posture\n- [Sandbox threat model](SANDBOX.md) — platform enforcement and fallbacks\n- [Runtime API](RUNTIME_API.md) — approval events and remote resolution\n"
|
||
}
|
||
]
|
||
},
|
||
{
|
||
"id": "trust",
|
||
"slug": "trust",
|
||
"sources": [
|
||
{
|
||
"path": "docs/SANDBOX.md",
|
||
"sha256": "84e4604e2f9788bc39ba96b52302a847be8861b59740c7894d37d4b17d577c7f",
|
||
"content": "# Sandbox threat model\n\n> 阅读简体中文版:[zh_hans/SANDBOX.md](zh_hans/SANDBOX.md)。\n\nCodewhale can launch shell commands proposed by a model. Approval policy,\nworkspace-aware tools, and an operating-system command wrapper are separate\ncontrols: an approval is not a sandbox, and selecting `workspace-write` does\nnot prove that the current platform has an OS wrapper available.\n\nThis document describes only behavior wired into the command execution path.\nSee [Authorization order](AUTHORIZATION_ORDER.md) for the policy layers that\nrun before execution reaches this boundary.\n\n## Platform overview\n\n| Mechanism | Platform | Selection | What Codewhale reports |\n|---|---|---|---|\n| Seatbelt (`sandbox-exec`) | macOS | Automatic when the runtime probe succeeds | `macos-seatbelt` |\n| Bubblewrap (`/usr/bin/bwrap`) | Linux | Default when installed and a wrapped probe run succeeds; `prefer_bwrap = false` opts out | `linux-bwrap` |\n| No OS wrapper | Linux without usable bwrap, or opted out | `prefer_bwrap = false`, or bwrap absent/unusable | `none` |\n| No OS wrapper | Windows | Current implementation | `none` |\n| OpenSandbox-compatible service | Any supported host | `sandbox_backend = \"opensandbox\"` | External execution path |\n\nThe repository also contains a future Windows helper contract. It is not\nwired into child-command launch, so Codewhale does not advertise it as an\nactive sandbox. Source-only sandbox code is not evidence that a command was\nrestricted.\n\n## macOS: Seatbelt\n\nCodewhale probes `/usr/bin/sandbox-exec` by running a minimal profile. When the\nprobe succeeds and the selected `SandboxPolicy` requests a sandbox, the child\ncommand is wrapped with a generated Seatbelt profile.\n\nThe profile can provide:\n\n- broad filesystem reads;\n- writes limited by the selected policy, including the workspace and specific\n runtime/cache paths needed by supported tools;\n- network access only when the policy enables it. A `read-only` policy with\n network (the agent `read_only: true` shell mode) gets outbound IPv4 and the\n system DNS resolver only. IPv6 and the host's own addresses, loopback\n included, are denied, except the port of a local proxy named in\n `HTTP_PROXY`, `HTTPS_PROXY` or `ALL_PROXY`, and other local sockets stay\n denied.\n\nIf the probe fails or `sandbox-exec` is unavailable, Codewhale reports no OS\nsandbox and launches the command without a Seatbelt wrapper. It does not set a\nSeatbelt marker on that fallback.\n\n## Linux: default bubblewrap\n\nLinux command sandboxing is on by default whenever bubblewrap works. Set the\ntop-level configuration key to opt out:\n\n```toml\nprefer_bwrap = false\n```\n\nCodewhale selects bubblewrap only when `/usr/bin/bwrap` is a regular\nexecutable file AND a wrapped probe run proves it can create its namespaces\non this host — the exec bit alone lies where user namespaces are restricted\n(e.g. Ubuntu 24.04 with `kernel.apparmor_restrict_unprivileged_userns`),\nwhere every wrapped command would fail rather than run unsandboxed. The\nwrapper derives its mounts and network namespace from the resolved\n`SandboxPolicy`:\n\n```text\n/usr/bin/bwrap \\\n --unshare-all \\\n [--share-net] \\\n --ro-bind / / \\\n --dev /dev \\\n --proc /proc \\\n --tmpfs /tmp \\\n [--dev-bind <device-root> <device-root> ...] \\\n --bind <writable-root> <writable-root> ... \\\n --ro-bind <protected-descendant> <protected-descendant> ... \\\n [--ro-bind <extra-ro-root> <extra-ro-root> ...] \\\n --chdir <cwd> \\\n -- <program> <args>\n```\n\nThe sandbox always gets a private `/dev` (fresh device nodes, so `>/dev/null`\nworks), a private `/proc`, and a tmpfs `/tmp` (#5410). Two optional top-level\nconfig keys extend the mounts: `bwrap_ro_roots` (extra host paths bind-mounted\nread-only, applied last so they can narrow a policy-writable path) and\n`bwrap_dev_roots` (host character/block device nodes bind-mounted read-write;\ndirectories are never honored). Missing paths are skipped silently.\n\nThat gives the child a read-only root view. For `workspace-write`, every safe,\nexisting policy root is mounted read-write: the working directory, configured\nadditional roots, `/tmp` and `TMPDIR` unless excluded, and verified Git\nworktree metadata roots. Existing `.codewhale` and `.deepseek` descendants are\nremounted read-only after their writable parent. Missing paths, non-directory\npaths, and `/` are not promoted to writable mounts.\n\nFor `read-only`, there are no writable binds, so the working directory remains\ninside the read-only root view. `--unshare-all` isolates the network namespace\nby default. Codewhale adds `--share-net` only when the policy's\n`network_access` is true. The agent `read_only: true` shell mode turns it on\nonly in Full Access (see [Subagents](SUBAGENTS.md)); it then shares the host\nnetwork namespace, including loopback and abstract unix sockets, like a\nnetworked `workspace-write` shell. Bubblewrap does not filter either.\n`danger-full-access` and `external-sandbox` bypass the local wrapper entirely.\n\nIf the user opts out, or `/usr/bin/bwrap` is missing, non-executable, or\ncannot actually confine a child, Codewhale reports `none` and launches the\ncommand without a Linux OS wrapper. There is no marker-only fallback to a\ndifferent Linux sandbox.\n\nInstall bubblewrap separately to get enforcement on Linux:\n\n- Ubuntu/Debian: `apt install bubblewrap`\n- Fedora: `dnf install bubblewrap`\n- Arch: `pacman -S bubblewrap`\n\nCodewhale does not vendor bubblewrap.\n\n## Windows: no advertised OS sandbox\n\nThe Windows command path currently reports no OS sandbox. The source tree has\na future helper contract for Job Object process-tree cleanup, but it is not\nwired into selection and must not be described as any of the following:\n\n- read-only filesystem or workspace-write enforcement;\n- network blocking;\n- registry isolation;\n- restricted-token or AppContainer isolation.\n\nWindows host permissions and approval policy still apply, but they are not a\nCodewhale OS command sandbox.\n\n## Linux process hardening is not a command sandbox\n\nAt startup on Linux, Codewhale best-effort applies `PR_SET_DUMPABLE=0`,\n`PR_SET_NO_NEW_PRIVS=1`, and `RLIMIT_CORE=0` to its own process. Each failure is\nlogged and startup continues. These controls reduce process-inspection,\nprivilege-escalation, and core-dump risk; they do not create filesystem or\nnetwork isolation for a child command and are not listed as a sandbox backend.\n\nThe one exception is the startup posture itself: when the startup sandbox mode\nresolves to `danger-full-access` (via `CODEWHALE_SANDBOX_MODE` or the config\nfile's `sandbox_mode` key), `PR_SET_NO_NEW_PRIVS` is skipped so that\n`sudo`/`su`/setuid helpers keep working from the agent shell (#5723) — \"full\naccess\" means it. Every narrower posture keeps the flag as defense-in-depth,\nand `CODEWHALE_NO_NEW_PRIVS` overrides the posture in both directions\n(#5413): a falsey value always skips the flag, a truthy value always sets it.\nThe flag is irreversible for the process tree, so the decision can only be\nmade at launch; per-call sandbox escalation inside a session cannot lift it.\n\n## External OpenSandbox execution\n\nWhen `sandbox_backend = \"opensandbox\"` is configured, shell execution is sent\nto the configured OpenSandbox-compatible HTTP endpoint instead of starting a\nlocal child. Codewhale validates the request/response contract, but isolation\nguarantees belong to the configured service and its operator.\n\n```toml\nsandbox_backend = \"opensandbox\"\nsandbox_url = \"http://localhost:8080\"\nsandbox_api_key = \"YOUR_API_KEY\"\n```\n\n`sandbox_backend = \"none\"` (or omitting the key) keeps local execution.\nUnsupported backend settings refuse shell execution; they never silently select\nlocal execution. Choose a supported backend or explicitly select `none`.\n\n## Policies and fallbacks\n\nThe local `sandbox_mode` values are:\n\n```toml\nsandbox_mode = \"workspace-write\" # read-only | workspace-write | danger-full-access | external-sandbox\n```\n\n- `read-only` and `workspace-write` are enforced by Seatbelt or bubblewrap only\n when that wrapper is selected and available.\n- The configured value can only narrow the sandbox the permission mode\n picks: Full Access (`yolo`) runs without the wrapper, Ask and Auto-Review\n use `workspace-write`, and Plan uses `read-only`. A configured\n `danger-full-access` or `external-sandbox` therefore takes effect only in\n Full Access; it never loosens Ask or Auto-Review.\n- `danger-full-access` deliberately bypasses the local OS wrapper. On Linux it\n also skips the `PR_SET_NO_NEW_PRIVS` process-hardening flag at startup so\n `sudo`/setuid workflows keep running (#5723); see the process-hardening\n section above.\n- `external-sandbox` declares that execution is already externally isolated\n and bypasses a second local wrapper.\n- When no wrapper is selected, the shell command runs without Codewhale OS\n isolation. Approval rules and workspace-aware native file tools remain\n separate controls.\n\nCanonical environment overrides exist for `sandbox_mode` and the external\nbackend:\n\n- `CODEWHALE_SANDBOX_MODE`\n- `CODEWHALE_SANDBOX_BACKEND`\n- `CODEWHALE_SANDBOX_URL`\n- `CODEWHALE_SANDBOX_API_KEY`\n\n`CODEWHALE_PREFER_BWRAP` (legacy alias `DEEPSEEK_PREFER_BWRAP`) overrides the\npreference explicitly; the top-level `prefer_bwrap` config key is the durable\nsetting.\n\n## Diagnostics and failure attribution\n\n`codewhale setup --status`, `codewhale doctor`, `codewhale doctor --json`, and\nthe `diagnostics` tool report the locally available wrapper after applying the\nresolved bubblewrap preference. An individual command can still bypass that\nwrapper when its policy does not request sandboxing. On Linux, merely finding\na sandbox-related syscall or source module does not make `sandbox_available`\ntrue.\n\nDenial attribution is intentionally conservative:\n\n- Seatbelt uses its wrapper-specific denial patterns.\n- Bubblewrap setup errors must be prefixed by `bwrap:`; a read-only-filesystem\n error from the bwrap filesystem view can also identify the boundary.\n- A child command's generic `Permission denied` or `Operation not permitted`\n is not, by itself, proof that Codewhale's sandbox blocked it.\n- Unsandboxed command failures are never labeled sandbox denials.\n\n## Limitations\n\n- Availability is checked before launch; the selected wrapper can still fail\n because of host policy, container restrictions, or a race after the probe.\n- Bubblewrap ignores a configured writable root if it is missing, is not a\n directory, or canonicalizes to `/`; a path can also disappear between policy\n resolution and wrapper launch.\n- Seatbelt profiles are generated at runtime and must be tested against the\n commands they are expected to support.\n- No current local wrapper is advertised on Windows.\n- An external sandbox backend is only as strong as its configured service.\n- No sandbox protects against kernel vulnerabilities or all resource-exhaustion\n and side-channel attacks.\n\n## Implementation references\n\n- `crates/sandbox/src/lib.rs` — truthful selection and public capability markers\n- `crates/sandbox/src/seatbelt.rs` — macOS wrapper and availability probe\n- `crates/sandbox/src/bwrap.rs` — Linux wrapper and functional availability probe\n- `crates/sandbox/src/process_hardening.rs` — Linux parent-process hardening\n- `crates/sandbox/src/backend.rs` — external backend selection\n- `crates/engine/src/tools/diagnostics.rs` — machine-readable diagnostics\n"
|
||
},
|
||
{
|
||
"path": "docs/AUTHORIZATION_ORDER.md",
|
||
"sha256": "218dcab34337314dbf4083086a9451de4bece1f833d5bc33e5b5963d6cdf45bd",
|
||
"content": "# Authorization order\n\n> 阅读简体中文版:[zh_hans/AUTHORIZATION_ORDER.md](zh_hans/AUTHORIZATION_ORDER.md)。\n\nCodewhale combines tool availability, hooks, typed permission rules, approval\nposture, repository policy, and sandboxing. An approval from one layer is not a\nuniversal bypass: a later safety layer can still require review or block the\ncall, and an approval is not an operating-system sandbox grant.\n\nThis page records the order implemented by the interactive engine's\nmodel-requested tool path. Entrypoints that use only part of that path, such as\nthe core runtime's direct tool API, keep the relative order of the layers they\ndo use.\n\n## Model tool-call pipeline\n\nThe interactive engine evaluates a model-requested tool call in this order:\n\n| Order | Layer | Result |\n|---:|---|---|\n| 1 | Effective configuration and posture | User settings, command/runtime overrides, and the project overlay resolve before the turn. A project overlay may tighten `approval_policy`, `sandbox_mode`, or shell availability, but may not loosen them. |\n| 2 | Mode and tool admission | Plan-mode restrictions, input parse errors, per-command tool deny/allow lists, caller restrictions, and missing execution registrations fail before policy rules are considered. A tool present in both command lists is denied. |\n| 3 | Preparation, then `tool_call_before` hooks | Registry preparation is side-effect free. Foreground hooks then fold as `deny > ask > allow`, and a strict matching hook that produces no verdict fails closed. The last `updatedInput` wins; the engine prepares the rewritten input again before later gates inspect it. |\n| 4 | Registered-tool baseline | The prepared tool's `ApprovalRequirement` establishes its ordinary approval need. A hook `ask` is applied after that assignment so the baseline cannot erase it. Non-bypassable registered holds remain forced in postures that can prompt; Full Access auto-approves them instead of opening a contradictory modal. Plan mode also blocks write-capable tools here. |\n| 5 | Typed `permissions.toml` rule | A matching `deny` blocks. A matching `allow` may clear only ordinary registry approval; it cannot clear a hook `ask` or a non-bypassable registered hold. A matching `ask` forces review only in a posture that can prompt. Full Access/auto-approval is not downgraded into a prompt, while an explicit typed `deny` still blocks. |\n| 6 | Auto-review policy and built-in safety floor | Configured block rules run before the built-in floor, then configured allow rules and the deterministic fallback. This layer runs after typed permissions and can add a prompt or block, but cannot remove an earlier hold. The built-in floor refuses a shell command, shell input or verifier containing a hidden Unicode character, and holds publish-like actions, destructive background/headless actions and `curl`/`wget` credential uploads with a prompt no grant or allow rule can answer (a block in Auto-Review and Never). Full Access skips the built-in floor. |\n| 7 | Repository law | Protected path invariants can only add a prompt or block. A repo-law prompt becomes a hard block in Full Access, which has no contradictory approval modal. |\n| 8 | Human approval | A remaining prompt is sent to the approval channel. Denial stops the call. Approval authorizes this planned call; session and persistent choices affect later matching calls but do not erase a later gate from this call. |\n| 9 | Tool authority and execution sandbox | Worker authority envelopes, native tool path checks, and the selected OS or external sandbox still apply during execution. A sandbox denial remains a denial unless the user separately authorizes a supported elevation path. |\n\nThe ordering is intentionally monotonic after the typed permission layer:\nauto-review and repository law can tighten a result, not turn a previous block\nor prompt into an unreviewed execution. There are explicit posture choices\ninside those layers—for example, Full Access does not create an interactive\npublish prompt—but those choices do not let an earlier remembered grant erase\na hold that the layer actually produced.\n\n## Typed permission-rule selection\n\n`permissions.toml` is currently a sibling of the active user `config.toml`.\nThere is no project-local permission-rule source today. An optional `workspace`\nfield scopes one user rule to a repository; it does not create a project\noverlay. `/permissions` reports that source, matcher, scope, and whether the\nscope applies to the current workspace.\n\nThe execution-policy engine evaluates matching rules as follows:\n\n1. Normalize the tool, command, workspace, and any workspace-relative path.\n Unsafe or external paths do not become matchable file rules.\n2. Check denied command prefixes against the whole command and every chained\n segment. These hard prefix denies are merged across rulesets and always win.\n3. Compute a trusted-prefix candidate for an unchained shell command. This is a\n candidate for the approval-mode fallback, not an immediate decision.\n4. Select one matching typed rule by this lexicographic precedence:\n 1. higher source layer: `User > Agent > BuiltinDefault`;\n 2. stronger action inside that layer: `deny > ask > allow`;\n 3. the more specific matcher when layer and action tie.\n5. Apply the selected typed action. `deny` forbids the call, `allow` skips the\n execution-policy approval, and `ask` requires approval. A typed `ask`\n overrides a trusted prefix.\n6. If no typed action decides the result, apply the approval-mode fallback\n using the trusted-prefix candidate.\n\nSource layer is compared before typed action. Consequently, a user-layer typed\n`allow` can override an agent-layer typed `deny`; inside the same layer, `deny`\nstill beats `ask`, which beats `allow`, regardless of file order or matcher\nspecificity. Hard denied prefixes are the exception: they are checked before\ntyped-layer selection and cannot be overridden by a typed allow.\n\nSpecificity is only a tie-breaker after source and action. A constrained\ncommand, exact-command, path, or workspace matcher beats a tool-wide rule with\nthe same action in the same layer. Specificity never lets a narrow allow beat a\nsame-layer deny.\n\nFor chained shell commands, a trusted prefix never approves the whole chain. A\ntyped deny that wins for any individual segment blocks the full invocation.\n\n## Approval posture and missing prompts\n\nThe execution-policy result is combined with the registered-tool baseline; the\ntwo should not be interpreted independently.\n\n- Ask and Auto-Review may surface tool safety approvals. Model-authored user\n questions (`request_user_input`) are a separate channel and reach the user in\n every interactive posture, Auto-Review included. Only headless `exec` runs,\n which have no responder, withhold the tool. Runtime threads (app\n conversations, background tasks and automations) keep it: a question parks\n the turn until someone answers or cancels it through the app or runtime API,\n or until a positive `tools.user_input_timeout_seconds` expires. An omitted or\n zero timeout waits indefinitely, the same way an Ask-posture approval parks.\n- Full Access and YOLO-compatible auto-approval paths do not let a typed `ask`\n downgrade the session into prompting. Non-bypassable registered holds\n auto-approve in Full Access. Typed deny, catastrophic background/headless\n safety holds, and repository law still fail closed where their respective\n layers apply.\n- With `approval_policy = \"never\"`, a matching typed `ask` is forbidden because\n the required prompt cannot be shown.\n\nRuntime adapters may transport an approval decision differently from the\ninteractive modal. That transport and any continuation protocol are separate\nfrom this ordering contract; see [Runtime API](RUNTIME_API.md).\n\n## Project overlays\n\nThe project config overlay at `<workspace>/.codewhale/config.toml` is not a\npermission-rule layer. It can only move approval and sandbox posture toward\nmore restrictive values:\n\n- approval: `auto` → `on-request`/`untrusted` → `never`;\n- sandbox: `danger-full-access` → `workspace-write` → `read-only`;\n- shell availability: `true` may become `false`, never the reverse.\n\nProject config cannot add credentials, hooks, provider authority, or a\nproject-local `permissions.toml`. See\n[Configuration](CONFIGURATION.md#per-project-overlay-485) for the complete\noverlay allow-list.\n\n## Regression coverage\n\nThe contract is exercised by tests at the layers that own each decision:\n\n- `authorization_order_contract_matches_documented_precedence` covers hard\n prefix denial and the typed `layer → action → specificity → approval-mode`\n sequence through the public execution-policy API.\n- `hook_fold_deny_wins_over_ask_and_allow` covers foreground hook folding.\n- `non_bypassable_registered_tools_auto_approve_in_full_access` covers\n registered holds.\n- `full_access_permission_allow_cannot_bypass_background_catastrophic_floor`\n and `full_access_permission_allow_cannot_bypass_repo_law` cover later safety\n layers overriding a remembered allow.\n- `project_merge_only_tightens_approval_and_sandbox_policy` covers project\n overlay monotonicity.\n\nFocused commands:\n\n```bash\ncargo test -p codewhale-execpolicy --test authorization_order --locked\ncargo test -p codewhale-tui --lib --locked full_access_permission_allow_cannot_bypass\ncargo test -p codewhale-config --locked project_merge_only_tightens_approval_and_sandbox_policy\n```\n\n## Related references\n\n- [Configuration](CONFIGURATION.md) — rule schema, `/permissions`, hooks, and\n project overlays\n- [Modes](MODES.md) — Plan/Work/Operate and permission posture\n- [Sandbox threat model](SANDBOX.md) — platform enforcement and fallbacks\n- [Runtime API](RUNTIME_API.md) — approval events and remote resolution\n"
|
||
},
|
||
{
|
||
"path": "docs/TELEMETRY.md",
|
||
"sha256": "932402de6fcd127d73d9df574df8b49ab1b7219cd64f799303ab73e6e364dc8d",
|
||
"content": "# Codewhale product telemetry\n\n> 阅读简体中文版:[zh_hans/TELEMETRY.md](zh_hans/TELEMETRY.md)\n\n**Usage analytics are on by default in the current source.** The first\ninteractive launch gives a localized, nonblocking disclosure naming **Codewhale\nand PostHog**, with a direct route to turn counting off. Notice version `6`\ndescribes this policy; displaying it is not recorded as human acceptance.\nMissing an older acceptance record does not turn a fresh installation off.\nExisting explicit opt-outs remain off across upgrades, and unreadable privacy\nstate fails closed. Environment and CLI kill switches still take precedence.\n\nRead the disclosure with `codewhale config telemetry`. Turn usage off in\n`/settings` or run `codewhale config set telemetry false`. Turning it back on\nin Settings or with `codewhale config set telemetry true` updates the existing\ndurable preference and privacy records for new sessions. The compatibility\ncommand `codewhale config telemetry --accept-notice 5` remains available, but\nis not required for a fresh installation. Collection never includes work content or credentials.\n\nThe PostHog sink is optional and inert until the ingest operator explicitly\nconfigures a project token and approved regional host, and records staging\nproof that the actual egress path does not forward the original client IP.\nSource readiness does not establish a deployment, an activated processor,\nan egress receipt, or a retention setting.\n\nCodewhale does not collect conversations, code, prompts, files, file/repo/branch\nnames, model content, or credentials. It sends no per-turn or per-tool timeline.\nIt does send the closed, aggregate schema below: version and platform classes,\nsession duration/outcome, feature/error counters, and a random install id that\nrotates every 90 days.\n\n**There is now a real endpoint.** An enabled session sends its batches to the\nfirst-party ingest service at `https://telemetry.codewhale.net/v1/telemetry`,\nwhich is the shipped default for `telemetry_endpoint`. What that service is,\nwhat it stores, and what it structurally cannot store is spelled out in \"What\nthe endpoint does\" below.\n\n**To send nothing anywhere, turn telemetry off** (see \"Turning it off\"). To stay\nenabled but contact nobody, set `telemetry_endpoint = \"\"`: batches are then\nappended to `$CODEWHALE_HOME/telemetry/dryrun.jsonl` on your own machine, byte\nfor byte what the server would have received, and no HTTP client is ever\nconstructed. That file is how you audit this document against reality.\n\nThis document is the schema. It is not a summary of the schema: a test in\n`crates/telemetry` parses the field names out of this file and asserts set\nequality against the structs the serializer actually uses, so a field that is\nhere and not in the code — or in the code and not here — fails the build.\n\nLocal saved sessions and `codewhale metrics` remain available with telemetry\ndisabled and require no account or hosted reporting service. Disabling telemetry\ndoes not disable local usage receipts.\n\n## Turning it off\n\nThere are two off switches and they do different things. Both stop collection\ncompletely; only one of them erases anything.\n\n```sh\ncodewhale config set telemetry false # opt out: stops collection and erases state\nCODEWHALE_TELEMETRY=0 codewhale # kill switch: stops collection, erases nothing\nDO_NOT_TRACK=1 codewhale # the same kill switch, by the common convention\ncodewhale --telemetry false # the same kill switch, for one command\n```\n\n**`telemetry = false` in the config file is the opt-out.** It is a floor:\n`--telemetry true` and `CODEWHALE_TELEMETRY=1` both lose to it, because a\nsetting you can undo by accident from a wrapper script is not a setting. It\ndeletes the random install id, truncates every buffered event and every dry-run\nrecord, and writes a tombstone. Appends, identity/state writes, and delivery all\nshare the wipe's ordering lock, so once opt-out returns no pre-opt-out write or\nPOST remains in flight. If any part of that wipe fails, the tombstone is still\nthere and the buffer is undrainable — a failed wipe fails closed. Every later\nrun re-asserts the same tombstone for as long as the setting stands, so it\nsurvives. Turning telemetry back on in `/settings` records that explicit preference\nin both existing privacy registers; a fresh launch can then clear the tombstone. Nothing buffered before that point is\never sent.\n\n**The environment variable and the flag are kill switches, not opt-outs.**\nTelemetry is off for the run, nothing is written, nothing is sent — and\nnothing on disk is touched or deleted. That is deliberate: a harness or agent\nthat sets `CODEWHALE_TELEMETRY=0` for one command must not silently discard the\ninstall id and the dry-run records of the person who owns the machine. If you\nwant the erasing kind, use the config file.\n\n`CODEWHALE_TELEMETRY` (and its `DEEPSEEK_TELEMETRY` alias) accepts\n`0`, `1`, `true`, `false`, `yes`, `no`, `on`, `off`, `enabled`, `disabled`.\nA value this list cannot read also resolves to off — a typo in a kill switch\nmust never resolve to \"on\".\n\nThe first-run notice is not shown when telemetry is already persistently off or\na run-scoped kill switch is active. The notice never rewrites a\n`telemetry = false` you put there yourself.\n\nA repo-local `.codewhale/config.toml` can set neither `telemetry` nor\n`telemetry_endpoint`, and a workspace `.env` can set neither. Someone else's\nrepository cannot turn your telemetry on or aim it at a host of their choosing.\n\n## Where it lives, and how much of your disk it uses\n\nEverything is under `$CODEWHALE_HOME/telemetry/` (`0700`), every file `0600`:\n\n| file | role |\n|---|---|\n| `buffer.jsonl` | pending events, one JSON object per line |\n| `buffer.jsonl.lock` | the sibling ordering lock shared by writes, delivery, arming, and wipe |\n| `dryrun.jsonl` | where batches go when the endpoint is configured empty |\n| `state.json` | the last app version seen and the last flush attempt |\n| `install_id.json` | the random install id and when it was minted |\n| `disabled` | the tombstone; present means nothing is appended or sent |\n\nBoth `buffer.jsonl` and `dryrun.jsonl` are rings capped at 512 records or\n256 KiB, whichever comes first, with the oldest dropped. The documented\nfootprint ceiling for the whole directory is therefore **512 KiB plus a few\nhundred bytes of metadata**.\n\nThe install id is a random v4 UUID. It is never derived from your hostname,\nMAC address, `machine-id`, home directory, username, or executable path — a\nderived id is a device fingerprint that survives reinstall and re-identifies\nyou across your own opt-out. It is regenerated whenever\n`$CODEWHALE_HOME/telemetry/` is cleared, which opting out does automatically,\nand in any case every 90 days.\n\nThere is no factory-reset command in Codewhale, so this document does not\nclaim one.\n\n## When anything is sent, and where\n\nNothing is collected or sent when a persistent opt-out or run-scoped kill\nswitch is in force, or when privacy state is unreadable. TUI and `exec` sessions have exactly one network flush point: an attempt\nduring shutdown, bounded at three seconds. Short CLI commands such as `config`,\n`doctor`, and `auth` do not wait for that network request. They record\n`session_end`, seal the event to the local buffer with a much shorter bound, and\nreturn; a configured endpoint sends those buffered events with the next\ninteractive shutdown flush. An explicitly empty endpoint finalizes its local\ndry-run batch immediately.\n\nThere is no startup flush, mid-session flush, per-turn flush, or per-tool-call\nflush. Every network shutdown flush re-resolves your setting from disk\nimmediately beforehand, so `codewhale config set telemetry false` written from\nanother terminal stops the flush of a session that is already running.\n\nA flush is **one `POST`** to the resolved endpoint — by default\n`https://telemetry.codewhale.net/v1/telemetry`. The request carries a\n`content-type: application/json` header, a\n`user-agent: codewhale-telemetry/<app_version>`, and the batch body. That is\nall: no cookies (the HTTP client is built without a cookie jar to disable), no\nredirects (refused outright), no `Authorization` header, no custom headers, and\nno query string. The response body is discarded unread; only the status class is\nlooked at.\n\n`https://` is required. Plain `http://` is accepted only for a loopback host, so\nyou can point the client at a recorder of your own and read the wire form\ndirectly; no environment variable overrides that refusal. An endpoint the client\nrefuses turns telemetry off for the run rather than falling back to a different\ndestination.\n\nAny failure — DNS, connect, TLS, timeout, non-2xx — drops the batch. There is\nno retry, no backoff, and no re-queue. A permanently offline machine attempts at\nmost once per flush point and never grows a queue.\n\n---\n\n## Event schema\n\n`SCHEMA_VERSION = 4`. `notice_version = 6` identifies the disclosed default-on\npolicy; it does not assert human consent or that a notice was seen. Schema 4\nadds the `model_usage` and `tool_usage` events and the `ci` session source.\nSchema-3 batches with `notice_version = 5` are still accepted without them, which\nis what the web and desktop control-plane relay sends. The closed\nv2 contract still accepts only `consent_version = 4`, retaining its original\nexplicit opt-in meaning. Fields cannot be mixed between versions. The unchanged\nv1 contract remains first-party only and **never sent to PostHog**; it refuses\nnew fields, surfaces, and `product_usage` events.\n\n Every field is an integer, a boolean, or a **closed enum string**, except exactly four bounded strings: `app_version`, `git_sha`, `panic_site`, and `model_usage.model`, which is closed against the bundled model catalog. Each of the four has a written rule and a check on the drain path. **There is no free-form string type in this schema, and no open-keyed map.** That is the property that makes red line 3 enforceable rather than aspirational.\n\n### Batch envelope — sent on every POST\n\n```jsonc\n{\n \"schema_version\": 4,\n \"notice_version\": 6,\n \"sent_at\": \"2026-08-03T18:04:11Z\", // RFC3339 UTC, second precision\n \"install_id\": \"3f2a…\", // uuid v4, rotates every 90 days\n \"app_version\": \"0.9.12\",\n \"git_sha\": null, // non-null only for SHA-stamped builds\n \"surface\": \"tui\",\n \"os\": \"macos\",\n \"arch\": \"aarch64\",\n \"libc\": \"none\",\n \"tty\": true,\n \"events\": [ … ]\n}\n```\n\n| Field | Type | Source anchor | Rule |\n|---|---|---|---|\n| `schema_version` | `u32` | const in `crates/telemetry/src/event.rs` | Bumped on any field add/remove/retype. Never reused. Pinned by a golden snapshot test. |\n| `notice_version` | `u32` | policy constant in `crates/telemetry/src/event.rs` | Exactly `6` for schema 4; identifies the disclosed opt-out policy, not a human acceptance record. |\n| `sent_at` | RFC3339 | `chrono::Utc::now()` | Second precision. Per-**batch** only — events carry no timestamps at all. |\n| `install_id` | uuid v4 | `crates/telemetry/src/envelope.rs` | Random, never derived, rotated every 90 days. See \"Where it lives\" above. |\n| `app_version` | string | `env!(\"CARGO_PKG_VERSION\")`, as at `crates/telemetry/src/lib.rs:112` | Must match `^\\d+\\.\\d+\\.\\d+(-[0-9A-Za-z.]+)?$`. |\n| `git_sha` | string \\| null | `option_env!(\"CODEWHALE_RELEASE_BUILD_SHA\")` — a **new** rustc-env | First 12 hex chars. Emitted **only** when `codewhale_build_support::release_build_sha` saw a valid full SHA in `CODEWHALE_BUILD_SHA`, the legacy `DEEPSEEK_BUILD_SHA`, or `GITHUB_SHA`, in that precedence order. `null` for every unstamped build, with no runtime lookup of any kind. **Never** `CODEWHALE_BUILD_COMMIT` — that falls back to `git_commit` and is the builder's private HEAD. **Never** `Thread.git_sha` (`crates/state/src/lib.rs:93`) — that is the user's workspace commit and a red line, one identifier away by name. |\n| `surface` | enum | explicitly set by the emitting client | `tui \\| exec \\| cli \\| app-server \\| mcp-server \\| serve \\| website \\| web-app \\| desktop \\| control-plane \\| vscode-extension`. Runtime metrics keep their existing collector. Browser product counts use the same closed envelope. A declared surface is not proof its client is deployed. `vscode-extension` is an extension client of this schema, deliberately generic the way `web-app` and `desktop` are: the editor's own name and version are **not** collected. Widening this whitelist changes no field, so the schema version is unchanged. |\n| `os` | enum | `std::env::consts::OS`, as at `crates/cli/src/update.rs:41` | Whitelist: `linux \\| macos \\| windows \\| freebsd \\| android \\| other`. |\n| `arch` | enum | `std::env::consts::ARCH` | `x86_64 \\| aarch64 \\| other`. |\n| `libc` | enum | `cfg!(target_env)` — **compile time** | `gnu \\| musl \\| none`. Runtime detection reads distro vendor strings; compile-time is free and leaks nothing. |\n| `tty` | bool | `std::io::IsTerminal`, as at `crates/telemetry/src/envelope.rs:196` | `stdin().is_terminal() && stdout().is_terminal()`. |\n| `events` | array | the drained buffer | Every element is one of the six events below and nothing else. Capped at 200 events or 64 KiB per batch; a batch that would exceed either cap leaves the remainder buffered for the next flush. |\n\n**`os_major` is not collected.** Reading it costs unsafe FFI on two platforms plus a file parser on a third, in the one crate whose entire value is being small enough to audit — and `os`, `arch`, and `libc` are free and answer the platform question. It may be reconsidered if the stored data ever shows that the OS-version cut is what triage is missing; a hunch is not that evidence.\n\n### Which surfaces emit\n\nEvery runtime surface uses the same `decide` and flush recheck: readable privacy\nstate, resolvable home, valid endpoint, and no persistent opt-out or run kill\nswitch. Missing preferences default on. The interactive TUI shows the disclosure;\nheadless commands never synthesize consent. Existing runtime events and counters\nare unchanged; adding PostHog does not add a second runtime collector.\n\n### An embedder declares the server it started\n\nA client that starts `codewhale serve` may name itself, so its sessions are not\ncounted as anonymous `serve` traffic from nobody in particular. It sets\n`CODEWHALE_TELEMETRY_SURFACE` to a value in the `surface` whitelist before\nspawning the server; the extension client uses `vscode-extension`.\n\nThree properties of that read are deliberate:\n\n- **`serve` only.** The variable is inherited by descendant processes, and an\n agent's shell command runs `codewhale` descendants carrying this environment.\n Only the branch that *is* the embedder's own server consults it, so a nested\n `codewhale exec` cannot report itself as the embedder.\n- **`tui` is refused**, and `serve --mcp` still reports `mcp-server`: nothing\n starts the interactive terminal UI on a user's behalf, and a server is not an\n MCP server because an embedder said so.\n- **An unrecognised name is dropped, not repaired.** A value outside the\n whitelist falls back to `serve` rather than failing startup or being guessed\n at; the field is a label, and the default is the honest one.\n\nThis changes how an embedder's sessions are *labelled*. It does not change\nwhether anything may be collected: the server resolves that itself, from the\nsame durable config, the same setup state, and the same `CODEWHALE_TELEMETRY`\nrun-scoped kill switch. An embedder cannot enable collection where the user has\nturned it off, and a durable opt-out in one client is a durable opt-out for every\nclient on the machine, because they share one home and one install identity.\n\nThe extension client is the worked example: it holds a single on/off setting and\npasses `CODEWHALE_TELEMETRY=0` when the user turns reporting off. It contains no\npayload builder, no buffer, and no transport of its own — so its sessions share\nthe home's `install_id` rather than minting a second one, and one installation is\none install in every count derived from that id.\n\nBecause that switch is run-scoped rather than a durable opt-out, turning it off\nstops the engine this extension starts and leaves the shared buffer and identity\nalone: those belong to every surface on the machine, not to one window, and\nclearing them would silently change what the user's terminal sessions do. A user\nwho wants reporting stopped and cleared everywhere does that once, with\n`codewhale config set telemetry false`.\n\nWebsite and application clients use `product_usage`, with notice version `5`,\na random browser-local v4 ID rotated after 90 days, `os = other`, `arch = other`,\n`libc = none`, `tty = false`, and `git_sha = null`. They do not inspect the browser\nfingerprint. Their configured same-origin proxy forwards only this bounded JSON\nto the first-party ingest, with no incoming cookies, headers, URLs, or identities.\nThe browser endpoint is inert if unconfigured. Browser opt-out clears pending\ncounts and local identity; actions taken while disabled are never backfilled.\nProduct usage settings belong in the app and runtime; marketing privacy details\nand any website opt-out belong on the privacy page.\n\n### Event: `install_or_upgrade`\n\nEmitted once when `state.json`'s `last_version` differs from `app_version`.\n\n```jsonc\n{ \"event\": \"install_or_upgrade\", \"kind\": \"upgrade\", \"previous_version\": \"0.9.3\" }\n```\n\n| Field | Type | Source | Rule |\n|---|---|---|---|\n| `kind` | enum | derived | `install` (no prior record) \\| `upgrade` \\| `downgrade`. |\n| `previous_version` | string \\| null | `$CODEWHALE_HOME/telemetry/state.json` **only** | Same regex as `app_version`. Never derived from session history or config mtimes — those files have a different privacy contract. |\n\n### Event: `session_start`\n\n```jsonc\n{ \"event\": \"session_start\", \"source\": \"interactive\" }\n```\n\n`source` is `SessionSource` (`crates/state/src/lib.rs:34-41`) stringified by `session_source_to_str` (`:1909-1917`): `interactive | resume | fork | api | unknown | ci`.\n`ci` replaces the others whenever `CI` or `GITHUB_ACTIONS` is set to a true\nvalue, including the Codewhale GitHub Action, so automated runs are never\ncounted as people. Only that boolean environment fact is read.\n\n### Event: `session_end`\n\nThe workhorse. Everything a session accumulated ships here, once.\n\n```jsonc\n{\n \"event\": \"session_end\",\n \"duration_bucket\": \"1m_10m\",\n \"exit_class\": \"clean\",\n \"cold_start_bucket\": \"250_1000\",\n \"providers\": [\"deepseek\", \"custom\"],\n \"counters\": { \"turns\": 14, \"tool_calls\": 61, \"fleet_dispatch\": 0, \"workflow_run\": 0,\n \"subagent_spawn\": 2, \"mcp_server_connected\": 0, \"memory_search\": 0,\n \"approval_modal_shown\": 0, \"approval_auto_allowed\": 0,\n \"command_palette_open\": 3 },\n \"errors\": { \"auth_preflight_failed\": 0, \"provider_http_4xx\": 0, \"provider_http_5xx\": 1,\n \"tool_denied_by_policy\": 0, \"tool_timeout\": 0, \"network_error\": 0 },\n \"turn_wall\": { \"lt_5s\": 9, \"5_30s\": 4, \"30_120s\": 1, \"gte_120s\": 0 }\n}\n```\n\n**`counters` and `errors` are `#[derive(Serialize)]` structs of named `u32` fields, not maps.** Every field is serialized including zeros. The key set is closed by the compiler: adding a counter requires editing `crates/telemetry/src/event.rs`, which is where the doc-match test lives.\n\n**`duration_bucket`** — `chrono` delta from `app.session_started_at` (`crates/tui/src/tui/app.rs:1889`). Half-open, seconds: `lt_1m` (`d < 60`), `1m_10m` (`60 ≤ d < 600`), `10m_60m` (`600 ≤ d < 3600`), `gt_60m` (`d ≥ 3600`).\n\n**`exit_class`** — `clean | signal | panic | error`. **Derived from an explicit `AtomicU8`, never from an exit code.** `RunTerminationReason::Canceled` maps to exit 130 (`crates/engine/src/core/runtime_contract/termination.rs:53`), the same value the signal task uses (`crates/tui/src/lib.rs:682`, 128+SIGINT), so a code-based derivation would report every Esc-cancelled turn as a signal. The atomic is set by the panic hook (`crates/tui/src/lib.rs:1582`), by the signal task (`:678-696`) before `std::process::exit`, and on the clean path from `RunTerminationReason::is_success()` (`crates/engine/src/core/runtime_contract/termination.rs:44-46`) — `error` otherwise. Exit codes remain insufficient: an approval-required exit (3) and a terminal exec failure (1) need their actual termination classification.\n\n**`cold_start_bucket`** — from `startup_trace::elapsed_ms()`, which reads `PROCESS_START` directly and is independent of the startup summary's buffer clear (`crates/runtime/src/startup_trace.rs:39-45`). Boundaries: `lt_250`, `250_1000`, `1000_3000`, `gte_3000`. Absent on non-TUI surfaces.\n\n**`providers`** — sorted, deduplicated array of `ProviderKind::as_str()` (`crates/config/src/provider_kind.rs:295`, a `&'static str` from a closed enum; `Custom` yields the literal `\"custom\"`). **The API takes `ProviderKind` by value, never `&str`.** Do not call `ProviderKind::parse` or `parse_config_identity` (`:300`, `:330`) — those are for config-table resolution. **Do not read** `provider_identity_for_persistence()` (`crates/tui/src/tui/app.rs:5049`), `provider_id_for_persistence()` (`:5058`), `ExecStreamMeta.provider_id` (`crates/app-server/src/exec_agent.rs`), or `PlannedTurnRoute.effective_provider_label` (`crates/engine/src/turn_route_plan.rs:189-193`) — all four return the customer's own `[providers.<name>]` table key when the route is Custom. This is the single most likely leak in the feature: it is one field away from the natural seam and `/status` already prints it (`crates/tui/src/commands/groups/config/status.rs:24-28`). **No model id is ever sent, for any provider** — `crates/runtime/src/safe_label.rs:11-15` documents that a model id can be a path, a URL, or a deployment id that is itself a credential.\n\n**`counters`** — closed field set. Every bump happens at the **call site**, never inside a conditionally-entered handler:\n\n| field | source anchor |\n|---|---|\n| `turns` | `run_event_loop` in `crates/tui/src/tui/ui/event_loop.rs`, immediately before it calls `execute_turn_end_observer_hook`. Never inside that hook: its first statement is `if !app.hooks.has_hooks_for_event(HookEvent::TurnEnd) { return Ok(()); }` (`crates/tui/src/tui/ui/observer_hooks.rs`), and the natural future optimization hoists that check to the call site, silently zeroing the counter for every user without hooks. |\n| `tool_calls` | `execute_tool_with_lock` in `crates/engine/src/core/engine/tool_execution.rs` — surface-agnostic, fires for exec and CLI too |\n| `fleet_dispatch` | `create_queued_run_with_descriptor` in `crates/engine/src/fleet/manager.rs` — the single funnel that `create_run` and `create_queued_run` both land in; counting at either caller would double-count a plain `fleet run`. |\n| `workflow_run` | bumped in `WorkflowTool::execute` (`crates/engine/src/tools/workflow/mod.rs`) only after `parse_workflow_action` returns an `Ok(WorkflowAction)`, never from `input[\"action\"]`. The JSON Schema `enum` in `WorkflowTool::input_schema` is what is published *to the model* — a declaration, not a guard; the real parse also accepts `spawn\\|wait\\|list\\|inspect\\|stop\\|abort`, and its reject arm (`Invalid workflow action '…'`) embeds the model string verbatim, so a rejected action is never counted. |\n| `subagent_spawn` | `apply_agent_spawned_status_and_observer` in `crates/tui/src/tui/ui/apply.rs` |\n| `mcp_server_connected` | bumped when a server snapshot's `.connected` is true, in `snapshot_from_config` (`crates/engine/src/mcp.rs`); never `name`, `command_or_url`, or `error` — server names are user-chosen and routinely internal infra |\n| `memory_search` | `tool_name == \"memory_search\"` (the tool registered in `crates/engine/src/tools/native_memory.rs`), counted at the same `execute_tool_with_lock` choke point |\n| `approval_modal_shown` | the `Event::ApprovalRequired` arm of `run_event_loop` (`crates/tui/src/tui/ui/event_loop.rs`; the event is defined in `crates/engine/src/core/events.rs`) |\n| `approval_auto_allowed` | `tool_ask_rule_decision_for_context` in `crates/engine/src/core/engine.rs`. Count only. Never `matched_rule`, `reason()`, the command, or argv — `auto_allow` patterns are user-authored command strings (`crates/execpolicy/src/command_safety.rs:35/309`) |\n| `command_palette_open` | the palette key path in `run_event_loop` (`crates/tui/src/tui/ui/event_loop.rs`) and `handle_context_menu_action` in `crates/tui/src/tui/mouse_ui.rs` |\n\n**`errors`** — closed field set. Every value is a **variant discriminant**, never `err.to_string()`:\n\n| field | source anchor |\n|---|---|\n| `auth_preflight_failed` | discriminant of `CredentialReadiness` (`crates/workflow/src/fleet_preflight.rs:37-58`) / `ProviderAuthClass` (`crates/engine/src/provider_readiness.rs:32`). Discriminant only — `Missing { detail }` carries free text |\n| `provider_http_4xx` | `status.as_u16() / 100 == 4`, captured at `crates/engine/src/client/chat.rs:595` and `:673` **before** the `bail!`. One row per field, because the doc-match test reads this table field for field |\n| `provider_http_5xx` | `status.as_u16() / 100 == 5`, same capture points |\n| `tool_denied_by_policy` | the `permission_denied` arm of the 8-variant match at `crates/engine/src/core/engine/tool_execution.rs:512-531` |\n| `tool_timeout` | the `timeout` arm, same match |\n| `network_error` | `retry_reason_label_and_human()`'s `&'static str` half, `crates/engine/src/client.rs:2659` |\n\nWhy discriminants and nothing else: `ToolError::PathEscape`'s `Display` *is* an absolute path (`crates/tools/src/lib.rs:61`); `fim.rs:48-50`'s `Display` *is* a literal source fragment the model emitted; `secrets/src/lib.rs:50`'s `Display` carries the secret store's absolute path; every `LlmError` variant carries the raw provider HTTP body verbatim (`crates/engine/src/llm_client/mod.rs:327`), and a 400 from a content filter routinely echoes the prompt.\n\n**`turn_wall`** — a per-session histogram of counts, never per-turn events. `lt_5s`, `5_30s`, `30_120s`, `gte_120s`. Recorded by `observe_turn_secs` next to the `turns` bump in `run_event_loop`, which already has the turn duration in hand.\n\n### Event: `panic`\n\nAppended **synchronously** by the panic hook, because a `session_end` may never be written.\n\n```jsonc\n{ \"event\": \"panic\", \"site\": \"crates/tui/src/lib.rs:1582:5\" }\n```\n\n`site` comes from `panic_info.location()` (`crates/tui/src/lib.rs:1597-1600`) or `Location::caller()` (`crates/util/src/utils.rs:523`). **Allowlist reduction, not optional:** emit verbatim only if `file()` starts with `crates/`; otherwise emit the literal `\"<dep>\"`. Must match `^crates/[A-Za-z0-9_/.-]+\\.rs:\\d+:\\d+$` or `^<dep>$`. There is no `--remap-path-prefix` in this repo (no `.cargo/config.toml`; `Cargo.toml:69-74` sets only `lto`/`strip`/`codegen-units`), so a panic inside a registry dependency yields `/Users/<builder>/.cargo/registry/src/…/ratatui-0.29.0/src/…` — the **build machine's username**, shipped from every user's binary.\n\n**The panic message is never sent.** The hook at `crates/tui/src/lib.rs:1590-1596` builds `msg` from the payload; telemetry must not read it. A slicing panic embeds the entire string being sliced, and this tree slices user and model text in dozens of places.\n\n### Event: `product_usage`\n\nOne aggregate of explicit product interactions, with every field a saturating\n`u32`, including zero values. No page paths, URLs, referrers, search terms,\naccount/session IDs, text, or action timestamps are permitted. Clients flush\naggregate counts on page exit or when hidden, rather than each action.\n\n```jsonc\n{\n \"event\": \"product_usage\",\n \"counters\": {\n \"page_view\": 1,\n \"docs_view\": 0,\n \"install_copy\": 0,\n \"download\": 0,\n \"signup\": 0,\n \"login\": 0,\n \"session_create\": 0,\n \"session_resume\": 0,\n \"turn_submit\": 0,\n \"turn_complete\": 0,\n \"settings_open\": 0,\n \"integration_connect\": 0,\n \"error_shown\": 0\n }\n}\n```\n\n`page_view` counts a page view and `docs_view` a documentation view; `install_copy`\nand `download` count installation actions. `signup` and `login` count successful\nauthentication actions, never identity. `session_create`, `session_resume`,\n`turn_submit`, and `turn_complete` count their named product actions.\n`settings_open`, `integration_connect`, and `error_shown` are counts only:\nnever the selected setting, integration name, error body, or work content.\nAn event's availability depends on the emitting client; zero is not proof a\nfeature was exercised or available.\n\n### Event: `operations_summary`\n\nAnonymous service health aggregates from the existing control-plane signal ledger.\nExactly these six saturating `u32` fields are accepted, only on `control-plane`.\nThis requires an explicit operator delivery/processor configuration, independently\nof the end-user analytics choice. Each batch uses a fresh random v4 `install_id`;\nthere is no user/account/session join. No per-request rows, routes, request\ndigests, IDs, error text, traces, or timing series are permitted. `requests` and\n`errors` count requests and failures; `duration_ms_total` and `duration_ms_max`\nare aggregate latency values; `probes` and `probes_failed` count health probes.\n\n```jsonc\n{\n \"event\": \"operations_summary\",\n \"requests\": 10,\n \"errors\": 1,\n \"duration_ms_total\": 1500,\n \"duration_ms_max\": 300,\n \"probes\": 2,\n \"probes_failed\": 0\n}\n```\n\n### Event: `model_usage`\n\nOne event per provider and model a session used, sent with `session_end`.\nIt is how Codewhale decides which providers and models to support first, which\nto make defaults, and whether prompt caching is working.\n\n```jsonc\n{\n \"event\": \"model_usage\",\n \"provider\": \"deepseek\",\n \"model\": \"deepseek-v4-flash\",\n \"counters\": {\n \"turns_ok\": 12,\n \"turns_failed\": 1,\n \"turns_interrupted\": 0,\n \"input_tokens\": 418230,\n \"output_tokens\": 9120,\n \"cache_read_tokens\": 380001,\n \"cache_write_tokens\": 0\n },\n \"turn_wall\": { \"lt_5s\": 3, \"5_30s\": 8, \"30_120s\": 2, \"gte_120s\": 0 }\n}\n```\n\n`provider` follows the `providers` rule of `session_end`. `model` is the\nlowercased model id **only when it appears in the public model catalog bundled\nwith Codewhale**; any other name (custom, local, fine-tuned or private models,\nand `[providers.<name>]` routes) is sent as `other`. Token counts are the\nprovider's own usage numbers for the main conversation; `input_tokens` includes\ncache reads. Turn counts are by outcome, and `turn_wall` buckets each turn's\nwall time as `session_end` does. A session reports at most 16 provider and model\npairs; later pairs are counted under `other`. No per-turn timeline is sent.\n\n### Event: `tool_usage`\n\nOne event per tool a session called, sent with `session_end`. It is how\nCodewhale decides which built-in tools to invest in, fix or retire.\n\n```jsonc\n{ \"event\": \"tool_usage\", \"tool\": \"read_file\", \"calls\": 41, \"errors\": 2 }\n```\n\n`tool` is a first-party tool name from this closed list, `mcp` for any MCP\nserver tool, or `other` for any other tool (plugins and configured tools):\n`agent`, `apply_patch`, `authenticate`, `bash`, `code_execution`, `create_goal`,\n`diagnostics`, `edit`, `edit_file`, `exec_shell`, `execute_tools`, `fetch_url`,\n`file_search`, `fim_edit`, `finance`, `get_goal`, `git_blame`, `git_commit_plan`,\n`git_diff`, `git_fetch`, `git_log`, `git_merge_tree`, `git_show`, `git_status`,\n`grep_files`, `handle_read`, `image_ocr`, `js_execution`, `list_dir`,\n`load_skill`, `lsp`, `memory_get`, `memory_search`, `note`, `notify`,\n`pandoc_convert`, `project_map`, `read`, `read_file`, `read_media`, `remember`,\n`request_plugin_install`, `request_user_input`, `retrieve_tool_result`,\n`revert_turn`, `review`, `rlm`, `run_tests`, `run_verifiers`, `session_get`,\n`session_search`, `start_mcp_server`, `task_create`, `task_shell_wait`,\n`todo_write`, `tool_search`, `update_goal`, `update_plan`, `validate_data`,\n`verify`, `wait_for_dev_server`, `web_search`, `workflow`, `write`, `write_file`.\n`errors` counts calls that returned an error, never the error text or the\narguments.\n\n### What the endpoint does — a shipping gate, not a footnote\n\nThis section was a gate on configuring any non-loopback endpoint. The endpoint is now configured by default, so this is a description of a service that exists rather than a promise about one that might.\n\n**What it is.** `https://telemetry.codewhale.net/v1/telemetry` — a Cloudflare Worker named `codewhale-telemetry-ingest`, whose complete source is in this repository at [`telemetry-ingest/`](../telemetry-ingest/). It is the single schema and storage authority; browser applications may use a same-origin proxy that forwards only the body. There is no telemetry queue or additional runtime collector. It is write-only: nothing in the Worker can read back what was stored, and querying happens out of band through Cloudflare's SQL API with the owner's token. The hostname is deliberately self-describing, so anyone inspecting their own network traffic can tell what it is from the name alone.\n\n**What it stores.** Everything in this document and nothing else, in Workers Analytics Engine — one row per event. The validator in `telemetry-ingest/src/schema.ts` is a **closed** field set: an unknown key anywhere in a batch rejects the whole batch with `400`. A future client bug that starts attaching a path, a prompt, or a provider table name gets refused by the server rather than quietly stored. `telemetry-ingest/test/schema-doc.test.ts` parses the field names and enum spellings back out of *this file* and asserts set equality against the validator, and `telemetry-ingest/test/ingest.test.ts` posts the Rust client's own pinned golden batch and asserts it is accepted byte for byte — so this document, the client, and the endpoint cannot drift apart without a red test.\n\nBatches are **IP-stripped at ingest**. No IP is stored, logged, or joined to `install_id` — and that is structural rather than a setting anyone could flip. An Analytics Engine row is exactly `_sample_interval`, `blob1`–`blob20`, `dataset`, `double1`–`double20`, `index1`, and `timestamp`. Every one of those columns is written by the Worker's own `writeDataPoint` call; there is no implicit column, so **there is no IP, country, or geo column** — no slot one could occupy even if the code wanted it there. And the code cannot want it:\n\n- The handler reads exactly **two** request headers — `content-type` and `content-length`. Nothing else, ever.\n- It never touches the request's `cf` property, so country, colo, city, region, ASN, timezone, and coordinates are never in scope.\n- The function that builds every stored row cannot see the request at all; its input type is the validated batch body.\n- Nothing logs. `invocation_logs` is off in `wrangler.jsonc` — Cloudflare describes those as \"enriched with information available to Cloudflare in the context of the invocation\", which is exactly the class of automatic per-request record this service will not keep — and there is no `console.*` call in the source.\n- Rate limiting is keyed on `install_id` from the validated body, never on a network address. An IP-keyed limiter would mean this Worker handles IPs. It is the weaker limiter and it is the right trade.\n- `telemetry-ingest/test/no-ip.test.ts` reads the shipped source as text and fails the build if any address or geo name appears, if the set of headers read grows past two, if a `console.*` call is added, or if a `Response` is ever constructed with a body.\n\n**Retention: three months.** That is Cloudflare's fixed window for Analytics Engine and it is not configurable, so it is a ceiling rather than a policy — there is no setting that could make it longer.\n\n**Optional PostHog processor.** After first-party storage, an explicitly\nconfigured ingest may forward validated schema-v3 / notice-v5 batches and\nlegacy schema-v2 / consent-v4 batches to\nPostHog's [batch capture API](https://posthog.com/docs/api/capture). The only\npermitted origins are `https://us.i.posthog.com` and `https://eu.i.posthog.com`.\nSchema-v1 and schema-v4 batches cannot reach that path in this source. PostHog receives the same bounded\nenvelope and event fields, the batch timestamp as its event timestamp, and\n`codewhale:<install_id>` as its anonymous `distinct_id`. Events are named\n`codewhale_install_or_upgrade`, `codewhale_session_start`,\n`codewhale_session_end`, `codewhale_panic`, `codewhale_product_usage`, or\n`codewhale_operations_summary`.\n`$process_person_profile = false`, `$geoip_disable = true`, and `$ip = null`\nare fixed transport controls. No request metadata, profile identification,\nSDK, autocapture, session replay, advertising, or work content is forwarded.\nA fresh server request has no user cookies or authorization headers, cannot\nfollow redirects, and is limited to 1.5 seconds. There are no retries or logs;\na failure does not change a successful first-party ingest response.\n\nThe PostHog project's retention and privacy settings are separate deployment\nconfiguration and must be reviewed before activation. The Analytics Engine\nthree-month limit does not establish a PostHog retention period.\n\n**The edge egress path also requires staging proof.** [Cloudflare documents](https://developers.cloudflare.com/fundamentals/reference/http-headers/#cf-connecting-ip-in-worker-subrequests)\nthat Worker subrequests to non-Cloudflare zones can receive platform-added\nclient-IP headers. Fresh JavaScript headers and local fetch tests cannot prove\nthose headers are removed. `POSTHOG_IP_SAFE_EGRESS_VERIFIED=\"true\"` is an explicit\noperator prerequisite alongside the host and token, and must remain unset\nuntil a receipt for the actual deployment and regional destination confirms\nno original client IP in any received header, including `CF-Connecting-IP`,\n`X-Forwarded-For`, and `X-Real-IP`. The flag does not strip platform headers.\nRequalify a changed egress path; if it cannot pass, keep the sink disabled or\nrun only its delivery in a detached first-party context. No such live proof\nis established by this source change.\n\n**Every response is a bare status with an empty body** — `204` accepted, `400` schema violation, `404`/`405` wrong path or method, `413` oversized, `415` wrong content type, `429` rate-limited, `500` internal. The endpoint cannot echo back what it received or what it holds, and because the client drops the batch on anything that is not 2xx, a rejection is invisible to you by construction and a server error can never surface as a client-visible failure.\n\n`install_id` rotates client-side every 90 days (`rotated_at` in `install_id.json`), so no single identifier spans a long history. This costs longitudinal accuracy and the docs say so: **no count derived from `install_id` is a user count.** It is a lower bound on distinct machine-installs in a window, and it undercounts a returning user across a rotation.\n\n**Turning it off deletes what was kept locally, not what was already sent.** `codewhale config set telemetry false` erases the install id, the buffer, and the dry-run records on your machine, and stops anything further. Rows already accepted by the endpoint are keyed only by a rotating random id that is now gone; first-party rows age out with the three-month window. If PostHog processing is activated, already-delivered processor records follow that project’s configured retention. The client has no remote deletion API and does not claim that local opt-out erases delivered records.\n\n### What the owner reads back — observed active installs\n\nThe one product metric derived from this data is **observed active installs**: the number of distinct rotating anonymous install ids that produced a `session_start` event on a UTC day. That is the whole definition. It is not a count of people, not a count of accounts, and not a count of total installs — the id is per installation, rotates every 90 days, and is deleted on opt-out, so no number derived from it can be any of those things.\n\nThe exact owner command is checked into the repository, so the routine query is reviewable code rather than SQL pasted from a chat:\n\n```sh\ncd telemetry-ingest\nCF_ACCOUNT_ID=... CF_API_TOKEN=... npm run report:active-installs\n```\n\nIt prints the daily series, a 7-day trend over complete UTC days, the freshness of the newest ingested event, and — with the numbers, in both text and `--json` output — the coverage caveats: clients older than the telemetry feature, opted-out installs, and non-emitting environments (kill switches, fleet workers, offline shutdowns, dropped flushes) are invisible, so every count is a lower bound; and because ids rotate, week-over-week comparisons are not a retention metric. The report's read path is itself tested: the install id appears only inside an aggregate, no payload column is selected, and the wording can never drift into calling the result users. (`npm run report:dau` remains as a compatibility alias for the same report.)\n\n### What is never collected — the public red-line list\n\nPrompts; completions; tool arguments; diffs; patches; file contents; filenames; absolute or relative paths; git remotes; repo names; branch names; workspace commit SHAs; memory entries; chat history; API keys, tokens, cookies, or `Authorization` headers (including any boolean asserting a key exists); model ids that are not in Codewhale's bundled public model catalog (custom, local, fine-tuned or private model names are sent only as `other`); custom provider table names; MCP server names, commands, URLs, or tool names; plugin and configured tool names; approval rule text; error message bodies; panic message text; per-event timestamps; keystrokes; clipboard; screenshots; microphone; camera; location; and any third-party ad or analytics SDK — there are none in the runtime binary and none may be added.\n\nTwo named traps for the implementer. `crates/state/src/lib.rs` persists `git_sha`, `git_branch`, `git_origin_url`, `cwd`, and `path` on the threads table (`:93, :399, :653`): a payload builder that accepts a `Thread` or `ThreadMeta` and derives `Serialize` breaches the contract in one line. **Never derive `Serialize` over an existing state type** — build every telemetry struct from scratch with explicit fields. And `crates/core/src/lib.rs:1389-1398` is the one place in the tree where the word `telemetry` sits inside a JSON object next to `prompt`, `base_url`, and `has_api_key`. It is the object someone will copy. Do not.\n\n---\n"
|
||
},
|
||
{
|
||
"path": "docs/public-surface-facts.json",
|
||
"sha256": "0d50e2452380e0acbadd27b4db1f6981b9be4d7c071a48cc555cc55ea2f4b7d8",
|
||
"content": "{\n \"schemaVersion\": 2,\n \"product\": {\n \"name\": \"Codewhale\",\n \"description\": \"Terminal coding agent for supported hosted and local models. One runtime on your machine. Rust, MIT.\",\n \"positioning\": \"Terminal-native, model/provider-neutral, local-first coding agent\",\n \"license\": \"MIT\",\n \"terminology\": {\n \"Fleet\": \"the user's model inventory: who is in the roster and which member is selected\",\n \"Workflow\": \"what order the work follows\",\n \"Lane\": \"one running Workflow instance\",\n \"Runtime\": \"where, how, and with what authority selected work executes\"\n },\n \"sources\": [\n \"Cargo.toml\",\n \"LICENSE\",\n \"README.md\",\n \"npm/codewhale/package.json\",\n \"docs/FLEET.md\"\n ]\n },\n \"sourceCandidate\": {\n \"version\": \"0.10.3\",\n \"providerCount\": 50,\n \"toolCount\": 82,\n \"sandboxBackends\": [\n \"seatbelt (macOS, when available)\",\n \"bubblewrap (Linux, default when installed and working)\"\n ],\n \"sources\": [\n \"Cargo.toml\",\n \"crates/runtime/src/config.rs\",\n \"crates/sandbox/src/lib.rs\",\n \"web/scripts/derive-facts.mjs\"\n ],\n \"providerCountDefinition\": \"Website descriptor identities in this source snapshot, excluding Custom and retired identities; includes protocol/plan variants. Not a count of distinct vendors, ProviderKind identities, live catalogs, or providers in any published release.\"\n },\n \"latestPublishedRelease\": {\n \"tag\": \"v0.10.2\",\n \"version\": \"0.10.2\",\n \"publishedAt\": \"2026-10-24T17:31:14Z\",\n \"url\": \"https://github.com/codewhale-hq/CodeWhale/releases/tag/v0.10.2\",\n \"sources\": [\n \"web/data/latest-published-release.json\"\n ]\n },\n \"install\": {\n \"recommended\": \"curl -fsSL https://codewhale.net/install.sh | sh\",\n \"binaries\": [\n \"codewhale\",\n \"codew\"\n ],\n \"channels\": {\n \"npm\": \"published releases only\",\n \"cargo\": \"published crates only\",\n \"prebuiltArchives\": \"published GitHub Releases only\",\n \"cnbMirror\": \"documented targets only\"\n },\n \"androidTermux\": {\n \"status\": \"preview\",\n \"npm\": \"preview\",\n \"requiresMatchingPublishedAssets\": true,\n \"sourceBuild\": true\n },\n \"sources\": [\n \"README.md\",\n \"npm/codewhale/package.json\",\n \"npm/codewhale/README.md\",\n \"npm/codewhale/scripts/artifacts.js\",\n \"docs/INSTALL.md\",\n \"docs/CNB_MIRROR.md\"\n ]\n },\n \"control\": {\n \"modes\": [\n \"Plan\",\n \"Work\",\n \"Operate\"\n ],\n \"permissionPostures\": [\n \"Ask\",\n \"Auto-Review\",\n \"Full Access\"\n ],\n \"shortcuts\": {\n \"mode\": {\n \"chord\": \"Tab\",\n \"when\": \"composer empty\"\n },\n \"permissionPosture\": {\n \"chord\": \"Shift+Tab\",\n \"when\": \"always (suppressed only under a non-Config modal)\"\n }\n },\n \"sources\": [\n \"docs/MODES.md\",\n \"docs/KEYBINDINGS.md\",\n \"crates/runtime/src/settings.rs\"\n ]\n },\n \"toolSurface\": {\n \"defaultActive\": [\n \"read\",\n \"write\",\n \"edit\",\n \"file_search\",\n \"bash\",\n \"agent\",\n \"workflow\",\n \"todo_write\",\n \"create_goal\",\n \"get_goal\",\n \"update_goal\",\n \"task_shell_wait\",\n \"present\",\n \"load_skill\",\n \"context_archive_get\",\n \"context_archive_search\"\n ],\n \"schemas\": {\n \"read\": [\n \"path\",\n \"offset?\",\n \"limit?\"\n ],\n \"write\": [\n \"path\",\n \"content\"\n ],\n \"edit\": [\n \"path\",\n \"edits\"\n ],\n \"bash\": [\n \"command\",\n \"timeout?\"\n ]\n },\n \"deferred\": {\n \"Web\": [\n \"search\",\n \"fetch\",\n \"wait\"\n ]\n },\n \"compatibility\": {\n \"legacyAliases\": \"hidden-exact\",\n \"modelVisible\": false,\n \"toolSearchDiscoverable\": false\n },\n \"activationCache\": {\n \"maximumNames\": 8,\n \"maximumSchemaBytes\": 16384,\n \"scope\": \"per conversation; each subagent owns an independent policy-filtered cache\"\n },\n \"agentConcurrency\": {\n \"defaultConfigured\": 64,\n \"maximumConfigured\": 128,\n \"maximumAdmitted\": 1024\n },\n \"sources\": [\n \"docs/TOOL_SURFACE.md\",\n \"docs/RUNTIME_SIMPLIFICATION_DESIGN.md\",\n \"crates/engine/src/core/engine/tool_catalog.rs\",\n \"crates/engine/src/tools/registry.rs\",\n \"crates/runtime/src/config/subagent_limits.rs\"\n ]\n },\n \"surfaces\": {\n \"availableInSourceCandidate\": [\n \"TUI\",\n \"codewhale exec\",\n \"Web client\",\n \"Runtime API\",\n \"MCP\",\n \"Fleet\"\n ],\n \"sources\": [\n \"docs/GUIDE.md\",\n \"docs/WEB.md\",\n \"docs/RUNTIME_API.md\",\n \"docs/MCP.md\",\n \"docs/FLEET.md\"\n ]\n },\n \"trust\": {\n \"localRuntime\": \"runtime, workspace state, and audit log stay on the user's machine\",\n \"hostedProviderBoundary\": \"the selected hosted provider receives turn context required for inference\",\n \"localInference\": \"a loopback local-model route can keep inference local\",\n \"telemetry\": \"Codewhale 0.10.3 counts anonymous usage by default and discloses it at first launch (policy notice version 5, naming Codewhale and PostHog as processors), while the earlier 0.9.11 release asked first; turning it off is a saved choice that later versions keep, an opt-out recorded under the earlier opt-in policy stays off, and showing the notice never records any acceptance on the user's behalf; Codewhale does not collect conversations, code, prompts, files, file/repo/branch names, model content, credentials, or per-turn/per-tool timelines; while on, sessions post aggregate counts and closed enums to the first-party endpoint https://telemetry.codewhale.net/v1/telemetry, whose storage has no IP, country, or geo column and a fixed three-month retention; optional PostHog forwarding requires separate operator configuration and verified IP-safe egress, and its retention is a separate project setting; telemetry_endpoint = \\\"\\\" writes only to a local dry-run file; no mandatory hosted relay\",\n \"account\": \"no account required for the local runtime\",\n \"plan\": \"Plan centrally refuses file mutation and shell execution; policy-allowed research may contact external services and local session state can still be persisted.\",\n \"sandbox\": \"Seatbelt is used on macOS when available; Linux bubblewrap is on by default once installed and verified working, and prefer_bwrap = false opts out; Windows currently reports no OS sandbox\",\n \"audit\": \"sensitive events append best-effort to $CODEWHALE_HOME/audit.log (default ~/.codewhale/audit.log); write failures are logged\",\n \"usage\": \"provider token and cache usage is shown locally when available\",\n \"sources\": [\n \"docs/PROVIDERS.md\",\n \"docs/MODES.md\",\n \"docs/SANDBOX.md\",\n \"docs/RUNTIME_API.md\",\n \"docs/WEB.md\",\n \"crates/runtime/src/audit.rs\",\n \"web/app/[locale]/faq/page.tsx\",\n \"web/app/[locale]/install/page.tsx\"\n ]\n },\n \"platforms\": {\n \"prebuilt\": [\n \"Linux x64\",\n \"Linux arm64\",\n \"macOS x64\",\n \"macOS arm64\",\n \"Windows x64\",\n \"Windows arm64\"\n ],\n \"preview\": [\n \"Android/Termux arm64\"\n ],\n \"sources\": [\n \"docs/INSTALL.md\"\n ]\n },\n \"repository\": {\n \"canonical\": \"https://github.com/codewhale-hq/CodeWhale\",\n \"mirrors\": [\n \"https://cnb.cool/codewhale.net/codewhale\",\n \"https://npmmirror.com/package/codewhale\"\n ],\n \"creditSources\": [\n \"CHANGELOG.md\",\n \"docs/CONTRIBUTORS.md\",\n \"web/lib/release-credits.ts\"\n ],\n \"requiredCandidateCredits\": [\n \"@printf033\",\n \"@isaacs\",\n \"@7jrxt42BxFZo4iAnN4CX\",\n \"@SparkofSpike\",\n \"@gaord\",\n \"@Lstarsky0\",\n \"@asto18089\",\n \"@Aboysky\",\n \"@ddlw\",\n \"@laozidabo\",\n \"@Ricardo-M-L\",\n \"@luochen211\",\n \"@gannp\",\n \"@Felixkw12\",\n \"@Gabriel-Degret\",\n \"@soarkit\",\n \"@liuyy2\",\n \"@luestr\",\n \"@M-Maciej\",\n \"@Statter\",\n \"@eugenicum\"\n ]\n },\n \"screenshot\": {\n \"readme\": \"web/public/codewhale-tui-0102-demo.png\",\n \"website\": \"web/public/codewhale-tui-0102-demo.png\",\n \"sourceVersion\": \"0.10.2\",\n \"sourceCommit\": \"bd0c32567024d5bc3efac7d13331c68711de031e\",\n \"sourceSha256\": \"43ebde941cedb690612945b712e9565e92a441045c78646bf8841f3ad43ac369\",\n \"sourceDirty\": true,\n \"sourceDiffSha256\": \"499468bbf066901c9b229c912fe64d7fdcb672674483ce8dbe59db7c66110c45\",\n \"binarySha256\": \"6ac94bff89046b5de4386c4ed39da2b86b2c53233d3c652f09022c83095e14dd\",\n \"terminal\": \"real PTY cell capture; the product page renders native cells as selectable HTML text, and the README image is the same frame exported as SVG by web/scripts/render-terminal-capture.mjs and rasterized as PNG\",\n \"theme\": \"Underwater (default)\",\n \"capture\": \"Captured on 2026-10-08 from the development build of a dirty 0.10.2 candidate based on bd0c32567024d5bc3efac7d13331c68711de031e by the opt-in website_current_terminal_capture test using crates/tui/tests/support/qa_harness. Exact source diff and binary hashes are recorded here and in the capture manifest. A deterministic loopback provider drives the real Engine to edit a JSON exporter, run three passing local Python tests and delegate a source-grounded read-only review. Home, unsent composer, configured provider manager, help, Tasks and Fleet are actual application output with no retouched cells or fixture history. Sealed HOME and Full Access apply only to the isolated demo workspace. This is a local scripted demo, not published-release or external-provider qualification.\",\n \"sources\": [\n \"crates/tui/tests/support/qa_harness\",\n \"crates/tui/tests/cucumber/launch_card_pty.rs\",\n \"web/lib/terminal-captures/manifest.json\",\n \"web/lib/terminal-captures/website-home-100x24.json\",\n \"web/scripts/render-terminal-capture.mjs\",\n \"web/public/codewhale-tui-0102-demo.png\"\n ]\n }\n}\n"
|
||
},
|
||
{
|
||
"path": "docs/PREVIEW_REQUEST.md",
|
||
"sha256": "41c0f86a0785f3261bf24852c6de3c395d659197938274b3612e4b9ca2ebb1f5",
|
||
"content": "# `/preview-request` — see the outbound request without sending it\n\n`/preview-request` renders a **request manifest**: a typed, redacted\ndescription of the request the next **primary agent turn** would send. It never\nsends that request, never appends to the conversation, and never writes engine,\nsession, or Work state. `/dryrun` and `/preview_request` are compatibility\naliases.\n\nIt is a human command. There is deliberately no model-visible tool for it.\n\n```\n/preview-request # session facts; route/body reported unavailable\n/preview-request json # same manifest as JSON\n/preview-request --prompt <text> # the next turn for that prompt\n/preview-request json --prompt <text> # both\n/preview-request base-prompt # exact base layer only; no runtime additions\n```\n\n### Argument grammar\n\n```\nargs := flag* [ \"--prompt\" <whitespace> prompt ]\nflag := json | --json | manifest | --manifest | prompt | base-prompt | --base-prompt\nprompt := every remaining byte, verbatim\n```\n\n**Flags come before `--prompt`, which is terminal.** Everything after it is\nprompt text — including a trailing `json`. `/preview-request --prompt fix it\njson` previews the prompt *\"fix it json\"* as a human table; the JSON spelling\nis `/preview-request json --prompt fix it`. There is exactly one reading of any\ninput, and an unknown argument before `--prompt` is rejected rather than\nguessed at.\n\n**The prompt keeps your bytes.** Repeated whitespace and newlines inside it\nsurvive; exactly one whitespace codepoint separates `--prompt` from its text\nand is consumed as syntax. Any additional leading whitespace, plus all\ntrailing whitespace and newlines, remains prompt data. The prompt is hashed\ninto the previewed body, so normalizing it would describe a request that\ndiffers from the real one in the one field you typed.\n\n`prompt` remains a compatibility alias for the ordinary protected manifest.\n`base-prompt` / `--base-prompt` are an explicit, human-only disclosure mode:\nthey print exactly the effective base-prompt bytes and nothing else. They\ncannot be combined with JSON or `--prompt`. Effective system text is never\nprinted because it can contain project instructions, skills, and memory.\n\n## `--prompt` is necessary for an exact manifest, and not always sufficient\n\nThe next user message is **part of the request**. Without it there is no\nnext-turn body to describe, and under auto model routing there is not even a\nroute: the route is decided by the text you have not typed yet.\n\nSo the manifest is sectioned, and each section is either exact or typed-absent:\n\n| Section | Exact when |\n| --- | --- |\n| `session` | always — posture, gates, base-prompt provenance, requested model/reasoning |\n| `route` | `--prompt` was supplied, an active goal has not exhausted its token budget, a fixed route is selected, no `message_submit` hooks are configured, and the shared planner resolved that route |\n| `tools` | the route is exact **and** the MCP tool state can be snapshotted without connecting |\n| `body` | the tool surface and authoritative Work snapshot are exact **and** no runtime transform would rewrite the request first |\n\n### Why a section stops being exact\n\n| Typed reason | What a real turn would do that an inspection may not |\n| --- | --- |\n| `auto-route-unresolved-until-next-prompt` | decide the route from text you have not typed |\n| `auto-route-classification-not-executed` | call the provider-backed Auto classifier; preview is strictly offline, so production must resolve it |\n| `no-hypothetical-prompt-supplied` | send a message the manifest does not have |\n| `message-submit-hooks-not-executed` | run mutable hooks that can rewrite or block the text — and therefore the route, tool policy, and body derived from it |\n| `prompt-resolution-failed` | fail the same way on skill authority or file mentions |\n| `route-plan-failed` | fail route resolution or preflight |\n| `mcp-state-not-snapshottable` | connect MCP servers and discover tools this catalog does not contain |\n| `runtime-transforms-before-send` | auto-compact, run context-overflow recovery, inject a background-shell completion, allow a running or terminal-undelivered child to complete, or flush pending LSP diagnostics before the first request |\n| `work-state-not-snapshottable` | read the current graph-backed Work projection; preview never substitutes an asynchronously published, potentially stale To-do view |\n| `goal-token-budget-exhausted` | stop the active goal before dispatch because durable token usage has reached the configured budget |\n| `goal-state-not-snapshottable` | decide whether the active goal's terminal budget gate permits another request |\n| `request-preparation-failed` | build no body at all |\n\n**`mcp-state-not-snapshottable` takes the body with it.** A catalog missing its\nMCP contribution is not \"the same request with no MCP tools\" — a real turn\nconnects, and may send a different tool list, a different tool region, and\ntherefore a different body and hashes. The body inherits the tool\nsection's reason rather than publishing an exact hash of a request that would\nnever be sent. The `route` section survives: the endpoint, dialect, and wire\nmodel do not depend on which tools are on the request.\n\n**Detection is read-only.** Nothing is drained, received, flushed, or\ncompacted to find out: eligible running and terminal-undelivered children are checked, LSP blocks\nare checked for emptiness, the shell manager is checked without polling or\nmarking completions reported, and the compaction decision is evaluated against\nthe borrowed hypothetical message list with its active slop gate pinned.\nInspecting the pending state does not consume it.\n\nWithout `--prompt` the route section is unavailable even on a fixed model.\nThat is deliberate: a route is only reported when it was resolved by the same\nplanner that would send the turn, for the same next message. A route that was\n\"probably still the current one\" is exactly the kind of almost-true fact this\ncommand exists to avoid.\n\nAn unavailable section publishes a typed reason and **no fields**. When auto\nrouting is unresolved there is no `provider_id`, `route_id`, `dialect`,\n`endpoint_host_class`, `endpoint_fingerprint`, `wire_model`, `billing`,\n`tool_surface_budget`, or `body_sha256` anywhere in the JSON — not a `null`,\nnot the previous turn's value. `requested_model` reads `auto`, because that is\nwhat you actually selected.\n\n## What runs, and what does not\n\nWith `--prompt`, the preview executes the **deterministic part of the\nproduction path** up to (but not including) the send:\n\n1. The prompt is resolved into model-facing content exactly as a real submit\n does — the pending active skill it would be wrapped with (**cloned**, not\n consumed), file mentions, git mentions, and the paused-command note — with\n the same error propagation. What a submit does that an inspection may not is\n run `message_submit` hooks; when any are configured the manifest says so and\n claims nothing downstream of the text.\n2. For a fixed route, that content goes through the **same shared route\n planner** (`plan_turn_route`) that `spawned_dispatch_inner` uses for a real\n turn: effective provider and model, route identity resolution, preflight,\n route limits, compaction policy, and reasoning-effort normalization. Auto\n stops before this step because the planner would call a model classifier.\n3. The engine projects the planned route into a throw-away client — the same\n client construction a turn installs, without installing it.\n4. It rebuilds the tool catalog and narrows it with the same planner the turn\n loop uses, composes the system prompt **for that route's model and context\n window**, and appends the hypothetical user message through the same\n constructor production uses (turn metadata, route stamp, and provenance),\n then resolves an `auto` reasoning tier against\n those messages the way the turn loop does. Production sends stored history\n and nothing else — Codewhale does not re-state the To-do list on model\n steps — so the previewed outbound message list is exactly that list, and\n one estimate over stored messages plus system covers both the manifest\n number and the overflow decision.\n5. It prepares the request through `CodewhaleClient::prepare_outbound_request`\n and describes the result — unless a runtime transform would rewrite it\n first, in which case the body is typed unavailable instead.\n\n**Nothing is installed, not even briefly.** Everything a turn installs before\nbuilding its request — the command-scoped tool gate, the effective mode and\napproval posture, the policy-narrowing event, the working set with the new\nmessage observed — is passed as a value or snapshotted onto a clone. There is\nno write-then-restore: a restore is not atomic across an `await`, and it does\nnot survive a cancellation or a panic. Terminal continuation state is read\nwithout mutating its counters. A regression test\nasserts that config, caches, session messages, model, system prompt, working\nset, provider, mode, and the MCP pool are all byte-identical afterwards.\n\n**No outbound call can happen.** With a fixed model, planning and request\npreparation are local. With Auto selected, the command reports\n`auto-route-classification-not-executed` and stops before the shared planner,\nbecause resolving the route would call a model classifier. Preview never\nreads or populates the classifier response cache and never changes provider\nretry or rate-limit state.\n\n**Nothing else has a side effect.** The tool-catalog build runs in a passive\nmode: it never creates the MCP pool, calls `connect_all`, reloads an MCP config\nsource, starts a server, spawns a sub-agent runtime task, captures a fork\nsnapshot, or emits a UI status event. When the connected MCP state is not\nalready exactly what a turn would use — no pool yet, a config source changed,\nor an enabled server is not connected — the `tools` section reports\n`mcp-state-not-snapshottable` instead of connecting to find out.\n\n## Scope: primary agent turns only\n\nThis manifest describes `LlmClient::create_message` /\n`create_message_stream` — the model turns the agent loop runs. It does **not**\ndescribe Codewhale's auxiliary provider calls, which have their own shapes:\n\n| Auxiliary call | Status |\n| --- | --- |\n| Chat-dialect translation (`translate`) | **Not** on the prepared seam: it builds a small fixed body (no tools, temperature 0.1) directly. Out of scope. |\n| Anthropic/Responses-dialect translation | Routed through `prepare_outbound_request` to avoid a second builder, but still an auxiliary call and still out of scope for this manifest. |\n| FIM completion, speech, provider-native search, `/models` listing | Separate endpoints and bodies. Out of scope. |\n| Auto-router classifier | A separate small turn on the router route. Out of scope and never executed by preview. |\n\nAny claim that \"every outbound request\" goes through the previewed seam would\nbe false, and this document does not make one.\n\n## Where the numbers come from\n\n**The prepared outbound request.** Every primary model turn reaches the wire\nthrough `CodewhaleClient::prepare_outbound_request`, which returns a\n`PreparedOutboundRequest`: dialect, endpoint identity, canonical wire model,\nthe final body, and a reasoning receipt. Production dispatch sends that value;\nthe preview describes it. There is no second body builder.\n\nParity tests do not feed a captured logical request back through that builder.\nThey run a real production turn against an HTTP mock, parse the first body the\nserver actually received, canonicalize those captured bytes independently,\nand compare that hash with preview. The coverage includes translated prompt\ncontext, paused-command detach, and native Anthropic Messages shaping.\n\nPreparation runs the full production sequence: tool-history repair and\nmodel-bound secret redaction, protocol binding and route model re-resolution,\nthe dialect's own body builder with every provider-specific sanitizer and\nreasoning shaper, and exact endpoint resolution.\n\nEvery production dialect is preserved end to end — nothing is projected\nthrough Chat Completions:\n\n| Dialect | Routes |\n| --- | --- |\n| `chat-completions` | DeepSeek, Moonshot/Kimi (including the Kimi Code K3 nested `thinking.effort` shape and the direct K3 fixed-sampling shape), Z.ai, xAI, OpenRouter, vLLM/Ollama/SGLang, OpenCode Zen chat routes, custom compatible endpoints |\n| `anthropic-messages` | Anthropic, DeepSeek Messages, MiniMax Messages, OpenModel |\n| `openai-responses` | OpenAI Codex (ChatGPT backend path), OpenCode Zen responses routes |\n\nThe manifest reports the dialect *and* the route shape\n(`standard`, `deepseek-beta-strict-tools`, `kimi-code-k3`,\n`direct-moonshot-k3`, `codex-responses`, `opencode-zen`,\n`custom-compatible`), so you can see which builder branch actually ran.\n\n**The engine.** The manifest is built by the engine, not the command layer,\nbecause only the engine can rebuild the exact next-turn tool catalog, active\nsubset, gates, permission posture, and tool choice. The session's last tool\ncatalog is never consulted — it is one turn stale and stores the\npre-activation catalog.\n\n## Streaming is reported as a wire fact\n\n`caller_entrypoint` says which transport entry point was described\n(`streaming` / `blocking`). `body_stream_field` says what the **body** says,\nread off the finished JSON:\n\n- Chat Completions streaming → `true`; Chat blocking → the field is absent\n (`null`), because the blocking body never carries it.\n- Anthropic Messages → mirrors the caller.\n- OpenAI Responses → **always `true`, including on the blocking entry point**,\n which opens an SSE stream and folds it into one response.\n\nThe manifest describes the body field exactly rather than inferring it from the\ncaller, so the Responses blocking case cannot be misreported as a non-streaming\nrequest.\n\n`tool_choice` is likewise read from the finished provider body, not the\nlogical request: Anthropic may carry an object, Responses carries its mapped\nstring, and DeepSeek thinking requests omit the field entirely.\n\n## What the manifest tells you\n\n| Section | Fields |\n| --- | --- |\n| `session` | exact primary-agent role/lane/Fleet non-assignment, requested model (`auto` when auto), routing mode, requested reasoning, whether a hypothetical prompt was supplied, mode, approval posture, allow/deny gate sizes, base-prompt origin + bytes + SHA-256 |\n| `route` | provider id + display name, named route id, typed routing source, dialect, route shape, safe endpoint host class/digest, endpoint fingerprint, **wire model**, caller entrypoint, body `stream` field, context ceiling + source (`configured`, `provider-reported`, static floor, catalog, or fallback), route input/output limits or `unknown`, typed billing |\n| `tools` | active count, catalog / deferred counts, logical catalog SHA-256, surface budget, Standard-vs-Full collapse, MCP servers and MCP tools |\n| `body` | reasoning resolution + wire control keys + wire effort **and the key path it came from**, `tool_choice`, system-prompt assembly + effective canonical JSON bytes/SHA-256, body/system/tool-schema/message/tool-result/attachment/framing canonical JSON sizes, per-class estimates, exact input-budget ceiling and headroom, literal wire output cap or `unknown`, provider-reported usage explicitly unavailable because no request ran, **whole-body SHA-256**, wire tool-schema SHA-256, local system/tools component SHA-256 |\n\nCounts and estimates are extracted **dialect-aware**: a Responses body's\n`instructions`/`input`, an Anthropic body's `system`/`messages`, and a Chat\nbody's inline `system`-role message are each read from where that dialect\nactually keeps them.\n\n### The byte classes are an exact accounting, not byte slices\n\n`system + tool_schemas + messages + framing == body_canonical_json_bytes`, exactly, in\nevery dialect and on both entry points. The first three are the canonical\nserializations of selected JSON values; they are not four disjoint ranges\nborrowed from the body buffer. **`framing` is the algebraic remainder** after\nthose selected values. It includes every other top-level field and whatever\nJSON structure was not already counted inside a selected array value. An\ninvariant test asserts the sum identity, and mutation tests check the intended\nattribution. Do not use these counts to reconstruct the request bytes.\n\n`tool_result` and `attachment` bytes are *subsets* of the message bytes,\nreported for attribution and never added again.\n\n### Headroom is measured against the input budget\n\n`estimated_input_headroom_tokens` subtracts production's conservative\nmessages-plus-system estimate from this route's\n**input budget ceiling** — `context_input_budget_for_route`, the same seam the\nturn loop checks before it sends, which is the context window minus the output\nreservation and the safety headroom. It is not the raw `context_limit_tokens`:\nsubtracting input from a window the route also has to fit its response into\nreports headroom the turn does not have. The value goes negative when the\nrequest would not fit, rather than clamping to zero and reading as \"it fits\" —\nand when it does, the body is reported unavailable, because the turn loop would\nrun context-overflow recovery and send something else.\n\nThis production gate is distinct from the manifest's independent conservative\nestimate over canonical JSON body bytes. The latter remains useful for\nprovider-body attribution, but it does not decide overflow or headroom.\n\nThe manifest publishes that exact ceiling as `input_budget_ceiling_tokens`.\nIt also keeps three different limit facts separate: the context ceiling and\nits resolver source, optional route/offering input and output limits, and the\noutput cap literally serialized on the wire. If a route or dialect supplies\nno such fact, the value is `unknown`; preview never fabricates one from a\nneighboring model or an installed route.\n\n### Reasoning controls are read wherever the dialect puts them\n\n`reasoning_wire_effort` reads the flat `reasoning_effort`, Kimi Code's nested\n`thinking.effort`, the Responses `reasoning.effort`, and the Anthropic\n`output_config.effort`, and `reasoning_wire_effort_source` names which one it\ncame from (a compile-time constant, never a key lifted out of the body).\nReporting only the flat key made the routes that think hardest read as \"no\neffort sent\".\n\n`reasoning_resolution` distinguishes an `explicit` user selection from a\n`route-default` control the user never asked for, and reports\n`not-applicable` when the body asks for no reasoning at all. A Responses\n`include` field *discloses* reasoning output rather than requesting a tier, so\n`include` alone is never reported as a reasoning request.\n\n## The whole-body hash\n\n`body_sha256` covers the complete canonicalized wire body, not a prefix. It\nchanges when any of these change: max-token fields, `tool_choice`, nested\nreasoning controls, provider-transformed tool schemas, attachments, stream\noptions, sampling parameters, or any message — including the appended\nhypothetical prompt. Canonicalization sorts object keys, so builder insertion\norder alone does not move the hash. Any real input change does, including\ndate/working-set/git metadata, runtime injections, tool discovery, prompt\nsettings, or routing.\n\n`tools.active_tool_catalog_sha256` is a separate, stable hash over the current\nactive tool catalog **before dialect shaping** (name, description, canonical\nlogical schema, in order). It moves on membership, ordering, and logical schema\nchanges. It is a catalog identity, not a wire fact: two routes can agree here\nand still send different bytes, because each dialect transforms schemas its own\nway and strict mode sanitizes them further.\n\n`body.omitted_tool_count` counts tools omitted by route compatibility, with\nat most eight sanitized names of 64 characters in `body.omitted_tool_names`.\nThese facts contain no schemas, references or endpoint details. Relaxed wire\nschemas do not change the logical argument contract used for execution.\n\n`body.tool_schema_wire_sha256` is the hash of the `tools` region **as the\nprovider receives it**. `body.local_system_tools_component_sha256` combines\nthat digest with the final wire system-region digest as a local comparison\nfingerprint. It is not a provider cache key, does not claim those regions are\nadjacent, and carries no route-specific cache-semantics guarantee. It is\nomitted when the tool surface is not exactly known.\n\n## Disclosure boundary\n\nThe manifest is a fixed set of counts, hashes, enums, and short provenance\nlabels. It has no field that can hold free-form request text. It cannot\ncontain:\n\n- the system prompt, project instructions, memory, or skill content;\n- message content, tool-result bodies, or attachment payloads;\n- credentials, `Authorization` headers, or query strings;\n- URL paths (which can themselves carry a deployment secret);\n- absolute workspace or home paths.\n\n**Identifiers are not trusted either.** A custom route id and a model id are\nuser-authored text that can be an absolute path, a URL, a URL path, or a\ndeployment id that is itself a credential. Every such value crosses an\nallowlist boundary (`crate::safe_label`) before it is printed: an\ngeneric identifier is published verbatim only when it contains no slash. A\nslash-bearing model id additionally has to match an exact entry in the active\nlocal model catalog; a vendor-looking prefix is not sufficient. Anything else\nis replaced by a stable `sha256:<12 hex>` fingerprint. Two previews of the same\nhostile id still compare equal; the id itself is never shown.\n\n**Error text is not trusted either, and is not scrubbed — it is\nallowlisted.** Preflight, MCP, prompt-resolution, and request-preparation\nfailures all interpolate host text, and that text routinely carries a route id,\na server name in quotes, an endpoint whose *path* is the secret, or a raw\ncredential. Every whitespace-separated token has to earn its place:\n\n- a token containing a control character is dropped;\n- a URL keeps only `scheme://host[:port]`, and only when both are themselves\n ordinary — path, query, fragment, and userinfo are never published, and a\n token-shaped \"host\" makes the whole token opaque rather than half-published;\n- anything path-shaped (POSIX absolute, `~/`, Windows drive, or containing a\n backslash) collapses to `<path-redacted>`;\n- a token carrying `\"`, `'`, or a backtick is replaced wholesale — quoted spans\n are where hostile identifiers hide;\n- everything else must be a short ordinary word (ASCII alphanumerics plus\n `-`, `_`, `.`, bounded in length, rejected if it looks token- or key-shaped),\n with only sentence punctuation allowed at its edges.\n\nAnything else becomes `<redacted>`, runs of redactions collapse, and the result\nis truncated. Ordinary diagnostic sentences survive intact; hostile ones become\na generic shape.\n\nEndpoints are published two ways only: a bounded host class (`http loopback`\nor `https remote sha256:<12 hex>`) and a SHA-256 fingerprint of the full URL\nfor \"same endpoint?\" comparisons. A remote authority is always digested — it\nmay be a credential-shaped tenant subdomain — and paths, IDNs, userinfo,\nqueries, and fragments are never shown.\n\n**This is an inspectability slice, not a request-body export.** A regression\ntest asserts that no manifest field carries a serialized message array or tool\nschema.\n\n## Estimates are estimates\n\nEvery token number is an **offline estimate** (~4 bytes/token plus a 5%\nconservative margin), never a provider-authoritative count. Use them to\ncompare requests to each other, not to predict a bill. Byte counts, hashes,\nand counts are exact.\n\nTool-result and attachment estimates are *subsets* of the message estimate and\nare reported for attribution; they are not added again into the total.\n\n## Base-prompt provenance (#3928)\n\nThe protected manifest distinguishes three things without printing effective\nsystem text. The explicit `/preview-request base-prompt` mode separately prints\nonly the exact effective base-prompt bytes:\n\n- **Origin** — where the base-prompt bytes came from:\n - `bundled in this codewhale-engine build (BASE_PROMPT, compiled in)`\n - `config-directory override installed at startup (prompts/constitution.md,\n opt-in enabled)`\n- **Assembly** — how the effective prompt was built on top of that base:\n `base prompt only`, `base prompt + configured static layers`, or\n `base prompt + configured layers + runtime/session additions`.\n- **Effective hash** — the SHA-256 of the system region of the *prepared\n request*: the prompt in its final wire form, not an independently\n recomposed string.\n\nIn a live session the assembly is normally\n`base prompt + configured layers + runtime/session additions`, because the\nenvironment block, project context, skills, and memory are appended after the\nconstitution. Codewhale does not claim the configured constitution is the\neffective base prompt, and no diagnostic cites a source-tree path that does not\nexist on an installed binary.\n\n## Tool-surface labels\n\n`standard_and_full_surfaces_collapsed` is **derived, not asserted**: the\nsurface shaper is run over the actual current catalog under both the Standard\nand the Full budget and the results are compared. Today it reports `true`\n— the two budgets produce the same catalog — and the manifest says so in\nplain words rather than implying a difference. The day the shaper narrows\nStandard differently from Full, the field flips on its own with no copy edit.\n\nAny benchmark claim about one surface offering \"more tools\" has to show a\ndifferent `active_tool_catalog_sha256` first.\n\n## Comparing routes before you spend anything\n\n`/preview-request` makes a provider-free fixed-route A/B possible: switch route,\npreview with the same prompt, compare.\n\n1. Select the route (`/model`, `/provider`, or your profile) — do not send a\n turn.\n2. Run `/preview-request json --prompt \"<the same text each time>\"` and save\n the output, e.g. `glm-5.2.json`.\n3. Repeat for each route.\n4. Diff the manifests.\n\nWhat to read in the diff:\n\n- **`route.dialect` / `route.route_shape`** — two routes on different dialects\n are sending structurally different requests, not the same request to a\n different host.\n- **`route.wire_model`** — the id that actually goes on the wire. Router\n entries frequently differ from the label you selected.\n- **`tools.active_tool_count` / `body.tool_schema_wire_sha256`** — identical\n wire hashes mean identical tool bytes, whatever the surface label says. Use\n `tools.active_tool_catalog_sha256` only to compare *logical* catalogs; two\n dialects can agree there and still send different schemas.\n- **`body.estimates.tool_schemas` vs `route.context_limit_tokens`** — the fixed\n overhead each route pays before the conversation starts, against the window\n it has.\n- **`body.reasoning_wire_control_keys` / `body.reasoning_wire_effort` /\n `body.reasoning_resolution`** — whether the route is actually being asked to\n think, in which dialect, and whether that came from you or from auto\n routing. Two routes with different effective effort are not comparable.\n- **`body.body_sha256`** — the whole request. If it is unchanged, nothing about\n the outbound bytes changed.\n- **`body.local_system_tools_component_sha256`** — whether the two locally\n measured wire components changed. It does not prove provider cache reuse or\n invalidation.\n- **`route.billing`** — subscription quota and metered API routes are not\n cost-comparable, and `unknown` means cost reporting fails closed.\n\nEach manifest is exact for the snapshot it describes. Repeated previews are\nidentical only when every contributing input is identical: route, current\ndate, git and working-set metadata, prompt settings, tool/MCP\nstate, session history, and pending runtime transforms. Previewing does not\nmutate session history or the response cache, but no cross-call byte-stability\nclaim is made. Auto is intentionally unavailable and cannot be used for this\ncomparison until production resolves a concrete route.\n\n`schema_version` is bumped whenever a field is renamed or removed, so scripted\nconsumers can detect an incompatible manifest instead of silently reading\n`null`. The current shape is `9`: v8's `work-state-not-snapshottable`\nunavailable reason is gone, because no request carries a To-do block for a\nsnapshot to fail on. The active goal token-budget terminal gate remains an\nexplicit fail-closed dependency of an exact outbound request.\n\n## What is still approximate\n\nAuto routing is not approximated: its provider-backed classifier is never run\nby preview, and the route-dependent sections are typed unavailable instead.\n\nWorking-set drift used to be listed here. It no longer applies: a real submit\ncalls `working_set.observe_user_message` before it builds `<turn_meta>`, and\nthe preview now performs that same observation on a **clone** of the working\nset and builds the block from that snapshot. Same bytes, no session write.\n\nEverything else that used to be \"approximate\" is now typed. If a runtime\ntransform would change the request, the body section says so and publishes no\nbytes at all, rather than publishing numbers that are nearly right.\n\n## Credit\n\nThe `dryrun` concept — preview the next request from the real request-building\nseam rather than a hand-rolled summary — was harvested from PR #1099 by\n[@GTC2080](https://github.com/GTC2080) (TaoMu). No code from that PR is\nreused; the implementation here is written against Codewhale's current\nmulti-dialect client.\n"
|
||
}
|
||
]
|
||
},
|
||
{
|
||
"id": "mcp",
|
||
"slug": "mcp",
|
||
"sources": [
|
||
{
|
||
"path": "docs/MCP.md",
|
||
"sha256": "d6d2f83cc1149151a9fa91b7d15973cfdeb72e8a5f5c03d5c80b9962d346c0ae",
|
||
"content": "# MCP (External Tool Servers)\n\nIn the terminal, `/mcp` (also `/mcps`) opens **Extensions → MCP**. Enter opens the selected server’s recovery action or read-only details. An empty inventory offers server suggestions; browsing them installs nothing. Explicit subcommands such as `/mcp status`, `/mcp doctor`, `/mcp login`, and `/mcp add` retain their existing behavior.\n\n\n> 阅读简体中文版:[zh_hans/MCP.md](zh_hans/MCP.md)\n\nCodewhale can load additional tools via MCP (Model Context Protocol). MCP servers can be local stdio processes that Codewhale starts, or remote URL-based servers that speak Streamable HTTP with legacy SSE fallback.\n\n## Version scope\n\nThis page describes the main candidate. As of November 7, 2026, the latest\npublished CLI release is [v0.10.2](https://github.com/codewhale-hq/Codewhale/releases/tag/v0.10.2).\nFor that installed release, use its [versioned MCP guide](https://github.com/codewhale-hq/Codewhale/blob/v0.10.2/docs/MCP.md).\nLocal stdio, remote HTTP/SSE, environment-backed authentication, direct remote\nOAuth, and reviewed plugin-contributed MCP are already supported in v0.10.2.\nSelected differences described below are candidate behavior:\n\n| Surface | Published v0.10.2 | Main candidate |\n| --- | --- | --- |\n| Default MCP client backend | Rust; SDK host is opt-in | Official MCP SDK 2.4.0 host; explicit Rust selection remains available |\n| Native stdio server protocols | Legacy revisions through 2025-06-18 | Adds 2025-11-25 and modern 2026-07-28 discovery |\n| Direct remote OAuth | Discovery, dynamic registration or a pre-registered public client | Adds issuer/resource-bound credentials and hosted client metadata support |\n| Catalogue freshness and MCP Skills | No TTL/list-change refresh or MCP Skills extension | Bounded refresh and verified skill resource reads; no native skill activation |\n| Package-runner connect budget | Default 30 seconds | At least 120 seconds unless the server has an explicit override |\n\nThe candidate's default host needs a supported Node runtime (`^22.19 || >=24`);\nBun 1.4.0 or newer is opt-in. See [extension runtime requirements](EXTENSIONS.md).\nAn MCP server's own executable, dependencies, authentication and OS requirements\nremain separate from Codewhale's transport support.\n\nBrowsing note:\n- `Web` is the canonical, deferred built-in browsing tool; it provides\n `search`, `fetch`, and `wait` actions when network policy permits.\n- `web_search`, `fetch_url`, and `wait_for_dev_server` are hidden replay-only\n aliases. New prompts and integrations should use `Web`.\n\nServer mode note:\n- `codewhale serve --mcp` runs the MCP stdio server.\n- `codewhale serve --http` runs the runtime HTTP/SSE API (separate mode).\n- `codewhale mcp-server` is an equivalent stdio entrypoint on the same\n consolidated runtime.\n\nIn 0.10.1, the old child-server aggregation proxy is removed. `mcp-server`\nnow uses the same native tool server as `serve --mcp`; it does not launch the\nlegacy `mcp.server_definitions` list. Saved definitions are left intact. Configure\nexternal servers in `mcp.json` for the existing MCP client, or connect to those\nservers directly from your external client. Native server write tools remain\nwithheld unless the operator explicitly permits them in its server configuration.\n\nThe native stdio server supports MCP **2026-07-28** with required per-request\nmetadata and `server/discover`; no initialization handshake is needed for that\nrevision. It also supports the explicit legacy revisions **2024-11-05**,\n**2025-03-26**, **2025-06-18** and **2025-11-25** through `initialize`.\nInitialization never selects a modern revision; unsupported initialization\nversions fall back to 2025-11-25. Legacy callers should send\n`notifications/initialized`, but calls may proceed after a valid initialize\nresponse. This describes Codewhale as a server; outbound MCP client version\nsupport is configured separately.\n\n`resources/list` pages through the effective native skill instruction catalog,\nand `resources/read` returns the accepted instruction body for an exact listed\n`codewhale://skills/...` URI. Discovery follows the configured skill roots,\nprecedence and workspace trust; disabled skills are omitted. Reads supply inert\nMarkdown and never activate a skill. These are instruction-body resources, not\noriginal SKILL.md files or complete skill packages. Plugin skills, arbitrary\nfilesystem paths and session transcripts are not exposed by this catalog.\nThe old workspace/session entries, which had no resource reader, are removed.\nResource templates are empty. Lists and reads use private caching with a zero\nTTL; restart pagination if a changed catalog invalidates a cursor.\n\nOnly tools and resources are advertised. Subscriptions, list-change\nnotifications, the Skills extension, Apps, tasks and interactive approval are\nnot implemented by this server. Tool execution remains serialized through the\nRust registry, and request cancellation reaches the existing tool context.\nKeep stdin open until responses arrive; closing it cancels outstanding work.\nMalformed JSON and oversized input frames are discarded by the bounded SDK\ntransport, while well-formed invalid JSON-RPC receives an error.\n\n## Setup wizard vs manual MCP setup (#3407)\n\nThe `/setup` hub includes an optional **Tools and MCP**\nstep. That step is discovery/readiness only:\n\n| Wizard can do | Still requires manual / explicit action |\n| --- | --- |\n| Show configured servers as `healthy` / `needs_config` / `off` | Start or connect MCP servers |\n| Report config path presence (global + project) | Write or edit `mcp.json` contents |\n| Safe static health probe (missing command/url, broken absolute path, missing bearer env) | `codewhale mcp validate`, live connect, OAuth login |\n| Point at safe on-ramps (`/mcp`, `codewhale mcp init`, `codewhale doctor`) | Install community skills, trust skills, enable plugins |\n| Share Hotbar source counts from the same skill/MCP adapters (#3399) | Bind Hotbar slots (Hotbar step / `H`) |\n| Record optional/`needs_action` setup_state without blocking first-run | Anything that spawns processes or installs packages |\n\nEmpty inventory is **not** an error: first-run users see “nothing configured\nyet, that’s fine.” Failing or incomplete configured servers surface as\n`needs_config` with an actionable hint and never block setup completion.\nEnumeration never executes MCP/plugin commands beyond the static probe.\nSummaries redact commands, args, env, headers, and tokens.\n\n`codewhale doctor` reports MCP/skills/tools/plugins health with the same\noptional-surface intent (paths, counts, static checks) so wizard and doctor\nstay consistent.\n\n## Plugin-contributed MCP\n\nA reviewed local plugin bundle may contribute MCP servers without creating a\nsecond transport or approval system. The servers use the same MCP manager,\ntool approval, resource, prompt, timeout, and network-policy paths documented\nhere, and appear under collision-resistant\n`plugin-<plugin-name-byte-length>-<plugin>-<server>` identities.\n\nAuthentication differs from direct user-authored MCP configuration. Plugin\nremotes may use `env_headers` or `bearer_token_env_var`, but plugin-contributed\nMCP OAuth is disabled in both v0.10.2 and the main candidate. Declaring OAuth,\nscopes or an OAuth resource in a plugin MCP manifest fails validation. To use\nOAuth, configure the remote server directly in your own `mcp.json` instead.\n\nThe bundle boundary is intentionally stricter than user-authored `mcp.json`:\nunknown fields and ambiguous transports fail closed; stdio environment values\nmust be exact environment-source references; remote literal headers and\nsecret-bearing URLs are rejected; declared network hosts must exactly match\nthe normalized endpoint host set; and redirects remain on the reviewed origin.\nReviewed plugin remotes also bypass ambient HTTP proxy configuration entirely;\nproxy credentials and proxy-observed traffic are not part of the v1 review.\nThe plugin review discloses local host-user authority, structural argv,\nenvironment provenance, endpoint, auth source names, and tool filters\nwithout reading or printing secret values.\n\nTrust stages reviewed content but does not enable it. Enablement attaches that\nstaged snapshot to the current workspace's MCP pool. Disable, revoke, and other\ncross-process generation changes remove catalog entries, cancel in-flight\noperations, and terminate plugin stdio children. Source or staged-tree drift is\nfully revalidated before each dispatch/catalogue boundary and fails the next\nboundary closed; v0.9.1 does not continuously hash mutable trees during an\nalready-running call and therefore does not promise drift-triggered mid-call\ncancellation. MCP subscriptions are not exposed through plugin bundles. See\n[Plugin bundles](PLUGIN_BUNDLES.md) for the complete lifecycle contract.\n\n## Bootstrap MCP Config\n\nCreate a starter MCP config at your resolved MCP path:\n\n```bash\ncodewhale mcp init\n```\n\n`codewhale setup --mcp` performs the same MCP bootstrap alongside skills setup.\n\nCommon management commands:\n\n```bash\ncodewhale mcp list\ncodewhale mcp tools [server]\ncodewhale mcp add <name> --command \"<cmd>\" --arg \"<arg>\"\ncodewhale mcp add <name> --url \"http://localhost:3000/mcp\"\ncodewhale mcp add <name> --url \"https://example.com/mcp\" --bearer-token-env-var MCP_TOKEN\ncodewhale mcp login <name>\ncodewhale mcp logout <name>\ncodewhale mcp enable <name>\ncodewhale mcp disable <name>\ncodewhale mcp remove <name>\ncodewhale mcp validate\n```\n\nShell commands inspect their own process's MCP pool. In particular,\n`codewhale mcp validate` starts and connects enabled, allowed servers and reports\nconnection/discovery failures; it is not a static configuration check and does\nnot attach those connections to a running TUI. In the TUI, `/mcp validate`\nreports the last recorded snapshot without reconnecting; `/mcp reload` rebuilds\nthat session's live pool. `codewhale doctor` is the static diagnostic path\nunless an explicit probe is requested.\n\n`codewhale mcp logout <name>` (and `/mcp logout`) clears locally stored\nOAuth credentials only — the provider may keep its standing grant. The next\nlogin forces the consent screen, so the authorized account/workspace can\nchange; to sever the grant remotely, revoke the app from the provider's\naccount settings.\n\n## In-TUI Manager\n\nInside the interactive TUI, `/mcp` opens a compact manager for the resolved\nMCP config path. It shows each configured server, whether it is enabled or\ndisabled, its transport, command or URL, timeout values, connection errors,\nand discovered tools/resources/prompts when discovery has been run.\n\nSupported in-TUI actions:\n\n```text\n/mcp init\n/mcp init --force\n/mcp import\n/mcp recommendations\n/mcp add recommended <id>\n/mcp add stdio <name> <command> [args...]\n/mcp add http <name> <url>\n/mcp login <name> [--scope scope]\n/mcp logout <name>\n/mcp enable <name>\n/mcp disable <name>\n/mcp remove <name>\n/mcp validate\n/mcp reload\n```\n\n### Suggested plugins and companion integrations\n\n`/mcp recommendations` is Codewhale's native, curated suggestions surface.\nThe entries are described as product plugins, with their component type and\nprovenance, but `/mcp add recommended <id>` still writes only the named MCP\nserver component. Viewing recommendations never fetches, installs, trusts, or\nenables anything. Adding one writes configuration; the server is first started\nonly after an explicit `/mcp restart`.\n\nThe v0.9.10 product suggestions use these reviewed, pinned definitions. The\nPlugins view is the product/install surface; MCP, Skills, and sandbox adapters\nare transparent component kinds and their own tabs remain operational and\ndiagnostic surfaces:\n\n| Plugin | Component | Pinned definition | Provenance and maturity | Installation boundary |\n| --- | --- | --- | --- | --- |\n| Chrome DevTools | MCP server (stdio) | `npx -y [email protected]` (`npx.cmd` on Windows) | [Official ChromeDevTools project](https://github.com/ChromeDevTools/chrome-devtools-mcp) | npm may download the pinned package when the user restarts MCP. |\n| Playwright | MCP server (stdio) | `npx -y @playwright/[email protected] --isolated` (`npx.cmd` on Windows) | [Official Microsoft project](https://github.com/microsoft/playwright-mcp) | `--isolated` starts a fresh browser profile; npm may download the pinned package only after an explicit restart. |\n| Computer Use | First-party plugin (MCP + skill) | Install `computer-use` from the first-party catalog; updates independently | Codewhale; install, review and enable through Connectors or `/plugin` | This is the only computer-use integration Codewhale recommends. Third-party desktop-control MCPs are not listed here. |\n| Browser Use | Skill plus separately installed Python runtime | Skill/runtime release `0.13.8` | [Official browser-use project](https://github.com/browser-use/browser-use) | Optional companion: not an MCP server. Codewhale does not auto-run the upstream Skill installer or install its browser/runtime dependencies. |\n| Anthropic Sandbox Runtime | Sandbox adapter companion | `@anthropic-ai/[email protected]` | [Official anthropic-experimental project](https://github.com/anthropic-experimental/sandbox-runtime); beta | Documentation-only adapter candidate in v0.9.10: not an MCP server and not an active Codewhale plugin adapter. It does not replace Codewhale's sandbox policy. |\n\n[Container Use](https://github.com/dagger/container-use) remains an additional\nexperimental suggestion with an MCP server component (`container-use stdio`).\nThe binary must be installed separately; `/mcp add recommended container-use`\nonly writes config and Codewhale never downloads it.\n\nThis presentation follows the same useful boundary found in the local\nGrokbuild extensions view (one product plugin may expose MCP or Skill\ncomponents while component tabs stay inspectable), the Kimi marketplace's\nexplicit display name/tier/source fields, and the Codex marketplace's explicit\nsource and install-policy fields.\nCodewhale keeps its stricter rule: provenance and foreign policy are display\nmetadata only, never inherited trust or automatic installation. For full\nbundle and marketplace semantics, see [Plugin bundles](PLUGIN_BUNDLES.md).\n\n`/mcp validate` (alias `/mcp doctor`) with no name reports each server's state\nfrom the last MCP snapshot the TUI recorded, in a pager over\n**Extensions → MCP**. That snapshot is usually the engine pool's latest report,\nbut some actions, such as `/mcp import`, replace it with a separate UI\ndiscovery pass that also connects `lazy` servers the engine has not started.\nValidate also re-reads the MCP config files without connecting: it\nreports a config load error first, lists configured servers the snapshot has\nnot observed, and says when a `/mcp reload` is pending. It does not reconnect\nanything or change the catalog the model gets; `/mcp reload` is the fresh\nprobe.\n\n`/mcp reload` (aliases `/mcp reconnect`, `/mcp restart`) is the hot-reload path.\nIt re-reads the MCP config sources and reconnects the servers session boot starts (see [Connection Lifecycle](#connection-lifecycle)) through the engine-owned pool,\nso the rebuilt catalog is the exact one the next model turn uses — no TUI\nrestart. Config edits made from the TUI are written immediately and the manager\nmarks the snapshot reload-required until you run it; a failed reload leaves the\nprevious live pool intact and says so.\n\nHeadless surfaces are the exception: the `ConfigReload` app-server request does\n**not** refresh MCP connections, so a headless runtime still needs a restart\nafter MCP config changes.\n\n## Remote network authority\n\nDirect HTTP/SSE requests to public hostnames validate every DNS answer and pin\nconnections to a public address. This also applies to configured servers,\nredirects, and OAuth HTTP requests. The configured network allow/deny policy\napplies to login and token refresh as well as MCP tool requests.\n\nA configured `localhost` name or private IP literal explicitly permits that\nlocal endpoint. For a private DNS name, opt in on the server configuration:\n\n```json\n{\n \"mcpServers\": {\n \"internal\": {\n \"url\": \"https://mcp.internal.example/mcp\",\n \"allow_private_network\": true\n }\n }\n}\n```\n\n`allow_private_network` defaults to false. This exception applies only to the\nconfigured origin (scheme, host, and port); it does not authorize a different\nredirect or OAuth origin. Servers added by the model during a session cannot\nuse this exception, even if their configuration contains the flag.\n\nOperator-configured servers continue to honor `HTTP_PROXY`, `HTTPS_PROXY`, and\n`NO_PROXY`. When a proxy is selected for the configured origin, destination DNS\nresolution and private-network filtering are delegated to that operator-chosen\nproxy; a local DNS pin cannot constrain a proxy's own resolution. A `NO_PROXY`\nmatch uses the direct guarded connection instead. Model-added servers,\nreviewed plugin remotes, and secondary redirect/OAuth origins do not inherit\nambient proxy authority.\n\n## Remote HTTP Auth\n\nDirect URL-based MCP servers can use static headers, env-derived headers,\nbearer-token env vars, or OAuth. Plugin-contributed remotes are limited to\nenvironment-backed headers/bearer tokens and cannot use MCP OAuth; see\n[Plugin-contributed MCP](#plugin-contributed-mcp). Authorization precedence for\ndirect servers is conservative:\n\n1. `headers` and `env_headers` are applied first.\n2. `bearer_token_env_var` adds `Authorization: Bearer <env value>` when no\n Authorization header was already set.\n3. Stored OAuth credentials are used only when no Authorization header exists.\n\nFor bearer-token auth, prefer env-backed config:\n\n```json\n{\n \"servers\": {\n \"remote\": {\n \"url\": \"https://example.com/mcp\",\n \"bearer_token_env_var\": \"EXAMPLE_MCP_TOKEN\"\n }\n }\n}\n```\n\nFor generic remote MCP OAuth, add the URL server and run login:\n\n```bash\ncodewhale mcp add remote --url \"https://example.com/mcp\"\ncodewhale mcp login remote\n```\n\nCodewhale discovers the server OAuth metadata, opens the authorization URL in\nyour browser, listens on a local callback, exchanges the code, and stores the\ntoken response through the Codewhale secrets backend. Stored OAuth tokens are\nlooked up by server name plus URL and refreshed when possible before requests.\nEach saved credential also records the exact discovered authorization issuer\nand resource. Reload compares both before installing credentials; a changed\nissuer or resource requires a new login. Existing records without this binding\nare preserved but cannot be reused: run `codewhale mcp login <name>` to replace\nthem after authorization.\nDuring login, the CLI prints the authorization URL and a waiting status while\nthe local callback listener is active. If a URL-based server returns 401 or\nUnauthorized during connect/discovery, `codewhale mcp connect <name>` reports\nthat OAuth authentication is required and points to\n`codewhale mcp login <name>`. Resource helper listings also surface an\n`authentication_required` entry for auth-shaped failures instead of silently\nlooking empty.\n\nOptional OAuth fields:\n\n```json\n{\n \"servers\": {\n \"remote\": {\n \"url\": \"https://example.com/mcp\",\n \"scopes\": [\"tools/read\"],\n \"oauth\": {\n \"client_id\": \"public-client-id\",\n \"issuer\": \"https://auth.example.com\"\n },\n \"oauth_resource\": \"https://example.com/mcp\"\n }\n }\n}\n```\n\nA pre-registered `oauth.client_id` requires its exact `oauth.issuer`. The CLI\naccepts `--oauth-client-id` together with `--oauth-issuer`. The optional\n`oauth_resource` is an exact pin for the SDK-discovered resource; it does not\nappend or override a second audience value.\n\nAlternatively, configure `oauth.client_metadata_url` (CLI:\n`--oauth-client-metadata-url`) with the HTTPS URL of an operator-hosted client\nmetadata document. Codewhale uses it when the authorization server advertises\nCIMD support, otherwise tries advertised dynamic registration. Pre-registration\ntakes priority. Codewhale identifies this desktop/CLI client as `native`; a\nhosted document must describe that client and its actual callback URLs. No\nCodewhale-hosted metadata document is supplied by this setting. Servers must\npublish discoverable authorization metadata; guessed legacy OAuth endpoints\nare not used for login or credential reload.\n\nUser-level config can set callback behavior when the provider requires a fixed\nredirect:\n\n```toml\nmcp_oauth_callback_port = 1455\nmcp_oauth_callback_url = \"http://127.0.0.1:1455/callback\"\n```\n\nThese callback fields are ignored from project-scope config overlays.\n\n## Hugging Face MCP\n\nHugging Face provides a hosted MCP server for Hub resources, documentation,\ndatasets, Spaces, and community tools. Codewhale does not call Hugging Face's\nHub HTTP APIs from `/hf`; it only helps you inspect and set up the MCP config\nthat the regular MCP manager will load.\n\nThe recommended setup path is Hugging Face's settings-generated configuration:\n\n1. Visit <https://huggingface.co/settings/mcp> while signed in.\n2. Choose the MCP client closest to your Codewhale config shape and copy the\n generated server snippet.\n3. Paste the Hugging Face server entry into your resolved MCP config file.\n4. Run `/mcp reload` to rebuild the live model-visible tool pool.\n\nCodewhale reads both `servers` and `mcpServers`, so settings-generated snippets\ncan be adapted without changing the rest of the MCP file. A placeholder-only\nshape looks like this:\n\n```json\n{\n \"servers\": {\n \"huggingface\": {\n \"url\": \"https://huggingface.co/mcp\",\n \"bearer_token_env_var\": \"HF_TOKEN\"\n }\n }\n}\n```\n\nSet `HF_TOKEN` in the environment that launches Codewhale; the example contains\nonly its variable name. Literal `headers` values do not expand `${HF_TOKEN}`.\nAdapt the settings-generated server entry and never commit real tokens.\n\nInteractive helpers:\n\n```text\n/hf mcp status\n/hf mcp setup\n/hf concepts\n```\n\n`/hf mcp status` checks the configured MCP file for common Hugging Face server\nnames or Hugging Face MCP URLs. `/hf concepts` explains the difference between\nthe Hugging Face provider route, Hugging Face MCP, and explicit Hub workflows.\n\nOfficial docs: <https://huggingface.co/docs/hub/hf-mcp-server>\n\n## OpenDesign\n\n[OpenDesign](https://github.com/nexu-io/open-design) ships a stdio MCP server\n(`od mcp`) that lets an agent read files from local OpenDesign projects and\ncreate project-scoped artifacts. Its `od mcp install` has no Codewhale target,\nso add the entry by hand:\n\n1. Start OpenDesign so its daemon is running.\n2. Copy the launch spec your install generated, from **Settings → MCP server**\n in the desktop app or from `curl -s http://127.0.0.1:7456/api/mcp/install-info`\n (use your daemon's port). It has a `command` (the absolute path of\n OpenDesign's Node-compatible runtime), `args` (the absolute path of its CLI\n entry, then `mcp`, then `--daemon-url http://127.0.0.1:<port>` unless the\n daemon runs as a sidecar) and an `env` map that pins `OD_DATA_DIR`.\n3. Put those three fields under one server name in your resolved MCP config,\n inside the server map the file already has. Files written by\n `codewhale mcp init` and `codewhale mcp add` use `servers`; OpenDesign's\n generic snippet uses `mcpServers`. Keep exactly one of the two keys: a file\n with both fails to parse.\n4. Run `/mcp reload`, or check from a shell with\n `codewhale mcp tools open-design`.\n\nA placeholder-only shape:\n\n```json\n{\n \"servers\": {\n \"open-design\": {\n \"command\": \"/absolute/path/to/node\",\n \"args\": [\"/absolute/path/to/open-design/cli.js\", \"mcp\", \"--daemon-url\", \"http://127.0.0.1:7456\"],\n \"env\": { \"OD_DATA_DIR\": \"/absolute/path/to/open-design-data\" }\n }\n }\n}\n```\n\nCopy every `env` entry the snippet gives you; a packaged app adds more, such\nas `ELECTRON_RUN_AS_NODE=1` when `command` is the app's own binary.\n`codewhale mcp add` cannot set `env`, so edit the file instead. By default the\n`od mcp` process exits after 30 idle minutes; set `OD_MCP_STDIO_IDLE_EXIT_MS`\nto `\"0\"` in `env` to keep it running until Codewhale disconnects.\n\nOpenDesign can also launch Codewhale as its agent. Its \"DeepSeek TUI\" runtime\nadapter runs `deepseek`, falling back to `codewhale`, as\n`exec --auto [--model <id>] <prompt>`, reads plain-text output, and caps the\nprompt at 30,000 bytes of argv. A dedicated adapter could instead pipe the\nprompt into `codewhale exec --auto --output-format stream-json --prompt-file -`,\nlist models with `codewhale models --json`, and continue a run with\n`codewhale exec --resume <id>`, taking the id from the `saved_session_id` field\nof the `session_capture` event; the terminal `metadata` event carries only a\nfingerprint. `--auto` enables Full Access by default (no Codewhale shell\nsandbox, trust mode on); adding `--sandbox workspace-write` keeps the shell\nsandbox and file-tool workspace restrictions, with trust mode off. These\ntool-offering exec runs are saved with either plain-text or streaming output.\n[Agent runtime](AGENT_RUNTIME.md#event-vocabulary) describes the event stream.\n\n## Config File Location\n\nDefault path:\n\n- `~/.codewhale/mcp.json` (`~/.deepseek/mcp.json` is still read when the Codewhale file is absent)\n\nOverrides:\n\n- Config: `mcp_config_path = \"/path/to/mcp.json\"`\n- Env: `CODEWHALE_MCP_CONFIG=/absolute/path/to/mcp.json` (takes precedence over\n the legacy `DEEPSEEK_MCP_CONFIG` spelling)\n\nCustom paths must be absolute; a relative `mcp_config_path` falls back to the\nuser-global path rather than changing with the launch directory.\n\nCodewhale also reads `<workspace>/.codewhale/mcp.json` after the workspace is\ntrusted in user-owned configuration. Project entries override same-named global\nservers; project-local files cannot grant that trust themselves or redirect the\nglobal `mcp_config_path`. Global and trusted-project entries take precedence\nover a colliding plugin-contributed identity. Project stdio commands default\ntheir working directory to the workspace. In the main candidate,\n`always_allow_tools` is accepted only from the global server configuration,\nnot from project or plugin declarations.\n\n`codewhale mcp init` (and `codewhale setup --mcp`) writes to this resolved path.\n\nThe interactive `/config` editor also exposes `mcp_config_path`. Changing it in\nthe TUI updates the path used by `/mcp` and marks the pool reload-required;\n`/mcp reload` then switches the live pool to the new config source.\n\nAfter editing the MCP file or changing `mcp_config_path`, run `/mcp reload`. No\nTUI restart is needed.\n\n## Tool Naming\n\nDiscovered MCP tools are exposed to the model as:\n\n- `mcp_<server>_<tool>`\n\nExample: a server named `git` with a tool named `status` becomes `mcp_git_status`.\n\nThe command palette includes MCP entries grouped by server. It shows disabled\nand failed servers instead of hiding them, and uses the same runtime tool names\nshown to the model.\n\n## Resource and Prompt Helpers\n\nThe CLI also exposes helper tools when MCP is enabled:\n\n- `list_mcp_resources` (optional `server` filter)\n- `list_mcp_resource_templates` (optional `server` filter)\n- `mcp_read_resource` / `read_mcp_resource` (aliases)\n- `mcp_get_prompt`\n\n## Minimal Example\n\n```json\n{\n \"timeouts\": {\n \"connect_timeout\": 30,\n \"execute_timeout\": 1800,\n \"read_timeout\": 120\n },\n \"servers\": {\n \"example\": {\n \"command\": \"node\",\n \"args\": [\"./path/to/your-mcp-server.js\"],\n \"env\": {},\n \"disabled\": false\n }\n }\n}\n```\n\nYou can also use `mcpServers` instead of `servers` for compatibility with other\nclients. Use exactly one map key in a file: a file containing both fails to\nparse.\n\n## Running Codewhale as an MCP Server\n\nYou can register your local Codewhale binary as a stdio MCP server for another\nCodewhale session or a client supporting one of its advertised protocol\nrevisions. This exposes native tools; it is separate from connecting Codewhale\nto external servers.\n\n### Client configuration vs server configuration\n\n`mcp.json` tells a client which servers to launch or contact. The native\n`codewhale serve --mcp` process separately reads `[server]` from the retained\nlegacy path `~/.deepseek/mcp_server.toml`. By default it exposes only\n`file_read` and `search`. Writing files or running commands requires naming\nthose tools in `expose_tools` and explicitly setting `require_approval = false`\nin that server file. This stdio server has no interactive approval channel;\nwith approval required, writing/executing tools are withheld and refused.\n\n### Quick Setup\n\n```bash\ncodewhale mcp add-self\n```\n\nThis resolves the current binary path, generates a config entry that runs\n`codewhale serve --mcp`, and writes it to your MCP config file. The default\nserver name is `codewhale`.\n\nOptions:\n\n- `--name <NAME>` — custom server name (default: `codewhale`)\n- `--workspace <PATH>` — workspace directory for the server\n\n### Manual Config\n\nEquivalent manual entry in `~/.codewhale/mcp.json`:\n\n```json\n{\n \"servers\": {\n \"codewhale\": {\n \"command\": \"/path/to/codewhale\",\n \"args\": [\"serve\", \"--mcp\"],\n \"env\": {}\n }\n }\n}\n```\n\nThe consolidated `codewhale` runtime supports `serve --mcp` directly and also\noffers the equivalent `codewhale mcp-server` stdio entrypoint. Release\ninstallers expose the same runtime as `codew`; `mcp add-self` automatically\nresolves the command that invoked it.\n\n### Prerequisites\n\n- The binary referenced in `command` must exist and be executable.\n- The MCP server runs as a child process via stdio — no network ports required.\n- Each MCP client session spawns its own server process.\n\n### Tool Naming\n\nTools from an MCP server follow the standard naming convention:\n\n- `mcp_<server>_<tool>`\n\nFor example, the `shell` tool from the default server (named `codewhale`)\nbecomes `mcp_codewhale_shell`.\n\n### MCP Server vs HTTP/SSE API vs ACP\n\n| | `codewhale serve --mcp` | `codewhale serve --http` | `codewhale serve --acp` |\n|---|---|---|---|\n| **Protocol** | MCP stdio | HTTP/SSE JSON-RPC | ACP stdio |\n| **Use case** | Tool server for MCP clients | Runtime API for apps | Editor agent for Zed/custom ACP clients |\n| **Config** | `~/.codewhale/mcp.json` entry | Direct URL connection | Editor `agent_servers` custom command |\n| **Lifecycle** | Spawned per client session | Long-running daemon | Spawned per editor agent session |\n\nUse `mcp add-self` when you want Codewhale tools available to other MCP clients.\nUse `serve --http` when building applications that consume the API directly.\nUse `serve --acp` when an editor wants to talk to Codewhale as an ACP agent.\n\n### Verification\n\nAfter adding, test the connection:\n\n```bash\ncodewhale mcp validate\ncodewhale mcp tools codewhale\n```\n\n## Connection Lifecycle\n\nAs a client, Codewhale connects servers through the official MCP SDK (2.4)\nbehind its Rust broker by default (`[extension_host] mcp_backend = \"host\"` in\nyour user config; `\"rust\"` selects the previous Rust client and stays available\nfor one release, and a host refusal never falls back to it). Servers that speak\nthe 2026-07-28 revision are discovered with `server/discover`. Other stdio and\nHTTP servers, including ones that answer the discovery probe with an error or\nan unrelated result, connect through the legacy `initialize` handshake, and\nconfigured SSE servers keep theirs. A server that rejects sign-in reports its\nreal 401 with the `/mcp login` hint, and one whose command cannot start says\nwhy. Rust keeps process, network, credential, catalog, approval and\ncancellation authority.\n\nSession boot connects in the background: every enabled, allowed server —\nuser-configured or contributed by an enabled plugin — is started at session\nstart, concurrently and without holding the first turn. A plugin server passes\nthe same review, trust and enablement checks at session start as it does on\nfirst use. Mark a heavy user-configured server `lazy = true` to keep it\nunstarted until something asks for it — a turn whose\n`allowed_tools`/`tools.always_load` selection covers its `mcp_<server>_*`\nnames, a model call that resolves to one of its tools, or an explicit\n`/mcp retry <name>`. Servers marked `required` connect at boot even if\n`lazy`, so their failure surfaces before the first turn. A server that has not\nconnected yet shows as `not connected` in `/mcp`, never `connecting`; the\nconnecting label only describes handshakes actually in flight. After an OAuth\nlogin finishes, that one server reconnects automatically. `codewhale mcp tools`\nand `codewhale mcp connect` run in their own process, so a server they just\nreached from the shell still reads `not connected` inside a running session\nuntil that session connects it.\n\n`/new`, `/resume`, `/load`, switching sessions from the resume picker, and a\nprovider, profile or plugin change keep user-configured stdio servers running:\neach live process moves to the new conversation or engine without a restart,\nprovided its config, the workspace and the MCP backend are unchanged. Anything\nthe server holds in memory, such as a browser session and its logins, comes\nalong; run `/mcp reload` for fresh servers. HTTP/SSE servers reconnect, so they\npick up current credentials and `[network]` policy. Servers added at runtime\nand plugin-contributed servers are not handed over and close at each of these\nboundaries, even when the conversation continues; plugin-contributed servers\nconnect again with the next session boot, while a server added at runtime must\nbe started again. A workspace switch restarts\nevery server. If an MCP call from the previous conversation is still running\nat the boundary, nothing is handed over and the servers restart as before;\notherwise later calls from that conversation's background work fail instead of\nreconnecting.\n\nAn MCP-focused `tool_search` is also explicit discovery intent: search a\nconfigured server name (for example `engram`), an exact `mcp_<server>_...`\nname, or use `{\"query\":\"mcp_.*\",\"match\":\"regex\"}`. The current turn's\nallow/deny ceiling filters the configured servers before a batch of at most\neight connects; the existing five-second wait and cancellation remain in\nforce. General searches do not boot all optional servers. Only actual\n`tools/list` schemas enter the deferred catalogue; failed, disabled, or\nrevoked servers do not acquire fabricated tools. Narrow a broad search by\nserver name when more than eight configured servers match.\n\nBefore a model request, one bounded runtime note records the current connection\nobservations for permitted servers. It uses fixed states: `configured` (no\nconnection outcome recorded), `connecting`, `failed`, `authorization_required`,\n`unknown` and `connected`. Failures and authorization needs are listed first;\nat most 24 names are shown, with 64 characters per name and an omission count.\nNames are inert data. Raw errors, commands, URLs and credentials are excluded.\n\nThe note supersedes earlier snapshots and lives in session history, after the\ncached system prompt. Unchanged observations are recorded once and are recorded\nagain if compaction removes them. No server is started or schema loaded by this\nobservation. When `tool_search` is available, the model can search a permitted\nconfigured name to obtain actual tools or retry a relevant connection. Without\nit, only servers identified by their actually exposed MCP tools are included.\nDisabled servers and servers outside the current discovery ceiling are omitted.\n`connected` means a current authorized transport was observed; it does not\npromise available schemas, tool permissions or a successful call.\n\nThe same nonblocking capture supplies server guidance. A busy pool, changed\nconfiguration source or removed scope withdraws old guidance and status instead\nof replaying stale success. Restore withdraws saved MCP notes until the live\nruntime observes the current pool. Child, RLM, ACP and isolated hosts do not\nacquire new discovery authority from these notes. Observations happen at request\nboundaries; this is not continuous monitoring or semantic verification of server\nclaims. The existing MCP manager owns configuration and authorization recovery.\n\n`codewhale mcp connect`, `validate`, and `tools` inspect their own process's\npool. They do not attach transports to a running TUI or exec session. Use\nin-session discovery or explicit tool selection. In the TUI,\n`/mcp retry <name>` connects through the current session's pool;\n`/mcp reload` re-reads its MCP configuration.\n\n### Catalog freshness\n\nA server's tools, resources, resource templates, prompts and skills are kept\non its own live connection, so they are never shared between servers,\ncredentials, sessions or reconnects. Every reconnect, login and config reload\ndiscovers them again, and allow/deny rules and approvals are checked on each\ncall rather than cached with the catalog. Within one connection the catalog is\nrevalidated in place, on the five-second supervision sweep, when either:\n\n- a list page carried `ttlMs` and the earliest of those TTLs (held between\n 60 seconds and 24 hours) has passed, or\n- the server sent `notifications/tools/list_changed`,\n `notifications/resources/list_changed` or\n `notifications/prompts/list_changed`.\n\nA list without `ttlMs` stays valid until one of those events. Because nothing\nis shared beyond the connection, `cacheScope: \"public\"` and `\"private\"` are\nboth held to the stricter private rule. Pagination, item and byte bounds are\nthe same as at connect, and ordering stays sorted. When revalidation changes\nthe catalog, a tool call resolved against the previous catalog is refused with\n\"retry the call\", a removed tool can no longer run, and the next model request\ncarries the new tool list. When revalidation fails, the connection is retired\nand ordinary reconnect supervision takes over; the stale catalog is not served.\n\nKnown limits: the Rust backend reads list-change notices only while it waits\nfor a reply, so an idle stdio server's notice is acted on after its next\nrequest. The 2026-07-28 `subscriptions/listen` stream and `resources/subscribe`\nare not opened; modern servers are kept fresh through their required `ttlMs`.\n\n### Skills from MCP servers\n\nA server that advertises the published Skills extension\n(`capabilities.extensions[\"io.modelcontextprotocol/skills\"]`) has its\n`skills/list` read with the rest of its catalog, under the same bounds and\nfreshness rules. An entry is kept only when its `SKILL.md` URI is listed among\nits own resources, its frontmatter has a name and description, every file sits\nbeneath the skill's root without `.`/`..`, `%`, `?` or `#` segments, every\ndigest is `sha256:` plus 64 lowercase hex characters, and it stays within 512\nfiles and 16 MiB. `\"dynamic\"` skills cannot be verified and are skipped.\n\nKept skills appear in `list_mcp_resources` with a `skill` field naming their\nfiles. Listing a skill never loads or activates it. Reading one of its files\nwith `read_mcp_resource` checks that the server returned exactly that URI with\nthe listed size and digest; a mismatch is refused and nothing is loaded. MCP\nskills are not yet merged into the native skill catalog, `/skills` or skill\nactivation.\n\n## Server Fields\n\nPer-server settings:\n\n- `command` (string, required for a local stdio server): executable to start.\n Remote servers use `url` instead; a URL-only entry is valid.\n- `args` (array of strings, optional)\n- `env` (object, optional)\n- `connect_timeout`, `execute_timeout`, `read_timeout` (seconds, optional). A per-server value overrides the global `timeouts` block, and each budget is independent of the others:\n - `connect_timeout` (default 30) covers spawn, `initialize` and the first `tools/list`, so a cold `uvx`/`npx` package download counts against it. A server launched through `npx`, `pnpx`, `bunx` or `uvx` without its own `connect_timeout` gets at least 120 seconds for that first download.\n - `execute_timeout` (default 1800) is the budget for each `tools/call` and `prompts/get`. An explicit shorter value is respected, and `read_timeout` never cuts a running tool short.\n - `read_timeout` (default 120) bounds each reply wait during the handshake and tool discovery, and is the budget for `resources/read`.\n - A request that runs out of budget fails with a timeout. The connection is kept, and a reply that arrives later is discarded instead of being handed to another call. Interrupting the turn stops the wait at once, but Codewhale does not send `notifications/cancelled` yet, so a server that handles one request at a time finishes the abandoned call before it answers the next one.\n - Streamable HTTP servers return the reply inside the POST itself; the request's own budget bounds that POST too. A request that expires while still sending closes the connection, which is rebuilt before the next call.\n- `disabled` (bool, optional)\n- `enabled` (bool, optional, default `true`)\n- `required` (bool, optional): startup/connect validation fails if this server cannot initialize.\n- `lazy` (bool, optional, default `false`): keep this server unstarted until a turn, tool call or `/mcp retry` asks for it. Without it, the server connects at session start. Plugin manifests have no `lazy` field: an enabled plugin's servers connect at session start, and disabling the plugin stops them.\n- `enabled_tools` (array, optional): allowlist of tool names for this server.\n- `disabled_tools` (array, optional): denylist applied after `enabled_tools`.\n- `always_allow_tools` (optional array): Exact tool names the person allows across workspaces on this computer. Set these in the global server configuration. Off takes precedence. Computer Use consent and scripts still require the person’s approval.\n- `url` (string, optional): Streamable HTTP endpoint for a remote MCP server.\n- `allow_private_network` (boolean, default false): operator opt-in for private DNS addresses on this configured origin; ignored for model-added servers.\n- `transport` (string, optional): set to `\"sse\"` for legacy SSE endpoints.\n- `headers` (object, optional): literal HTTP headers for URL-based servers.\n- `env_headers` or `env_http_headers` (object, optional): header names mapped to environment variable names.\n- `bearer_token_env_var` (string, optional): environment variable containing a bearer token.\n- `scopes` (array, optional): default OAuth scopes for `mcp login`.\n- `oauth.client_id` (string, optional): pre-registered OAuth client ID; requires `oauth.issuer`.\n- `oauth.issuer` (string, optional): exact authorization issuer pin; required for a pre-registered client.\n- `oauth.client_metadata_url` (string, optional): HTTPS URL with a non-root path for an operator-hosted client metadata document.\n- `oauth_resource` (string, optional): exact resource pin checked against SDK discovery.\n\n## Safety Notes\n\nMCP tools flow through the same approval framework as built-in tools. Read-only\nMCP helpers (resource/prompt listing and reads) can run without prompts in Ask\nand Auto-Review when policy permits, while side-effectful MCP tools require\napproval. Full Access does not bypass hard policy holds.\n\nYou should still only configure MCP servers you trust, and treat MCP server configuration as equivalent to running code on your machine.\nAvoid committing literal `Authorization` headers. Prefer `env_headers`,\n`bearer_token_env_var`, or OAuth login so secrets stay outside the MCP file.\n\n## Troubleshooting\n\n- Run `codewhale doctor` to confirm the MCP config path it resolved and whether it exists.\n- In the TUI, run `/mcp validate` to see each server's state from the last\n recorded MCP snapshot, plus any config load error and configured servers that\n have not connected.\n- If tools are missing from the model's catalog after a config or credential\n change, run `/mcp reload` — `/mcp validate` only reports the last recorded\n snapshot and never reconnects.\n- If the MCP config is missing, run `codewhale mcp init --force` to regenerate it.\n- If tools don’t appear, verify the server command works from your shell and that the server supports MCP `tools/list`.\n"
|
||
}
|
||
]
|
||
},
|
||
{
|
||
"id": "hooks",
|
||
"slug": "hooks",
|
||
"sources": [
|
||
{
|
||
"path": "docs/rfcs/1364-hooks-lifecycle.md",
|
||
"sha256": "ea7cbbc7695f0a7466d0177073f34aebfb141677495e4a166665be65c9c08799",
|
||
"content": "# RFC: Hook Lifecycle Data Flow\n\n**Issue:** #1364\n**Status:** Implemented (all three PRs landed)\n**Date:** 2026-05-28\n**Reference:** the shipped contract lives in [docs/HOOKS.md](../HOOKS.md);\nthis RFC is kept as the design record. Where the two disagree, `docs/HOOKS.md`\nis correct and this document is history.\n\n**Scope:** everything below landed as a **TUI runtime** feature. No hook in\nthis RFC fires from `codewhale exec`, the CLI subcommands, the app-server /\nACP surfaces, or the `workflow` tool. The RFC's use of \"Codewhale\" as the\nactor should be read as \"the Codewhale TUI\" throughout.\n\n## 0. Implementation status\n\nVerified against `crates/engine/src/hooks/` and its call sites:\n\n| Item | Status | Where |\n| --- | --- | --- |\n| PR 1 — mutable `message_submit` | implemented | `HookExecutor::execute_message_submit_transform`, called from `dispatch_user_message_with_recovery` |\n| PR 2 — `turn_end` | implemented | `HookEvent::TurnEnd`, `turn_end_payload`, fired by `execute_turn_end_observer_hook` |\n| PR 3 — subagent observers | implemented | `HookEvent::SubagentSpawn` / `SubagentComplete`, fired by `execute_subagent_observer_hook` |\n\nTwo deliberate deltas from the text below, both shipped:\n\n- **`tool_call_before` also became mutable** (#3026), which this RFC listed as a\n non-goal for PR 1. It carries its own stdout JSON contract\n (`decision` / `reason` / `updatedInput` / `additionalContext`) plus the legacy\n exit-code-`2` hard deny. `message_submit` is therefore no longer the only\n event with a stdout contract; `shell_env`'s `KEY=VALUE` contract also predates\n both.\n- **`turn_end` observers are not gated by `continue_on_error`.** The structured\n observer path runs every matching hook regardless, because an observer that\n fails must not suppress the observers behind it.\n\nStill unimplemented from the text below: nothing in scope. `stop_hook_active` is\nemitted as `false` and the re-entry protection it reserves room for has not been\nbuilt.\n\nFour contract points hardened after this RFC, all documented in\n`docs/HOOKS.md`:\n\n- **One session identity.** The hook session id is minted once per TUI launch\n and survives a workspace switch or a trust decision that adds project hooks.\n `tool_call_before` reports that id rather than the engine's own session id,\n and `mode_change` carries it at all (it previously carried no session id).\n- **Timeouts bind background hooks too.** A background hook is supervised on\n its own thread; on expiry its process group is killed and reaped.\n- **`background` describes scheduling, not capability.** A background hook\n receives the same stdin payload and environment as the foreground form; it\n is simply never awaited, so its `HookResult` is flagged as a submission and\n carries no exit code.\n- **Conditions that can never match are rejected at load** rather than left\n silently inert — per entry, so a broken hook does not take a same-named or\n likewise-unnamed sibling with it — and a `tool_call_before` gate that returns\n no verdict does not read as permission when *that gate* declared\n `continue_on_error = false`. Strictness rides on the hook result, so it\n applies only to the hooks whose conditions matched the call in question.\n\n## 1. Problem\n\nCodewhale already has lifecycle hooks and MCP support, but the current hook\nsurface is mostly observer-only. This blocks portable extensions that need to\nparticipate in the agent data flow:\n\n- memory/context injection before a user message reaches the model\n- post-turn background analysis that prepares context for the next turn\n- sub-agent lifecycle visibility for orchestration and audit extensions\n\nThe current `message_submit` event fires before dispatch, but its output is\nignored. `TurnComplete`, `AgentSpawned`, and `AgentComplete` exist internally,\nbut they are not exposed as configurable hook events.\n\n## 2. PR split\n\nThis issue should be implemented as three PRs. Each PR should be independently\nreviewable and should leave the hook system in a useful state.\n\n### PR 1: Mutable `message_submit`\n\nAdd a structured hook execution path for `message_submit` that can transform or\nblock the user's submitted text before it is sent to the engine.\n\nScope:\n\n- keep the existing `[[hooks.hooks]]` config shape\n- pass a JSON payload to the hook on stdin\n- interpret stdout JSON containing `text` as the replacement user text\n- treat exit code `2` as an intentional block\n- run multiple submit hooks serially in config order\n- keep existing env vars for compatibility\n- keep `shell_env` stdout parsing unchanged\n\nNon-goals:\n\n- no tool argument mutation\n- no global stdout JSON semantics for all hook events\n- no transcript or model response mutation\n\n### PR 2: `turn_end`\n\nExpose the existing turn completion lifecycle as a hook event.\n\nScope:\n\n- add `HookEvent::TurnEnd` with event name `turn_end`\n- fire from the UI's `EngineEvent::TurnComplete` branch after core app state,\n usage, cost, notifications, and receipt state have been updated\n- pass turn metadata on stdin as JSON\n- make failures non-blocking and warn-only\n- include a `stop_hook_active` field in the payload, initially `false`, so the\n contract can support re-entry protection later\n\nNon-goals:\n\n- no change to turn status\n- no blocking of user input\n- no transcript mutation from `turn_end`\n\nImplementation note for the v0.9 branch: the narrow #2578 harvest uses the\nshared structured observer path introduced for sub-agent lifecycle hooks. It\nfires before queued follow-up dispatch, after queue-recovery state is known, so\nthe payload can report the queued-message count without letting a hook change\nwhat gets sent next. Stdout is ignored for `turn_end`; only `message_submit`\nhas a stdout mutation contract.\n\n### PR 3: Subagent lifecycle observer hooks\n\nExpose subagent start and completion as observer-only hook events.\n\nScope:\n\n- add `HookEvent::SubagentSpawn` with event name `subagent_spawn`\n- add `HookEvent::SubagentComplete` with event name `subagent_complete`\n- fire from the existing `AgentSpawned` and `AgentComplete` UI branches\n- pass subagent metadata on stdin as JSON\n- make failures non-blocking and warn-only\n\nNon-goals:\n\n- no subagent spawn gating in the first version\n- no subagent prompt/result mutation\n- no changes to subagent scheduling\n\n## 3. PR 1 detailed plan\n\n### 3.1 Contract\n\nConfiguration:\n\n```toml\n[[hooks.hooks]]\nevent = \"message_submit\"\ncommand = \"~/.deepseek/hooks/inject-memory.sh\"\ntimeout_secs = 2\ncontinue_on_error = true\n```\n\nInput payload on stdin:\n\n```json\n{\n \"event\": \"message_submit\",\n \"text\": \"original user text\",\n \"session_id\": \"sess_xxxx\",\n \"workspace\": \"/path/to/workspace\",\n \"mode\": \"agent\",\n \"model\": \"deepseek-chat\",\n \"total_tokens\": 1234\n}\n```\n\nOutput payload on stdout:\n\n```json\n{ \"text\": \"replacement user text\" }\n```\n\nRules:\n\n- exit `0` with stdout JSON containing `text: string` replaces the current text\n- exit `0` with empty stdout leaves the current text unchanged\n- exit `0` with JSON that does not contain `text` leaves the current text\n unchanged\n- exit `2` blocks submission before the message is appended to history or sent\n to the engine\n- other non-zero exits follow `continue_on_error`\n - `true`: warn, keep the current text, continue later hooks\n - `false`: stop later hooks and block submission with an error message\n- `background = true` on `message_submit` remains observer-only and cannot\n transform or block submission\n\nMultiple hooks:\n\n- hooks run in config order\n- each hook receives the latest transformed text\n- the final transformed text is the only text used by file mention expansion,\n skill wrapping, auto routing, history, and `api_messages`\n\n### 3.2 Implementation steps\n\n1. Add structured submit outcome types in `crates/engine/src/hooks.rs`:\n\n```rust\npub enum MessageSubmitOutcome {\n Unchanged,\n Replaced(String),\n Blocked { reason: String },\n}\n```\n\n2. Add a stdin-capable sync executor:\n\n```rust\nfn execute_sync_with_stdin(\n &self,\n hook: &Hook,\n env_vars: &HashMap<String, String>,\n stdin_json: &serde_json::Value,\n) -> HookResult\n```\n\nThis should reuse the existing timeout, working directory, stdout, stderr, and\nerror handling behavior from `execute_sync`.\n\n3. Add a `message_submit` transform entrypoint:\n\n```rust\npub fn execute_message_submit_transform(\n &self,\n context: &HookContext,\n original_text: &str,\n) -> MessageSubmitOutcome\n```\n\nThis method should:\n\n- filter configured `MessageSubmit` hooks through existing condition matching\n- build a JSON payload for each hook using the current text\n- run non-background hooks through `execute_sync_with_stdin`\n- run background hooks with the existing observer-only path\n- parse stdout JSON only for non-background hooks\n- return the final text or a block result\n\n4. Apply the transformed message in `dispatch_user_message`:\n\n- run the transform before `last_submitted_prompt`, file mentions, history, and\n `api_messages`\n- create a local mutable `QueuedMessage` or replacement display text\n- if blocked, show a status message or toast and return without dispatch\n\n5. Update `/hooks events`:\n\n- keep `message_submit` listed\n- update description to say it can transform or block user text\n\n6. Update user-facing docs:\n\n- document the stdin/stdout contract\n- document exit code `2`\n- document that `shell_env` still uses `KEY=VALUE` stdout\n\n### 3.3 Test plan\n\nUnit tests in `crates/engine/src/hooks.rs`:\n\n- parses stdout `{\"text\":\"changed\"}` as replacement\n- empty stdout means unchanged\n- JSON without `text` means unchanged\n- malformed stdout means unchanged with warning semantics\n- exit `2` maps to blocked\n- multiple hooks apply transforms in order\n- background `message_submit` hook cannot transform\n- `continue_on_error = false` blocks on non-zero failure\n\nTUI integration or focused dispatch tests:\n\n- transformed text is written to `api_messages`\n- transformed text is written to visible history\n- transformed text is used by file mention expansion\n- blocked submit does not append user history\n- blocked submit does not push an API message\n- blocked submit leaves loading state false\n\nManual smoke test:\n\n1. Add a config hook that prepends `[hooked] ` to every submitted message.\n2. Submit `hello`.\n3. Verify the transcript and model input use `[hooked] hello`.\n4. Replace the hook with one that exits `2`.\n5. Submit `hello`.\n6. Verify no turn starts and the TUI shows the block reason.\n\n## 4. Shared payload conventions\n\nAll new structured hook payloads should include:\n\n- `event`\n- `session_id`\n- `workspace`\n- `mode`\n- `model`\n\nEvent-specific payloads should add only fields that are stable and useful for\nextension authors. Avoid leaking secrets, full tool outputs, or unbounded\ntranscript content in the first version.\n\n## 5. Compatibility\n\n- Existing hook config remains valid.\n- Existing observer-only hooks keep working.\n- Existing env vars remain available.\n- `shell_env` keeps its existing stdout `KEY=VALUE` contract.\n- Structured stdout is interpreted only by `message_submit` in PR 1. Structured\n observer hooks such as `turn_end`, `subagent_spawn`, and `subagent_complete`\n receive JSON on stdin, but their stdout is ignored by the caller.\n\n## 6. Review checkpoints\n\nPR 1 should be accepted only if:\n\n- submit mutation is covered by tests\n- submit blocking is covered by tests\n- the unchanged path preserves current behavior\n- `shell_env` tests still prove the old stdout contract\n- the docs clearly mark `message_submit` as the only mutable hook\n\nPR 2 should be accepted only if:\n\n- `turn_end` fires after `TurnComplete` app state updates\n- failure is warn-only\n- payload contains status and usage\n\nPR 3 should be accepted only if:\n\n- subagent hooks are observer-only\n- failures do not affect subagent lifecycle\n- payloads do not include unbounded or secret data\n"
|
||
},
|
||
{
|
||
"path": "docs/CONFIGURATION.md",
|
||
"sha256": "618dd41d6edfb50b884d8634663b9d7ab75b1526a3f3e9121903631804e75e43",
|
||
"content": "# Configuration\n\ncodewhale reads configuration from a TOML file plus environment variables.\nAt process startup it may also load literal built-in-provider credentials from\na workspace-local `.env` file. Use the tracked `.env.example` as the template;\ncopy it to `.env`, then add only credential values.\n\nA workspace is not configuration authority. Codewhale therefore ignores\nconfig/profile/home paths, provider/model/base-URL routing, MCP/plugin state,\napproval/sandbox/shell posture, executable paths, runtime settings, and every\nother non-credential `.env` entry. Variable expansion is rejected so a\nrepository cannot substitute an ambient secret into a credential value. Use\n`config.toml`, CLI flags, or values exported by the launching shell for those\nexplicit control-plane settings. `.env` is read from a stable regular-file\nhandle, is capped at 1 MiB, and symbolic links, reparse points, and multiply\nlinked files are rejected.\n\n## Reading and checking configuration from the CLI\n\n`codewhale config get <key>` reads scalar keys, whole tables such as `tools`,\nand nested paths such as `tools.user_input_timeout_seconds`. Displayed tables\nand nested values apply the same recursive credential redaction as `config dump`.\n\n`config set` supports its named scalar keys and the provider, route, and\nnotification commands. It refuses a key that nothing reads and suggests the\nnearest real key (`config set calm_mod on` names `calm_mode`). A settings.toml\nkey such as `calm_mode` or `tool_collapse` is validated and written to the\nuser-global settings.toml, not config.toml; `config get` reads it back from\nthere even when an old config.toml copy (which nothing reads) is still present,\nand names that copy so you can `config unset` it. Settings keys have no project\nscope, so `--project` refuses them. A config.toml key is checked against the\ntype its reader expects and stored as a TOML boolean or number where the\nreader needs one (`yolo = true`, `max_subagents = 4`); `reasoning_effort`\naccepts the same aliases as `/effort` (#6563). Other dotted writes fail before\nmodifying the file and name the TOML table to edit. For example, set a tools\ntimeout in the file as:\n\n```toml\n[tools]\nuser_input_timeout_seconds = 0\n```\n\n`codewhale config doctor` checks credential presence and endpoint shape, and\nwarns about each config.toml root key that nothing reads: a settings.toml key\nleft in config.toml is named as misplaced, and any other unread key gets a\ndid-you-mean. Keys read by any runtime reader are not reported. The warnings do\nnot fail the check, and a clean result does not validate every runtime setting's\nvalue (#6083, #6563).\n\n## Usage and execution statistics\n\n`codewhale stats` is an alias of `codewhale metrics`; both use the same local,\nread-only backend. Use `--json` for the structured `stats` report. For example:\n\n```sh\ncodewhale stats --days 7 --models 10 --tools 20 --roles 10\ncodewhale metrics --project /path/to/workspace --json\n```\n\n`--days N` accepts a positive number of days and conflicts with `--since`.\n`--since 24h` and the existing duration syntax remain available. `--models N`,\n`--tools N` and `--roles N` select the top positive N rows by known tokens,\nidentified tool executions and recorded Fleet tasks respectively. These limits\nonly shorten the display; they do not remove stored receipts.\n\nThe report separates saved-document and Runtime views, frozen native-source\nmoney and token subtotals by actual model, retained tool-result history\nestimates and outcomes, completed compaction observations, and recorded Fleet\nroles. USD and CNY stay separate. A cumulative correction replaces its proven\npredecessor; copied Session/Runtime receipts and inherited Runtime fork rows\nare not added as new provider work. Conflicting source versions are shown\nseparately and excluded from the subtotal. Saved legacy display totals remain\nvisible as document projections without being charged again globally.\n\nCoverage is explicit. A missing quote, usage receipt, tool outcome, output\nshape or recovery link is unknown, not zero. Empty scalar usage from older\ntransport records cannot prove a reported zero. Token counts describe known\nreported data; tool history tokens are estimates of retained text, not bytes\nproven to have been replayed on a provider request. Latest prepared-request\ncomponents and source-linked compaction/role economics are unavailable where\nthey were not captured. The report does not infer provider invoices or prove\ncomplete historical coverage.\n\n`--project WORKSPACE` filters current saved/thread/Fleet views. Original source\nworkspace is not yet captured, so native money and model subtotals are\nunavailable for that filter; mutable current metadata is not used to assign a\ncharge. Older receipt projections without reliable workspace scope are also\nunavailable under this filter. Without it, those legacy receipt views remain\nseparate from native subtotals and may overlap one another.\n\nInspection uses the existing session-size bound (16 MiB per record/frame and\nfor each retained query index). Unreadable, linked, oversized or incomplete\nrecords create visible gaps; an exhausted native index refuses the query\nwithout evicting evidence. Use a narrower time window where possible. Reading\nstatistics does not create a store, repair history, refresh pricing, start a\nmodel call or retire accounting receipts. Existing state-root overrides remain\nthe read boundary.\n\n`/cache calls [count]` shows recent retained native source receipts in the TUI;\nit defaults to 10 and displays at most 50. A source correction updates its row.\nThe view keeps frozen USD/CNY estimates and known subtotals separate from missing\nusage or prices; absent or contradictory cache classes show unknown. The header\ncounts retained sources, not physical requests or complete historical calls.\nThe command does not issue a model request or remove accounting receipts.\n\nPublished prices are frozen for DeepSeek's root, `/v1`, `/beta` and Anthropic\nendpoint variants, and for other providers' default endpoints. Each quote stays\nbound to the exact endpoint, identity, model and dispatch time when saved or replayed.\nOperator endpoint overrides do not become published pricing authorities, and\nsubscription or token-plan routes remain outside money coverage. Signed cloud\nfacts keep their separate route-scoping rules. Recognition as an official\ncredential endpoint alone does not authorize sharing prices across regions or plans.\n\nMissing request outcomes, omitted provider usage, unverified legacy usage and\nusage corrections have distinct cost reasons. Known limitation: saved receipts\nwithout a frozen price are not repriced from today's catalog; their complete\ncost remains unknown even after updating the binary.\n\n## Constitution, project instructions, and repo authority\n\nCodewhale has several instruction surfaces. They are deliberately separate so a\npersonal constitution, repo policy, project instructions, and runtime security\ncontrols do not blur together.\n\n- **Bundled global Constitution** — the compiled base law in the binary. It is\n the default floor for every session.\n- **User-global constitution** — the normal guided setup output. Manage it with\n `/constitution` or `/setup`; Codewhale stores structured data at\n `$CODEWHALE_HOME/constitution.json` (default `~/.codewhale/constitution.json`)\n and renders it into a separate `<codewhale_user_constitution>` prose block.\n This can express preferences and stop conditions, but it does not change\n runtime approval policy, sandbox, shell, network, trust, or MCP permissions.\n- **Repo-local constitution** — optional project policy in\n `.codewhale/constitution.json`, described below.\n- **`AGENTS.md`** — cross-agent **project instructions** (prose). This is the\n canonical file for \"how should an agent work in this repo.\" Run `/init` to\n scaffold one. Instruction files written for other tools (`CLAUDE.md`,\n `.claude/instructions.md`, `.cursorrules`, `.github/copilot-instructions.md`\n and similar) are read only when you opt in with the top-level\n `project_instruction_imports` key, for example `[\"claude\"]`, `[\"cursor\"]` or\n `[\"all\"]` (see `config.example.toml`); Codewhale warns when it finds one it\n did not load.\n\n Codewhale reads one instruction file per directory, from the repository\n root down to the workspace, and puts your global instruction file (such as\n `~/.codewhale/AGENTS.md`) in front of them. The nearest scope reads last\n and wins: the workspace file overrides its parents, and project files\n override the global file. Global and project instructions are budgeted\n separately in estimated tokens (see [Instruction budget](#instruction-budget-instruction_budget-6526)\n below): a scope over its budget drops whole files from the broadest end\n first, and a single file larger than the remaining budget keeps its head\n with a marker that names the bytes and tokens dropped. A file keeps at most\n its first 100 KiB, and a file larger than `file_max_bytes` (8 MiB, lowerable\n only) is skipped with a notice. Every loss — trimmed, skipped, or refused —\n reaches the model and `/context report`.\n- **Memory and handoffs** — recalled state. Useful, but lower authority than\n constitutions and project instructions.\n\n### Managing the user-global constitution (`/setup` and `/constitution`)\n\nThe bundled **working agreement** is the safe default and no longer adds a\nrequired first-run screen. Customize it later through `/constitution` or the\nprogressive `/setup` guide. Provider/model readiness, workspace trust, and\nruntime posture stay separate from this guidance.\n\nOn the **Working agreement** (constitution) step:\n\n- **`1`–`6`** tune the guided draft. **`G`** previews it, and **`G`** again\n ratifies and saves a fresh structured `constitution.json`.\n- **`A`** (shown only when a provider is configured) asks your first configured\n model to draft the constitution. Drafting is **not** saving: the draft is\n rendered through the same preview and you still press **`G`** to ratify\n before anything persists.\n- **`K`** keeps your existing loaded constitution unchanged (shown only when a\n valid file is already present).\n- **`U`** (or `/constitution bundled`) records the bundled/default law.\n\n`/constitution` (alias `/law`) is the primary management surface once you are\nset up. Subcommands: `status` (the default), `preview`, `review`, `repo` (the\nrepo-local law block), `explain`, `base`, `suggestions`, `ratify <id>`,\n`migrate`, `rollback`, `edit`/`guided`, `repair`, `posture`, and `bundled`.\nManaging the constitution never changes runtime approval, sandbox,\nshell, network, trust, default mode, or MCP authority — those stay in runtime\nposture/config.\n\nEach repo can carry two distinct, complementary files:\n\n- **`AGENTS.md`** — ordinary project working instructions.\n- **`.codewhale/constitution.json`** — Codewhale-specific **repo authority /\n prioritization policy**: when local sources conflict, which should Codewhale\n trust first, and what to verify before claiming a task is done. `.codewhale/`\n lives inside the repo (like `.github/`). Example:\n\n ```json\n {\n \"schema_version\": 1,\n \"authority\": [\n \"current user request\",\n \"live code and tests\",\n \"GitHub issue/PR details\",\n \"AGENTS.md\",\n \"memory\",\n \"old handoffs\"\n ],\n \"protected_invariants\": [\n \"do not break old-session transcript replay\"\n ],\n \"branch_policy\": \"PRs target the integration branch, not main\",\n \"verification_policy\": {\n \"before_claiming_done\": [\"run focused tests\", \"read changed files back\"]\n },\n \"escalate_when\": [\n \"a destructive action was not explicitly authorized\"\n ]\n }\n ```\n\n All fields are optional. When present, the file is rendered into the system\n prompt as concise prose in a higher-authority block. Legacy `WHALE.md` files\n are ignored and reported as migration-only diagnostics.\n\n Each `protected_invariants` entry may be either a plain string (advisory\n prose, the historical shape) or an object carrying path globs, which is\n additionally **mechanically enforced** in the tool gate. See\n [Enforced repo-law invariants](#enforced-repo-law-invariants) below.\n\n This is the **repo-local law** layer. When guidance conflicts, the\n **Whose word wins** section of the effective base constitution owns the\n ordering; the bundled source is\n [`BASE_PROMPT`](../crates/engine/src/prompts/text.rs). Use `/constitution base`\n to inspect the effective base, including any opted-in expert override.\n The order in which these files are described or assembled is not an\n authority ranking. Runtime policy (permissions, sandbox and cost limits\n enforced in code) is separate from prompt guidance; editing a constitution\n does not grant permissions.\n\n> **`WHALE.md` is deprecated.** It overlapped confusingly with `AGENTS.md`.\n> Codewhale no longer reads `WHALE.md` as project or global context. If one is\n> present, setup/context diagnostics report it as ignored so you can migrate it.\n> Move ordinary instructions to `AGENTS.md` and Codewhale-specific authority\n> policy to `.codewhale/constitution.json`. Personal standing guidance belongs\n> in `/constitution` / `$CODEWHALE_HOME/constitution.json`. (The global\n> Codewhale Constitution shipped in the model prompt is a separate thing and is\n> unaffected.)\n\n### Enforced repo-law invariants\n\nBy default a `protected_invariants` entry is advisory prose: it is rendered into\nthe prompt as guidance the agent should honor, but nothing stops a write. An\nentry written as an **object with `paths`** is different — it compiles into a\nmechanical write hold that the engine's tool gate evaluates before the write\nruns. The law becomes mechanism, not just a request.\n\nAn enforced entry has this shape:\n\n```json\n{\n \"schema_version\": 1,\n \"protected_invariants\": [\n \"Keep DeepSeek support first-class.\",\n {\n \"text\": \"The wire format is frozen; protocol changes need a human.\",\n \"paths\": [\"crates/protocol/**\"],\n \"action\": \"block\"\n },\n {\n \"text\": \"Release notes need human review.\",\n \"paths\": [\"CHANGELOG.md\"],\n \"action\": \"ask\"\n }\n ]\n}\n```\n\n- `text` — required. The reason surfaced on the hold. An empty `text` is skipped.\n- `paths` — workspace-relative globs (globset syntax, e.g. `crates/protocol/**`,\n `**/secrets.toml`, `CHANGELOG.md`). An object with no usable `paths` stays\n advisory-only despite the object shape.\n- `action` — optional, defaults to `ask`. `ask` force-prompts in Ask and\n Auto-Review; in Full Access it denies the protected write without opening a\n modal. `block` **denies the write outright** in every posture.\n\nSemantics:\n\n- **Tighten-only.** The schema has no allow/widen shape, so law can only *add*\n holds — a crafted constitution can never grant authority or weaken a gate\n above it.\n- **Not bypassable by mode.** Like the built-in safety floor, an `ask` hold\n force-prompts in Ask and Auto-Review. Full Access never opens approval\n modals, so the same hold fails closed as a hard block; `block` always denies.\n Mode cannot turn a hold off.\n- **Repo-local only.** Only the repo's `.codewhale/constitution.json`\n participates. The user-global constitution stays advisory prose and never\n reaches this mechanism.\n- **Fails safe.** A missing file, parse error, or invalid glob degrades to\n fewer or zero rules — never a hold on unprotected paths and never a poisoned\n gate. Across matches the strongest action wins, so `block` outranks `ask`.\n- **Leaves a receipt.** Every hold emits a `tool.repo_law_decision` tool-audit\n event naming the invariant, the matched path, and the source file; the\n approval/denial reason names the invariant too.\n\n**Coverage is deliberately limited.** Holds are evaluated only for the write\ntools `write_file`, `edit_file`, `apply_patch`, and `fim_edit`, and only\nagainst the filesystem targets named in their inputs (`path`/`target`/\n`destination`/`file_path`, `changes[].path`, and unified-diff /\n`apply_patch`-envelope headers). A shell command that writes a protected path is **not** held by\nrepo law — those writes are still governed by the ordinary approval, sandbox,\nand shell-write gates, not by this mechanism.\n\n### Expert full base-prompt override (#3638)\n\nThe global Constitution (the base system prompt, normally compiled in from\n`crates/engine/src/prompts/text.rs` as `BASE_PROMPT`) can be replaced per-user\nwithout rebuilding. This is\nan expert escape hatch, not the normal `/constitution` guided setup output.\nBecause this is a prompt trust boundary, it takes **two deliberate steps** — a\nfile alone is not enough:\n\n1. Drop the replacement at `~/.codewhale/prompts/constitution.md` (under\n `$CODEWHALE_HOME` when set).\n2. Set the explicit opt-in flag `CODEWHALE_ALLOW_BASE_PROMPT_OVERRIDE=1`\n (`true`/`on`/`yes` also accepted).\n\nIf the file exists but the flag is unset, the override is **ignored** (with a\nlog line pointing to the flag) and the bundled Constitution stays in place.\nThis is intended for repurposing the TUI beyond software engineering — e.g.\nlong-form writing or document review — where the engineering-oriented base\nprompt is a poor fit. It is loaded once at startup; a **missing or empty file\nis a no-op**, so existing installs keep the bundled prompt.\n\nScope is deliberately narrow: only the byte-stable **base prompt segment** is\noverridable. Mode deltas, the approval policy, the tool taxonomy, Context\nManagement, and the Compaction Relay are still owned by Codewhale's runtime\nassembly, so an override **cannot remove safety-relevant guidance** (sandbox,\napprovals) — it only swaps the task/voice framing. To customize ordinary\npersonal behavior, prefer `/constitution`; to customize per-repo behavior,\nprefer `AGENTS.md` + `.codewhale/constitution.json` above.\n\n## Where It Looks\n\nDefault config path:\n\n- `~/.codewhale/config.toml`\n- Legacy fallback: `~/.deepseek/config.toml`\n\nOverrides:\n\n- CLI: `codewhale --config /path/to/config.toml`\n- Env: `CODEWHALE_CONFIG_PATH=/path/to/config.toml`\n- Legacy env alias: `DEEPSEEK_CONFIG_PATH=/path/to/config.toml`\n\nIf both are set, `--config` wins. Environment variable overrides are applied after the file is loaded.\n\n### TUI editability audit\n\nInside the TUI, run `/config audit` to see which documented keys can be changed\nfrom the current session, which ones can also be persisted, and which ones stay\nfile-only or restart-only. The audit includes current values for the high-impact\nruntime controls such as `approval_policy`, `allow_shell`,\n`stream_chunk_timeout_secs`, `base_url`, `mcp_config_path`, and the\n`[subagents]` concurrency/depth/timeout keys.\n\nUse the command's \"Command / reason\" column as the source of truth before\nediting by hand. For example, `/config approval_mode on-request --save` writes\ntop-level `approval_policy = \"on-request\"`, while provider base URLs are saved\nbut still require restarting the model client.\n\n### User workspace entries\n\nInteractive Agent sessions expose shell tools by default with approval gating\nunless you explicitly disable them. For a shell opt-in that should live in the\nuser's global config for noninteractive or durable-task profiles rather than in\nthe repository, add a workspace-scoped entry:\n\n```toml\n[workspace.'/absolute/path/to/project']\nallow_shell = true\n```\n\nThe entry applies only when the launched workspace path matches the table key.\nThe legacy `[projects.\"/absolute/path/to/project\"]` table is also accepted for\nthis user-owned override.\n\nIn interactive mode, the per-project overlay\n`<workspace>/.codewhale/config.toml` is applied after this user entry. A\nproject-level `allow_shell = false` can still tighten the session; project-level\n`allow_shell = true` is ignored.\n\n### Per-project overlay (#485)\n\nWhen the TUI starts in a workspace that contains a regular-file\n`<workspace>/.codewhale/config.toml`, the safe values declared in that file are\nmerged on top of the global config. Legacy\n`<workspace>/.deepseek/config.toml` files are still read when the Codewhale path\nis absent. Symlinked project config files are rejected. This lets a repo suggest\na model or tighten local safety posture without touching the user's\n`~/.codewhale/config.toml`. Pass `--no-project-config` to skip the overlay for\none launch.\n\nSupported keys in the project overlay (top-level fields only):\n\n| Key | Effect |\n|---|---|\n| `model` | override `default_text_model` |\n| `reasoning_effort` | force `\"high\"` / `\"max\"` for a complex repo |\n| `approval_policy` | only values that tighten the user's current permission posture |\n| `sandbox_mode` | only values that tighten the user's current sandbox posture |\n| `max_subagents` | clamp sub-agent concurrency for a constrained repo (clamped to 1..=128) |\n| `allow_shell` | `false` can disable shell access; `true` is ignored |\n\nThe overlay is intentionally narrow — it covers the fields a repo\nmaintainer is most likely to want to standardize across contributors.\nCredential, endpoint, provider-selection, MCP config, hooks, skills,\nretry, hotbar bindings, and `instructions = [...]` settings stay user-global.\nIf a repo-local config declares `api_key`, `base_url`, `providers`, `provider`,\n`mcp_config_path`, `notes_path`, `hotbar`, `allow_shell = true`, or `instructions`,\nCodewhale ignores that key and keeps the user's global setting.\n\nThe consolidated `codewhale` runtime uses `~/.codewhale/config.toml` for\nprovider and model settings. `codewhale auth set --provider deepseek` saves\nthe key in the local secret store (a private file under `~/.codewhale/secrets/`\nby default; the OS keychain is an explicit option), rather than writing it\ninto that config file. Legacy `~/.deepseek/config.toml` is migrated on first\nlaunch when needed. `codewhale --model deepseek-v4-flash` is forwarded to the\nTUI as `CODEWHALE_MODEL`. The dispatcher no longer writes `DEEPSEEK_*` twins;\na value you set yourself remains a legacy alias when its `CODEWHALE_*`\nequivalent is unset.\n\n### Account provider keys\n\n`/connect` offers Continue with Codewhale or Connect on this computer. The\naccount path signs in and lists usable model connections reported by the\naccount service. When none are ready, it offers retry or on-computer setup.\nSign-in alone does not supply model access or change the selected model.\nLocal provider keys and local models never need an account; use `/provider`\nor `codewhale auth set --provider <provider>` to manage them.\n\nFor an account that supports saved provider keys, these commands manage them:\n\n```bash\ncodewhale account keys list # configured status, never secret values\ncodewhale account keys remove deepseek # remove the account's saved key\n```\n\nLogin never uploads local keys, and removing an account key does not change\nlocal copies or revoke the key at the provider.\n\nThe account session is separate from provider keys. `/login` signs in inside\nthe TUI; `codewhale login` does so from the shell. `codewhale account status`\ninspects it. `/logout`, `codewhale logout` and `codewhale account logout` remove\nonly the account session for the selected `--profile`. The explicit\n`codewhale logout --all` action also clears saved model credentials.\nAccount sessions use the private `0600`\nCodewhale secrets file, scoped to the profile and account API origin; they do\nnot use the OS keychain. The older `codewhale cloud ...` spelling remains an\nalias for account commands.\n\n### Portable config bundles\n\n`codewhale config export --portable [--project] [--out FILE]` writes a\nportable, secret-free bundle of your configuration: sorted TOML with\ncredential and machine-specific keys (API keys, base URLs, socket paths)\ndropped, never a redacted placeholder in their place. Without `--out` the\nbundle goes to stdout. Typed tables, arrays, numbers, booleans, and datetimes\nremain typed. Machine-bound authority is deliberately non-portable: project\ntrust overlays, credential readers, auto-running hooks, executable LSP\ndefinitions, and local path bindings are omitted rather than copied to a new\nhost. So is trust posture (`yolo`, `allow_shell`, `approval_policy`,\n`sandbox_mode`, `sandbox_network_access`, and the sandbox read-denylist\npaths) and local endpoints or executables (`[lifecycle_outbox]`,\n`[extension_host]`, `[control_socket]`). URLs that carry a credential —\nuserinfo, a token-named query parameter, or a Slack/Discord/Teams webhook\npath — are treated as credentials.\n\n`codewhale config import <FILE|HTTPS_URL|-> [--dry-run] [--yes] [--project]`\napplies a bundle. The envelope is strict (`schema_version = 1`, kind\n`codewhale.portable-config`; unknown fields fail). Import prints a\ndeterministic plan — added / changed / skipped / conflicting / rejected —\nthen asks for consent unless `--yes` is given; headless use requires it.\nCredential-shaped entries are rejected by key name and by value shape;\nrejections name the field, never a value. Remote bundles come from HTTPS\nonly (loopback http excepted) with a 5 MiB cap. Application backs up the\ntarget document to `<config>.bundle-backup-<timestamp>-<random>`, rolls back on any\nfailure, and re-importing an applied bundle changes nothing.\n\nImport also rejects the non-portable authority classes omitted by export,\nincluding nested/camel/dotted credential keys and cookie headers. This keeps a\nhand-authored or remote bundle from reintroducing machine trust, local\ncredential access, or automatically executable commands that a local export\nwould refuse to carry. Structured tables are deep-merged: a portable model or\npreference update does not erase target-local provider credentials, endpoints,\nhooks, or executable definitions that were deliberately omitted from the\nbundle. Arrays and scalar values still replace the corresponding portable\nvalue.\n\nSections map to scope: `[project]` entries land only in the workspace\ndocument (`--project`, which must target an actual workspace config),\n`[global]` only in the user-global one; `preferences`, `profiles`, and\n`plugins` apply at either scope. A global bundle operation refuses a workspace\ndocument just as a project operation refuses the user-global document.\n\n### Credential read precedence (#5197)\n\nCredential reads are **folder-independent by default**: every layer below is\nuser-global or process-scoped, so a key saved in one repo resolves identically\nin every other repo. Repo-local config never carries credential material — the\nproject overlay above reads only its allowlisted keys and ignores `api_key`,\nand a credential write aimed at a workspace-scoped config is rescoped to the\nuser-global `~/.codewhale/config.toml` (#5045, #5193).\n\nFor the active provider, the runtime resolves the API key in this exact order\n(first match wins):\n\n1. **Route-specific auth contract.** Routes whose `auth_mode` disables API\n keys stop here with no credential. OAuth routes use their explicitly\n granted token: the official `openai-codex` plan route accepts only\n Codewhale-owned credentials from `codewhale auth chatgpt`, bound to its\n issued registration and verified account. Imported Codex CLI and process\n tokens cannot authorize this route;\n `[providers.xai] auth_mode = \"oauth\"` reads Codewhale's own xAI\n device-login store (or a consent-granted Grok CLI file).\n2. **Explicit CLI key.** `--api-key` forwarded with its source marker wins\n over every saved slot.\n3. **Config file `api_key`.** The `[providers.<name>] api_key` table slot for\n the active provider. `deepseek-CN` also reads `[providers.deepseek]`. An\n older top-level `api_key` is read as `[providers.deepseek] api_key` (see\n [Legacy top-level `base_url` and `api_key`](#legacy-top-level-base_url-and-api_key)).\n File-owned keys stay bound to their file-owned endpoint: when the\n environment replaces the route's base URL with a custom host, the saved\n key is not sent there.\n4. **`api_key_env` binding.** `[providers.<name>] api_key_env = \"VAR\"` reads\n the named environment variable. For custom providers an unset or empty\n binding is a loud error, not a silent fallback (#5104).\n5. **Secret store.** The durable per-provider slot written by\n `codewhale auth set` (file-backed under `~/.codewhale/secrets/` by\n default; the OS keyring only when explicitly selected). Skipped for named\n custom routes, self-hosted providers, custom endpoints other than an\n explicitly authenticated loopback, and routes whose `auth_mode` needs no\n key.\n6. **Ambient environment.** The provider's own variable\n (`DEEPSEEK_API_KEY`, `OPENROUTER_API_KEY`, `MOONSHOT_API_KEY`, …).\n Ambient keys are only ever sent to the provider's official endpoint and\n are skipped under the same conditions as the secret store.\n7. **Keyless fallback.** Self-hosted providers and loopback endpoints may run\n with no credential; every other route fails with provider-specific setup\n guidance.\n\nLegacy compatibility: `~/.deepseek/config.toml` is migrated into\n`~/.codewhale/config.toml` on first launch, `DEEPSEEK_*` environment\nvariables remain accepted aliases for the `CODEWHALE_*` forms, and\n`DEEPSEEK_SECRET_BACKEND` is the legacy alias for `CODEWHALE_SECRET_BACKEND`.\n\nRun `codewhale auth status` to inspect the active provider's config\nfile, OS keyring backend, environment variable, winning source, and last-four\nlabel without printing the key itself. The command only probes the active\nprovider's keyring entry.\n\nFor hosted, generic OpenAI-compatible, self-hosted, OpenAI Responses, or native\nAnthropic providers, set `provider = \"<id>\"` or pass\n`codewhale --provider <id>`. The canonical provider IDs are `deepseek`,\n`nvidia-nim`, `openai`, `atlascloud`, `wanjie-ark`, `volcengine`,\n`openrouter`, `orcarouter`, `xiaomi-mimo`, `novita`, `fireworks`,\n`siliconflow`, `arcee`, `siliconflow-CN`, `moonshot`, `sglang`, `vllm`,\n`ollama`, `ollama-cloud`, `huggingface`, `modelscope`, `together`, `qianfan`, `openai-codex`,\n`anthropic`, `openmodel`, `zai`, `stepfun`, `minimax`, `deepinfra`,\n`sakana`, `longcat`, `opencode-go`, `opencode-zen`, `meta`, `xai`,\n`mistral`, `telecomjs`, `modelstudio-token-plan`, `google`,\n`edenai`, `concentrate`, `codewhale`, and `custom` (a user-defined OpenAI-compatible endpoint via\n`[providers.<name>]`).\nFor the provider-by-provider registry, including wire protocol, auth variables,\ndefault base URLs, model IDs, and capability metadata, see\n[PROVIDERS.md](PROVIDERS.md).\nThe facade saves provider credentials in the local secret store and forwards\nthe resolved key, base URL, provider, and model to the TUI process. Use\n`codewhale auth set --provider nvidia-nim --api-key \"YOUR_NVIDIA_API_KEY\"` or\n`codewhale auth set --provider openai --api-key \"YOUR_OPENAI_COMPATIBLE_API_KEY\"` or\n`codewhale auth set --provider atlascloud --api-key \"YOUR_ATLASCLOUD_API_KEY\"` or\n`codewhale auth set --provider wanjie-ark --api-key \"YOUR_WANJIE_API_KEY\"` or\n`codewhale auth set --provider xiaomi-mimo --api-key \"YOUR_XIAOMI_KEY\"` or\n`codewhale auth set --provider fireworks --api-key \"YOUR_FIREWORKS_API_KEY\"` or\n`codewhale auth set --provider siliconflow --api-key \"YOUR_SILICONFLOW_API_KEY\"` or\n`codewhale auth set --provider arcee --api-key \"YOUR_ARCEE_API_KEY\"` or the\nmatching provider ID from [PROVIDERS.md](PROVIDERS.md) to save provider keys\nthrough the facade. The generic `openai` provider defaults\nto `https://api.openai.com/v1`, accepts `OPENAI_BASE_URL`, and defaults to\n`gpt-5.6`. A custom OpenAI-compatible gateway can still select its own model\nexplicitly. `atlascloud` defaults to\n`https://api.atlascloud.ai/v1`, accepts `ATLASCLOUD_BASE_URL`, and uses\n`deepseek-ai/deepseek-v4-flash` as its default model. `wanjie-ark` targets\nWanjie Ark's OpenAI-compatible endpoint at\n`https://maas-openapi.wanjiedata.com/api/v1`, defaults to `deepseek-reasoner`,\nand passes model IDs through unchanged because Wanjie model access is\naccount-scoped. SGLang, vLLM, and Ollama are\nself-hosted and can run without an API key by default. Ollama defaults to\n`http://localhost:11434/v1` and sends model tags such as `codewhale-coder:1.3b`\nor `qwen2.5-coder:7b` unchanged. Self-hosted providers and loopback custom\nURLs (`localhost`, `127.0.0.1`, `[::1]`, `0.0.0.0`) do not read the secret store\nunless API-key auth is explicitly requested; use an env var or config-file key\nwhen a local server does require bearer auth.\nOllama Cloud is the separate hosted `ollama-cloud` provider. It defaults to\n`https://ollama.com/v1` and `gpt-oss:120b`; save its key with\n`codewhale auth set --provider ollama-cloud`. Ambient auth reads\n`OLLAMA_CLOUD_API_KEY` first, then Ollama's official `OLLAMA_API_KEY`.\nSiliconFlow defaults to `https://api.siliconflow.com/v1`, accepts\n`SILICONFLOW_BASE_URL`, and uses `deepseek-ai/DeepSeek-V4-Pro` by default.\n`provider = \"siliconflow-CN\"` selects the China regional default\n`https://api.siliconflow.cn/v1` with the `[providers.siliconflow_cn]` table and\n`SILICONFLOW_API_KEY` credential slot.\nArcee AI defaults to `https://api.arcee.ai/api/v1`, accepts `ARCEE_BASE_URL`,\nand uses `trinity-large-thinking` by default for Codewhale agent work.\n`trinity-large-preview` is also listed as a direct Arcee API model; OpenRouter's\n`arcee-ai/trinity-large-thinking` remains the OpenRouter namespaced form, while\nthe direct Arcee provider uses the bare `trinity-large-thinking` ID. Direct\nArcee large-model API calls are tracked as 256K-context BF16 serving; Thinking\nis reasoning-capable, while Preview is not marked as a thinking model.\n\n### OpenRouter vendor pinning\n\nOpenRouter serves each model through several upstream vendors, and Codewhale\ncan pin requests to a vendor with `[providers.openrouter] vendor` (#6007):\n\n```toml\nprovider = \"openrouter\"\n[providers.openrouter]\nmodel = \"deepseek/deepseek-v4-pro\"\nvendor = \"deepinfra\" # copy the vendor slug from the model's OpenRouter page\n```\n\nThis sends `\"provider\": {\"order\": [\"deepinfra\"], \"allow_fallbacks\": false}`\non OpenRouter requests. A base slug can match multiple endpoint variants;\ncopy a full slug such as `deepinfra/turbo` to select one variant. An unavailable\npin fails at OpenRouter. Codewhale's separate `fallback_providers` setting can\nstill switch the whole route after a recoverable error.\n\nThe pin applies across OpenRouter models, including auxiliary requests on that\nroute. Set `vendor = \"\"` to clear it. Reload config or restart to apply edits;\nrequests already in flight keep their captured route. Other providers do not\ninherit the pin. `/preview-request` shows the primary request's routing fields.\n\nModel strings still pass through verbatim: `:floor` sorts by price, `:nitro`\nsorts by throughput, and `@preset/my-team-preset` references an account preset.\nSee OpenRouter's [provider routing](https://openrouter.ai/docs/guides/routing/provider-selection)\nand [presets](https://openrouter.ai/docs/guides/features/presets) documentation.\nCodewhale does not fetch per-vendor endpoint prices or availability; pinned\nusage reports a routing-dependent unknown cost instead of a catalog estimate.\n\n### Custom OpenAI-Compatible Gateways\n\nFor a single third-party service that implements the OpenAI Chat Completions\nAPI, the simplest setup is the built-in `openai` provider name pointed at the\ngateway:\n\n```toml\nprovider = \"openai\"\ndefault_text_model = \"your-model-id\"\n\n[providers.openai]\napi_key = \"YOUR_OPENAI_COMPATIBLE_API_KEY\"\nbase_url = \"https://your-gateway.example/v1\"\n```\n\nPut the endpoint under `[providers.openai]`, not the legacy top-level\n`base_url`, so the OpenAI-compatible provider receives it. `default_text_model`\nis the model ID sent to the gateway; `[providers.openai].model` can be used as\nthe OpenAI-provider-specific override.\n\nIf you keep several OpenAI-compatible gateways, or need a stable name for an\nAgentProfile provider pin, define a user-named custom provider table:\n\n```toml\nprovider = \"lm-studio\"\n\n[providers.lm-studio]\nkind = \"openai-compatible\"\nbase_url = \"http://127.0.0.1:1234/v1\"\napi_key = \"lm-studio\"\nmodel = \"qwen-2.5-7b\"\n```\n\nCustom provider names may be selected with `provider = \"<name>\"`,\n`--provider <name>`, or an AgentProfile `provider = \"<name>\"` when the matching\n`[providers.<name>]` table exists.\n\nA custom table sends Chat Completions requests by default. Add\n`wire = \"responses\"` to send OpenAI Responses requests to\n`<base_url>/responses` instead, or `wire = \"anthropic\"` for Anthropic Messages:\n\n```toml\n[providers.my-gateway]\nkind = \"openai-compatible\"\nbase_url = \"https://gateway.example/v1\"\napi_key_env = \"MY_GATEWAY_API_KEY\"\nmodel = \"gpt-5.6\"\nwire = \"responses\"\n```\n\nStepFun has a first-class provider entry, so keep Coding Plan credentials and\nbase URL scoped to `[providers.stepfun]`:\n\n```toml\nprovider = \"stepfun\"\n\n[providers.stepfun]\napi_key = \"YOUR_STEPFUN_API_KEY\"\nbase_url = \"https://api.stepfun.ai/step_plan/v1\"\nmodel = \"step-3.7-flash\"\n```\n\n`/provider` setup asks which StepFun billing route the key belongs to —\npay-as-you-go (`https://api.stepfun.ai/v1`) or a Step Plan subscription\n(`https://api.stepfun.ai/step_plan/v1`) — and validates the key against the\nendpoint you pick before saving it. The answer is written to\n`[providers.stepfun].base_url` and nowhere else. If that key already holds a\nbase URL Codewhale does not recognize as one of those two routes, the question\nis skipped and your value is left untouched.\n\nAlibaba Bailian / Model Studio DashScope Qwen routes use the same OpenAI\nprovider shape:\n\n```toml\nprovider = \"openai\"\n\n[providers.openai]\napi_key = \"YOUR_DASHSCOPE_API_KEY\"\nbase_url = \"https://dashscope-intl.aliyuncs.com/compatible-mode/v1\"\nmodel = \"qwen-plus\"\ncontext_window = 1000000\n```\n\nUse the regional DashScope `compatible-mode/v1` base URL that matches the\nregion of your API key. Codewhale keeps `qwen-plus` scoped to the `openai`\nprovider route and does not infer a different provider from the model prefix.\nThe same rule applies to all provider-prefixed model strings: a prefix such as\n`deepseek-ai/...` or `deepseek/...` is a provider-owned wire ID under the\nselected provider, not an automatic switch to the DeepSeek provider.\nSet `context_window` to the gateway/model's real total context window when it\ndiffers from Codewhale's static model metadata. See\n[Context length (context window)](#context-length-context-window) for the full\nresolution order and for how to check which value is in effect.\n\nIf the gateway accepts `POST /chat/completions` but rejects\n`/v1/chat/completions`, set a provider-local `path_suffix`:\n\n```toml\n[providers.openai]\nbase_url = \"https://your-gateway.example/v1\"\npath_suffix = \"/chat/completions\"\n```\n\nThe suffix applies only to chat-completion requests. Model listing and\nDeepSeek beta paths keep their built-in routing so a generic gateway override\ndoes not accidentally rewrite `/models` or `/beta/completions`.\n\nFor private gateways with broken or intercepted certificates, use\n`SSL_CERT_FILE` with a trusted CA bundle. The legacy provider-table key\n`insecure_skip_tls_verify = true` is still parsed so `codewhale doctor` can\nreport stale configs, but provider clients reject it instead of disabling TLS\ncertificate verification.\n\nLocal HTTP endpoints such as Ollama, SGLang, and vLLM are allowed by default\nwhen they use localhost or loopback addresses. For a non-local `http://`\ngateway, launch with `DEEPSEEK_ALLOW_INSECURE_HTTP=1` only on a trusted network:\n\n```bash\nDEEPSEEK_ALLOW_INSECURE_HTTP=1 codewhale\n```\n\nThird-party OpenAI-compatible gateways that need extra request headers can set\n`http_headers = { \"X-Model-Provider-Id\" = \"your-model-provider\" }` at the top\nlevel or under a provider table such as `[providers.deepseek]`. When configured,\ncodewhale sends those custom headers on model API requests. The equivalent\nenvironment override is `DEEPSEEK_HTTP_HEADERS`, using comma-separated\n`name=value` pairs such as\n`X-Model-Provider-Id=your-model-provider,X-Gateway-Route=dev`. `Authorization`\nand `Content-Type` are managed by the client and are not overridden by this\nsetting.\n\n### Vision Model\n\nCodewhale's chat provider and `image_analyze` tool are configured separately.\nThe main chat path remains the selected text/tool provider; image analysis runs\nthrough `[vision_model]` when the `vision_model` feature is enabled.\n\nXiaomi's current image-understanding docs include `mimo-v2.5` for image input.\nTo use MiMo for `image_analyze`, configure the vision model explicitly:\n\n```toml\n[features]\nvision_model = true\n\n[vision_model]\nmodel = \"mimo-v2.5\"\napi_key = \"YOUR_XIAOMI_KEY\"\nbase_url = \"https://api.xiaomimimo.com/v1\"\n```\n\nThe example above uses Xiaomi MiMo's pay-as-you-go OpenAI-compatible endpoint.\nIf you are using a Token Plan key (`tp-...`) for `[vision_model]`, you must set\n`base_url` explicitly because this generic OpenAI-compatible block does not\nauto-select MiMo endpoints. Use\n`https://token-plan-sgp.xiaomimimo.com/v1` for Singapore accounts,\n`https://token-plan-cn.xiaomimimo.com/v1` for China-region accounts, or\n`https://token-plan-ams.xiaomimimo.com/v1` for Europe/Amsterdam accounts.\n\n### Auto Model Routing (`[auto]`, `[auto.router]`)\n\nWith `model = \"auto\"`, each turn runs on your **declared default model** unless\nyou have opted into something else. Auto never guesses a cheaper or stronger\nmodel from how a request is worded; the old keyword-and-length heuristic was\nremoved (`auto_route_declared_fallback` in `crates/engine/src/model_routing.rs`).\nTwo optional layers change that:\n\n- an `[auto.router]` classifier, which you write down, that picks a model per\n turn; and\n- `[auto] cost_saving`, which prefers the active provider's fast sibling.\n\nWith neither set, Auto is local and free: the turn uses the default model and\nno classifier call is made.\n\n**There is no default classifier.** With `[auto.router]` unset, no classifier\ncall happens, whatever keys you hold. Holding a DeepSeek key used to elect\n`deepseek-v4-flash` automatically. That was removed because it spent tokens on\na route the user never chose and privileged one provider over the rest\n(`AutoRouterConfig` in `crates/tui/src/config.rs`). Electing a network\nclassifier is now something you write down.\n\nPoint the classifier at any configured provider with `[auto.router]`:\n\n```toml\n[auto.router]\nprovider = \"zai\"\nmodel = \"glm-5-turbo\"\nthinking = \"off\" # optional; defaults to off\ntimeout_secs = 4 # optional; default 4, 0 = default, capped at 300\n```\n\nA classifier call happens only when `[auto.router]` names both `provider` and\n`model` *and* that provider has a key:\n`router_available = router_configured && has_api_key_for(...)` in\n`ModelInventory::from_config` (`crates/engine/src/model_inventory.rs`). If either\ncondition fails, or the classifier call errors or times out, the local\nfallback decides: the default model, or the fast sibling under `cost_saving`.\nThe turn's route receipt (Turn Inspector, Ctrl+Alt+O or `/turn inspect`, \"Model\nroute + tokens/cost\") records which path was taken, and a\nrouter you configured that cannot run or fails (missing key, HTTP error,\ntimeout, invalid answer) is shown as `Auto router: failing — …` rather than\nsilently ignored.\n\n#### Set up model routing\n\n`/router` (also `/model router`) opens one Router setup view with these\npresets. Each writes only `[auto.router]`; none is ever chosen for you.\n\n| Preset | What it writes | Cost and privacy |\n| --- | --- | --- |\n| `/router jev` | Jev, TypeSafe's decision model, over OpenRouter (`typesafe/jev-1.13`) or TypeSafe direct, whichever key you have | About $0.00002 per turn ($0.042 per million input tokens, output free). Your latest request and up to six recent context lines go to OpenRouter → TypeSafe (or TypeSafe). |\n| `/router fast` | The active provider's runnable fast tier with thinking off | Your existing key; the classifier sees the same request text. |\n| `/router off` | Removes `[auto.router]` | No router call; Auto turns use the default model, or the fast tier while `[auto] cost_saving = true` (Off leaves that setting alone). |\n| `/router custom` | Nothing; prints the TOML to edit | — |\n\nChoosing Jev or Fast makes **one test call** with a fixed sample request and\nshows the tier it picked, the probabilities and confidence, the latency and the\nprovider-reported cost. `Enter` (or `/router save <preset>`) then saves through\nthe normal config writer; `Esc` discards it. TypeSafe paused new signups on\n2026-09-22, so OpenRouter is the default route for new users. A TypeSafe key is\nread from `TYPESAFE_API_KEY`, the `typesafe` secret-store entry, or\n`[providers.typesafe] api_key` / `api_key_env`.\n\n#### Decision routers (`kind = \"decision\"`)\n\nA decision router asks a non-generative decision model typed Choice questions\nfor the active provider's `fast` and `strong` tiers and thinking level, then\nreads calibrated probabilities. No prose is parsed.\n\n```toml\n[auto.router]\nkind = \"decision\" # default \"chat\"\nprovider = \"openrouter\" # or \"typesafe\"\nmodel = \"typesafe/jev-1.13\" # \"~typesafe/jev-latest\" also works; TypeSafe direct: \"jev-latest\"\ntimeout_secs = 2\nmin_confidence = 0.5 # default 0.5, clamped to 0..1\n```\n\n- The router is called only when the active provider has a runnable strong/fast\n pair; otherwise there is no call and no spend.\n- An answer below `min_confidence` takes the local fallback. Under\n `[auto] cost_saving`, a `strong` answer also needs a probability of at least\n 0.75, or the turn stays on the fast tier.\n- An unknown `kind`, or a decision `provider` other than `openrouter` /\n `typesafe`, leaves the router unconfigured and shown as failing.\n- `[auto.router] thinking` is ignored for decision routers. Both routes settle tokens through\n the originating session's routed-usage ledger. TypeSafe is a named Custom route\n with unknown billing; its price is never borrowed from the active chat provider.\n Provider-reported cost remains verbatim on the decision receipt.\n- OpenRouter uses `POST /api/alpha/decisions`; TypeSafe uses `POST /v1/systemone`.\n The shared client validates Choice, Noul and Score against the offered questions\n and bounds responses to 256 KiB. Malformed answers fail open with usage retained.\n\n#### Shadow Decision Gate (experimental, off by default)\n\nThe Superfast Decision Gate asks a decision model three typed questions about\nthe latest user message — does it need a tool, can it be answered from the\nconversation, and what is its intent — and logs a conservative recommendation.\nIt is shadow-only: it never changes routing, never skips or delays the model\ncall, and fails open on any error, timeout or malformed answer. It uses the same\nSystem One client as the decision router above; there is no separate HTTP\nclient. It is configured from the environment and reads it when a turn starts:\n\n```sh\nSUPERFAST_ENABLED=1 # off unless set\nSUPERFAST_PROVIDER=typesafe # or openrouter; required when enabled\nSUPERFAST_BASE_URL=http://localhost:8000/v1 # optional TypeSafe-route base, e.g. self-hosted\nSUPERFAST_MODEL=jev-latest # default jev-latest / ~typesafe/jev-latest\nSUPERFAST_TIMEOUT_MS=150 # 1..=10000, default 150\n```\n\n- Enabling the gate never picks an endpoint by itself: without\n `SUPERFAST_PROVIDER` nothing is sent and a warning is logged.\n- The key comes from the same place the decision router reads it. The TypeSafe\n route always authenticates, so a self-hosted server that ignores auth still\n needs a placeholder `TYPESAFE_API_KEY`.\n- Only the latest user message is sent, truncated to 4,000 characters and\n redacted of configured secrets. The log (target `superfast`) carries the\n route, failure class and latency, never prompt text.\n- The gate retains the originating turn's accounting owner and cancellation.\n Its tokens settle through the shared ledger; missing usage or a cancelled/timed\n out request after dispatch creates an explicit coverage gap. Unknown TypeSafe\n pricing is recorded as unpriced rather than free.\n- Provider-reported cost survives rejected answers and late responses in bounded\n receipts on the originating turn or session. These are diagnostic evidence;\n unknown pricing never becomes an authoritative dollar total. Incomplete token\n counters record a coverage gap while preserving the reported raw evidence.\n\nThe wire contracts are documented in [TypeSafe's OpenAPI schema](https://api.typesafe.ai/openapi.json)\nand [OpenRouter's Decisions examples](https://openrouter.ai/blog/insights/what-is-jev/).\n\nThe Decision Gate concept and reference implementation are by Andrea Bruno,\nreleased under CC BY 4.0:\n[harness-superfast](https://github.com/Andrea-Bruno/harness-superfast).\n\nTwo `[auto]` keys shape routing (`AutoConfig` in `crates/tui/src/config.rs`):\n\n```toml\n[auto]\ncost_saving = false # default false\ncross_provider = false # default false\n```\n\n- **`cost_saving`** (default `false`). Without a classifier, Auto pins the\n active provider's validated fast sibling instead of the default model. A\n provider with no runnable fast sibling stays on the default. With a\n classifier, the classifier is told to prefer the fast tier for routine or\n ambiguous work and to pick the strong tier only for clearly agentic,\n multi-step, architecture, security or debugging work. Cost-saving never\n switches provider just to save money.\n- **`cross_provider`** (default `false`). Auto stays on the provider the session\n is configured to use. The classifier is only shown that provider's models,\n and the fallback never leaves it. Setting `cross_provider = true` lets the\n classifier choose among every runnable provider. There is no interactive\n toggle for `cross_provider`; it has to be set in config. A decision router\n always chooses within the active provider.\n\nTo bootstrap MCP and skills directories at their resolved paths, run `codewhale setup`.\nTo only scaffold MCP, run `codewhale mcp init`.\n\nNote: `setup`, `doctor`, `mcp`, `features`, `sessions`, `resume`/`fork`, `exec`,\n`review`, and `eval` are all available from the installed `codewhale` command.\nThe consolidated dispatcher also provides `auth`, `config`, `model`, `thread`, `sandbox`,\n`app-server`, `mcp-server`, `completions`, `login`/`logout`, `account`,\n`metrics`, `update`, `lane`, `workflow`, and `web`. Plain prompts enter the\nin-process TUI runtime. Release installers expose the same bytes as `codew`.\n\n### Startup Update Checks\n\nBy default, the TUI starts a background check for the latest stable Codewhale\nrelease and shows a short toast only when a newer release is available and the\nofficial release assets are complete. The check never blocks startup, never\nblocks a turn, and fails silently when offline.\n\nDisable the startup check entirely for air-gapped, corporate-proxy, or managed\ndesktop environments:\n\n```toml\n[update]\ncheck_for_updates = false\n```\n\n#### Automatic updates\n\n`mode` decides what happens when a newer release is found (release-binary\ninstalls only; package-manager and desktop-app copies always just notify):\n\n```toml\n[update]\nmode = \"auto\" # auto (default): download in the background, apply at next start\n # notify: tell me only; off: no checks\n```\n\n`codewhale config set update.mode notify` writes it. `CODEWHALE_UPDATE_MODE`\noverrides it, and an organization policy file overrides both; see\n[INSTALL.md](INSTALL.md#automatic-updates) for the full flow, signing, the\npolicy file and `codewhale update --rollback`.\n\n#### Throttling\n\nThe answer is cached in `~/.codewhale/update-check.json` and reused for\n`check_interval_hours` (default `6`). Only the *network request* is throttled —\nthe notice still appears on every launch while an update is outstanding. Set `0`\nto check on every launch.\n\n```toml\n[update]\ncheck_interval_hours = 6\n```\n\nA failed check keeps the last answer (so the notice stays) and retries about an\nhour later rather than on every launch, which keeps offline and rate-limited\nmachines quiet.\n\n#### Automatic suppression\n\nChecks are skipped, without contacting the network, when any of these is set to\na non-falsey value:\n\n| Variable | Why |\n| --- | --- |\n| `CODEWHALE_NO_UPDATE_CHECK` | Explicit opt-out. |\n| `NO_UPDATE_NOTIFIER` | The cross-CLI convention, honored for compatibility. |\n| `CI`, `CONTINUOUS_INTEGRATION`, `GITHUB_ACTIONS`, `GITLAB_CI`, `BUILDKITE`, `CIRCLECI`, `JENKINS_URL`, `TEAMCITY_VERSION`, `TF_BUILD` | Automated build; nobody is at the terminal. |\n\nValues of `\"\"`, `0`, `false`, `no`, and `off` do not count as set, so a\n`CI=false` export does not disable checks for ordinary users.\n\n#### Which update command is offered\n\nCodewhale never installs anything on its own — it only tells you an update\nexists. The command it names depends on how the running binary was installed,\ndetected from its path:\n\n| Install | Command offered |\n| --- | --- |\n| GitHub release binary (including Termux) | `codewhale update` |\n| npm (`node_modules` on the path) | `npm install -g codewhale@latest` |\n| Homebrew (`Cellar` / `linuxbrew` prefix) | `brew upgrade codewhale` |\n| `cargo install` (`~/.cargo/bin`) | `cargo install codewhale-cli --locked --force` |\n\nFor package-managed installs the notice also warns against `codewhale update`:\nreplacing a binary Homebrew or npm owns leaves the manager describing a version\nthat is no longer on disk, and the next upgrade silently reverts you.\n\nOverride the detection with `CODEWHALE_INSTALL_METHOD=npm|homebrew|cargo|binary`\nif you relocated the binary somewhere the path heuristics cannot read.\n\nTo redirect the startup check, set `update_uri` to an internal endpoint that\nreturns GitHub-compatible latest-release JSON. Minimal mirror metadata with a\n`tag_name` field is accepted; if `assets` are present, Codewhale requires the\nsame uploaded asset set as the official release before showing the toast.\n\n```toml\n[update]\ncheck_for_updates = true\nupdate_uri = \"https://internal.mirror.example/codewhale/releases/latest\"\n```\n\nWhen `update_uri` is not set, startup checks honor release mirror environment\nvariables such as `CODEWHALE_RELEASE_BASE_URL` before falling back to the\nofficial GitHub API endpoint. If a configured `update_uri` cannot be fetched or\nparsed and a release mirror env var is set, the TUI falls back to that mirror\ninstead of failing startup.\n\n#### What's new after an update\n\nThe first time Codewhale starts after its version goes up, it says so once and\nlinks that version's release notes on codewhale.net:\n\n```text\nUpdated to 0.10.2. Got 12 features, 31 fixes and 8 other changes.\nRelease notes: https://codewhale.net/en/changelog/0.10.2\n```\n\nThe counts come from the changelog compiled into the binary. Entries under\nAdded or New count as features; Fixed, Security and Latest fixes count as\nfixes; Contributors, Thanks, Community and Known limits or issues are not\ncounted; every other section counts as other changes. The link uses your UI\nlanguage where the site has it (`zh` for Chinese) and English otherwise.\n\nThe TUI shows the notice on the startup card and moves it into the transcript\nwhen the card leaves (at once when the session opens from `--resume`,\n`--continue` or a prompt), adding `/change <version>` for reading the notes in\nplace. The `doctor`, `models`,\n`sessions`, `receipts`, `setup`, `init`, `install`, `features`,\n`integrations`, `mcp`, `config`, `model`, `metrics`, `providers`, `account`,\n`login`, `logout` and `update` commands print it to stderr instead, when\nstdout and stderr are both terminals and no JSON output is requested. `exec`,\nservers, JSON output and non-terminal runs never show it; the next start that\ncan show it does.\n\nNothing is shown on a fresh install or after a downgrade. The version that\nlast started is recorded in `~/.codewhale/last-launch.json`. The notice reads\nonly local files, so it is independent of `check_for_updates`. To turn it off:\n\n```toml\n[update]\nshow_whats_new = false\n```\n\n## Workshop output budgets\n\nBy default an oversized tool result uses bounded spillover: a result\nunder the byte threshold stays inline, a larger one gets a head/tail\npreview plus a session artifact the model can read back. There is no\nsynthesis sub-agent and no per-call `raw = true` escape.\n\n`[workshop] large_output_threshold_tokens` and\n`[workshop.per_tool_thresholds]` (exact model-visible tool names such as\n`bash`) only take effect when the process opts in to adaptive evidence\nrouting with `CODEWHALE_ADAPTIVE_OUTPUT_ROUTING=1`; they set where a result\nstops being inline and becomes handle-only evidence.\n\nTwo optional byte ceilings (#5367) apply either way. They raise the\nmodel-visible floor and never lower it:\n\n- `read_result_max_bytes` — cap for a single `read` / `read_file`\n result. Absent keeps the compile-time defaults (100000 bytes for\n `read`, which has no line cap; 16KiB / 500 lines for `read_file`).\n For `read` this is the middle of a three-layer budget: the model's\n own per-call `max_bytes` (hard maximum 500000) raises the budget for\n one call, this setting raises the floor for the whole process, and\n either way 2MiB is the absolute ceiling. Highest wins; neither layer\n can lower a budget the other granted.\n- `tool_result_max_bytes` — cap for a generic tool result after\n spillover. Absent keeps the 12K-character compact floor (48K on\n windows ≥500K tokens). Hard cap is 2MiB.\n\n## Context length (context window)\n\nAlso called context size, context limit, max context, or window. This is the\ntotal token window Codewhale budgets against, and it drives the header/footer\ncontext percent, the auto-compaction trigger, context-pressure checks, and the\nrequest output cap. If Codewhale compacts at 128K on a model you know serves a\n1M window, this is the setting to change (#5134).\n\n**See what is in effect, and where the value came from.** Every one of these\nprints the resolved window *and* its source:\n\n- `/status` — a `Context window:` row with the percent and token counts, and a\n `Window source:` row naming the provenance and the exact key that overrides\n it.\n- `/config` → Provider — `Context window` (your override, or `(not set)`) and\n `Effective context window` (`1048576 tokens · configured`). Typing\n `context length` in the `/config` filter jumps straight to them.\n- `/context report` — `Window: 1048576 tokens (12.4% used, ...; source: configured)`.\n- `/context json` — machine-readable `context_window_tokens` and\n `context_window_source`.\n\n**Change it** with the provider-table key `context_window`:\n\n```toml\n[providers.moonshot]\ncontext_window = 1048576\n```\n\nor from the CLI:\n\n```bash\ncodewhale config set providers.moonshot.context_window 1048576\ncodewhale config unset providers.moonshot.context_window # back to automatic\n```\n\nUse the table for the provider you are actually on (`providers.openai`,\n`providers.deepseek`, `providers.moonshot`, …); `/status` names it for you. The\nvalue is a positive token count for the route's *total* window.\n\nWhen one gateway fronts models with heterogeneous windows, scope the override\nto an exact wire model id with `[providers.<name>.model_context_windows]`:\n\n```toml\n[providers.command_code]\ncontext_window = 204800\n\n[providers.command_code.model_context_windows]\n\"MiniMaxAI/MiniMax-M2.5\" = 204800\n\"google/gemini-3.1-flash-lite\" = 1000000\n```\n\nKeys are the exact wire model ids the route sends (dotted and `org/model`\nspellings both work as TOML keys when quoted); each value must be a positive\ntoken count. A matching entry beats the provider-level `context_window` for\nthat model only — every other model on the provider still resolves against\n`context_window` and the rungs below. From the CLI:\n\n```bash\ncodewhale config set 'providers.command_code.model_context_windows.\"MiniMaxAI/MiniMax-M2.5\"' 204800\ncodewhale config unset 'providers.command_code.model_context_windows.\"MiniMaxAI/MiniMax-M2.5\"'\n```\n\n### How the effective window is resolved\n\nFirst match wins, and the source label each surface prints is exactly this\nrung:\n\n1. `configured (per-model)` — a `[providers.<name>.model_context_windows]`\n entry keyed by the route's exact wire model id. A hard override for that\n model only; it never rewrites another model's window.\n2. `configured` — `[providers.<name>] context_window` in `config.toml`. A hard\n override for every model on the provider: nothing below it can raise or\n lower the result. Read-time aliases:\n `contextWindow`, `context_window_tokens`, `contextWindowTokens`,\n `context_length`, `contextLength`.\n3. `provider-reported` — route-scoped 1M metadata a provider actually reported\n for the Kimi Code `k3` route, when it was observed within the last 24 hours.\n4. `static Kimi Code safe floor` — 262,144 tokens for Kimi Code memberships,\n because 1M access is plan-gated (Allegretto and above).\n5. `catalog` — the bundled route catalog (hand-curated offerings first, then\n the bundled Models.dev rows). The official `openai-codex` roster supplies\n account-specific model choices, not context-window metadata. Refresh it\n with `codewhale models --update --provider openai-codex`; Codewhale does\n not read `$CODEX_HOME` for this route.\n6. `model-name hint` — an `_Nk` suffix parsed from the model name itself\n (`qwen3-32b-256k` → 256,000), vendor-agnostic. A naming convention the\n serving engine may not honor is not a fact about the route, so this rung\n sits *below* the catalog: any catalog row for the same id beats it (#5441).\n7. `fallback` — the static per-provider capability table: 200,000 for\n Anthropic-wire routes, 128,000 for `openai-codex`, 8,192 for Ollama,\n otherwise Codewhale's static per-model metadata, and finally 128,000 when\n the model is unknown.\n\n### What \"(unverified)\" means\n\nThe `model-name hint` and `fallback` rungs still drive real budgets — the\ncompaction trigger, the context meter, and the output reservation all use the\nnumber — but they are guesses, not capabilities anyone checked. Every surface\nthat renders one of these windows appends `(unverified)` to its source label\n(the status line, the context-pressure message, `/status`, `/config`, and the\nmodel picker chip), so a window you did not configure and no provider reported\ncan never read as a verified limit (#5239, #5441). The `context_window` and\n`model_context_windows` provider-table keys above are the fix: a configured\nwindow is a hard override and renders as `configured` (or\n`configured (per-model)`) with no marker.\n\nOutput ceilings follow the same rule (#5440): an Anthropic-family model the\ncatalog does not describe keeps the 64K Messages floor as its clamp, labeled\n`unverified` (or an \"assumed floor\") instead of `documented`. The official\nChatGPT plan preview does not accept `max_output_tokens`; Codewhale sends no\noutput-token cap on that route. A local request budget is not an upstream\nlimit or a guarantee about the completed response length.\n\nThere is no environment variable for the context window; the provider-table\n`context_window` and per-model `model_context_windows` keys are the user\nknobs. They are the right ones to set when a gateway or self-hosted runtime\nserves a window Codewhale's catalog does not model — per-model when only some\nof a provider's routes differ, provider-wide when they all do. Codewhale will\nnot invent a window it cannot justify — it falls back to a conservative value,\nlabels it `fallback`, and marks it `(unverified)` at every surface that shows\nit.\n\n### Adjacent knobs\n\n- `auto_compact_threshold_percent` (settings.toml; also accepted as\n `auto_compact_threshold`; `10`–`100`, default `80`): the share of the full\n route context window at which auto-compaction fires, clamped so it can never\n cross the spendable input ceiling after output reservation and headroom.\n Editable from `/config`. Raising the window without touching this raises the\n absolute compaction point along with it.\n- `auto_compact` (settings.toml, on/off): turns automatic compaction off\n entirely; `/compact` and Ctrl+L stay available.\n- `[compaction] summary_instructions` and\n `[compaction] retained_user_message_tokens` (config.toml): standing\n summarizer instructions and the verbatim user-message retention budget. See\n the `compaction.*` entry in the config-key reference below.\n- `CODEWHALE_MAX_OUTPUT_TOKENS` (environment variable; legacy alias\n `DEEPSEEK_MAX_OUTPUT_TOKENS`): overrides the requested output cap. Without an\n override, Codewhale starts at the safe `65536` request cap and intersects it\n with any smaller documented model or route ceiling; a catalog `max_output`\n such as DeepSeek V4's 384K remains a capability ceiling, not the amount every\n response requests. Explicit overrides are preserved within the resolved\n route context window and any route output ceiling, and preflight/emergency\n budgeting reserves the same effective value that can reach the wire. A\n separately documented route input ceiling also clamps preflight and\n compaction even when the total context window is larger. A blank canonical\n variable falls through to a nonblank legacy value; a nonblank invalid or\n zero canonical value is authoritative and falls back to the safe automatic\n default instead of activating a stale legacy setting. There is no\n `max_output_tokens` key in `config.toml`.\n\nBefore compaction replaces conversation history, Codewhale durably saves the\noriginal messages to the session's `artifacts/context-transfer-<id>.json` and,\nwhen a model summary is produced, its handoff to the matching `.md` file.\nThese use the existing session artifact store and persistence redaction.\nA failed write aborts compaction without replacing context. Pruning-only passes\nsave the original messages without making an extra model call. The model can\nread them back through `context_archive_search` (ASCII case-insensitive\nsubstring search returning short snippets and an archive id) and\n`context_archive_get` (paged read of one message by id); the handoff note names\nboth so the model knows the archive exists. Pressure metadata\nshows estimated input tokens and the configured trigger; it is an estimate, not\nan exact promise about a provider's remaining context.\n\nCompaction history is available through `codewhale metrics` (or `--json`) and\n`audit.log` in the Codewhale home. Completed passes record their trigger,\nsummary/pruning path, message and estimated-token counts, effective threshold,\nand summarizer token usage. Automatic refusals are recorded once per turn with\ntheir reason. These are local diagnostics, not provider invoice totals; earlier\nartifacts are not retroactively counted. A text-mode `exec` that attempts\ncompaction saves its owning session at turn completion so its recovery artifacts\nremain discoverable. A killed process may leave artifacts without that final\nsession snapshot; the audit writer reports I/O failures instead of inventing data.\n\nSee [Settings File](#settings-file-persistent-ui-preferences) for the\ncompaction settings and [Token Quantities and\nDrivers](#token-quantities-and-drivers) for what each displayed token number\nactually measures.\n\n## Profiles\n\nYou can define multiple profiles in the same file:\n\n```toml\ndefault_text_model = \"deepseek-flash\"\n\n[providers.deepseek]\napi_key = \"PERSONAL_KEY\"\n\n[profiles.work.providers.deepseek]\napi_key = \"WORK_KEY\"\nbase_url = \"https://api.deepseek.com/beta\"\n\n[profiles.nvidia-nim]\nprovider = \"nvidia-nim\"\ndefault_text_model = \"deepseek-ai/deepseek-v4-pro\"\n\n[profiles.nvidia-nim.providers.nvidia_nim]\napi_key = \"NVIDIA_KEY\"\nbase_url = \"https://integrate.api.nvidia.com/v1\"\n\n[profiles.fireworks]\nprovider = \"fireworks\"\ndefault_text_model = \"accounts/fireworks/models/deepseek-v4-pro\"\n\n[profiles.siliconflow]\nprovider = \"siliconflow\"\ndefault_text_model = \"deepseek-ai/DeepSeek-V4-Pro\"\n\n[profiles.siliconflow.providers.siliconflow]\nbase_url = \"https://api.siliconflow.com/v1\"\n\n[profiles.openai-compatible]\nprovider = \"openai\"\n\n[profiles.openai-compatible.providers.openai]\nbase_url = \"https://openai-compatible.example/v4\"\nmodel = \"glm-5\"\n\n[profiles.atlascloud]\nprovider = \"atlascloud\"\n\n[profiles.atlascloud.providers.atlascloud]\nbase_url = \"https://api.atlascloud.ai/v1\"\nmodel = \"deepseek-ai/deepseek-v4-flash\"\n\n[profiles.sglang]\nprovider = \"sglang\"\n\n[profiles.sglang.providers.sglang]\nbase_url = \"http://localhost:30000/v1\"\nmodel = \"deepseek-ai/DeepSeek-V4-Pro\"\n\n[profiles.vllm]\nprovider = \"vllm\"\n\n[profiles.vllm.providers.vllm]\nbase_url = \"http://localhost:8000/v1\"\nmodel = \"deepseek-ai/DeepSeek-V4-Pro\"\n\n[profiles.ollama]\nprovider = \"ollama\"\n\n[profiles.ollama.providers.ollama]\nbase_url = \"http://localhost:11434/v1\"\nmodel = \"qwen2.5-coder:7b\" # an installed chat-capable tag\n\n[profiles.ollama-cloud]\nprovider = \"ollama-cloud\"\n\n[profiles.ollama-cloud.providers.ollama_cloud]\nbase_url = \"https://ollama.com/v1\"\nmodel = \"gpt-oss:120b\"\n```\n\nSelect a profile with:\n\n- CLI: `codewhale --profile work`\n- Env: `DEEPSEEK_PROFILE=work`\n\nIf a profile is selected but missing, codewhale exits with an error listing available profiles.\n\nFor local Ollama, omit `providers.ollama.model` to select a tag whose native\ncatalog confirms completion support. `/provider ollama`, exec, and Runtime API\ndefault-model admission refresh that catalog; embedding-only catalogs have no\nautomatic chat default. An explicit model keeps its exact tag.\n\nA loaded model's `/api/ps` context observation is usable for 60 seconds.\n`/api/show` reports the trained maximum, which does not establish the served\nwindow. An unloaded model or an expired observation uses the unverified 8192-token\nfallback; explicit context-window configuration still takes precedence.\n\n## Legacy top-level `base_url` and `api_key`\n\nOlder releases kept DeepSeek's endpoint and key at the top of `config.toml`,\nand each reader decided for itself which other routes inherited them. They\nnow live in provider tables only. An older file keeps working unchanged:\nevery load reads the top-level keys as if they were already in their table,\nby one rule, per file and per `[profiles.<name>]`:\n\n1. `provider = \"custom\"` with no `[providers.custom]` table: the endpoint, key\n and a copy of the model become `[providers.custom]`.\n2. An endpoint on another vendor's official host (for example\n `integrate.api.nvidia.com`, `xiaomimimo.com`, `openrouter.ai`, or the\n ChatGPT Codex endpoint) belongs to that vendor's table, so a DeepSeek route\n never sends requests there.\n3. Anything else belongs to `[providers.deepseek]`, which DeepSeek-CN also\n reads for its endpoint and key.\n\nThe key goes with DeepSeek (or the literal custom route). It follows the\nendpoint to another vendor only when the same table sets `provider` to that\nvendor, the host is that vendor's own, and the vendor's table has no key. When\nno `provider` is set, a NIM host still selects `nvidia-nim` and\n`api.deepseeki.com` still selects `deepseek-cn`. A `[vision_model]` without a\nkey of its own keeps using the top-level key. A profile's top-level\n`base_url` now overrides the base file's `[providers.deepseek] base_url`\n(before, the base table silently won).\n\nWhen a top-level value and its table disagree, the table's `base_url` and the\ntop-level `api_key` are used, which is what the runtime sent before.\n\nLoading never rewrites the file. The next save Codewhale makes (for example\n`codewhale config set`, `/config ... --save`, or `auth set`) moves the keys,\nkeeps comments, writes a one-time credential-free copy of the old file to\n`config.toml.pre-migrate.bak`, and prints one line saying what moved. A\ndisagreeing pair is never resolved in the file on its own; both values stay\nuntil you choose, and `codewhale config doctor` / `codewhale doctor` report\nit (sources only, never values). `codewhale config get|set|unset base_url`\n(and `api_key`) address the active provider's table.\n\nTo tidy the file yourself:\n\n```bash\ncodewhale config migrate --dry-run # show what would move\ncodewhale config migrate # move it (backup first)\ncodewhale config migrate --prefer top-level # resolve a conflict: keep the top-level value\ncodewhale config migrate --prefer table # resolve a conflict: keep the table value\n```\n\n## Environment Variables\n\nMost runtime environment variables override config values. API-key variables are\nfallbacks after saved config and keyring credentials.\n\nThe three user-facing slots — provider, model, base URL — expose `CODEWHALE_*`\naliases. When both forms are set the `CODEWHALE_*` value wins; the\n`DEEPSEEK_*` form is kept for older shells:\n\n- `CODEWHALE_PROVIDER` (preferred) / `DEEPSEEK_PROVIDER` (legacy alias) —\n `deepseek|deepseek-anthropic|nvidia-nim|openai|atlascloud|wanjie-ark|volcengine|openrouter|xiaomi-mimo|novita|fireworks|siliconflow|arcee|siliconflow-CN|moonshot|sglang|vllm|ollama|ollama-cloud|huggingface|modelscope|together|qianfan|openai-codex|anthropic|openmodel|zai|stepfun|minimax|deepinfra|mistral`\n- `CODEWHALE_MODEL` (preferred) / `DEEPSEEK_MODEL` (legacy alias) — default model for the active provider\n- `CODEWHALE_BASE_URL` (preferred) / `DEEPSEEK_BASE_URL` (legacy alias) — base URL for the active provider\n\n`CODEWHALE_BASE_URL` applies to the **active** route only. A request pinned to\nanother provider — a subagent or fleet child, a routed tool, the per-turn\nauto-router, a picker preview — resolves its endpoint from that provider's own\n`[providers.<table>]`, then its provider-scoped variable (`MOONSHOT_BASE_URL`,\n`OPENAI_BASE_URL`, …), then that provider's default. It never inherits the\nactive session's host, and a custom route with no configured `base_url` fails\nclosed on a loopback placeholder rather than borrowing another provider's\nendpoint. The environment writes the value into the active identity's own\ntable: DeepSeek-CN falls back to `[providers.deepseek]` for its endpoint and\nkey, but never to a DeepSeek value the environment addressed to DeepSeek alone.\nA managed-config overlay that supplies or reselects the effective\nroute's endpoint takes the generic override away from every route.\n\nRemaining variables:\n\n- `CODEWHALE_LOCAL_PROXY_FALLBACK` (`0`) — keeps reqwest's build-time proxy snapshot; the loopback fallback is on by default. Read once when the first client is built.\n- `DEEPSEEK_API_KEY`\n- `DEEPSEEK_ANTHROPIC_BASE_URL`\n- `DEEPSEEK_HTTP_HEADERS` (custom model request headers, comma-separated `name=value` pairs)\n- `DEEPSEEK_DEFAULT_TEXT_MODEL` (extra legacy alias of `DEEPSEEK_MODEL`)\n- `DEEPSEEK_STREAM_IDLE_TIMEOUT_SECS` (stream idle timeout in seconds; default `900`, clamped to `1..=3600`)\n- `DEEPSEEK_STREAM_OPEN_TIMEOUT_SECS` (connection setup + response-header wait in seconds; default `45`, clamped to `5..=300`; distinct from the per-chunk idle timeout; `stream.open_timeout_secs` (legacy `tui.stream_open_timeout_secs` fallback) wins when positive)\n- `CODEWHALE_CACHE_MAXIMAL` (`1`/`true`/`on`/`yes`) — cache-maximal context mode (#528). When on, the Repo Working Set block materializes the **full current contents** of the top active files into the system prompt each turn (deterministic order, byte-bounded), instead of only listing their paths. The block stays byte-stable while those files are unchanged so DeepSeek's KV prefix cache keeps hitting; editing a file cache-misses from its block onward. Off by default (path list only). Byte caps default to 24 KB per file / 96 KB total.\n- `NVIDIA_API_KEY` or `NVIDIA_NIM_API_KEY` (when provider is `nvidia-nim`)\n- `NVIDIA_NIM_BASE_URL`, `NIM_BASE_URL`, or `NVIDIA_BASE_URL`\n- `NVIDIA_NIM_MODEL`\n- `OPENAI_API_KEY`\n- `OPENAI_BASE_URL`\n- `OPENAI_MODEL`\n- `ATLASCLOUD_API_KEY`\n- `ATLASCLOUD_BASE_URL`\n- `ATLASCLOUD_MODEL`\n- `WANJIE_ARK_API_KEY`, `WANJIE_API_KEY`, or `WANJIE_MAAS_API_KEY`\n- `WANJIE_ARK_BASE_URL`, `WANJIE_BASE_URL`, or `WANJIE_MAAS_BASE_URL`\n- `WANJIE_ARK_MODEL`, `WANJIE_MODEL`, or `WANJIE_MAAS_MODEL`\n- `VOLCENGINE_API_KEY`, `VOLCENGINE_ARK_API_KEY`, or `ARK_API_KEY`\n- `VOLCENGINE_BASE_URL`, `VOLCENGINE_ARK_BASE_URL`, or `ARK_BASE_URL`\n- `VOLCENGINE_MODEL` or `VOLCENGINE_ARK_MODEL`\n- `OPENROUTER_API_KEY`\n- `OPENROUTER_BASE_URL`\n- `OPENROUTER_MODEL`\n- `XIAOMI_MIMO_TOKEN_PLAN_API_KEY`, `MIMO_TOKEN_PLAN_API_KEY`, `XIAOMI_MIMO_API_KEY`, `XIAOMI_API_KEY`, or `MIMO_API_KEY`\n- `XIAOMI_MIMO_BASE_URL` or `MIMO_BASE_URL`\n- `XIAOMI_MIMO_MODEL` or `MIMO_MODEL`\n- `XIAOMI_MIMO_MODE` or `MIMO_MODE` (`token-plan-sgp`, `token-plan-cn`,\n `token-plan-ams`, or `pay-as-you-go`)\n- `NOVITA_API_KEY`\n- `NOVITA_BASE_URL`\n- `NOVITA_MODEL`\n- `FIREWORKS_API_KEY`\n- `FIREWORKS_BASE_URL`\n- `FIREWORKS_MODEL`\n- `HUGGINGFACE_API_KEY` or `HF_TOKEN` (`HF_TOKEN` is a fallback alias accepted when provider is `huggingface`)\n- `MODELSCOPE_API_KEY`\n- `HUGGINGFACE_BASE_URL` or `HF_BASE_URL`\n- `HUGGINGFACE_MODEL` or `HF_MODEL`\n- `SILICONFLOW_API_KEY`\n- `SILICONFLOW_BASE_URL`\n- `SILICONFLOW_MODEL`\n- `ARCEE_API_KEY`\n- `ARCEE_BASE_URL`\n- `ARCEE_MODEL`\n- `TOGETHER_API_KEY`\n- `TOGETHER_BASE_URL`\n- `TOGETHER_MODEL`\n- `QIANFAN_API_KEY` or `BAIDU_QIANFAN_API_KEY`\n- `QIANFAN_BASE_URL` or `BAIDU_QIANFAN_BASE_URL`\n- `QIANFAN_MODEL` or `BAIDU_QIANFAN_MODEL`\n- `OPENAI_CODEX_ACCESS_TOKEN` or `CODEX_ACCESS_TOKEN` (legacy process tokens; ignored by the official ChatGPT plan route)\n- `OPENAI_CODEX_BASE_URL` or `CODEX_BASE_URL`\n- `OPENAI_CODEX_MODEL` or `CODEX_MODEL`\n- `OPENAI_CODEX_ACCOUNT_ID` or `CODEX_ACCOUNT_ID` (legacy identity hints; ignored by the official ChatGPT plan route)\n- `CODEWHALE_CHATGPT_NEW_ACCOUNT=1` explicitly replaces the single selected ChatGPT registration after a validated browser sign-in\n- `ANTHROPIC_API_KEY`\n- `ANTHROPIC_BASE_URL`\n- `ANTHROPIC_MODEL`\n- `ZAI_API_KEY` or `Z_AI_API_KEY`\n- `ZAI_BASE_URL` or `Z_AI_BASE_URL`\n- `ZAI_MODEL` or `Z_AI_MODEL`\n- `STEPFUN_API_KEY` or `STEP_API_KEY`\n- `STEPFUN_BASE_URL` or `STEP_BASE_URL`\n- `STEPFUN_MODEL` or `STEP_MODEL`\n- `MINIMAX_API_KEY`\n- `MINIMAX_BASE_URL`\n- `MINIMAX_MODEL`\n- `DEEPINFRA_API_KEY` or `DEEPINFRA_TOKEN`\n- `DEEPINFRA_BASE_URL`\n- `DEEPINFRA_MODEL`\n- `MISTRAL_API_KEY`\n- `MISTRAL_BASE_URL`\n- `MISTRAL_MODEL`\n- `MOONSHOT_API_KEY` or `KIMI_API_KEY`\n- `MOONSHOT_BASE_URL` or `KIMI_BASE_URL`\n- `MOONSHOT_MODEL`, `KIMI_MODEL_NAME`, or `KIMI_MODEL`\n- `SGLANG_BASE_URL`\n- `SGLANG_MODEL`\n- `SGLANG_API_KEY` (optional; many localhost SGLang servers do not require auth)\n- `VLLM_BASE_URL`\n- `VLLM_MODEL`\n- `VLLM_API_KEY` (optional; many localhost vLLM servers do not require auth)\n- `OLLAMA_BASE_URL`\n- `OLLAMA_MODEL`\n- `OLLAMA_API_KEY` (optional; many localhost Ollama servers do not require auth)\n- `OLLAMA_CLOUD_BASE_URL`\n- `OLLAMA_CLOUD_MODEL`\n- `OLLAMA_CLOUD_API_KEY` (preferred Cloud key; `OLLAMA_API_KEY` is the official fallback)\nFor every product-level `CODEWHALE_*` variable below, the matching legacy\n`DEEPSEEK_*` name is still read as a compatibility fallback; when both are set,\nthe `CODEWHALE_*` value wins.\n\n- `CODEWHALE_TIMING=1` emits local JSON phase durations on stderr for startup, model preparation, tool planning, approval waits and postprocessing. Nested intervals overlap. The records contain only phase, process ID, end time and elapsed microseconds; stream-json stdout is unchanged.\n- `CODEWHALE_LOG_LEVEL` or `RUST_LOG` (`info`/`debug`/`trace` enables lightweight verbose logs)\n- `CODEWHALE_SKILLS_DIR`\n- `CODEWHALE_MCP_CONFIG`\n- `CODEWHALE_NOTES_PATH`\n- `CODEWHALE_MEMORY` (`1|on|true|yes|y|enabled` turns user memory on)\n- `CODEWHALE_MEMORY_PATH`\n- `CODEWHALE_TELEMETRY` / `DEEPSEEK_TELEMETRY` (legacy alias) — anonymous usage\n counting is on by default in the current 0.9.12 source, with a disclosure\n naming Codewhale and PostHog and an easy durable opt-out. Prior explicit\n declines remain off. Accepts `0|1|true|false|yes|no|on|off|enabled|\n disabled`. An explicit \"off\" is a **floor**: it beats `--telemetry true` and\n `telemetry = true` in config, and a value this list cannot read also resolves\n to off, because a typo in a kill switch must never resolve to \"on\". See\n [`TELEMETRY.md`](TELEMETRY.md).\n- `CODEWHALE_TELEMETRY_ENDPOINT` / `DEEPSEEK_TELEMETRY_ENDPOINT` (legacy alias)\n — `https://`, or plain `http://` only for loopback. Overrides the config file.\n Unset selects the shipped default,\n `https://telemetry.codewhale.net/v1/telemetry`; setting it to the **empty\n string** routes batches to a local dry-run file and contacts nobody. Either\n way it only decides where a session sends — it cannot override an opt-out.\n- `CODEWHALE_ALLOW_SHELL` (`1`/`true` enables)\n- `CODEWHALE_APPROVAL_POLICY` (`on-request|untrusted|never`)\n- `CODEWHALE_SANDBOX_MODE` (`read-only|workspace-write|danger-full-access|external-sandbox`)\n- `CODEWHALE_NO_NEW_PRIVS` (`0`/`false`/`no`/`off`/`disabled` opts out) — Linux only. The\n TUI process sets the kernel's irreversible no-new-privileges flag at startup\n as defense-in-depth, which blocks `sudo`/`su`/setuid helpers for Codewhale's\n whole process tree. The flag is already skipped when the startup sandbox\n mode resolves to `danger-full-access` (#5723), so this variable is the\n explicit override in the remaining cases: set it to a falsey value before\n launching if you administer through Codewhale as a wheel-group user under a\n narrower posture and need escalation to work (#5413), or set a truthy value\n to force the flag on even under `danger-full-access`. Unset, the posture\n decides; the other startup hardening (no ptrace, no core dumps) always\n stays on.\n- `CODEWHALE_MANAGED_CONFIG_PATH`\n- `CODEWHALE_REQUIREMENTS_PATH`\n- `CODEWHALE_MAX_SUBAGENTS` (clamped to `1..=128`)\n- `CODEWHALE_TASKS_DIR` (runtime task queue/artifact storage, default\n `~/.codewhale/tasks`, with legacy `~/.deepseek/tasks` fallback when only the\n legacy directory exists)\n- `CODEWHALE_RUNTIME_DIR` (override the runtime thread store root). Interactive\n sessions default to `$CODEWHALE_HOME/sessions/<session-id>/runtime` so each\n Codewhale process owns its own store (#5630). The store is single-owner: a\n second process on the **same** root fails at startup. Set this variable to\n share one store across processes, or when the runtime API server should use a\n stable non-session path. Unset, the API/server path remains\n `$CODEWHALE_HOME/tasks/runtime`. Legacy alias: `DEEPSEEK_RUNTIME_DIR`.\n- `CODEWHALE_ALLOW_INSECURE_HTTP` (`1`/`true` allows non-local `http://` base URLs; default is reject)\n- `CODEWHALE_FORCE_HTTP1` (`1|true|yes|on` pins the HTTP client to HTTP/1.1, disabling HTTP/2; useful on Windows or behind proxies that mishandle long-lived H2 streams)\n- `CODEWHALE_HOME` (override the base data directory; defaults to `~/.codewhale`).\n If you previously exported `DEEPSEEK_HOME`, rename it to `CODEWHALE_HOME`;\n the old env var is not used for new Codewhale state paths.\n- `CODEWHALE_RELEASE_BASE_URL` (release asset mirror used by `codewhale update`\n and by TUI startup update checks when `[update].update_uri` is not set, or as\n a fallback when that configured URI cannot be fetched)\n- `CODEWHALE_AUTOMATIONS_DIR` (override the automations storage directory; uses\n `~/.codewhale/automations` by default, with legacy `~/.deepseek/automations`\n fallback when only the legacy directory exists)\n- `NO_ANIMATIONS` (`1|true|yes|on` forces `low_motion = true` and\n `fancy_animations = false` at startup, regardless of the saved\n settings; see [`docs/ACCESSIBILITY.md`](./ACCESSIBILITY.md)).\n- `SSL_CERT_FILE` — corporate-proxy / TLS-inspecting MITM users\n point this at a PEM bundle (or single DER cert) and the cert(s)\n get added alongside the platform's system trust store. Failures\n log a warning and continue — the existing system roots still\n apply.\n\n### Instruction sources (`instructions = [...]`, #454)\n\nAdd a list of additional system-prompt sources that get\nconcatenated, in declared order, alongside the auto-loaded\n`AGENTS.md`:\n\n```toml\ninstructions = [\n \"./AGENTS.md\",\n \"~/.codewhale/global.md\",\n \"~/team/agents-shared.md\",\n]\n```\n\nRules:\n\n- Paths run through `expand_path` so `~` and env vars work.\n- These entries are **global-scope** instruction authority: they are charged\n to the global budget together with the global `AGENTS.md`/`instructions.md`\n fallbacks and `~/.claude/CLAUDE.md`.\n- A file keeps at most its first 100 KiB. A file larger than\n `[instruction_budget] file_max_bytes` (8 MiB by default) is skipped with a\n notice. Missing and empty files are skipped with a notice so a stale entry\n doesn't fail the launch.\n- Only user-owned config, profiles, and managed config may set this array.\n Project config (`<workspace>/.codewhale/config.toml`, or legacy\n `<workspace>/.deepseek/config.toml`) ignores `instructions` so a cloned repo\n cannot choose arbitrary local files to place into the prompt.\n\n### Instruction budget (`[instruction_budget]`, #6526)\n\nStanding instruction files are assembled into two independent scopes and\nbudgeted in **estimated tokens** — the same conservative estimator `/context`\nuses, never a `bytes/4` shortcut, which underestimates Cyrillic by 30–50%.\n\n- **global** — `~/.codewhale/AGENTS.md` (or the `.agents`/`.deepseek`\n fallbacks), the global `instructions.md` fallbacks, `~/.claude/CLAUDE.md`\n as a last resort when `claude_fallback` is on, and the\n `instructions = [...]` array.\n- **project** — the repository-root → workspace `AGENTS.md` chain, co-read\n foreign files, and the `.codewhale/rules`/`.claude/rules` block.\n\nEach scope is capped at `min(absolute_tokens, percent × usable_window)`. The\nusable window is the route window minus the output reservation the request\nwill actually carry and a safety reserve of 10% of the window (minimum 2,048\ntokens). A route whose context window is unknown has no usable window: the\npercentage ceilings and the guard are reported as `unknown` and only the\nabsolute caps apply.\n\n```toml\n[instruction_budget]\nglobal_budget_tokens = 100000\nproject_budget_tokens = 100000\nglobal_budget_percent = 25\nproject_budget_percent = 25\nstatic_prefix_guard_percent = 80\nfile_max_bytes = 8388608\npolicy = \"trim\"\nclaude_fallback = false\n```\n\nWith a 64K (65,536-token) output reservation:\n\n| window | usable | per scope | both scopes | guard 80% |\n| --- | --- | --- | --- | --- |\n| 128,000 | 49,664 | 12,416 | 24,832 | 39,731 |\n| 1,000,000 | 834,464 | 100,000 (absolute cap) | 200,000 | 667,571 |\n\nBehaviour:\n\n- **Nothing is trimmed while a source is within its scope budget.**\n- A scope over budget drops whole files from the broadest end first — the\n root-most ancestor goes first, the workspace file last — and a single file\n that alone exceeds the remaining budget keeps its **head** and drops its\n tail with a marker carrying bytes and estimated tokens.\n- Every instruction, rules, and configured file retains at most its first\n 100 KiB of text, ending in a marker that names the bytes kept and dropped.\n- Rules are charged to the project scope after the instruction chain and keep\n a head-first trim.\n- `file_max_bytes` is a hard ceiling that can only be lowered from its 8 MiB\n default: a larger file is **skipped**, never trimmed, and the read is\n bounded on the open file, so a FIFO or a growing file cannot exhaust memory.\n The same applies to a whitespace-only file and to files beyond the\n 50-per-directory rules cap.\n- `policy = \"trim\"` (default) trims and reports. `policy = \"refuse\"` stops the\n request before dispatch when a scope budget, the rules budget, or the\n 100 KiB retention would trim, or when the request prefix exceeds the guard;\n skips stay skips, because one bad file must not make the project unopenable.\n- `static_prefix_guard_percent` caps the request prefix: every system block\n plus the tool schemas the request carries. When it trips, the two scopes are\n narrowed largest first until the prefix fits, only the installed instruction\n block is replaced, and the change is recorded as a declared prefix change.\n If the prefix still exceeds the guard afterwards, the residual excess is\n reported rather than looped on. The guard must leave at least 10 points\n above the two scope percents. Prompts owned by a host (child agents,\n isolated chat, RLM) are measured and can be refused, but are never trimmed.\n- `claude_fallback = true` (opt in; default `false`): a directory with no\n `AGENTS.md` (or codewhale `instructions.md`) uses its `CLAUDE.md`. A skipped\n `AGENTS.md` counts as absent; a file already loaded from a wider scope (a\n symlink) is a reported duplicate, not a fallback trigger. Other tools'\n instruction files are never imported unless you opt in here or through\n `project_instruction_imports`.\n- Every loss — trimmed, skipped, refused — reaches the model as one\n runtime-provenance message whenever the list changes, and appears per source\n in `/context report`. `codewhale doctor`'s report is a preview built from the\n current files and config; it does not claim a request passed the tool-schema\n guard.\n- The table is **user config only**, like `instructions`: project config is\n ignored and a cloned repository cannot shrink the global budget or force\n `refuse`. An invalid table (unknown key, zero budget, percent outside\n 1–100, guard without room left, `file_max_bytes` outside 1–8388608) fails\n the launch.\n\n### Hooks\n\nHooks are a **TUI runtime feature**. They fire from the interactive TUI and\nthe engine turn loop it drives; `codewhale exec`, the CLI subcommands, the\napp-server / ACP surfaces, and the `workflow` tool do not fire them.\n\n[`docs/HOOKS.md`](HOOKS.md) is the authoritative reference for all eleven hook\nevents — their firing points, environment variables, stdin payloads, timeout\nand background semantics, and which three of them can steer Codewhale. The\nsections below cover the configuration surface and the steering contracts in\nmore depth.\n\nTwo contract points worth reading there before writing a hook:\n\n- `background = true` means **submitted and never awaited**. The hook still\n gets the documented stdin payload and the same timeout, but it has no exit\n code and cannot steer.\n- A condition that references context its event never carries (an `exit_code`\n condition outside `tool_call_after` / `on_error`, a `mode` condition on\n `shell_env`, a tool condition on a non-tool event) is **rejected at load**,\n logged, and shown in `/hooks list`. It does not silently never match.\n Rejection is per entry, so a broken hook never drops another one that merely\n shares its `name` or is likewise unnamed.\n\n### `/hooks` listing\n\nRun `/hooks` (or `/hooks list`) inside the TUI to see every\nconfigured lifecycle hook grouped by event, including each\nhook's name, command preview, effective timeout, and condition. When\n`[hooks].default_timeout_secs` is set it replaces every per-hook\n`timeout_secs`, and the listing shows that effective value and names the\noverride rather than echoing the per-hook number. A\n`default_timeout_secs = 0` is rejected at load — it would expire every hook\nin the config immediately — so the override is ignored, per-hook\n`timeout_secs` applies, the listing shows that per-hook value with no\noverride provenance, and the rejection appears under `configuration\nproblems`. The\n`[hooks].enabled` flag's state is shown at the top so it's\nobvious when hooks are globally suppressed, and any entry rejected\nat load is listed under `configuration problems` with the reason.\nHooks are configured under `[[hooks.hooks]]` entries — see\n[`docs/HOOKS.md`](HOOKS.md) for the full schema.\n\n### Mutable `message_submit` hooks\n\n`message_submit` hooks run before a submitted message is added to\nhistory or sent to the model. Unlike observer-only lifecycle hooks,\nnon-background `message_submit` hooks can replace or block the\nsubmitted text.\n\n```toml\n[[hooks.hooks]]\nevent = \"message_submit\"\ncommand = \"~/.codewhale/hooks/inject-context.sh\"\ntimeout_secs = 2\ncontinue_on_error = true\n```\n\nThe hook receives JSON on stdin:\n\n```json\n{\n \"event\": \"message_submit\",\n \"text\": \"original user text\",\n \"text_bytes\": 18,\n \"text_original_bytes\": 18,\n \"text_truncated\": false,\n \"session_id\": \"sess_12345678\",\n \"workspace\": \"/path/to/workspace\",\n \"mode\": \"agent\",\n \"model\": \"deepseek-chat\",\n \"total_tokens\": 1234\n}\n```\n\nThe entire serialized document is capped at 32 KiB. Codewhale retains the\nlargest UTF-8-safe `text` prefix that fits after JSON escaping and bounded\nmetadata, and the three `text_*` fields make truncation explicit. Immediate\nmessages, restored queue entries, merged steers, and prior-hook replacements\nall cross this same serialization boundary.\n\nIf the hook exits `0` and prints JSON with a non-empty string `text` field,\nthat value replaces the submitted text:\n\n```json\n{ \"text\": \"replacement user text\" }\n```\n\nExit `0` with empty stdout, or stdout JSON without `text`, leaves\nthe current text unchanged. A JSON `text` field must not be empty;\n`{\"text\":\"\"}` is treated as invalid stdout and ignored. Exit `2`\nblocks the submission before the turn starts; a structured `reason` field can\nprovide the bounded, redacted status message shown in the TUI. Raw stdout,\nstderr, and process-error text are not copied into denial receipts.\nOther non-zero exits follow the hook's `continue_on_error` setting.\nTimeouts and spawn failures are also surfaced as transient TUI status\nmessages when `continue_on_error = true` lets submission continue.\n\nMultiple `message_submit` hooks run in config order, and each hook\nreceives the text produced by the previous hook. Hooks marked\n`background = true` are observer-only and cannot transform or block\nthe message — they still receive the same stdin payload and the same\nenvironment, they are simply never awaited. Existing environment\nvariables remain available.\n`shell_env` hooks keep their existing `KEY=VALUE` stdout contract;\nJSON stdout contracts exist for `message_submit` (above) and\n`tool_call_before` (below).\n\n### `tool_call_before` decision hooks\n\n`tool_call_before` hooks run before each tool call executes. In\naddition to the legacy hard deny (exit code `2`, which always wins\nregardless of stdout), a foreground hook may print a JSON decision on\nstdout with exit code `0`:\n\n```json\n{\n \"decision\": \"allow\" | \"deny\" | \"ask\",\n \"reason\": \"human-readable explanation (used for deny)\",\n \"updatedInput\": { \"command\": \"ls -la\" },\n \"additionalContext\": \"text appended to the tool result for the model\"\n}\n```\n\nAll fields are optional. Empty stdout, non-JSON stdout, and JSON\nwithout a `decision` field behave exactly as before (allow). An\nunrecognized `decision` string logs a fixed warning without echoing the\nuntrusted value and is treated as allow.\n\n- `deny` blocks the tool; the model receives a permission-denied tool\n result containing `reason`.\n- `ask` forces the interactive approval prompt in Ask and Auto-Review even for\n tools that would otherwise auto-run. Full Access does not open tool-approval\n prompts, so hook `ask` does not downgrade that posture.\n- `updatedInput` must be a JSON object; it replaces the tool input\n before execution. When several hooks supply it, the last hook wins.\n- `additionalContext` is appended to the tool result sent back to the\n model as `[hook context] ...`. Multiple hooks' contexts are\n concatenated.\n\nWhen multiple hooks match, precedence is deny > ask > allow. Hooks\nmarked `background = true` cannot steer tool calls — they are\nsubmitted and never awaited, so they have no verdict to contribute.\n\nA foreground hook that produced no verdict at all — it hit its timeout, the\nprocess could not be started, or a strict process exited non-zero without an\nexplicit JSON decision — is not treated as permission. If\n*that* hook is configured with `continue_on_error = false`, the outcome\ndenies the tool call and the denial names the hook and a bounded reason.\nStrictness is read off the hooks that actually matched this call, so a\nstrict gate scoped to another tool cannot deny it, and a lenient hook's\ntimeout does not deny merely because a strict hook exists elsewhere in\nconfig. Under the default `continue_on_error = true` the outcome is\nlogged and the call proceeds.\n\n`reason` and `additionalContext` are capped (2 000 characters per field,\n8 000 for the concatenated context of one call) and stripped of control\ncharacters before they reach the TUI or the model.\n\nExample deny hook:\n\n```toml\n[[hooks.hooks]]\nevent = \"tool_call_before\"\ncommand = '''echo '{\"decision\":\"deny\",\"reason\":\"blocked by project policy\"}' '''\ncondition = { type = \"tool_name\", name = \"exec_shell\" }\n```\n\nExample ask hook (force approval for every MCP tool):\n\n```toml\n[[hooks.hooks]]\nevent = \"tool_call_before\"\ncommand = '''echo '{\"decision\":\"ask\"}' '''\ncondition = { type = \"tool_name\", name = \"mcp__*\" }\n```\n\nExample input rewrite:\n\n```toml\n[[hooks.hooks]]\nevent = \"tool_call_before\"\ncommand = \"~/.codewhale/hooks/clamp-shell-timeout.sh\"\ncondition = { type = \"tool_name\", name = \"exec_shell\" }\n```\n\nwhere the script reads the hook context, then prints\n`{\"updatedInput\": {...}}` with the adjusted arguments.\n\n`tool_name` conditions support `*` globs: `mcp__*` matches every MCP\ntool (e.g. `mcp__github__create_issue`) but not built-ins like\n`read_file`; exact names keep matching exactly. Other regex\nmetacharacters in the pattern are matched literally.\n\n### Project-local hooks\n\nRepositories can ship policy in `<workspace>/.codewhale/hooks.toml`,\nusing the same shape as the `[hooks]` table (top-level fields plus\n`[[hooks]]` entries). Project hooks are executable shell\nconfiguration, so Codewhale only loads them after the workspace has\nbeen trusted in user-owned config through the trust prompt or a\n`[projects.\"<workspace>\"] trust_level = \"trusted\"` entry. Session\n`/trust on` mode does not enable repo-supplied hooks by itself, and\nrepo-local legacy markers such as `.deepseek/trusted` do not enable\nproject hooks. Once trusted, project hooks are appended after global\nhooks from `config.toml`, so they run last and, for `updatedInput`,\nwin ties. A malformed trusted project file logs a warning and startup\nfalls back to global hooks only.\n\n```toml\n# .codewhale/hooks.toml\n[[hooks]]\nevent = \"tool_call_before\"\ncommand = '''echo '{\"decision\":\"deny\",\"reason\":\"no shell in this repo\"}' '''\ncondition = { type = \"tool_name\", name = \"exec_shell\" }\n```\n\n### Turn-end observer hooks\n\n`turn_end` hooks observe the end of each model turn after post-turn\nstate, usage totals, cost accounting, notifications, receipts, and\nqueue recovery have been updated. They receive JSON on stdin and are\nobserver-only: stdout is ignored, failures are logged as warnings, and\nthe hook cannot block user input, mutate the transcript, or change the\nnext queued follow-up.\n\nObserver-only UI events share one 32-entry queue and two persistent workers;\nthe terminal loop uses non-blocking submission and does not create a thread per\nevent. A full queue or unavailable dispatcher drops that observer event and is\nkept as an event-specific error toast, independent of ordinary agent/turn\nstatus text.\n\n```toml\n[[hooks.hooks]]\nevent = \"turn_end\"\ncommand = \"~/.codewhale/hooks/turn-audit.sh\"\ntimeout_secs = 2\ncontinue_on_error = true\n```\n\nThe payload includes common hook metadata plus post-turn accounting:\n\n```json\n{\n \"event\": \"turn_end\",\n \"session_id\": \"sess_12345678\",\n \"workspace\": \"/path/to/workspace\",\n \"mode\": \"agent\",\n \"created_at\": \"2026-07-12T10:30:00+00:00\",\n \"model_backed\": true,\n \"provider\": \"deepseek\",\n \"model\": \"deepseek-chat\",\n \"billing_surface\": null,\n \"turn_id\": \"turn_12345678\",\n \"status\": \"completed\",\n \"error\": null,\n \"duration_ms\": 1834,\n \"usage\": {\n \"input_tokens\": 1200,\n \"output_tokens\": 180,\n \"prompt_cache_hit_tokens\": 900,\n \"prompt_cache_miss_tokens\": 300,\n \"prompt_cache_write_tokens\": 0,\n \"reasoning_tokens\": null,\n \"reasoning_replay_tokens\": null\n },\n \"totals\": {\n \"session_tokens\": 1380,\n \"conversation_tokens\": 1380,\n \"input_tokens\": 1200,\n \"output_tokens\": 180\n },\n \"tool_count\": 2,\n \"queued_message_count\": 1,\n \"stop_hook_active\": false\n}\n```\n\n`created_at` anchors time-window pricing; `provider` and `model` identify the\neffective route used for model-backed turns. `billing_surface` is an optional,\nnon-secret classification derived from the endpoint that actually served the\nturn. Recognized StepFun routes emit `stepfun-payg` or `stepfun-plan`; the raw\nbase URL is never written to hook or runtime records. Runtime `TurnRecord`\nexports call the same field `effective_billing_surface`, which `scorecard`\naccepts as an alias. This keeps subscription quota separate from token-priced\nusage. Unrecognized and custom endpoints remain `null` and unpriced.\n\nShell-only lifecycle completions set `model_backed` to `false` and may report a\n`null` provider; offline scorecards exclude those records from model token and\ncost totals. Completion-only shell, manual-compaction, and purge events that do\nnot have a matching `TurnStarted` retain the observer notification with a\nsynthetic `lifecycle_<uuid>` turn id and the time the completion was observed.\n\nFor `interrupted` or `failed` turns, `status` reflects that terminal\nstate and `error` carries the engine error string when one is available.\n`stop_hook_active` is reserved for future re-entry protection and is\ncurrently always `false`.\n\n### Sub-agent lifecycle hooks\n\n`subagent_spawn` and `subagent_complete` hooks observe sub-agent lifecycle\nevents. They receive bounded JSON metadata on stdin and are observer-only:\nhook failures are logged as warnings and do not block sub-agent scheduling,\nchange prompts, or change results. For these observer events,\n`continue_on_error` has no effect: later matching hooks still run even when an\nearlier hook exits non-zero.\n\n```toml\n[[hooks.hooks]]\nevent = \"subagent_complete\"\ncommand = \"~/.codewhale/hooks/subagent-audit.sh\"\ntimeout_secs = 2\ncontinue_on_error = true\n```\n\n`subagent_spawn` receives:\n\n```json\n{\n \"event\": \"subagent_spawn\",\n \"agent_id\": \"agent_12345678\",\n \"session_id\": \"sess_12345678\",\n \"workspace\": \"/path/to/workspace\",\n \"mode\": \"agent\",\n \"model\": \"deepseek-chat\",\n \"total_tokens\": 1234,\n \"prompt_preview\": \"bounded prompt preview\",\n \"prompt_truncated\": false\n}\n```\n\n`subagent_complete` receives the same common fields plus terminal metadata:\n\n```json\n{\n \"event\": \"subagent_complete\",\n \"agent_id\": \"agent_12345678\",\n \"session_id\": \"sess_12345678\",\n \"workspace\": \"/path/to/workspace\",\n \"mode\": \"agent\",\n \"model\": \"deepseek-chat\",\n \"total_tokens\": 1234,\n \"status\": \"completed\",\n \"result_preview\": \"bounded result preview\",\n \"result_truncated\": false\n}\n```\n\nPreviews are capped before delivery so lifecycle hooks do not receive full\nsub-agent prompts, transcripts, or unbounded results. Use the transcript handle\nreturned by `agent` when full sub-agent details are needed.\n\n### Running-turn input\n\nComposer shortcuts keep the same role throughout a session:\n\n- **Enter** sends when idle and queues a next-turn follow-up while busy. The\n behavior does not change before versus after the provider's first token.\n- With an empty composer and queued follow-ups visible, **Enter** sends the\n oldest queued follow-up into the active turn now.\n- **Ctrl+Enter** (or **Cmd+Enter** when the terminal forwards it) explicitly\n steers the active turn. It sends normally when idle.\n- By default, **Shift+Enter**, **Alt+Enter**, and **Ctrl+J** insert a newline.\n- Set `composer_multiline_mode = true` to make **Enter** insert a newline and\n **Alt+Enter** send instead. **Shift+Enter** also sends when the terminal\n reports it distinctly; legacy terminals may report it as plain **Enter**.\n **Ctrl+J** still inserts a newline, and supported **Ctrl+Enter** /\n **Cmd+Enter** behavior is unchanged.\n- **Ctrl+G** and **Ctrl+S** only stash drafts; they never send or steer.\n\n### Composer stash (`/stash`, Ctrl+G / Ctrl+S)\n\nPress **Ctrl+G** in the composer to park the current draft to\n`~/.codewhale/composer_stash.jsonl`. `/stash list` shows parked\ndrafts with one-line previews and timestamps; `/stash pop`\nrestores the most recently parked draft (LIFO); `/stash clear`\nwipes the file. Capped at 200 entries; multiline drafts round-trip intact.\n**Ctrl+S** remains an alias in terminals that forward it; Cursor and VS Code\nreserve Ctrl+S for Save, so Ctrl+G is the portable default.\n\n## Settings File (Persistent UI Preferences)\n\ncodewhale also stores user preferences in:\n\n- `~/.codewhale/settings.toml` on new installs\n- `~/.deepseek/settings.toml` or the legacy platform config-dir\n `deepseek/settings.toml` when an existing settings file is present\n\nNotable settings include `auto_compact`, which uses a model-aware default-on\npolicy for known context windows up to the 1M-token V4 class. Automatic\ncompaction runs before the active model limit and carries the compacted summary\nforward into the next request. The trigger defaults to\n`auto_compact_threshold_percent = 80`. Users who prefer manual continuity can\npersist `auto_compact = false`; manual `/compact` / Ctrl+L remains available.\nYou can inspect or update these from the TUI with `/settings` and `/config`\n(interactive editor).\n\nCommon settings keys:\n\n- `theme` (`system`, `terminal`, `underwater`, `underwater-retro`,\n `shoreline`, `shoreline-light`, `dark`, `light`, `grayscale`,\n `catppuccin-mocha`, `tokyo-night`, `dracula`, `gruvbox-dark`, `claude`,\n `matrix`, `solarized-light`, `uwu`; default `underwater`): `underwater` is\n the dark navy fresh-install default, `shoreline` is its warm charcoal\n alternative and `shoreline-light` the light variant, `system` follows terminal\n background detection, `dark`/`light` use the Codewhale Whale pair,\n `terminal` inherits the host terminal, `grayscale` is the low-opinion\n black/white theme, and the named community presets apply across the TUI.\n Aliases such as `whale`, `mono`, `black-white`, `tokyonight`, and `gruvbox`\n are accepted. In Whale, cobalt blue owns action/focus, seafoam owns live\n work, Signal Gold owns human decisions and the whale, coral owns warnings,\n rose owns danger, violet owns Operate, and green remains completed/verified.\n Text labels, markers, and motion policy carry the same states when color is\n unavailable; color is never the only cue.\n User-authored overlays live only at `~/.codewhale/themes/<name>.json` (or\n `$CODEWHALE_HOME/themes/<name>.json`) and are selected with\n `/theme custom:<name>`. The filename is a bounded slug, symlinks and files\n over 64 KiB are refused, colors must be `#RRGGBB`, and unknown fields fail\n validation. `/theme schema` prints the embedded JSON Schema and `/theme path`\n shows the exact directory. An overlay names one compiled `base` theme and\n changes only listed semantic colors; it cannot include or read another file.\n Open `/theme` to browse valid overlays, preview them live, and keep the\n active `custom:<name>` selector when the picker is opened without moving.\n- `auto_compact` (on/off, model-aware default on for known context windows\n unless explicitly configured)\n- `auto_compact_threshold_percent` (10-100, default `80`): pre-send\n auto-compaction threshold used only when `auto_compact` is enabled.\n- `paste_burst_detection` (on/off, default on): fallback rapid-key paste\n detection for terminals that do not emit bracketed-paste events. This is\n independent of terminal bracketed-paste mode.\n- `work_surface_placement` (`bottom`, `top`, `left`, `right`, or `off`;\n default `bottom`): places the workbar — Tasks / To-do / Workers — under the\n composer (the default bottom workbar), above the transcript, in a side\n workbar, or hides it entirely (`off`). Side choices fall back to the top\n layout on narrow terminals without changing the saved preference. Set it\n live with `/config work_surface_placement right --save` (or `left` / `top` /\n `bottom` / `off`).\n- `rail_panel` (`tasks`, `agents`, `background`, `files`, `notepad`,\n `context`, `git`, `price`; default `tasks`, alias key `rail`): which panel\n the workbar shows. Panel selection is orthogonal to placement. `tasks` is\n the full live work list (to-dos, then sub-agents); `agents` narrows to the\n sub-agent rows; `background` lists background shells and automations;\n `files` lists touched files; `notepad` shows the workspace notes; `context`\n is a read-only session-facts list; `git` shows branch status; `price`\n shows cost. In every panel except `context`, rows are selectable and\n clickable and open their detail surface. `Alt+!`/`Alt+@`/`Alt+#`/`Alt+$`\n switch panels live.\n- `work_surface_top_height` (2–16) and `work_surface_side_width` (26–80):\n ceilings for the top strip's height and the side workbar's width. Both are\n normally persisted by dragging the divider rather than edited by hand; the\n strip still auto-fits its content below the ceiling.\n- `focus_texture` (`off`, `scrim`, or `grain`; default `off`): focus-context\n texture for modal views. `scrim` dims the already-rendered background\n outside the focused modal toward the theme surface; `grain` sprinkles\n sparse dots over blank cells there. The texture is static (no time\n component, so it is unaffected by `low_motion`), never writes over a cell\n that carries text, and preserves the 4.5:1 body-text contrast floor\n wherever both colors are resolvable. It is skipped entirely on frames\n below the ambient-life minimum size and when the focused modal already\n covers 90% or more of the frame. Set it live with\n `/config focus_texture scrim --save`.\n- `mention_menu_limit` (integer, default `128`): maximum number of\n `@`-mention popup candidates retained before the composer renders the\n visible window. The visible rows still depend on terminal height.\n- `mention_walk_depth` (integer, default `10`): maximum workspace depth for\n `@`-mention completion walks. Set to `0` for unlimited depth in deeply\n nested workspaces; keep the default in very large repos unless needed.\n- `mention_menu_behavior` (`fuzzy`, `browser`; default `fuzzy`): controls how\n `@`-mention completions are populated. `fuzzy` searches the workspace and\n applies mention frecency. `browser` lists only the immediate children of the\n currently typed directory segment in deterministic alphabetical order.\n- `show_thinking` (on/off)\n- `thinking_default_expanded` (on/off, default off): renders thinking blocks\n expanded initially when `show_thinking` is enabled. Space still toggles the\n selected block, and it decides only the blocks you have not touched: a block\n you expanded or collapsed yourself keeps that state if you change this\n setting later. This is useful in SSH/tmux environments where the Space\n binding may be intercepted.\n- `thinking_preview_lines` (integer, default `2`): how many body rows a\n **collapsed** completed thought still shows. `0` is header-only; `10` is\n the older dump. Live streaming preview is unchanged. Expand a block with\n Space, or set `thinking_default_expanded` to open every block.\n- `help_expand_groups` (on/off, default off): start Help/shortcuts with every\n group expanded. Default folds the long tail (Grok-style); type-to-filter\n still unfolds matches.\n- `pin_last_prompt` (on/off, default on): pin the last user prompt at the top\n of the transcript viewport after it scrolls off.\n- `show_tool_details` (on/off)\n- `inline_diffs` (`full`, `summary`, or `off`; default `full`): controls the\n inline presentation of successful structured File mutations. `full` shows a\n bounded red/green diff and semantic statistics, `summary` keeps only the\n statistics, and `off` keeps the calm changed-file outcome. All three retain\n the exact applied change in the selected File receipt's Alt/Option+V detail.\n Failure and cancellation never render a successful diff. Save the choice\n with `/config inline_diffs <mode> --save`.\n- `locale` (`auto`, `en`, `ja`, `zh-Hans`, `zh-Hant`, `pt-BR`, `es-419`, `vi`,\n `ko`; default `auto`): UI chrome locale. `auto` checks `LC_ALL`,\n `LC_MESSAGES`, then `LANG`; unsupported locale selections resolve to English.\n Every shipped pack holds full `en.json` parity, so no string falls back\n to English. The runtime also exposes the resolved locale in the system\n prompt as the fallback natural language for V4 reasoning and replies when the\n latest user message is ambiguous. Clear user language still takes priority;\n Chinese turns should produce Chinese `reasoning_content` and Chinese final\n replies even when the resolved locale is English.\n- `background_color` (`#RRGGBB`, `RRGGBB`, or `default`): optional main TUI\n background color applied to the root, header, transcript, and footer\n surfaces while preserving panel contrast.\n- `cost_currency` (`usd`, `cny`; default `usd`): currency used by the footer,\n context panel, `/cost`, `/tokens`, and long-turn notification summaries. The\n aliases `rmb` and `yuan` normalize to `cny`.\n- `default_mode` (`agent`, `plan`, or `operate`; legacy values are accepted for migration but are not live mode vocabulary)\n- `launch_screen` (legacy, migration-only): this historical `on`/`off` value\n is still accepted when reading an existing settings file, but it no longer\n changes behavior and is omitted from new saves. A fresh interactive launch\n always opens Tideline Startup; only an explicit resume or an explicit\n initial prompt enters the live session directly.\n- `sidebar_focus` (legacy, migration-only): the classic right sidebar this key\n configured was removed in the 0.9.4 rail unification. The key is still read\n once so old settings carry forward, then folds into the live keys:\n `pinned`/`work`/`plan`/`todos` become `rail_panel = \"pinned\"`,\n `agents`/`subagents` become `rail_panel = \"agents\"`, `context`/`session`\n become `rail_panel = \"context\"`, `tasks`/`auto` (the old default) become the\n `tasks` panel, `sessions` enables `sessions_rail`, and `hidden` turns the\n workbar off via `work_surface_placement = \"off\"`. An explicit `rail_panel`\n in the file always wins over the migrated value. Configure the workbar with\n `rail_panel` and `work_surface_placement`, not this key.\n- `sessions_rail` (`on`/`off`; default `off`): show the persistent Sessions\n list in the workbar. Rows list this workspace's recent\n non-archived sessions, newest first, with the active one marked; activating a\n row opens the session picker preselected on it (`/sessions open <id>`), so\n resume keeps its single implementation. Rows are projected from cached\n session metadata — the list never reads a transcript per frame, and never\n contacts a provider.\n- `session_auto_resume` (`on`/`off`; default `off`): reattach to this\n workspace's most recent session when Codewhale starts. Off by default so\n plain `codewhale` keeps starting fresh. `--resume`, `--continue`, and\n `--fresh` always take precedence. When it is on, startup still refuses to\n resume a session that is archived, fails to load, or is recorded against a\n different workspace; each of those falls back to a fresh transcript and says\n which session was skipped and why. It applies to the interactive launch only\n — `codewhale \"<prompt>\"` and `codewhale exec` are never silently prefixed\n with a prior conversation.\n- `max_input_history` (number of submitted input history entries; cleared\n drafts are also kept locally for composer history search). Note the spelling:\n the serde field on disk is `max_input_history`\n (`crates/tui/src/settings.rs:426`, default 100). `max_history` is the key\n name accepted by `/config set` and `settings.set()` (`settings.rs:1388`), not\n a settings.toml key — writing `max_history` into the file is silently\n ignored.\n- `default_model` (model name override)\n\n`/task digest` (alias `/tasks digest`) renders the canonical Work Graph\noperations and four-state To-do list as plain text, running work first. It\nreads the same snapshots as the styled Work surface and owns no parallel\nprogress state.\n\nPlan and Work are the everyday visible modes in the UI; Operate is an explicit\npreview entry while its Workflow control surface is still being built. Switch\nbetween them with `/mode`. For compatibility, older settings files with\n`default_mode = \"normal\"` still load as `agent`.\n\nLocalization scope is tracked in [LOCALIZATION.md](LOCALIZATION.md). The v0.7.6\ncore pack covers high-visibility TUI chrome only; provider/tool schemas,\npersonality prompts, and full documentation remain English unless explicitly\ntranslated later.\n\nReadability semantics:\n\n- Selection uses a unified style across transcript, composer menus, and modals.\n- Footer hints use a dedicated semantic role (`FOOTER_HINT`) so hint text stays readable across themes.\n\n### Token Quantities and Drivers\n\nDeepSeek V4 prefix caching makes token labels matter. These quantities are kept\nseparate:\n\n| Quantity | Meaning | Allowed to drive |\n|---|---|---|\n| Active request input estimate | Conservative estimate of the next request's live system prompt and transcript payload. | Header/footer context percent, auto-compaction trigger, opt-in Flash seam trigger, and emergency overflow preflight. |\n| Reserved response headroom | The effective request cap plus `1024` safety tokens. Normal no-override requests start at `65536`; a smaller route/provider ceiling narrows that value, and an explicit output override raises it only within the resolved route window and output ceiling. The identical cap reaches the wire and drives preflight, except on a route that states an output limit above half its known context window with no explicit output override: there compaction aims to keep a quarter of the window free (at most `65536`, and never more than the request cap), preflight refuses a request only when fewer than `128` output tokens would fit, and every request's `max_tokens` is what the window has left after that request's estimated input and the `1024` safety tokens, never above the request cap. Reasoning effort does not add a second hidden reservation. A separately published route input ceiling independently clamps the spendable input budget. | Emergency overflow budget checks only. |\n| Cumulative API usage | Provider-reported input plus output tokens summed across completed API calls; multi-tool turns may count the same stable prefix more than once. | Session usage and approximate cost telemetry only. |\n| Prompt cache hit/miss | Provider cache telemetry for the most recent call when available. | Cache-hit display and cost estimation only; never compaction or seam triggers. |\n| Context percent | Active request input estimate divided by the model context window. | Display only; it mirrors the active-input basis used by context safeguards. |\n| Cost estimate | Approximate spend from provider usage and configured DeepSeek rates. | Display only. |\n\nFor known context-window models, including 1M-class V4 models, replacement\ncompaction is enabled by default unless the user explicitly configures\n`auto_compact = false`. It fires at the active model's compaction threshold and\nreplaces old history with recent user context followed by one ordinary\ncheckpoint message. The standing system prompt remains unchanged. Unknown model\nids remain opt-in.\n\n### Command Migration Notes\n\nIf you are upgrading from older releases:\n\n- Old: `/codewhale`\n New: `/links` (aliases: `/dashboard`, `/api`)\n- Old: `/set model deepseek-reasoner`\n New: `/config` and edit the `model` row to `deepseek-v4-pro` or `deepseek-flash`\n- Old: visible `Normal` mode or `default_mode = \"normal\"`\n New: use `Agent` / `default_mode = \"agent\"`; legacy `normal` still maps to `agent`\n- Old: discover `/set` in slash UX/help\n New: use `/config` for editing and `/settings` for read-only inspection\n\n## Key Reference\n\n### Kimi Code membership model IDs\n\nThe exact `https://api.kimi.com/coding/v1` endpoint accepts `k3`, `k3-256k`,\n`kimi-for-coding`, and `kimi-for-coding-highspeed`. Use `k3-256k` for a fixed\n262,144-token K3 window; use bare `k3` with `context_window = 1048576` only\nwhen the membership plan includes the 1M entitlement. Both K3 ids use the same\nreasoning contract, and all four membership ids omit generic sampling fields.\n\n### Core keys (used by the TUI/engine)\n\n- `provider` (string, optional): `deepseek` (default), `deepseek-anthropic`, `nvidia-nim`, `openai`, `atlascloud`, `wanjie-ark`, `volcengine`, `openrouter`, `xiaomi-mimo`, `novita`, `fireworks`, `siliconflow`, `arcee`, `siliconflow-CN`, `moonshot`, `sglang`, `vllm`, `ollama`, `ollama-cloud`, `huggingface`, `modelscope`, `together`, `qianfan`, `openai-codex`, `anthropic`, `openmodel`, `zai`, `stepfun`, `minimax`, `deepinfra`, `sakana`, `longcat`, `opencode-go`, `meta`, `mistral`, `telecomjs`, `xai`, `orcarouter`, `modelstudio-token-plan`, `google`, `edenai`, or `custom`. Legacy `deepseek-cn` configs are still accepted as an alias for `deepseek`; DeepSeek uses the same official host [`https://api.deepseek.com`](https://api-docs.deepseek.com/) worldwide. `deepseek-anthropic` targets DeepSeek's Anthropic Messages-compatible endpoint at `https://api.deepseek.com/anthropic` using `DEEPSEEK_API_KEY`; `nvidia-nim` targets NVIDIA's NIM-hosted DeepSeek endpoints through `https://integrate.api.nvidia.com/v1`; `openai` targets a generic OpenAI-compatible endpoint, defaulting to `https://api.openai.com/v1`; `atlascloud` targets AtlasCloud's OpenAI-compatible endpoint at `https://api.atlascloud.ai/v1`; `wanjie-ark` targets Wanjie Ark's OpenAI-compatible endpoint at `https://maas-openapi.wanjiedata.com/api/v1`; `volcengine` targets Volcengine Ark's OpenAI-compatible coding endpoint at `https://ark.cn-beijing.volces.com/api/coding/v3`; `openrouter` targets `https://openrouter.ai/api/v1`; `xiaomi-mimo` targets Xiaomi MiMo's OpenAI-compatible endpoint, using `https://token-plan-sgp.xiaomimimo.com/v1` by default for Token Plan keys (`tp-...`) and `https://api.xiaomimimo.com/v1` for pay-as-you-go keys. For Token Plan accounts outside the Singapore default, set `base_url` explicitly or use `mode = \"token-plan-cn\"` for China and `mode = \"token-plan-ams\"` for Europe/Amsterdam; `novita` targets `https://api.novita.ai/openai/v1`; `fireworks` targets `https://api.fireworks.ai/inference/v1`; `siliconflow` targets SiliconFlow, defaulting to `https://api.siliconflow.com/v1`; `arcee` targets Arcee AI's OpenAI-compatible endpoint at `https://api.arcee.ai/api/v1`; `siliconflow-CN` targets the SiliconFlow China regional endpoint through `[providers.siliconflow_cn]`; `moonshot` targets Moonshot/Kimi, defaulting to `https://api.moonshot.ai/v1`; `sglang` targets a self-hosted OpenAI-compatible endpoint, defaulting to `http://localhost:30000/v1`; `vllm` targets a self-hosted vLLM OpenAI-compatible endpoint, defaulting to `http://localhost:8000/v1`; `ollama` targets Ollama's OpenAI-compatible endpoint, defaulting to `http://localhost:11434/v1`; `huggingface` targets Hugging Face Inference Providers at `https://router.huggingface.co/v1`; `modelscope` targets ModelScope's OpenAI-compatible inference API at `https://api-inference.modelscope.cn/v1`; `together` targets Together AI at `https://api.together.xyz/v1`; `qianfan` targets Baidu Qianfan at `https://api.baiduqianfan.ai/v1`; `openai-codex` targets official ChatGPT plan inference with Codewhale-owned OAuth at `https://api.openai.com/v1`; `anthropic` targets Claude's native Messages API; `openmodel` targets OpenModel's Anthropic-compatible Messages API at `https://api.openmodel.ai`; `zai` targets Z.ai at `https://api.z.ai/api/coding/paas/v4`; `stepfun` targets StepFun at `https://api.stepfun.ai/v1`; `minimax` targets MiniMax at `https://api.minimax.io/v1`; `deepinfra` targets DeepInfra at `https://api.deepinfra.com/v1/openai`; `sakana` targets Sakana AI Fugu at `https://api.sakana.ai/v1`; `longcat` targets Meituan LongCat at `https://api.longcat.chat/openai/v1`; `opencode-go` targets the subscription-backed OpenCode Go model-aware route (Chat Completions, Responses, or Messages according to the documented model) at `https://opencode.ai/zen/go/v1`; `meta` targets Meta Model API; `mistral` targets Mistral AI's OpenAI-compatible endpoint at `https://api.mistral.ai/v1`; `telecomjs` targets TelecomJS TokenHub at `https://aigw.telecomjs.com/v1`; and `xai` targets xAI's API-key or OAuth route.\n- `opencode-zen` (string provider value): selects the model-aware OpenCode Zen gateway through `[providers.opencode_zen]`. The default base URL is `https://opencode.ai/zen/v1`, the default model is `gpt-5.6`, and credentials come from `api_key`, `OPENCODE_ZEN_API_KEY`, or fallback `OPENCODE_API_KEY`—never ChatGPT/Codex OAuth. `OPENCODE_ZEN_BASE_URL` and `OPENCODE_ZEN_MODEL` are accepted. The selected model's wire comes from the curated Zen snapshot, then from the AI SDK package its Models.dev `opencode` row names (refresh with `codewhale models --update`): GPT, Grok, and Muse Spark use Responses; Claude and most Qwen rows use Anthropic Messages; DeepSeek, MiniMax, GLM, Kimi, `qwen3.8-max`, and the free rows use Chat Completions. Gemini, Models.dev rows marked `deprecated`, and models no loaded catalog lists fail closed because Codewhale has no proven supported wire contract for them. See the exact current model groups in [`PROVIDERS.md`](PROVIDERS.md#opencode-zen-protocol-catalog).\n- `minimax-anthropic` (string provider value): selects MiniMax's Anthropic-compatible Messages route through `[providers.minimax_anthropic]`. The default Base URL is `https://api.minimax.io/anthropic`; set `https://api.minimaxi.com/anthropic` for China. Keep the `/anthropic` suffix because Codewhale appends `/v1/messages`. The route uses `MINIMAX_API_KEY` and defaults to `MiniMax-M3`; `MiniMax-M2.7` is also registered. Official M3 input modalities are text, image, and video, with adaptive or disabled thinking. M2.7 is text-only and always keeps thinking enabled.\n- `api_key` (string, required for hosted providers): must be non-empty for DeepSeek/hosted providers (or set the provider API key env var). Self-hosted SGLang, vLLM, and local `ollama` can omit it. `ollama-cloud` requires a key saved for that provider or supplied by `OLLAMA_CLOUD_API_KEY`, then `OLLAMA_API_KEY`.\n- `auth_mode` (string, optional provider-table key): selects a provider-specific authentication contract. Kimi Code membership uses `auth_mode = \"api_key\"` (or omit the field), a key created in the [Kimi Code console](https://www.kimi.com/code/console), `base_url = \"https://api.kimi.com/coding/v1\"`, and bare `model = \"k3\"` for K3. Codewhale gives that route a safe 262,144-token baseline; set `context_window = 1048576` only when the Kimi Code plan includes 1M access (Allegretto and above). `k3[1m]` is a Claude Code-only convention, not an API model ID, and Codewhale rejects it instead of silently changing the wire model or assuming an entitlement. `model = \"kimi-for-coding\"` remains the valid K2.7 compatibility route available to all Kimi Code members. Legacy `auth_mode = \"kimi_oauth\"` fails closed with API-key guidance and never probes, reads, refreshes, or rewrites `kimi_cli`/`kimi_code_cli` credential files. First-class OAuth requires Codewhale's own vendor-registered client identity and remains tracked in #4417.\n- `base_url` (string, optional, `[providers.<name>]` key; see [Legacy top-level `base_url` and `api_key`](#legacy-top-level-base_url-and-api_key) for the older top-level spelling): defaults to `https://api.deepseek.com/beta` for DeepSeek's OpenAI-compatible Chat Completions API, including legacy `provider = \"deepseek-cn\"` configs. Other defaults are `https://api.deepseek.com/anthropic` for `deepseek-anthropic`, `https://integrate.api.nvidia.com/v1` for `nvidia-nim`, `https://api.openai.com/v1` for `openai`, `https://api.atlascloud.ai/v1` for `atlascloud`, `https://maas-openapi.wanjiedata.com/api/v1` for `wanjie-ark`, `https://ark.cn-beijing.volces.com/api/coding/v3` for `volcengine`, `https://openrouter.ai/api/v1` for `openrouter`, `https://token-plan-sgp.xiaomimimo.com/v1` for `xiaomi-mimo` when the API key starts with `tp-...` and `https://api.xiaomimimo.com/v1` otherwise, `https://api.novita.ai/openai/v1` for `novita`, `https://api.fireworks.ai/inference/v1` for `fireworks`, `https://api.siliconflow.com/v1` for `siliconflow`, `https://api.siliconflow.cn/v1` for `siliconflow-CN`, `https://api.arcee.ai/api/v1` for `arcee`, `https://api.moonshot.ai/v1` for `moonshot`, `https://api.minimax.io/v1` for `minimax`, `https://api.openmodel.ai` for `openmodel`, `https://api.z.ai/api/coding/paas/v4` for `zai`, `https://api.stepfun.ai/v1` for `stepfun`, `https://api.deepinfra.com/v1/openai` for `deepinfra`, `https://api.sakana.ai/v1` for `sakana`, `https://router.huggingface.co/v1` for `huggingface`, `https://api-inference.modelscope.cn/v1` for `modelscope`, `https://api.together.xyz/v1` for `together`, `https://api.baiduqianfan.ai/v1` for `qianfan`, `https://api.openai.com/v1` for `openai-codex`, `https://api.anthropic.com` for `anthropic`, `https://api.mistral.ai/v1` for `mistral`, `http://localhost:30000/v1` for `sglang`, `http://localhost:8000/v1` for `vllm`, `http://localhost:11434/v1` for `ollama`, and `https://ollama.com/v1` for `ollama-cloud`. Set `base_url = \"https://token-plan-cn.xiaomimimo.com/v1\"` for China-region Xiaomi MiMo Token Plan accounts or `base_url = \"https://token-plan-ams.xiaomimimo.com/v1\"` for Europe/Amsterdam accounts. Mistral-specific reasoning fields and polymorphic replay are enabled only on the documented first-party HTTPS `/v1` hosts; a custom Mistral base URL keeps generic Chat semantics. Set `https://api.deepseek.com` or `https://api.deepseek.com/v1` explicitly to opt out of DeepSeek beta features.\n- `ollama-cloud` route: select `provider = \"ollama-cloud\"`, configure `[providers.ollama_cloud]` when overriding the default `https://ollama.com/v1` / `gpt-oss:120b` tuple, and save a key from [Ollama account settings](https://ollama.com/settings/keys) with `codewhale auth set --provider ollama-cloud`. Ambient precedence is `OLLAMA_CLOUD_API_KEY`, then `OLLAMA_API_KEY`; arbitrary Ollama model IDs pass through unchanged.\n- Legacy Ollama Cloud migration: a released `provider = \"ollama\"` config whose normalized `[providers.ollama].base_url` is exactly `https://ollama.com/v1` is upgraded to the `ollama-cloud` runtime identity in memory. Only that exact tuple may read its old `ollama` provider table and secret slot. The config and secrets are never rewritten, and neighboring paths, HTTP downgrades, lookalike hosts, or an explicit `ollama-cloud` selection never consume the fallback.\n- `telecomjs` base URL and catalog: `[providers.telecomjs]` defaults to `https://aigw.telecomjs.com/v1`; `TELECOMJS_BASE_URL` overrides it. With `TELECOMJS_API_KEY`, `/models` refreshes a key-scoped catalog without mixing rows into another provider.\n- `edenai` gateway: select `provider = \"edenai\"`; `[providers.edenai]` defaults to `https://api.edenai.run/v3` and `deepseek/deepseek-v4-pro`. `EDENAI_API_KEY`, `EDENAI_BASE_URL`, and `EDENAI_MODEL` are accepted. Use `EDENAI_BASE_URL = \"https://api.eu.edenai.run/v3\"` for Eden AI's documented EU endpoint; the default `deepseek/deepseek-v4-pro` is only listed on the global catalog, so pair the EU endpoint with an EU-listed model such as `qwen/deepseek-v4-pro` via `EDENAI_MODEL` or `model`. The provider refreshes Eden AI's `/models` catalog, but leaves model-specific reasoning controls untouched because the gateway spans multiple model families.\n- `codewhale` (Codewhale API): select `provider = \"codewhale\"`; `[providers.codewhale]` defaults to `https://api.codewhale.net/v1` and `deepseek/deepseek-v4-pro`. The credential is a Codewhale account API key (`cwc_key_…`) with the `models:infer` scope, read from `CODEWHALE_API_KEY` or the `codewhale` secret-store slot; `codewhale account api-keys create --name <name> --use` mints one and saves it locally. `CODEWHALE_API_BASE` overrides the origin and must be HTTPS except on loopback. The model catalog is the account's own authenticated `GET /v1/models`: ids are `provider/model` and each row states its wire (`chat-completions` → `/v1/chat/completions`, `anthropic-messages` → `/v1/messages`, `responses` → `/v1/responses`). Connect the underlying provider keys with `codewhale account keys set <provider>`.\n- `concentrate` gateway: select `provider = \"concentrate\"`; `[providers.concentrate]` defaults to `https://api.concentrate.ai/v1` and `deepseek-v4-pro` over the OpenAI Responses wire. `CONCENTRATE_API_KEY`, `CONCENTRATE_BASE_URL`, and `CONCENTRATE_MODEL` are accepted. Model ids pass through verbatim (`gpt-5.6-sol`, `openai/gpt-5.6-sol`, or `concentrate/auto` for the gateway router). BYOK only; see [PROVIDERS.md](PROVIDERS.md#concentrate-notes).\n- `mistral` model and reasoning contract: `[providers.mistral]` defaults to `mistral-code-latest`; `MISTRAL_MODEL` overrides it and the generic `CODEWHALE_MODEL` override wins when both are set. The current picker also lists `mistral-medium-latest`, `mistral-small-latest`, and `mistral-large-latest`. On exact first-party HTTPS `/v1` routes, Medium and Small accept only `reasoning_effort = \"none\" | \"high\"` and replay polymorphic thinking blocks. Deprecated native Magistral IDs may still be configured explicitly, remain always-reasoning, and never receive the adjustable effort field.\n- `context_window` (integer, optional provider-table key): override the total context window for the active `[providers.<name>]` route when an OpenAI-compatible gateway, hosted model alias, or self-hosted runtime has a different limit than Codewhale's static model table. For example, `[providers.openai] context_window = 1000000` lets an OpenAI-compatible DashScope/Qwen route budget against a 1M-token window instead of the conservative fallback. For Kimi Code K3, keep `model = \"k3\"` and set `[providers.moonshot] context_window = 1048576` only when the membership plan includes 1M access; otherwise omit it to retain the 262,144-token safe baseline. The value must be greater than 0 and affects prompt context notes, compaction thresholds, context-pressure checks, and request output caps. Full resolution order, and how to see which rung produced the current window: [Context length (context window)](#context-length-context-window).\n- `path_suffix` (string, optional provider-table key): override the chat-completions path for OpenAI-compatible gateways that do not serve `/v1/chat/completions`. For example, `[providers.openai] path_suffix = \"/chat/completions\"` sends chat requests to the unversioned base URL plus `/chat/completions`; `models` and `beta/*` requests keep their normal routing.\n- `reasoning_stream_style` (string, optional provider-table key): override how streaming reasoning is separated from answer text for the active provider route. Use `separate_field` for `reasoning_content` / `reasoning` deltas, `inline_tags` for gateways that stream `<think>...</think>` inside `delta.content`, or `none` to render incoming content exactly as answer text. When unset, every Chat Completions route uses `separate_field` (Mistral's first-party route uses its typed thinking blocks); set `none` only for a gateway that streams its answer inside `reasoning_content`.\n- `[providers.<name>.auth]` (table, optional): provider-scoped auth source metadata. `source = \"command\"` stores a command argv plus optional `timeout_ms`; `source = \"secret\"` stores a `secret_id`. This slice lets provider readiness, `/provider`, and doctor JSON report the auth source class without exposing command argv output or secret values; executing commands and resolving external secret material is handled by the follow-up resolver work.\n- `insecure_skip_tls_verify` (bool, optional provider-table key): legacy compatibility key, disabled by default. When true on the active provider table, provider clients reject the configuration instead of skipping TLS certificate verification. Use `SSL_CERT_FILE` for corporate or private CA bundles; `codewhale doctor` reports stale uses of this setting.\n- `default_text_model` (string, optional): defaults to `deepseek-flash` for DeepSeek and `deepseek-anthropic`, `gpt-5.6` for OpenAI, `grok-4.6` for xAI, `deepseek-ai/deepseek-v4-pro` for NVIDIA NIM, `deepseek-ai/deepseek-v4-flash` for AtlasCloud, `deepseek-reasoner` for Wanjie Ark, `DeepSeek-V4-Pro` for Volcengine Ark, `deepseek/deepseek-v4-pro` for OpenRouter and Novita, `mimo-v2.6-pro` for Xiaomi MiMo, `accounts/fireworks/models/deepseek-v4-pro` for Fireworks, `deepseek-ai/DeepSeek-V4-Pro` for SiliconFlow and DeepInfra, `trinity-large-thinking` for Arcee AI, `kimi-k2.7-code` for Moonshot, `MiniMax-M3` for MiniMax, `GLM-5.3` for Z.ai, `step-3.7-flash` for StepFun, `ernie-4.0-turbo-8k` for Qianfan, `fugu` for Sakana AI, `deepseek-ai/DeepSeek-V4-Pro` for SGLang/vLLM, no fixed id for local Ollama (Codewhale adopts a chat-capable tag from the live local catalog — coder or tool-capable tags first, then the largest context — and never an embedding or reranker model), and `gpt-oss:120b` for Ollama Cloud. Hugging Face and Together AI both default to `deepseek-ai/DeepSeek-V4-Pro`; `openai-codex` defaults to `gpt-6.1-sol`; `anthropic` defaults to `claude-sonnet-5-5`; `openmodel` defaults to `deepseek-v4-flash`. Current public DeepSeek IDs include `deepseek-v4-pro` and `deepseek-flash` (V4.1 Flash, shipped as the unversioned id), both with 1M context windows, 384K max output, and thinking mode enabled by default. DeepSeek's live pricing/model page now labels the Pro backend `DeepSeek-V4-Pro-0813`; the callable API ID remains `deepseek-v4-pro`, so Codewhale does not send the backend label or the Claude Code-specific `deepseek-v4-pro[1m]` selector. DeepSeek retired `deepseek-chat` and `deepseek-reasoner` on July 24, 2026; direct first-party routes migrate both to `deepseek-flash`, with omitted reasoning settings preserving their former non-thinking (`off`) and thinking (`high`) intent. DeepSeek has also retired the `deepseek-v4-flash` and `deepseek-v4-flash-vision-exp` models: it still accepts both names, serves them with V4.1 Flash, and bills them at the `deepseek-flash` price, which Codewhale applies to turns from 2026-09-10 04:00 UTC. The `flash`, `deepseek-v4flash`, `flash-vision`, and `deepseek-v4flashvisionexp` shorthands select `deepseek-flash`. Explicit `reasoning_effort` wins, and provider-owned ids on Wanjie Ark, aggregators, self-hosted runtimes, and custom endpoints are not globally rewritten. SiliconFlow retains its own mapping: `deepseek-reasoner` and `deepseek-r1` select its Pro model while `deepseek-chat` and `deepseek-v3` select Flash. Provider-specific mappings translate `deepseek-v4-pro` / `deepseek-v4-flash` to each provider's model ID where supported. OpenRouter also recognizes recent large IDs such as `arcee-ai/trinity-large-thinking`, `minimax/minimax-m3`, `minimax/minimax-m2.7`, `xiaomi/mimo-v2.5-pro`, `qwen/qwen3.6-flash`, `qwen/qwen3.6-35b-a3b`, `qwen/qwen3.6-max-preview`, `qwen/qwen3.6-27b`, `qwen/qwen3.6-plus`, `qwen/qwen3.7-max`, `google/gemma-4-31b-it`, `moonshotai/kimi-k2.7-code`, `moonshotai/kimi-k2.6`, `nvidia/nemotron-3-nano-omni-30b-a3b-reasoning:free`, and `nvidia/nemotron-3-ultra-550b-a55b`; direct Arcee uses bare IDs such as `trinity-large-thinking` and `trinity-large-preview`; direct Moonshot recognizes `kimi-k3`, `kimi-k2.7-code`, and `kimi-k2.6`. The exact Kimi Code endpoint recognizes bare `k3` for K3 and `kimi-for-coding` for K2.7; those membership IDs are distinct from the direct Moonshot IDs and are never rewritten across routes. Direct MiniMax recognizes `MiniMax-M3` and the documented M2.x chat model IDs; direct Z.ai recognizes `GLM-5.3` (the default), `GLM-5.2`, `GLM-5.1`, and `GLM-5-Turbo`, and OpenRouter recognizes the matching `z-ai/glm-5.1`, `z-ai/glm-5.2`, `z-ai/glm-5.3`, and `z-ai/glm-5-turbo` IDs — `GLM-5.3` has been live on the Z.ai Coding Plan since 2026-08-13; it inherits its catalog metadata from `GLM-5.2` until Z.ai publishes distinct 5.3 numbers and carries no price, and an explicit `GLM-5.2` selection keeps its own id; direct Sakana recognizes `fugu` and `fugu-ultra-20260615`; direct Xiaomi MiMo recognizes chat IDs `mimo-v2.6-pro`, `mimo-v2.6-flash`, and `mimo-v2.5-pro-ultraspeed` (the retired `mimo-v2.5-pro` and `mimo-v2.5` are sent as `mimo-v2.6-pro` and `mimo-v2.6-flash`), while TTS IDs are selected through `codewhale speech` / `tts`. Generic `openai`, `atlascloud`, `wanjie-ark`, `xiaomi-mimo`, `arcee`, `moonshot`, `minimax`, `openmodel`, `zai`, `stepfun`, `qianfan`, `sakana`, local Ollama, and Ollama Cloud model IDs are passed through unchanged after known aliases are normalized. OpenRouter and SiliconFlow provider configs with a custom `base_url` also preserve explicit model values, which lets OpenAI-compatible gateways accept bare model IDs. Use `/models` or `codewhale models` to discover live IDs from your configured endpoint. `CODEWHALE_MODEL` overrides this for a single process; `DEEPSEEK_MODEL` is the legacy alias.\n- TelecomJS uses `deepseek-v4-pro` only as a conservative pre-refresh fallback. Once its key-scoped `/models` catalog is available, the picker uses those live rows; Codewhale omits unsupported reasoning request fields on this route.\n- `reasoning_effort` (string, optional): `auto`, `off`, `minimal`, `low`, `medium`, `high`, `xhigh`, `ultra` (`ultracode` is an alias of `ultra`), or `max`; defaults to the configured UI tier. DeepSeek Platform receives top-level `thinking` / `reasoning_effort` fields. Ollama Cloud's OpenAI-compatible Chat Completions route preserves its documented `none` / `low` / `medium` / `high` / `max` ladder (`off` is sent as `none`; `xhigh` and `ultracode` normalize to `max`). Direct xAI `grok-4.7` and `grok-4.6` on exact `https://api.x.ai/v1` receive top-level `reasoning_effort = \"low\" | \"medium\" | \"high\" | \"xhigh\"` (the ladder comes from the bundled catalog row; `grok-4.5` maps `xhigh` to `high`, and rows without a documented effort such as `grok-4.3` get no field); Grok reasoning cannot be disabled, so `off` normalizes to `high`, `max`/`ultracode` to `xhigh`, and `auto` leaves the field omitted so xAI's documented default `high` applies. A custom xAI-compatible `base_url` does not inherit that dialect. Direct Moonshot `kimi-k3` on exact `https://api.moonshot.ai/v1` is always-thinking and receives only top-level `reasoning_effort = \"low\" | \"high\" | \"max\"`; `off` normalizes to `low`, and `medium` to `high`. Kimi Code membership `k3` on exact `https://api.kimi.com/coding/v1` instead receives nested `thinking.effort`, and its `off` setting also normalizes to enabled `low`. Normal dispatched `auto` uses Codewhale's auto-reasoning selector and sends a concrete route-normalized tier; only an omitted reasoning setting leaves the provider default in control. Neighboring gateways and model/endpoint combinations retain the generic Moonshot contract. OpenAI Codex normalizes stale `off` to `low` and sends `max` / `ultracode` as Responses `xhigh`. Z.ai receives documented `thinking` controls and treats enabled thinking as the GLM coding high/max lane. NVIDIA NIM receives equivalent settings through `chat_template_kwargs`.\n- `verbosity` (string, optional): `normal` or `concise`. `normal` keeps the\n default conversational prompt. `concise` appends a prompt discipline block\n for direct, low-chatter output; CLI noninteractive commands (`exec` and\n `eval`) default to `concise` unless config/env/CLI overrides it.\n Override per process with `CODEWHALE_VERBOSITY` or the legacy\n `DEEPSEEK_VERBOSITY` alias.\n- `telemetry` (bool, optional): anonymous usage counting, **`true` by default\n in the current 0.9.12 source**. Notice version `5` names Codewhale and PostHog\n and describes the opt-out policy; no acceptance is invented for a default\n user. Existing explicit declines remain off. An explicit `false` here is the\n durable *opt-out*: it deletes the random install id, truncates buffered and\n dry-run events, and leaves a tombstone reasserted while the setting is false.\n It is a floor: `--telemetry true` and `CODEWHALE_TELEMETRY=1` lose to it.\n Use `/settings` or `codewhale config set telemetry true` to turn counting\n back on explicitly for new sessions through the existing privacy transition. `CODEWHALE_TELEMETRY`\n (legacy alias `DEEPSEEK_TELEMETRY`) and `--telemetry false` provide a run-scoped\n kill switch that stops collection and delivery without erasing the owner's\n state. A repo-local `.codewhale/config.toml` cannot set this preference.\n `codewhale config telemetry` shows the disclosure; `codewhale config get\n telemetry` reports preference and privacy status. Full schema and opt-out\n behavior: [`TELEMETRY.md`](TELEMETRY.md).\n- `telemetry_endpoint` (string, optional): where batches are POSTed. Leaving it\n unset selects the shipped default,\n **`https://telemetry.codewhale.net/v1/telemetry`** — the first-party ingest\n service described in [`TELEMETRY.md`](TELEMETRY.md), whose source is in\n `telemetry-ingest/`. This key decides only *where* a permitted session sends;\n it cannot override an opt-out. Setting it\n to the **empty string** is how you stay enabled and contact nobody: each batch\n is then written to `$CODEWHALE_HOME/telemetry/dryrun.jsonl` and no HTTP client\n is constructed at all, so you can read exactly what would have been sent. Any\n other value replaces the default outright. `https://` is required; plain\n `http://` is accepted only for loopback hosts, and there is no environment\n variable that overrides that refusal. A rejected endpoint turns telemetry off\n for the run rather than falling back to plaintext or to the default. Override\n per process with `CODEWHALE_TELEMETRY_ENDPOINT` (legacy alias\n `DEEPSEEK_TELEMETRY_ENDPOINT`), where an empty value means the same \"contact\n nobody\". A repo-local `.codewhale/config.toml` cannot set it.\n- `allow_shell` (bool, optional): in interactive TUI Agent sessions, omitting\n this keeps shell tools available with approval prompts; setting it to `false`\n hides shell tools. Headless, durable-task, and other noninteractive profiles\n keep the conservative omitted-field default and require `allow_shell = true`\n to expose shell. Plan mode always hides shell; Full Access enables shell and\n auto-approval.\n- `approval_policy` (string, optional): `on-request`, `untrusted`, or `never`. Runtime `approval_mode` editing in `/config` also accepts `on-request` and `untrusted` aliases.\n- `[approval] default_selection` (string, optional): which option an approval\n card highlights when it first appears — `deny` (default) or `allow_once`.\n `deny` means a reflexive Enter on a card you have not read refuses the call.\n The highlighted row is tagged `(Enter)`; press `y` to allow once.\n Set `allow_once` to restore the pre-v0.9.6 Enter-to-approve muscle memory\n (#5293). It moves the highlight only: which calls are prompted for is still\n `approval_policy` plus the rules in `permissions.toml`.\n\n ```toml\n [approval]\n default_selection = \"allow_once\"\n ```\n- `[approval] timeout_seconds` (integer, optional): bound how long an\n approval may wait — the TUI card and the Runtime API approvals the desktop\n app and web use alike. When the window elapses the call is refused\n (fail-closed) and recorded as a timeout, not as the operator's denial.\n Omitted or `0` waits indefinitely, which is the default everywhere: no\n approval is ever denied on your behalf unless you set this. Values above\n 24h clamp with a warning (#6101).\n\n ```toml\n [approval]\n timeout_seconds = 300\n ```\n- `sandbox_mode` (string, optional): `read-only`, `workspace-write`, `danger-full-access`, `external-sandbox`.\n The value can only narrow the sandbox the permission mode picks (Full\n Access: no wrapper; Ask and Auto-Review: `workspace-write`; Plan:\n `read-only`), so `danger-full-access` and `external-sandbox` take effect\n only in Full Access (`yolo`).\n Platform support is not identical. macOS uses Seatbelt when its runtime\n probe succeeds. Linux uses bubblewrap by default whenever `/usr/bin/bwrap`\n is installed and a probe proves it can confine a child; `prefer_bwrap =\n false` opts out and reports no OS command sandbox. Windows does not currently advertise an OS sandbox; its planned helper contract starts\n with process-tree containment only and must not be described as read-only\n filesystem isolation, workspace-write enforcement, network blocking,\n registry isolation, or AppContainer isolation until those are implemented.\n- The cross-layer relationship between mode admission, hooks, registered tool\n requirements, typed rules, auto-review, repo law, human approval, and the\n execution sandbox is defined in\n [Authorization order](AUTHORIZATION_ORDER.md).\n- **Read deny-list.** Every sandbox posture — `read-only` included — grants\n read access to the whole filesystem; the postures differ in what they may\n *write* and whether they may reach the network. The read deny-list narrows\n that, in two scopes. Codewhale's own file tools (`read`, `grep`,\n `file_search`, `read_media`) refuse every listed path. A shell command\n refuses only the *key material and other-principal* entries — SSH and GPG\n keys, `/etc/shadow`, `/etc/sudoers`, system and login keychains, browser\n profiles, other agents' credential stores, Codewhale's own secret stores,\n runtime daemon sockets, and anything in `sandbox_denied_read_paths`. A\n tool's own auth config is refused to the model but stays readable to the\n tool: `gh` cannot start without `~/.config/gh/hosts.yml`, so denying it to\n the process would disable the CLI without keeping its token out of reach.\n - `sandbox_read_denylist_defaults` (bool, default `true`): apply the built-in\n credential-store set — `~/.ssh`, `~/.gnupg`, cloud credential directories\n (`~/.aws`, `~/.config/gcloud`, `~/.azure`, `~/.kube`, …), `~/.netrc`,\n `~/.npmrc`, `~/.git-credentials`, macOS keychains, browser profiles,\n Codewhale's own secret stores, and `.env` files (but not `.env.example`\n and friends). Ordinary source, `Cargo.toml`, `~/.gitconfig`, `~/.cargo`,\n and `~/.npm` stay readable so builds and tests still work. Set `false` to\n restore the pre-0.9.12 full-disk-read behavior.\n - `sandbox_denied_read_paths` (list of paths): additional denied subpaths.\n `~` expands. These can never be exempted.\n - `sandbox_read_denylist_exempt` (list of paths): subtract a path from the\n *built-in defaults* when a project genuinely needs it. Deny wins over\n allow: this never reopens anything in `sandbox_denied_read_paths`.\n\n Exemption granularity is **whole-rule**, not per-file. An exempt path\n removes a built-in rule only when the rule's own path is at or below it,\n so exempting `~/.ssh/config` does nothing: the `~/.ssh` rule still denies\n it, because `~/.ssh` does not lie within `~/.ssh/config`. To reopen that\n one file you must exempt `~/.ssh` itself — which also reopens the private\n keys next to it. That is the documented tradeoff: there is no shipped way\n to narrow a built-in rule to \"everything except one file\"; copy what you\n need out of the denied tree instead. The one name-shaped rule, `.env`\n files, is exempted by *name* rather than by path: any exempt entry whose\n file name is exactly `.env` — bare `.env`, `~/.env`,\n `some/project/.env` — disables the entire `.env` filename rule, i.e.\n every `.env` and `.env.<name>` on disk rather than one project's.\n (`.env.example` and friends are never denied, so they need no exemption.)\n\n Enforced at two points, with different reach. Codewhale's own in-process\n tools, which the OS sandbox never wraps, refuse every rule — `read_file` /\n `read` / `read_media` for contents, `list_dir` / `file_search` for\n *enumeration* (listing a denied directory, or searching one, is refused just\n as Seatbelt blocks its readdir; a name search rooted above a denied tree\n skips entries inside it). Sandboxed shell commands (Seatbelt last-match-wins\n `deny file-read*` rules; bubblewrap masks each path) refuse only the\n key-material and other-principal rules, so an authenticated CLI can still\n read its own config. A refused read is always an explicit error, never an\n empty result, and the error names the path as the caller spelled it rather\n than a symlink target's real location.\n\n **This is defense-in-depth, not a security boundary.** It does not stop a\n hardlink to a denied file, a secret already copied into the workspace, an\n indirect read (`ssh-agent`, `security find-generic-password`, `aws sts …`),\n reads under `danger-full-access` shell commands, reads by MCP servers or\n other unwrapped child processes, or exfiltration of anything that *was*\n read. Keep least-privilege credentials and short-lived tokens doing the real\n work.\n- `permissions.toml` (sibling file, optional): typed permission rule records\n loaded next to `config.toml`, for example `~/.codewhale/permissions.toml`.\n This active user file is the only permission-rule source today; project\n config overlays do not load a project-local `permissions.toml`. A rule's\n optional `workspace` field is its repository scope, not a second source.\n Manually authored `[[rules]]` entries accept `tool`, optional `command` or\n `path`, optional absolute `workspace`, optional `command_exact = true`, and\n optional `action = \"deny\" | \"ask\" | \"allow\"`; omitted `action` defaults to\n `\"ask\"`. `workspace` limits a rule to that repository, while\n `command_exact = true` changes a command rule from the historical\n arity-aware prefix match to a complete-command match. `deny` blocks matching\n invocations before mode-based\n approval handling, `allow` skips approval for matching invocations, and\n `ask` forces approval only in modes that can prompt. Outside the TUI\n auto-approve path, a matching `ask` rule under `approval_policy = \"never\"`\n is rejected because no prompt can be shown. In Full Access / auto-approval sessions,\n `ask` rules do not downgrade the session into prompting or blocking; explicit\n `deny` rules still block according to the current execution-policy logic.\n\n In a supported approval card, press `S` to allow the request once and append\n exact `action = \"ask\"` rules to this file. For eligible safe requests, choose\n **Always allow this exact rule in this repo** (shortcut `P`) to append an\n `action = \"allow\"` rule with the current absolute `workspace` scope.\n Remembered shell grants set `command_exact = true`, so later commands with\n extra arguments do not inherit the grant. File and patch grants retain the\n exact workspace-relative paths produced by the existing validation path.\n Supported saves are intentionally narrow:\n `exec_shell` stores the exact approved command string; `write_file` and\n `edit_file` store the exact workspace-relative file path; `apply_patch`\n stores one exact workspace-relative `path` rule per validated touched file\n from apply-patch preflight. Existing exec command matching remains\n arity-aware for manually authored prefix rules; approval-card allow grants\n use complete-command matching. File paths are normalized to the same\n workspace-relative form used by runtime matching.\n\n `read_file` rules can still be authored manually when you want future reads\n of a specific path to ask, allow, or deny, but the approval UI does not save\n `read_file` rules. Commands classified as requiring approval or dangerous,\n critical approval cards, and repo-law prompts cannot save allow grants and\n continue to require review.\n\n `/permissions` (or `/permissions list`) is the narrow rule-management\n surface. It lists each numbered rule with the active user-file source, its\n exact effective matcher (tool-wide, command prefix, exact command, or exact\n normalized path), global or repository scope, and whether that scope\n applies in the current workspace. `/config ask-rules` remains a compatibility\n entry to the same list.\n\n Deletion is review-gated: `/permissions remove <number>` only previews the\n selected rule and prints a confirmation command. That command carries an\n opaque token bound to the exact file bytes and rule index; if another writer\n changes `permissions.toml`, confirmation fails instead of deleting a rule\n that moved into the old position. Confirmed removal and approval-card appends\n share the adjacent `permissions.toml.lock`, preserve unrelated TOML comments\n and formatting, and atomically replace the file. The running TUI reloads the\n user ruleset without clearing session-only approvals.\n\n This editor intentionally does not create or rewrite rules, persist deny\n choices from approval cards, expand globs, or create broad\n directory/recursive rules. Author those supported exact/prefix records\n manually when needed.\n- `[[hotbar]]` (array of tables, optional): user-owned 1-8 slot bindings for\n the TUI hotbar. Each entry has `slot`, `action`, and optional `label`.\n The Hotbar is hidden by default (#3807): omitting `hotbar` and setting\n `hotbar = []` both mean no hotbar. `/hotbar on` writes the eight default\n slots (`slash.workflow`, `slash.goal`, `slash.auto`, `mode.plan`,\n `mode.agent`, `mode.operate`, `palette.open`, `sidebar.toggle`) as explicit\n `[[hotbar]]` tables. When one or more `[[hotbar]]` tables are present, that\n list is the whole bar; missing slots stay empty. Invalid slots outside `1..=8` are skipped with a warning, duplicate\n slots use the later entry, and unknown action IDs are kept so the UI can show\n a disabled/unknown cell instead of silently deleting user config. Trusted\n user config, profiles, and managed config replace the whole list; project\n overlays cannot change hotbar bindings. Setup or wizard flows that persist\n hotbar bindings write this same schema to the resolved `~/.codewhale/config.toml`\n path, preserving legacy `~/.deepseek/config.toml` only when that fallback file\n is already the active config.\n\n ```toml\n [[hotbar]]\n slot = 1\n action = \"mode.plan\"\n label = \"Plan\"\n\n [[hotbar]]\n slot = 2\n action = \"session.compact\"\n ```\n- `[auto_review]` (table, optional): tool-call review policy — a deterministic floor plus a model guardian tier.\n This layer sits on top of the existing permission posture; it can hold or block a\n tool call, but it is not an auto-push, auto-merge, or hosted review service.\n Block rules are checked first, then the built-in safety floor, then allow\n rules. In Ask, a safety hold opens approval that no session grant or allow\n rule can answer; in Auto-Review or a non-interactive `never` posture it\n fails closed as a hard block. Full Access skips the built-in floor. The\n safety floor covers publish-like actions, destructive background/headless\n actions, and `curl`/`wget` uploads whose request body carries a credential\n file or a secret-named variable, or reads stdin fed from one of those or\n from `env`/`printenv`, even if an allow rule matches; a shell command,\n shell input or verifier over 64 KiB or 4,096 words whose text contains\n `curl` or `wget` anywhere, or one nested too deeply to read, is held too.\n Outside Full\n Access it also refuses, in every posture, a\n shell command, shell input or verifier containing a hidden Unicode character\n (bidirectional controls, zero-width and other invisible characters, tag\n characters, stray variation selectors, and terminal control characters\n except ASCII control keys in shell input), naming the code point and its\n position.\n\n ```toml\n [[auto_review.allow]]\n id = \"read-only-inspection\"\n action_kind = \"read\"\n reason = \"Read-only inspection is safe to run automatically.\"\n\n [[auto_review.block]]\n id = \"no-release-publish\"\n action_kind = \"publish\"\n reason = \"Release and publish actions require maintainer review.\"\n ```\n\n Rule matchers are exact `tool` and/or `action_kind`. At least one matcher is\n required. `action_kind` accepts the six decision-relevant kinds `read`,\n `write`, `shell`, `external`, `publish`, and `destructive`. Invalid names\n fail config validation instead of silently broadening into another policy\n class. In block rules, the old names remain conservative compatibility\n aliases: `network`, `git`, `mcp_action`, `browser`, and `unknown` map to\n `external`; `secret` maps to `destructive`; and `mcp_read` maps to `read`.\n Retired narrow kinds in allow rules fail validation rather than widening to\n a broader class. The retired `text_contains` matcher likewise fails\n validation instead of silently broadening an old intent-dependent rule.\n Fallback holds in interactive Auto-Review escalate to one stateless guardian\n request. The request contains the exact held call and deterministic\n observations as separate JSON fields. Conversation history, skill\n instructions, attached file contents, and other expanded model context are\n excluded. The guardian does not infer user intent or compute an authorization\n score. It exposes no tools and returns a risk level, allow/deny, and a\n rationale. High or critical risk cannot auto-run even if the model says\n allow. An oversized exact call is denied rather than truncated. Exactly one\n reviewer request is made; incomplete or malformed output, timeout,\n cancellation, provider failure, or an empty rationale all fail closed. The\n deterministic floor is never model-reviewed, and headless adapters use the\n deterministic-only tier. The pinned Codex, Kimi, and DeepSeek source\n boundaries are linked from [Permission Posture](MODES.md#permission-posture).\n Reviewer outcomes emit `tool.auto_review` audit events with\n `gate = \"guardian\"`.\n\n Auto-review decisions emit `tool.auto_review` audit events with\n `gate = \"deterministic\"` when tool\n audit logging is enabled. Future PreToolUse/PostToolUse hooks can add\n observer input around this layer, but the configured auto-review policy is\n evaluated before a tool call is allowed to proceed.\n- `managed_config_path` (string, optional): managed config file loaded after user/env config.\n- `requirements_path` (string, optional): requirements file used to enforce allowed approval/sandbox values.\n- `max_subagents` (int, optional): defaults to `64` and is clamped to `1..=128`.\n- `subagents.*` (optional compatibility table): manual per-role model pins\n for direct and Workflow `agent` starts. An explicit saved profile wins,\n then a manual role pin, then a unique saved role pin. Conflicting tool\n `model` or `model_strength` choices are refused before admission. Unpinned\n roles allow task model/strength choices before inherited defaults.\n `[subagents.roles.<role>] model = \"provider/model\"` folds into the existing override\n map and wins over `[subagents.models]`, then the convenience keys. Structured\n canonical role keys win over legacy aliases. Only this structured syntax\n separates the explicit provider from the model suffix; unknown providers\n fail before admission. Bare structured model ids inherit the active provider.\n Legacy scalar/map values preserve namespaced provider-owned ids unchanged.\n Supported convenience keys are\n `default_model`, `worker_model`, `scout_model`, `planner_model`,\n `reviewer_model`, `custom_model`, `max_concurrent`, `max_admitted`,\n `launch_concurrency`, `token_budget`, `api_timeout_secs`, and\n `heartbeat_timeout_secs`. The v0.9.x keys `explorer_model`, `awaiter_model`,\n and `review_model` remain accepted as aliases. The `[subagents]\n max_concurrent` value overrides\n top-level `max_subagents` and is also clamped to `1..=128`. `[subagents]\n max_admitted` (aliases: `max_total`, `admission_limit`) is the bounded total\n of queued plus running sub-agents; it defaults to `1024`\n (`MAX_SUBAGENT_ADMISSION`, `crates/tui/src/config/subagent_limits.rs:21`,\n applied at `config.rs:6400`) so high-fanout turns can queue and drain while\n runtime launch pressure remains bounded, and is clamped to\n `max_concurrent..=1024`. `[subagents]\n launch_concurrency` sets how many direct children start at once before the\n rest queue for a launch slot; it defaults to the resolved `max_subagents` cap\n and is clamped to `1..=max_subagents` (the deprecated\n `interactive_max_launch` key is accepted as an alias, with the new key\n winning when both are set). `[subagents] token_budget` is an optional\n aggregate token ceiling for each root `agent` run and its descendants; unset\n or `0` preserves unlimited legacy behavior. `[subagents] api_timeout_secs`\n controls the per-step API timeout for sub-agent model calls and is clamped to\n `1..=3600`, with `0` or unset preserving the 600 second default. A timeout\n after possible dispatch interrupts with a preserved checkpoint and unknown\n outcome. Only a transient connection timeout proven to precede dispatch uses\n exponential backoff, up to 5 retries.\n `[subagents] heartbeat_timeout_secs` controls stale running agent cleanup,\n defaults to `300`, and is clamped to `30..=3600` while staying above the\n resolved API timeout. `[subagents.providers.<provider>]` accepts the same\n fanout, depth, budget, and timeout knobs (`enabled`, `max_concurrent`,\n `max_admitted`, `launch_concurrency`, `max_depth`, `token_budget`,\n `api_timeout_secs`, `heartbeat_timeout_secs`) and inherits the global\n `[subagents]` value for any key you omit. Provider keys accept canonical\n names such as `deepseek`, `zai`, `openrouter`, `anthropic`, plus convenience\n aliases such as `glm` for Z.ai and `deepseek_api` for direct DeepSeek:\n\n ```toml\n [subagents]\n max_concurrent = 20\n launch_concurrency = 20\n max_admitted = 200\n max_depth = 6\n\n [subagents.providers.deepseek]\n max_concurrent = 20\n launch_concurrency = 20\n max_admitted = 200\n\n [subagents.providers.glm]\n max_concurrent = 4\n launch_concurrency = 3\n max_admitted = 12\n max_depth = 2\n\n [subagents.providers.openrouter]\n max_concurrent = 5\n launch_concurrency = 3\n max_admitted = 20\n ```\n\n `/config subagents status` prints both global values and the active\n provider's resolved profile so rate-limit tuning is visible in the TUI.\n `[subagents.models]` accepts lower-case Fleet role keys such as `worker`,\n `scout`, `planner`, `reviewer`, `builder`, and `verifier`; legacy type keys\n remain accepted during v0.9.x. Values are validated\n against the active provider at spawn time; direct DeepSeek requires DeepSeek\n IDs, while OpenAI-compatible/custom provider routes pass explicit model IDs\n through to that provider. To route a child to a different provider than the\n parent session, save a Fleet/AgentProfile with explicit `provider` and\n `model` fields (including user-named custom providers such as `lm-studio`)\n and call `agent(profile: \"...\")`; see [SUBAGENTS.md](SUBAGENTS.md).\n- `skills_dir` (string, optional): defaults to `~/.codewhale/skills` (each skill is\n a directory containing `SKILL.md`). After workspace trust, project skills\n take precedence, with `.codewhale/skills` ahead of compatible roots such as\n `.agents/skills`. Flat `./skills` requires explicit opt-in below. The runtime\n also discovers global agentskills.io-compatible `~/.agents/skills` and the\n broader Claude-ecosystem\n `~/.claude/skills`. With the default bundle policy and no `skills_dir` override, first launch installs versioned bundled skills for common\n workflows including skill creation, delegation, MCP/plugin scaffolding,\n documents, presentations, spreadsheets and PDFs. Feishu/Lark is outside the\n default pack. Only\n Codewhale-owned roots (`<workspace>/.codewhale/skills` and\n `~/.codewhale/skills`) are writable install/import targets; compatible harness\n roots stay read-only. Bare `/skills` opens Extensions → Skills (owned-only,\n zero network); `/skills manage` opens the manager. See [SKILLS.md](SKILLS.md)\n for the manager, audit statuses, provenance markers, and mutation rules, and\n [CLAUDE_PLUGIN_COMPAT.md](CLAUDE_PLUGIN_COMPAT.md) for the supported boundary\n between portable `SKILL.md` bundles and Claude Code plugin runtimes.\n- `[skills.bundled].install` (`\"auto\"` default, `\"all\"`, `\"none\"`): controls bundle writes independently of discovery. `auto` keeps deletion-as-opt-out and disables installation when `skills_dir` is set. `all` repairs enabled missing skills into `$CODEWHALE_HOME/skills` (normally `~/.codewhale/skills`) even with a discovery override. `none` writes nothing and leaves the marker untouched. Modified and managed bodies are preserved in every mode.\n- `[skills.bundled].selected` / `disabled` / `explicit_only` (arrays of names, default `[]`): selection limits installation and discovery; empty selects the whole bundle. Disabled wins and reveals lower-root copies. Explicit-only retains user invocation while removing the shipped copy from the model index and model loading. Aliases share canonical policy. These settings affect recognized global bundle copies only and never delete files. Profile fields merge separately; `[]` clears an inherited list. `doctor` warns about unknown/ignored entries and `/skills inspect` reports per-path reasons. See [bundled skills](SKILLS.md#bundled-skills).\n- `[skills].listing_max_description_chars` (integer, default `120`, range `1..=40000`): ambient-index description cap, shared by parent and child prompts and `/skills inspect`.\n- `[skills].listing_budget_fraction` (finite number, default `0.002`, greater than `0` and at most `1`): share of the route window for that same index, estimated at four characters per token and clamped to `2400..=40000` characters. Explicit profile fields override inherited values independently; `/context` uses the same resolution. Full skill loading is unaffected.\n- `[skills].scan_codewhale_only` (bool, default `false`): when `true`, session\n skill discovery ignores cross-tool roots such as `.claude/skills`,\n `.opencode/skills`, `.cursor/skills`, and `~/.agents/skills`. Codewhale still\n scans `<workspace>/.codewhale/skills`, `~/.codewhale/skills`, and any explicit\n `skills_dir` override. The Skills Manager can still toggle a local compatible\n audit scan independently of this runtime knob — see [SKILLS.md](SKILLS.md).\n- `[skills].flat_workspace_root` (bool, default `false`): opt in to the flat\n `<workspace>/skills` compatibility root after workspace trust. Without this\n opt-in it is an audit candidate only; an explicit `skills_dir` remains an\n alternative. `scan_codewhale_only = true` excludes the flat compatibility\n root regardless of this flag, unless it is the explicit `skills_dir`.\n- `[skills].registry_url` / `[skills].max_install_size_bytes` (optional): used by\n `/skills --remote`, `/skills suggest <task>`, `/skills sync`, and `/skill\n install|update`. The default manager open path does not contact the registry.\n- `[verifier].enabled` (bool, default `false`): enables automatic\n claim-of-done verifier preview once that runtime trigger is active. The\n manual `run_verifiers` tool is still available when this is false.\n- `mcp_config_path` (string, optional): defaults to `~/.codewhale/mcp.json`, with\n legacy `~/.deepseek/mcp.json` fallback when the Codewhale path is absent.\n Custom paths must be absolute; a relative value falls back to the user-global\n path so changing the launch directory cannot silently change the MCP pool.\n It is visible in `/config` and can be changed from the TUI. The new path is\n used immediately by `/mcp`; run `/mcp reload` to rebuild the model-visible\n MCP tool pool without restarting the TUI. A headless runtime still needs a\n restart because its `ConfigReload` request does not refresh MCP connections.\n- `notes_path` (string, optional): defaults to `~/.codewhale/notes.txt`, with\n legacy `~/.deepseek/notes.txt` fallback when the Codewhale path is absent, and\n is used by the model-visible `note` tool.\n- `[memory].enabled` (bool, optional): defaults to `false`. When `true`,\n the TUI loads the user memory file into a `<user_memory>` prompt block,\n enables `# foo` quick-capture in the composer, surfaces the `/memory`\n slash command, and registers the `remember` tool. The same toggle is\n available via `DEEPSEEK_MEMORY=on`.\n- `memory_path` (string, optional): anchors the native memory store. The\n configured filename is **not** the file that is written. Under the Native\n backend (the only backend) the store is re-rooted to\n `<parent-of-memory_path>/memory/global/MEMORY.md` — so the default\n `~/.codewhale/memory.md` yields `~/.codewhale/memory/global/MEMORY.md`\n (plus workspace-scoped files and a rebuildable SQLite FTS5 index). See\n [`MEMORY.md`](MEMORY.md) for the full feature surface (`# foo` composer\n prefix, `/memory` slash command, `remember` tool, opt-in toggle).\n- `snapshots.*` (optional): side-git workspace snapshots for file rollback:\n - `[snapshots].enabled` (bool, default `true`)\n - `[snapshots].max_age_days` (int, default `7`)\n - snapshots live under\n `~/.codewhale/snapshots/<project_hash>/<worktree_hash>/.git`, with legacy\n `~/.deepseek/snapshots/...` fallback when only the legacy state exists, and\n never use the workspace's own `.git` directory\n- `context.*` (optional):\n - `[context].project_pack` (bool, default `false`): include a deterministic\n project context pack (a large pretty-printed directory listing) in the\n stable prompt prefix (#4781). Useful for weak tool-calling models; the\n model can rebuild the same information with one `File` call.\n - The removed seam-manager keys (`enabled`, `verbatim_window_turns`,\n `l1_threshold`, `l2_threshold`, `l3_threshold`, `seam_model`) are\n ignored if an older config still carries them; they no longer load.\n- `compaction.*` (optional, config.toml): how a compaction pass behaves once\n it fires. `auto_compact` / `auto_compact_threshold_percent` (settings.toml)\n still decide *when* it fires. Both keys are absent by default, and absent\n means the built-in behaviour, unchanged:\n - `[compaction].summary_instructions` (string, default empty): standing\n operator instructions appended to the summarizer prompt as a clearly\n delimited \"Additional instructions from the operator\" section on **every**\n pass, manual and automatic — the effort-free counterpart to a one-off\n `/compact <focus>`, which still composes after this text. Useful for\n \"always list exact file paths and line numbers\", \"always restate open\n decisions and their trade-offs\", \"always write a TL;DR first\". Truncated\n at 4 000 characters with a warning naming the key; whitespace-only reads\n as unset. The summarizer still runs with no system prompt and no tools —\n this suffix is the only operator-authored input it sees.\n - `[compaction].retained_user_message_tokens` (int, default `20000`, clamped\n to `2000`–`200000`; also accepted as `retained_user_message_max_tokens`):\n token budget for the recent plain user messages kept **verbatim** in the\n replacement history. Raising it keeps more of the user's own earlier\n messages instead of only whatever the lossy summary captured; the\n last-round survival contract still applies on top. The `/compact` receipt\n names the effective budget and whether operator instructions were applied,\n so you can tell the knob took effect.\n- `retry.*` (optional): HTTP retry settings. The following backoff fields govern\n ordinary transport retries; HTTP 429 uses the independent policy below:\n - `[retry].enabled` (bool, default `true`)\n - `[retry].max_retries` (int, default `3`): ordinary transport retries\n - `[retry].initial_delay` (float seconds, default `1.0`)\n - `[retry].max_delay` (float seconds, default `60.0`)\n - `[retry].exponential_base` (float, default `2.0`)\n - `[retry].jitter` (bool, default `true`): randomize ordinary transport backoff\n - `[retry].jitter_factor` (float, default `0.1` = ±10%; clamps to `0.0..=1.0`)\n - `[retry].respect_retry_after` (bool, default `true`): wait for a server\n `Retry-After` preference retained for legacy retry configuration; it does not\n disable HTTP 429 server minima\n\n HTTP 429 recovery has its own validated `[retry.rate_limit]` fields:\n\n - `max_attempts` (integer, default `20`, range `1..=1000`): total attempts,\n including the first\n - `recovery_budget_secs` (integer seconds, default `600`, range `1..=86400`)\n - `initial_delay_secs` (integer seconds, default `5`)\n - `max_delay_secs` (integer seconds, default `60`), with\n `1 <= initial_delay_secs <= max_delay_secs <= 86400`\n\n This policy always honors server minima and uses equal jitter, independently\n of the legacy `initial_delay`, `max_delay`, `exponential_base`, `jitter`,\n `jitter_factor` and `respect_retry_after` fields. `max_retries = 0` does not\n disable 429 recovery; set `retry.rate_limit.max_attempts = 1` to disable only\n automatic 429 retries, or `retry.enabled = false` to disable this HTTP retry\n loop. A safe pre-dispatch HTTP/1.1 fallback inside a stream-opening operation\n remains possible. The one-hour limit on actual provider pauses and automatic sleeps\n still applies. See [HTTP rate-limit retries](rate-limit-retries.md) for the\n budget, server-minimum and active-time boundaries.\n\n `[retry]` schedules HTTP-request retries inside the client. Each physical\n streaming request gets its own `stream.open_timeout_secs` response-header\n wait. Provider backoff and honored `Retry-After` waits happen between\n attempts, outside that timer, so the configured retry count remains\n reachable. Inference retries require an ordinary rate limit or a proven\n transient connection failure before dispatch. Only that connection failure\n can switch once to HTTP/1.1; a header stall cannot. Both inference clients\n disable redirects and reqwest protocol retries. Explicit plan-window\n exhaustion is a terminal quota error. Server failures, uncertain timeouts\n and interrupted streams preserve unknown coverage and stop without\n automatically reissuing inference. Read-only catalog GETs retain bounded\n retries at their actual method owner. The `[stream]` keys below bound safe\n pre-dispatch recovery and stream consumption; legacy `tui.stream_max_*`\n keys remain configuration fallbacks.\n- `[notifications]`: notification delivery, attention, categories and audio share one\n policy. `quiet = true`, `method = \"off\"`, `condition = \"never\"` and disabled\n categories suppress both the banner and Codewhale's selected sound.\n- `notifications.method`: `auto` (default), `osc9`, `kitty`, `ghostty`, `bel`, `off`.\n- `notifications.condition`: `unfocused` (default), `always`, `never`. When absent,\n the legacy `tui.notification_condition` remains the fallback. `always` also\n bypasses the duration threshold; `unfocused` requires two seconds away.\n- `notifications.threshold_secs`: nonnegative integer, default `30`.\n- `notifications.include_summary`: boolean, default `false`.\n- `notifications.sound`: optional `off`, `whale`, `bell`, `beep`, `file`.\n A selected value controls audio across enabled categories. Absent keeps legacy\n `completion_sound` and `event_sound` choices; `off` overrides both.\n- `notifications.sound_file`: custom local WAV path for `sound = \"file\"` or legacy\n `completion_sound = \"file\"`.\n- `notifications.subagent_completion`: `always`, `final-only` (default), `off`.\n Covers all background work that finishes: sub-agents, background shells and\n durable tasks. `final-only` sends one notice naming everything that finished\n once no agent, workflow or durable task is still running; a running shell\n (a dev server, a watcher) never holds it back. `always` sends one per item.\n- `notifications.quiet`: boolean, default `false`.\n- `notifications.events`: six boolean categories, all enabled by default; see below.\n- `notifications.completion_sound`: legacy completion cue, default `off`, with the\n same values as `sound`. Used only when `sound` is absent.\n- `notifications.event_sound`: legacy `enabled` (default `false`), `events`\n (default `[\"turn-complete\", \"approval-needed\"]`), and `quiet` (default `false`).\n `min_interval_ms` (default `2000`) applies to each category's audio in both modes.\n- `tui.alternate_screen` (string, optional, default `auto`): which screen an interactive session starts on. `auto` and `always` start on the TUI-owned alternate screen; `never` starts in inline mode — a ratatui viewport the full height of the terminal with no alternate screen, so the shell's scrollback survives the session and stays scrollable after exit. `/fullscreen` and `/inline` switch it in-process; a switch that the terminal refuses rolls back and says why. Inline mode paints the whole transcript inside its viewport — nothing is written into the host scrollback while the session runs.\n- `tui.mouse_capture` (bool, optional, default `true` on non-Windows terminals and on Windows Terminal/ConEmu/Cmder when the alternate screen is active; `false` on legacy Windows console and inside JetBrains JediTerm — PyCharm/IDEA/CLion/etc. — where mouse-event escapes leak into the input stream as garbled text, see #878 / #898): enable internal mouse scrolling, transcript selection, right-click context actions, and transcript scrollbar dragging. TUI-owned drag selection copies the intersected cells, removes visual wrap-column line breaks from paragraphs, and keeps selection scoped to the transcript pane; the payload is Markdown source by default, see `tui.selection_copy_markdown` below. Set this to `false` or run with `--no-mouse-capture` for raw terminal selection; set it to `true` or run with `--mouse-capture` to opt in anywhere it's defaulted off. On raw terminal selection, especially on legacy Windows console or when mouse capture is disabled, selection may cross the right workbar and include visual wraps because the terminal, not the TUI, owns the selection.\n On Linux, finishing a transcript or composer selection quietly copies text to\n PRIMARY, leaving the regular clipboard unchanged. Middle-click inside the\n composer pastes PRIMARY at the pointer without submitting it. This uses native\n X11 or Wayland data control; compositors must support PRIMARY selection. Over\n SSH without a forwarded graphical display, use your terminal's selection/paste\n gestures or `--no-mouse-capture`. Explicit Copy still uses the regular clipboard.\n\n- `tui.selection_copy_markdown` (bool, optional, default `true`): copy TUI-owned\n transcript selections (drag release, context-menu Copy, and `Cmd+C`/`Ctrl+C`\n on an active selection) as Markdown source instead of rendered text. Every\n intersected cell serializes through the same canonical projection `Ctrl-Y`\n and `/copy` use — user and assistant cells keep their authored Markdown,\n other cells keep their full transcript form — partial intersections round out\n to whole cells, cells join with blank lines, and a toast names the copied\n cell count. Set `false` to copy the rendered text as displayed. Composer\n selections and the Linux PRIMARY auto-copy are unchanged; PRIMARY always\n carries rendered text.\n\n- `tui.stream_chunk_timeout_secs` (int, optional, default `900`): per-SSE-chunk idle timeout for streamed model responses. Slow local or compatible servers can raise this with `/config stream_chunk_timeout_secs <seconds>` (add `--save` to write canonical `stream.chunk_timeout_secs`); `0` maps to the default and explicit values must be `1..=3600`. The legacy `DEEPSEEK_STREAM_IDLE_TIMEOUT_SECS` env var is still honored when this key is omitted.\n- `tui.osc8_links` (bool, optional, default on for macOS/Linux, off for Windows): emit OSC 8 escape sequences around URLs in transcript output so supporting terminals (iTerm2, Terminal.app 13+, Ghostty, Kitty, WezTerm, Alacritty, recent gnome-terminal/konsole) can open them with the terminal's link gesture—usually Cmd-click on macOS and Ctrl-click on Linux/Windows. Terminals without OSC 8 support render the plain label and ignore the escape. The escapes are emitted out-of-band (not inside buffer cells), so column corruption is not a concern; set `false` only for terminals that misrender the OSC 8 terminator itself. Windows legacy consoles default off; opt in with `true`.\n- `tui.max_model_steps` (int, optional, default uncapped): optional model-step ceiling for one ordinary turn. Omission or `0` leaves model steps uncapped; explicit positive values are preserved (`4294967295` is the compatibility value for no limit). Headless `exec` and Fleet workers also have no implicit model-step ceiling; `exec --max-turns N` and positive worker budgets still apply. At exhaustion the turn ends `Failed` with `Maximum model steps reached before completion (limit: N)` after one bounded final-report response when needed. Cumulative wall-clock and per-stream limits remain independent. Active interactive goal turns use `goal.max_steps` instead (also uncapped by default); see the Goal loop section below.\n- `tui.turn_wall_clock_secs` (int, optional, default: no limit): cumulative per-turn wall-clock budget in seconds, measured across every model step of one turn (not per request). Time blocked on a human approval is excluded. Omitted or `0` means no limit; positive values are preserved without an added runtime ceiling. When exhausted the turn stops before authorizing another billable request with a message naming the limit and the key to raise.\n- `tui.stream_max_resumes` (int, optional, default `3`): turn-level recovery after a proven transient connection failure before request dispatch. `0` disables this additional recovery; values clamp to `0..=10`. Missing response headers, empty or partial streams, sleep and network drops never authorize inference replay.\n- `tui.stream_max_transparent_retries` (int, optional, default `2`): retained for configuration compatibility, clamped to `0..=10`. Interrupted streams no longer trigger transparent inference reissues, including before visible content.\n- `tui.stream_max_errors` (int, optional, default `5`): retained for configuration compatibility; `0` selects the default and positive values clamp to `1..=50`. Core ends an uncertain stream on its first read error or error frame regardless of this value.\n- `tui.stream_open_timeout_secs` (int, optional, default `45`): wait for a streaming request's response headers (connection setup included). A header stall ends with an unknown outcome and does not replay over HTTP/1.1. Omitted or `0` falls back to `CODEWHALE_STREAM_OPEN_TIMEOUT_SECS`, then the default; values clamp to `5..=300`.\n- `tui.connect_timeout_secs` (int, optional, default `30`): TCP/TLS connect timeout for the model HTTP client. Omitted or `0` uses the default; values clamp to `1..=300`.\n- `tui.force_http1` (bool, optional, default `false`): pin the model HTTP client to HTTP/1.1, for provider edges or proxies that mishandle long-lived HTTP/2 streams. `CODEWHALE_FORCE_HTTP1=1` does the same; either one pins.\n- `tui.stream_max_content_mb` (int, optional, default `10`) and `tui.stream_max_duration_secs` (int, optional, default `1800`): per-step caps on one stream's accumulated content and wall-clock duration. `0` selects the default; values clamp to `1..=512` MB and `10..=86400` seconds.\n- `transcript.prose_measure` (positive integer, optional, default absent = full width): wrap cap, in columns, for prose cells — user messages, assistant answers, and reasoning/thinking blocks — in the live transcript (#5436). Absent (or `0`) spends the full content width, consistent with tool/status cells and the #5322 wide-frame decision; the former 105-column prose rail is gone. Set a positive whole number (e.g. `prose_measure = 120` under `[transcript]`) to restore a bounded reading measure on ultrawide terminals. Narrow terminals always keep their content width — the cap clamps from above only. Tool, diff, and status cells never inherit this cap. Invalid values (negative or non-integer) are rejected at startup with a `transcript.prose_measure` config error. Resolved once per render pass, so the main transcript cache and the full-screen overlay always agree on the effective width.\n- `hooks` (optional): lifecycle hooks configuration (see `config.example.toml`).\n- `features.*` (optional): feature flag overrides (see below).\n\n### Stream and transport settings\n\n`[stream]` is the canonical table for model-stream policy and its HTTP clients.\n`codewhale config dump` and `codewhale config get stream` show effective values\nfrom the runtime resolver, including environment fallbacks and clamps; this does\nnot persist defaults. Set or unset individual fields with, for example,\n`codewhale config set stream.open_timeout_secs 120` and\n`codewhale config unset stream.open_timeout_secs`. Per-run\n`--set stream.open_timeout_secs=120` uses the same validation.\n\n| `[stream]` key | Default | Accepted behavior | Legacy `[tui]` fallback |\n| --- | --- | --- | --- |\n| `open_timeout_secs` | 45 | Positive values clamp to 5–300; 0 or omission falls through to the environment/default | `stream_open_timeout_secs` |\n| `chunk_timeout_secs` | 900 | 0 selects default; positive values clamp to 1–3600 | `stream_chunk_timeout_secs` |\n| `max_resumes` | 3 | Proven transient pre-dispatch recovery only; 0 disables, maximum 10 | `stream_max_resumes` |\n| `max_transparent_retries` | 2 | Compatibility value; interrupted streams never reissue inference | `stream_max_transparent_retries` |\n| `max_stream_errors` | 5 | Compatibility value; Core ends on the first uncertain stream error | `stream_max_errors` |\n| `max_duration_secs` | 1800 | Per-stream wall clock; 0 selects default, positive values clamp to 10–86400 | `stream_max_duration_secs` |\n| `max_content_mb` | 10 | Per-stream content; 0 selects default, positive values clamp to 1–512 MiB | `stream_max_content_mb` |\n| `connect_timeout_secs` | 30 | TCP/TLS setup; 0 selects default, positive values clamp to 1–300 | `connect_timeout_secs` |\n| `force_http1` | false | Boolean; a truthy environment pin always enables HTTP/1.1 | `force_http1` |\n| `tcp_keepalive_secs` | 30 | Idle time before TCP keepalive probes; 0 disables, positive values clamp to 1–3600 | none |\n| `http2_keep_alive_interval_secs` | 15 | PING interval on active HTTP/2 connections; 0 disables, positive values clamp to 1–3600 | none |\n| `http2_keep_alive_timeout_secs` | 20 | PING acknowledgement deadline; 0 selects default, positive values clamp to 1–3600 | none |\n\nFor each field, an explicit canonical value wins over the legacy `[tui]` field,\nincluding zero or false; an absent canonical field preserves the legacy value.\nThe existing environment precedence is unchanged: a positive configured header\nwait wins over `CODEWHALE_STREAM_OPEN_TIMEOUT_SECS` (then its `DEEPSEEK_` alias);\nzero falls through to those variables. An omitted chunk timeout uses\n`CODEWHALE_STREAM_IDLE_TIMEOUT_SECS` (then its `DEEPSEEK_` alias), whereas an\nexplicit zero uses 900. `CODEWHALE_FORCE_HTTP1` (legacy `DEEPSEEK_FORCE_HTTP1`)\nis ORed with the selected config flag, so a false config flag cannot defeat a\ntruthy environment pin. Profile overlays merge canonical fields independently.\n\nHTTP transport values apply to all newly constructed model, catalog and HTTP/1\nfallback clients. They do not rebuild an active client, govern MCP/other network\nservices, or send HTTP/2 PINGs on idle pooled connections. HTTP/2 knobs have no\neffect when HTTP/1.1 is pinned. The operating system controls TCP probe details;\nthese settings do not disable certificate validation. `[retry]` still owns the\nHTTP-request backoff schedule independently of stream-level budgets.\n\n### Workspace notes\n\n`/note` manages a simple notes file in the current workspace at\n`.codewhale/notes.md` (legacy `.deepseek/notes.md` is the fallback path when\nno `.codewhale/notes.md` exists yet).\nExisting `/note <text>` usage still appends a note.\nThe management forms are:\n\n| Command | Action |\n|---|---|\n| `/note <text>` | Append a note (legacy shorthand) |\n| `/note add <text>` | Append a note explicitly |\n| `/note list` | List notes with temporary 1-based numbers |\n| `/note show <n>` | Show the full note at number `n` |\n| `/note edit <n> <text>` | Replace note `n` with new text |\n| `/note remove <n>` | Delete note `n`; `rm` and `delete` are aliases |\n| `/note clear` | Empty the workspace notes file |\n| `/note path` | Show the resolved workspace notes path |\n\nThe numbers shown by `/note list` are not stored in the file; they are derived\nfrom the current order each time notes are read. This keeps the file format\ncompatible with the existing `---`-separated notes.\n\n### User memory\n\nUser memory is split across one top-level path setting and one opt-in\ntoggle table:\n\n```toml\n# Anchors the store only — actual writes go to\n# ~/.codewhale/memory/global/MEMORY.md (see MEMORY.md).\nmemory_path = \"~/.codewhale/memory.md\"\n\n[memory]\nenabled = true\n```\n\nNotes:\n\n- `memory_path` stays at the top level beside `notes_path` and\n `skills_dir`; it is not nested under `[memory]`.\n- The configured path is an **anchor**: its parent directory gains\n `memory/global/MEMORY.md`, workspace-scoped files, and `index.db`.\n Pointing `memory_path` at the native layout path itself would double-nest\n (`…/memory/global/memory/global/MEMORY.md`); keep the legacy-style\n anchor filename.\n- `DEEPSEEK_MEMORY_PATH` overrides the anchor path from the environment.\n- `DEEPSEEK_MEMORY=on` (also `1`, `true`, `yes`, `y`, or `enabled`)\n flips the feature on without editing `config.toml`.\n- The feature is inert when disabled: no file is injected, `# foo`\n falls through to normal message submission, and the model does not\n see the `remember` tool.\n- See [`MEMORY.md`](MEMORY.md) for examples and the full `/memory`\n command surface.\n\n### Goal loop (`[goal]`)\n\nOperate-mode goals run to their completion gate with no default token, time, or\ncontinuation ceiling (#5052). Token/time budgets, when explicitly supplied,\nare telemetry only and do not stop a goal. Users who want a circuit breaker can\nopt into one:\n\n```toml\n[goal]\n# Optional safety backstop on automatic goal continuation passes.\n# Default: 0 (unlimited). Set a positive value to opt into a ceiling.\nmax_continuations = 100\n\n# Optional cancellable quiet period between successful turns. This is useful\n# for coordinator goals that should poll on a cadence instead of keeping one\n# provider turn open. Default: 0 (continue immediately).\ncontinuation_delay_seconds = 300\n\n# Optional per-turn model-step ceiling while a goal is active (#6512).\n# Default: uncapped (0 or absent). Positive values are preserved.\n# This bounds each provider turn, never the number of continuation passes.\nmax_steps = 0\n\n# Optional independent judges (#6013). Default: false (the model's own\n# update_goal call is final). When true, one adversarial critique of the claim\n# runs before `update_goal` completes (complete_judge) or blocks (blocked_judge)\n# the goal. Each judged call is billed as auxiliary usage.\ncomplete_judge = false\nblocked_judge = false\n```\n\nThe effective delay is capped at 86,400 seconds (24 hours); use an automation\nfor schedules that are less frequent than once per day.\n\nWhen an explicit backstop fires, the goal pauses with a status message naming\n`[goal] max_continuations` and a warning is logged; resume the goal after\ninspecting progress, or raise/disable the backstop.\n\n`[goal] max_steps` governs one engine turn at a time. Like ordinary interactive\nturns, omitted or `0` leaves model steps uncapped. Explicit per-invocation\nceilings — `exec --max-turns N`, child-worker caps — always win over it. At exhaustion\nit gets one bounded final report and the turn classifies as budget-exhausted. An\nunfinished goal then pauses with the BudgetLimit reason instead of re-arming\nanother goal turn — resume it explicitly after reviewing the report. Wall-clock\nand stream protections are separate and still apply.\n\nWith `[goal] complete_judge = true`, a refuted completion claim keeps the goal\nactive and records the judge's findings as critical gaps. A missing,\nunparseable, uncertain or failed verdict never completes the goal; it records\none fixed gap, so a persistent judge outage ends in the existing no-progress\npause rather than a silent pass. With `[goal] blocked_judge = true`, a refuted\nblocker keeps the goal active, and an unverifiable one hands the goal back as a\nyield that your next message resumes. The judge reads only the objective, the\nevidence and the verification receipt, not the transcript or the diff. The\noperator `POST /v1/threads/{id}/goal/complete|block` routes and `/goal`\ncommands stay ungated.\n\nThe delay starts only after a successful turn while an explicitly created goal\nis still active. `/goal pause`, `/goal done`, `/goal blocked`, `/goal clear`,\nEsc, or Ctrl+C cancels a pending continuation before another provider request\nstarts. Failed turns and policy/route failures never schedule another turn.\nOnly the numeric cadence is stored in config; no prompt, credential, or secret\nis persisted for the loop.\n\n### Reasoning-only recovery (`[reasoning_only]`)\n\nWhen a model finishes with only reasoning and no answer or tool call, Codewhale\ncan request an answer again. A clean stop permits two retries by default:\nthe first keeps the request unchanged, and later retries carry a request-scoped\nnudge. The default nudge is `Continue: give your answer, or make the next tool call.`\n\n```toml\n[reasoning_only]\nmax_reprompts = 2\non_output_limit = false\n```\n\nSet `on_output_limit = true` to also retry `length`/`max_tokens` stops that\nproduced no answer or tool call, including providers that did not stream their\nhidden reasoning. This option is off by default. Output-limit retries carry the\nnudge immediately and share the same `max_reprompts` allowance. Set\n`max_reprompts = 0` to disable both reasoning-only and output-limit recovery;\ntransport and empty-stop recovery have their own existing limits.\n\nThis applies to ordinary interactive, exec and Runtime API engines and Fleet\nworkers. Workers use the captured parent configuration. RLM keeps its separate\ncompletion/FINAL/REPL stop protocol and does not use this option.\n\nEvery retry is a fresh request and may repeat the model's reasoning. It does not\ncarry hidden reasoning forward, raise output limits or bypass route, context,\nturn, cancellation or tool-admission controls. Each request can incur provider\ncost. Existing output-limit overrides still apply, and request preparation may\nreduce or refuse an allowance when input grows. Restart existing engines after\nchanging these settings.\n\nThe nudge appears in one outbound request without joining conversation history\nor later resumed model context. Retry status retains its stable protocol prefix\nand localizes its human text. Its receipt is visible in the transcript; Fleet\nalso stores an owner-bound prepared-request receipt in its existing private\nartifact before dispatch. A prepared receipt does not claim a completed model\ncall. Existing version-1 artifacts remain readable.\n\nTo customize or disable the nudge:\n\n```toml\n[reasoning_only]\nreprompt_message = \"Give your answer, or make the next tool call.\"\n```\n\nAn empty `reprompt_message = \"\"` disables the nudge, including on output-limit\nretries; the retry still counts against `max_reprompts` and uses the unchanged\nrequest. Configuration with recovery enabled does not guarantee the model will\ncomplete a long proof within its allowance.\n\n### Notifications\n\nNotification controls are available in the existing `/config` settings view,\nfrom terminal commands, and through the CLI. All write the same `config.toml`\nkeys. Terminal changes apply immediately; add `--save` to keep them. CLI writes\napply when the next process loads its configuration.\n\n```sh\ncodewhale config set notifications.sound whale\ncodewhale config set notifications.events.approval-needed false\ncodewhale config set notifications.quiet true\ncodewhale config get notifications\ncodewhale config unset notifications.quiet\n```\n\n```text\n/config notifications sound whale --save\n/config notifications condition unfocused --save\n/config notifications quiet true\n/config notifications status\n```\n\nNested CLI edits preserve TOML types, unrelated keys and comments. Unset removes\nonly the selected leaf. `notifications.sound legacy` in the TUI, or CLI unset of\n`notifications.sound`, restores previous sound choices. Invalid values are\nrejected before file or session changes. In an active TUI profile, saved edits\nupdate its notification table when it owns one; otherwise they update the\ninherited root table. The settings detail keeps saved and current values distinct.\n\n```toml\n[notifications]\nmethod = \"auto\" # auto | osc9 | kitty | ghostty | bel | off\ncondition = \"unfocused\" # unfocused | always | never\nthreshold_secs = 30\ninclude_summary = false\nsound = \"whale\" # optional; sound is opt-in, not enabled by default\nquiet = false\n\n[notifications.events]\nturn-complete = true\nsubagent-terminal = true\napproval-needed = true\ninput-needed = true\nelevation-needed = true\nmodel-notify = true\n```\n\n`quiet = true` mutes every category without changing saved choices. `method =\n\"off\"` also stops both banner and selected audio. Disabling a category stops its\nsound. Attention and duration gates apply before any sink runs. Title animation\ncompletion is silent; only the authorized notification event can request audio.\nSuccessful turn completion is a notification category; failed/cancelled turns do\nnot create a success notification.\n\n`auto` chooses a recognized terminal protocol or the existing macOS native\nfallback; unknown terminals remain unsupported and never invent a bell. `bel`\nis an audio-only transport: one selected cue is dispatched, without a second\ntransport bell. With `sound = \"off\"`, that transport is silent. `osc9`, `kitty`\nand `ghostty` use their terminal notification protocols; tmux passthrough is\npreserved. Terminal/OS notification preferences still govern display, attribution\nand any sound the host itself adds.\n\nBy default the terminal must stay unfocused for two seconds. `condition =\n\"always\"` allows foreground notifications and bypasses the duration threshold;\n`\"never\"` suppresses all delivery. The canonical condition takes precedence over\nlegacy `[tui].notification_condition`.\n\nThe bundled `whale` is a 1.55-second original whale-inspired cue, with no\nthird-party recording. It remains an opt-in candidate pending listening approval.\nWAV playback uses a background worker: macOS `/usr/bin/afplay`, Linux `aplay`\nfrom [ALSA utilities](https://github.com/alsa-project/alsa-utils), or Windows\n`PlaySoundW`. A missing player/file or unsupported platform has no fallback bell.\nOnly one WAV plays at a time. A worker-start receipt is a dispatch attempt, not\nproof of audible playback or OS acceptance. The existing macOS `osascript`\nbanner retains Script Editor attribution; this Core change does not provide a\nbranded native Apps banner.\n\n#### Previous sound settings\n\nWhen `notifications.sound` is absent, completion uses a non-off\n`completion_sound` selection, and other events use the existing event allow-list.\nThese are compatibility inputs to the same audio decision, not separate playback\npaths. When the global sound is selected, it takes precedence over the previous\ncompletion/event choices. The default remains silent unless a legacy sound or\nexplicit `bel` transport was already selected.\n\n```toml\n[notifications.event_sound]\nenabled = false\nevents = [\"turn-complete\", \"approval-needed\"]\nmin_interval_ms = 2000\nquiet = false\n```\n\nLegacy event cues use one bell for completion, subagent completion, input and\nmodel notices; approval/elevation cues use two. The per-category repeat interval\nsurvives settings refreshes. Old unknown event names are ignored on load; new\nCLI/TUI edits require names from the six categories above.\n\nLocal approval/input/elevation prompts and error receipts remain available when\nexternal notifications are muted. Action prompts use the selected UI language\nand retire when that request settles. Repeated live notices keep their first\nexpiry; a later routine update does not hide an unresolved warning at completion.\nOptional plugin suggestion toasts and contextual tips share one session guidance\nbudget. `/config contextual_tips off` hides those toasts while preserving\nrequired notices and explicit plugin review requests.\n\n#### What a notification can contain\n\nA desktop notification is a glance surface: on macOS it can appear on the\nlock screen, and on every platform it is visible to anyone near the machine.\nCodewhale therefore builds notifications from a typed payload with a fixed\nper-event disclosure policy rather than from whatever text was on hand:\n\n| Event | Shown | Never shown |\n|---|---|---|\n| Turn complete | status line (+ elapsed/cost when `include_summary`), preview of the assistant's reply | — |\n| Sub-agent finished | status line, agent id, preview of the child's summary line | — |\n| Approval needed | the tool name | the tool description, the command, the arguments |\n| Input needed | \"Answer the question in the terminal to continue\" | the question |\n| Sandbox elevation needed | the tool name and the denial reason | the command |\n| `notify` tool | model-supplied title and body | — |\n\nEvery field is capped (80 characters for the status line, 120 for the\nidentifier, 200 for the preview), stripped of control bytes and escape\nsequences, and passed through a redactor that replaces credential-shaped\nstrings with `[redacted]`, reduces absolute local paths to `…/basename`,\nand replaces raw tool JSON with `[details hidden]`. The redactor is\ndeliberately over-eager: an unbroken 40-character run has no word\nstructure, so it is redacted even when it is not a secret.\n\n#### macOS: why the banner says \"Script Editor\"\n\nOn macOS terminals that provide no notification escape of their own —\nApple Terminal, the VS Code and JetBrains embedded terminals, plain tmux\nwithout `LC_TERMINAL` — `method = \"auto\"` falls back to `osascript`'s\n`display notification`. That command posts on behalf of the *bundled*\nhost process, and `/usr/bin/osascript` is unbundled, so macOS attributes\nthe banner to `com.apple.ScriptEditor2`. That attribution supplies the\nScript Editor icon and owns the System Settings → Notifications entry\n(alert style, previews, Do Not Disturb). `display notification` has no\nicon parameter, so this cannot be fixed from the notification code; it\nneeds Codewhale to ship a real `.app` bundle. Tracked in\n[#4834](https://github.com/codewhale-hq/CodeWhale/issues/4834). In the meantime,\niTerm2, WezTerm, Ghostty, and kitty are matched first and use their own\nnotification protocols, and `method = \"osc9\"` / `\"bel\"` / `\"off\"` opt out\nof the `osascript` path explicitly.\n\n## Automations in the terminal\n\nOpen `/automation`, then choose **New automation** (`n`) or **Edit automation**\n(`e`). The form edits the name, multiline prompt, schedule, model, workspace,\nand enabled status. `Tab` moves between fields; `Enter` inserts a newline in\nthe prompt. Schedule presets include daily, weekly, hourly, once, and a custom\nRRULE. Time fields accept `HH:MM`; their arrow controls change the time by\n15 minutes. Weekly day buttons and model search support mouse and keyboard.\n\nThe next-run preview uses the scheduler's local time zone, shown with its UTC\noffset. An enabled automation can run after **Save** (`Ctrl+S`); a paused one\nhas no scheduled next run. **Cancel** (`Esc`) discards the draft. Editing keeps\nexisting permission settings, custom schedules, and additional workspace\nentries unless the corresponding supported field is explicitly changed.\n\nSchedules are evaluated in the machine's local time zone against the wall\nclock. A wall time that does not exist on a spring-forward day is skipped and\nan ambiguous fall-back time fires once. Occurrences missed while Codewhale was\nclosed, asleep, or still running the previous occurrence are coalesced: the\nnext start runs one catch-up occurrence and then continues from the next\nfuture slot, never replaying every missed slot. An occurrence never starts\nwhile an earlier run of the same automation is still queued or running. Each\nrun is recorded durably with its status, timing and error; a run that needs a\ntool approval has no operator to ask and fails once the approval wait expires.\n\nChoosing a concrete model pins both the model and its exact configured\nprovider, including named custom routes. Later changes to the active provider\ndo not move that automation's pin. The default-model choice and legacy\ndefinitions without a provider pin keep the runtime's existing default\nbehavior. New automation records use schema v2 and tasks use v3 so older\nruntimes reject records whose provider pins they cannot preserve.\n\n## Lifecycle Outbox (`[lifecycle_outbox]`)\n\nThe lifecycle outbox is an opt-in, machine-readable stream of session,\nturn, and sub-agent lifecycle events. With a path configured, Codewhale\nappends one JSON line per event to that file — for interactive TUI\nsessions *and* headless `codewhale exec` runs — so a supervisor\n(terminal multiplexer wrapper, automation harness, alerting setup) can\nreact to what happened without scraping the screen or installing per-hook\nshell commands. Unset or empty `path` = the feature is **off** and\nbehavior is unchanged.\n\n```toml\n[lifecycle_outbox]\npath = \"~/.codewhale/notifications/outbox.jsonl\" # unset/empty = OFF\nwebhook_url = \"\" # optional; POSTs events as JSON when set\nwebhook_token = \"\" # optional bearer token for webhook_url\n```\n\n### Events emitted\n\n| Event | Kind | Fired at |\n|---|---|---|\n| `turn_start` | `turn.started` | a new turn begins (TUI TurnStarted; `exec` at message dispatch) |\n| `turn_end` | `turn.completed` / `turn.failed` / `turn.interrupted` | turn completion, kind projected from the turn status |\n| `turn_stalled` | `turn.stalled` | the stall watchdog recovers a wedged turn |\n| `subagent_spawn` | `subagent.spawned` | a sub-agent is spawned |\n| `subagent_complete` | `subagent.completed` | a sub-agent reaches a terminal state |\n| `session_start` | `session.started` | interactive session start |\n| `session_end` | `session.ended` | interactive session end |\n\n### File contract\n\nEach line is a `RuntimeEventEnvelope`:\n\n```json\n{\"schema_version\": 1, \"seq\": 3, \"event\": \"turn_start\", \"kind\": \"turn.started\",\n \"thread_id\": \"…\", \"turn_id\": \"…\", \"item_id\": null, \"timestamp\": \"…\",\n \"created_at\": \"…\", \"payload\": {…}}\n```\n\n- `seq` is monotonic per outbox file and recovers from the last written\n line when a new process opens the file.\n- Lines are written one complete JSON line per append, serialized by an\n internal writer task and flushed before the next event; concurrent\n sessions writing the same path do not interleave bytes mid-line, but\n separate processes each continue from their own recovered `seq`, so seqs\n can repeat across processes sharing one file — prefer one file per\n process for strict uniqueness.\n- Parent directories are created lazily on the first event.\n- Payloads are constructed from bounded, pre-redacted fields only — never\n raw tool arguments, environment, or full transcript text. Free-form\n fields (error messages, previews) are capped at the notification limits\n (80 headline / 120 detail / 200 preview characters) and stripped of\n control bytes.\n\n### Webhook delivery\n\nWith `webhook_url` set, every event is additionally POSTed as\n`{\"at\": \"<ISO 8601 timestamp>\", \"event\": {…}}` with\n`Authorization: Bearer <webhook_token>` when a token is configured.\nDelivery is best-effort: failures are logged and dropped, never retried\ninto the agent loop, and a failing webhook never blocks the local file\nappend.\n\n## Control Socket (`[control_socket]`)\n\nPer-session control surface for supervised operation: with the feature\nenabled, the interactive TUI binds one unix domain socket per *running*\nsession at `<sessions-dir>/<session-id>/control.sock` (mode `0600`;\n`<sessions-dir>` is the same directory the session store uses, typically\n`~/.codewhale/sessions`). The socket is removed with the session's\nartifact directory, and a stale socket left by a crashed process is taken\nover by the next launch. Unset or `enabled = false` = the feature is\n**off** (the default) and behavior is unchanged. Unix-only; on other\nplatforms the key parses but no socket is bound.\n\n```toml\n[control_socket]\nenabled = false # default: OFF\n```\n\nThe socket speaks newline-framed JSON-RPC, one request per connection:\nwrite one request line, read one response line, close.\n\n```json\n{\"id\":\"1\",\"method\":\"message\",\"params\":{\"text\":\"hello\"}}\n{\"id\":\"2\",\"method\":\"interrupt\",\"params\":{}}\n{\"id\":\"3\",\"method\":\"status\",\"params\":{}}\n```\n\n- `message` — delivers `text` as a structured user message through the\n ordinary composer dispatch path; dispatched immediately when idle,\n queued when a turn is in flight (the response's `delivery` field says\n which).\n- `interrupt` — the Esc-shaped cancel of the active turn; `cancelled`\n reports whether active work was in flight.\n- `status` — answers `turn_state` (`idle` / `in_progress` / `waiting`) and\n `goal` (`objective`, `status`, `paused`).\n\nSuccess responses echo the request id with a `type`-tagged result;\nfailures carry `error.code` (`invalid_request`, `command_error`,\n`timeout`, `server_unavailable`). Requests are bounded at 1 MiB per line\nand a handler that does not answer within 5 s is reported as `timeout`.\n\n## Tool Catalog\n\nCodewhale loads a small core native tool catalog by default and leaves less\ncommon native tools discoverable through ToolSearch. To keep specific native\ntools loaded on every request, add them to `[tools].always_load`:\n\n```toml\n[tools]\nalways_load = [\"Git\", \"notify\"]\n```\n\n### Script tools and overrides\n\nScripts in `~/.codewhale/tools/` (or `[tools].plugin_dir`) that start with a\n`# name:` header become model-visible tools, and `/plugin tools` lists them.\nThe script reads the tool's JSON input on stdin and writes a JSON\n`ToolResult` (`{\"content\": \"...\", \"success\": true}`) on stdout.\n\n```sh\n#!/usr/bin/env sh\n# name: word_count\n# description: Count words in the given text\n# schema: {\"type\":\"object\",\"properties\":{\"text\":{\"type\":\"string\"}}}\n# approval: required\n```\n\n`# approval:` takes `suggest` (the default) or `required`; either way the\ntool follows the session's approval setting. A script cannot approve itself:\n`approval: auto` is no longer supported, so such a script gets the default,\nand the runtime log (`~/.codewhale/logs/`) and `/plugin tools` name it.\n\nA script cannot replace a built-in tool either. A script whose `# name:` is\nalready registered is not loaded. `[tools.overrides]` may disable a built-in,\nor add a script or command tool under a name of its own:\n\n```toml\n[tools.overrides]\n\"Web\" = { type = \"disabled\" } # turn a built-in off\n\"audited_shell\" = { type = \"script\", path = \"audit-shell.sh\" } # a new tool\n\"Bash\" = { type = \"script\", path = \"audit-shell.sh\" } # refused: Bash is built in\n```\n\nA `script` or `command` override keyed by a built-in is refused, and the\nbuilt-in stays active. A status line names the key once per session, and the\nruntime log records it. To route a\nbuilt-in through your own wrapper, disable the built-in and register the\nwrapper under a new name. An override keyed by a drop-in script's name still\nreplaces that script. Relative `path` values resolve against the plugin\ndirectory.\n\n### `request_user_input` limits\n\n`request_user_input` asks the user a short batch of multiple-choice questions.\nBoth ceilings are configurable (#5949): raise `user_input_max_questions` when a\nresearch or planning workflow legitimately needs more clarifications, lower it\nwhen interactive triage should stay terse.\n\n```toml\n[tools]\nuser_input_max_questions = 6 # default 6, clamped to 1..=10\nuser_input_max_options = 4 # default 4, clamped to 2..=10\n```\n\nThe effective values are applied in three places at once: the tool's JSON\nschema (`minItems` / `maxItems`), its model-visible description, and the\npayload validator. A rejected payload names the key to raise, so the model can\neither resize the batch or tell the user which setting to change.\n\n### User-input wait timeout\n\nQuestions from `request_user_input` wait until answered or canceled by default\n(#6003). An omitted setting or `0` leaves the wait unbounded; a positive value\ncancels the question when that many seconds pass, capped at 86,400 (24 hours).\nHeadless `exec` runs have no\nresponder, so `request_user_input` is withheld there by default:\nthe model reports the tool absent and finishes instead of stalling.\n\n```toml\n[tools]\nuser_input_timeout_seconds = 300 # opt into 5 minutes; omitted or 0 waits indefinitely; maximum 86400\n```\n\nThis key governs question waits only. Approvals have their own clock,\n`[approval] timeout_seconds`, which waits indefinitely unless you set it.\nWall-clock and stream protections elsewhere are unaffected.\n\n## Feature Flags\n\nFeature flags live under the `[features]` table and are merged across profiles.\nDefaults are enabled for built-in tooling, so you only need to set entries you\nwant to force on or off.\n\n```toml\n[features]\nshell_tool = true\nsubagents = true\nweb_search = true # enables deferred Web; the flag name is retained for config compatibility\napply_patch = true\nmcp = true\nexec_policy = true\ncode_mode = true # execute_tools composes MCP/plugin/native calls; false defers it behind tool_search\nverify_tool = true # agent-callable `verify` self-critique; false removes it from the tool catalog\nvision_model = false # true routes image analysis to the [vision_model] model (see above)\nextension_host = false # experimental: run reviewed plugins' native code (see EXTENSIONS.md)\n```\n\n`code_mode` is on by default: `execute_tools` is advertised from the first\nrequest and nested calls go through the same permission gate as direct calls\n(see [Tool surface](TOOL_SURFACE.md#code-mode-execute_tools)). Set\n`code_mode = false` to defer it behind `tool_search` again.\n\n`extension_host` is experimental and off by default. Turning it on lets reviewed\nplugins run their `native` TypeScript/JavaScript tool and slash-command code in\na separate Node (or, opt-in, Bun) process; toggling it in either direction\nchanges the plugin activation policy, so every plugin is reviewed again after a\nrestart. Its `[extension_host]` table (`runtime`, `node`, `bun`) and what the\nhost's sandbox does on each platform are in\n[Writing an extension tool](EXTENSIONS.md).\n\n`[extension_host] mcp_backend` (user config only) chooses the MCP client\nindependently of the `extension_host` flag: `\"host\"` (default) uses the official\nMCP SDK behind the Rust broker; `\"rust\"` keeps the previous Rust client for one\nrelease. A host refusal never falls back to Rust. See\n[MCP connection lifecycle](MCP.md#connection-lifecycle).\n\nEvery flag has a row in [`docs/features.toml`](features.toml), the feature\nregistry; a test fails when a flag and its row disagree.\n\nYou can also override features for a single run:\n\n- `codewhale --enable web_search`\n- `codewhale --disable subagents`\n\nUse `codewhale features list` to inspect known flags and their effective state.\nThe native `/config` view also includes a read-only **Experimental** section\nfor experimental feature flags. It shows each flag's effective enabled/disabled\nstate and whether that state comes from the default or a configured override.\nChange feature flags in `[features]` or with `--enable` / `--disable`; the\n`/config` section is an audit surface, not a stability promise. Goal and\nWorkflow preview rows may appear there as reserved entries until those workflows\ngraduate behind real gated flags.\n\n## Web Search Provider\n\n`web_search` uses keyless Firecrawl by default. Runtime failure or an exhausted\nkeyless quota degrades visibly through DuckDuckGo and Bing. China deployments\ncan explicitly select Baidu, Metaso, Volcengine, or a trusted SearXNG endpoint;\nCodewhale does not guess geography from locale or model provider.\n\nConfigured API providers are attempted first. Runtime failure or an empty\nresult visibly degrades through DuckDuckGo and then Bing; the structured search\nreceipt records every hop. Missing configuration and network-policy denials\nfail closed without sending the query to another provider.\n\n**Provider-native search.** On routes whose provider offers its own web-search\ntool (OpenAI, xAI, Anthropic, DeepSeek, Kimi and others), that search can run\nahead of the configured provider. It is a separate model call on the active\nroute. `[search] native` decides the order:\n\n- unset (default): native search leads only when no search provider is\n configured; a provider chosen in `[search] provider`,\n `CODEWHALE_SEARCH_PROVIDER`, or a Tavily key wins;\n- `native = true`: native search leads even when a provider is pinned;\n- `native = false`: native search is never used.\n\nNative citations are link-checked before they are returned. Each cited URL is\nrequested through the same guarded fetch as `Web` fetch (SSRF, DNS and redirect\nchecks, `[network]` policy): a `HEAD`, then a one-byte ranged `GET` when `HEAD`\nis refused, at most four at a time and five seconds each. The request uses the\naddress as it will appear in `results`, without userinfo, fragment or\ncredential-like query parameters; an address whose query string repeats the\nsearch query is not requested and is removed. Only sources that\nanswer with a 2xx status stay in `results`; the receipt lists each removed one\nas `citation_unverified` with its reason. When no citation passes, or when the\nquery asks for `recency` (no native search tool can enforce a time window), the\nsearch falls through to the configured provider. A citation whose host the\n`[network]` policy denies is removed. A citation whose host the policy would\nask you about is not fetched: it stays in `results` and the receipt records it\nas `citation_unchecked` until you allow the host.\n\nThe provider's prose answer is returned whole in a separate\n`unverified_synthesis` field: it is the provider model's summary, and neither\nits claims nor its links are checked. Oversized tool output spills to a session\nartifact the model can page back.\n\n**Recency and locale.** `recency` and `locale` are forwarded where the\nbackend's API takes them, and the search receipt reports each as honored or\nignored:\n\n| Backend | Recency | Locale |\n| --- | --- | --- |\n| Firecrawl | `tbs=qdr:d/w/m/y` | `country` from the region (`de-DE` → `DE`); a bare language is ignored |\n| Tavily | `time_range` | not sent (Tavily takes country names) |\n| SearXNG | `time_range` | `language`, as given |\n| Serply | not sent (undocumented) | `hl` language, `gl` country |\n| DeepSeek | not sent | not sent (`localization` takes an IP address) |\n| Provider-native | declined; the search falls through to the configured provider | not sent |\n\nRecency is rounded up to the backend's nearest window (day, week, month,\nyear), so `recency = 10` searches the last month. Other backends ignore both\nknobs and say so in the receipt.\n\nFor a private/internal search service that serves DuckDuckGo-compatible HTML,\nkeep `provider = \"duckduckgo\"` and set `base_url`; Codewhale appends the `q`\nquery parameter to that endpoint and applies network policy to its host.\nCustom endpoints do not fall back to public Bing. `CODEWHALE_SEARCH_BASE_URL`\ncan override this per process; `DEEPSEEK_SEARCH_BASE_URL` remains accepted as\nthe legacy alias.\n\n**SearXNG** ([docs](https://docs.searxng.org/dev/search_api.html)) uses the\nconfigured instance's JSON API. Set `provider = \"searxng\"` and\n`base_url = \"https://your-searxng.example\"`; Codewhale calls\n`/search?q=...&format=json`. Codewhale does not use a public SearXNG instance\nby default because public instances often disable JSON output or rate-limit API\ntraffic.\n\nSelf-host it as a separate process (Docker is fine); Codewhale never bundles or\nmanages the search engine itself:\n\n- Enable JSON on the instance (`settings.yml`, `search.formats` must include\n `json`) and restart it. An HTML-only instance answers the API with HTTP 403;\n Codewhale reports that as a JSON/API-access problem on the SearXNG hop rather\n than silently returning no results.\n- Bind it to loopback, or to a host and port your network policy allows. The\n instance keeps its own engine list, limiter, and limits.\n- Point Codewhale at it with `[search] provider = \"searxng\"` and `base_url`\n (required; either the root URL or the `/search` endpoint). No instance ships\n as a default, and none is discovered automatically.\n- `codewhale doctor --probe-search` sends a transport-only `HEAD` to that\n origin — no `q=`, no credentials, no redirects, no audit receipt — so a green\n probe proves reachability and network-policy admission, not that JSON is on.\n\nConfirm the JSON API itself before assuming a Codewhale bug:\n\n```sh\ncurl -sS \"$BASE/search?q=codewhale&format=json\" | jq '.results[0] | {title,url,score}'\n```\n\nCodewhale ranks the returned rows by `score`, highest first, and applies\n`max_results` to that ranking; rows an instance reports without a usable score\nkeep their original relative order.\n\n**Metaso** ([metaso.cn](https://metaso.cn)) requires a user-supplied key. Set\n`METASO_API_KEY` or `[search] api_key`; Codewhale does not ship a shared key.\n\n**Firecrawl** ([docs](https://docs.firecrawl.dev/sdks/cli)) searches Firecrawl\nCloud without a key using its bounded per-IP daily quota. Set\n`FIRECRAWL_API_KEY` or `[search] api_key` for authenticated limits. Codewhale\nsends no `Authorization` header in keyless mode.\n\n**Baidu** uses Baidu AI Search at\n`https://qianfan.baidubce.com/v2/ai_search/web_search`. Set\n`BAIDU_SEARCH_API_KEY` or `[search] api_key`. This is a search-tool backend\nonly; it does not add a Baidu model provider.\n\n**Sofya** ([sofya.co](https://sofya.co)) returns full extracted page content\nrather than snippets. Set `[search] api_key` to your `ay_live_...` key, or the\n`SOFYA_API_KEY` env var. This is a search-tool backend only; it does not add a\nSofya model provider.\n\n**Serply** ([serply.io](https://serply.io)) returns Google organic results with\ntitle, URL, and snippet. Set `[search] api_key` to your Serply key, or the\n`SERPLY_API_KEY` env var. This is a search-tool backend only; it does not add a\nSerply model provider.\n\n**DeepSeek** ([docs](https://api-docs.deepseek.com/guides/search)) uses\n`POST https://api.deepseek.com/v1/tools/search`. Set `provider = \"deepseek\"`\n(`deep-seek` and `deepseek-search` are also accepted) and supply `[search] api_key`\nor the `DEEPSEEK_API_KEY` env var, in that order. The saved DeepSeek model-provider\nkey is not reused by this search backend. Queries are truncated to 128 Unicode\ncharacters. The backend clamps counts to 1–50; the shared search tool currently\ncaps requests at 10 results. Query operators such as `site:`, `lang:`,\n`after:`, and `before:` are passed through; the `recency` and `locale` knobs are\nignored. Results include title, URL, and snippet, falling back to summary when a\nsnippet is empty.\n\n**Tavily** ([tavily.com](https://tavily.com)) is selected automatically when a\nTavily key is present and no provider is pinned: `TAVILY_API_KEY` set, or\n`[search] api_key` / `CODEWHALE_SEARCH_API_KEY` in the `tvly-` family. Doctor\nreports that as `source: tavily key`. Autodetect is runtime-only — Codewhale\nnever writes `[search] provider` for it, and `TAVILY_API_KEY` is never merged\ninto `[search] api_key`. An explicit `[search] provider` or\n`CODEWHALE_SEARCH_PROVIDER` always wins, so `provider = \"firecrawl\"` keeps\nFirecrawl even with a Tavily key in the environment. Pinned `tavily` accepts\nany non-empty `[search] api_key` and is configured by that key or\n`TAVILY_API_KEY`; with both empty it fails closed.\n\n```toml\n[search]\nprovider = \"firecrawl\" # also duckduckgo | bing | tavily | bocha | metaso | searxng | baidu | volcengine | sofya | serply | deepseek\n# base_url = \"https://search.example/\" # optional with provider = \"duckduckgo\"; required with \"searxng\"\n# api_key = \"YOUR_KEY\" # optional for firecrawl; required by the other API providers\n# native = false # provider-native search: unset = only when no provider is configured\n```\n\n## Local Media Attachments\n\nUse `@path/to/file` in the composer to add local text file or directory context\nto the next message. Use `/attach <path>` for local image/video media paths, or\n`Ctrl+V` to attach an image from a local clipboard or an explicitly forwarded\nX11/Wayland clipboard. SSH terminal paste without a forwarded graphical display\nis text-only; use the local terminal's paste command (`Cmd+V` on macOS or\n`Ctrl+Shift+V` on Linux/Windows), and use `/attach <path>` for remote image\nfiles. OpenSSH loopback X11 displays are detected automatically. For an\nexplicitly forwarded Wayland or non-loopback X11 display, set\n`CODEWHALE_SSH_CLIPBOARD=graphical`; set it to `terminal` to force terminal\ntransfer instead of an ambient remote display. DeepSeek's public Chat\nCompletions API currently accepts text message\ncontent, so media attachments are sent as explicit local path references instead\nof native image/video payloads.\nAttachment rows appear above the composer before submit; move to the start of\nthe composer, press `↑` to select an attachment row, then press `Backspace` or\n`Delete` to remove it without editing the sample text by hand.\n\n## Managed Configuration and Requirements\n\ncodewhale supports a policy layering model:\n\n1. user config + profile + env overrides\n2. managed config (if present)\n3. requirements validation (if present)\n\nBy default on Unix:\n- managed config: `/etc/deepseek/managed_config.toml`\n- requirements: `/etc/deepseek/requirements.toml`\n\nRequirements file shape:\n\n```toml\nallowed_approval_policies = [\"on-request\", \"untrusted\", \"never\"]\nallowed_sandbox_modes = [\"read-only\", \"workspace-write\"]\n```\n\nIf configured values violate requirements, startup fails with a descriptive error.\n\n## Notes On `codewhale doctor`\n\n`codewhale doctor` follows the same config resolution rules as the rest of the\nTUI. That means `--config`, `CODEWHALE_CONFIG_PATH`, and the legacy\n`DEEPSEEK_CONFIG_PATH` are respected, and MCP/skills\nchecks use the resolved `mcp_config_path` / `skills_dir` (including env overrides).\n\nTo bootstrap missing MCP/skills paths, run `codewhale setup --all`. You can\nalso run `codewhale setup --skills --local` to create the owned project\n`.codewhale/skills` directory. Project skills load after workspace trust.\n\nBoth plain `codewhale doctor` and `codewhale doctor --json` are structural and\noffline by default. They do not check the release service, hosted provider\nAPIs, local provider endpoints, or MCP processes, and they do not load a\nworkspace credential `.env`. Use `--check-updates`,\n`--probe-api`, `--probe-local`, or `--probe-mcp` to opt into the corresponding\nlive boundary; `--probe-local` may start a desktop-managed service such as\nOllama. Only the explicit API/local probe paths may load workspace credential\n`.env` values. Live flags conflict with `--json`, so machine-readable doctor\noutput is always offline. Top-level keys include `version`, `paths`, `secret_backend`,\n`config_path`, `config_present`, `workspace`, `api_key.source`,\n`api_key.availability`, `base_url`,\n`default_text_model`, `mcp`, `skills`, `tools`, `plugins`, `sandbox`,\n`platform`, `api_connectivity`, and `capability`. CI consumers should rely on\n`api_key.source` (`config_declared`/`env_declared`/`external_auth_declared`/\n`secret_store_unprobed`/`secret_store_unavailable`/`oauth_unprobed`/\n`external_consent`/`none`/`local_runtime`/`unknown`) and\n`api_key.availability`\n(`present`/`not_required`/`not_probed`/`unavailable`/`unknown`) rather than parsing the\nhuman-readable `doctor` text. Source is declaration metadata, not proof that a\ncredential exists or works. Only a non-empty, non-sentinel literal config value\nis structurally `present`; no-auth and local routes are `not_required`. Environment,\nexternal-auth, OAuth, consent, and secret-store declarations remain `not_probed`\nand cannot make structural Setup or Fleet readiness true. A secret-store sentinel\non a named/custom endpoint that is prohibited from using the shared store is\n`secret_store_unavailable`/`unavailable`, while `unknown` remains reserved for\nthe absence of a supported structural conclusion. Exact and whitespace-wrapped\nlegacy sentinels are never treated as literal credentials. The structural\nloader still honors safe environment routing/model/policy fields, but it never\nmaterializes environment HTTP headers, sandbox API keys, or search API keys;\nonly an explicit API/local probe switches to the normal credential-loading\npath. An opted-in update check also emits only typed generic failures: untrusted\nrelease metadata and transport errors are not echoed.\n\nIf configuration loading or validation fails, `doctor --json` returns nonzero\nand prints a bounded JSON error envelope with\n`status = \"error\"` and `error.kind = \"config_validation\"`. It does not emit a\nnormal route or capability report—or the underlying possibly sensitive error—\nfor an invalid configuration.\n\nMCP entries are configuration diagnostics unless an explicit MCP command is\nrun. `mcp.probe_scope` is `configuration`, `mcp.live_health_checked` is false,\nand each server separates `checks.configuration` / `checks.command` from\n`checks.process_reachable`, `checks.protocol_initialized`, and\n`checks.backend_tool_health`. The latter three remain `not_checked` in doctor\noutput. Run `codewhale mcp validate` to explicitly start enabled servers and\nverify protocol initialization/discovery; backend health still requires an\nappropriate explicit tool call. Doctor reports only safe structural MCP fields:\nURL userinfo/path/query/fragment and raw command arguments, environment values,\nheader values, and token material are omitted. Provider URLs follow the same\nrule and expose only `scheme://host[:explicit-port]`.\n\nThe `capability` key contains per-provider capability info derived from\nstatic knowledge (release docs, API guides) rather than live API probes.\nTop-level sub-keys: `resolved_provider`, `resolved_model`, `context_window`,\n`max_output`, `thinking_supported`, `cache_telemetry_supported`,\nand `request_payload_mode`.\n\nUse `capability.context_window` and `capability.max_output` for model-limit\nchecks in CI scripts; do not treat `capability.max_output` as the per-turn\nrequest budget. Use `capability.thinking_supported` to decide whether to\nconfigure reasoning effort.\n\n## Setup status, clean, and extension dirs\n\n`codewhale setup` accepts a few flags beyond the existing `--mcp`,\n`--skills`, `--local`, `--all`, and `--force`:\n\n- `--status` — print a compact one-screen status (api key, base URL, model,\n MCP/skills/tools/plugins counts, sandbox, `.env` presence). Read-only and\n network-free; safe to run in CI. If `.env` is missing and `.env.example` is\n present in the workspace, the status output points at `cp .env.example .env`.\n- `--tools` — scaffold `~/.codewhale/tools/` with a `README.md` describing the\n self-describing frontmatter convention (`# name:` / `# description:` /\n `# usage:`) and an `example.sh` that follows it. The directory is\n intentionally not auto-loaded; wire individual scripts into the agent via\n MCP, hooks, or skills.\n- `--plugins` — scaffold `~/.codewhale/plugins/` with a `README.md` and an\n `example/plugin.toml` plus a namespaced example Skill. Bundles are discovered\n read-only, untrusted, and disabled; review them through `/plugin` before\n enabling. v0.9.1 activates only declared Skills and MCP servers. See\n [PLUGIN_BUNDLES.md](PLUGIN_BUNDLES.md).\n- `--all` now scaffolds MCP + skills + tools + plugins together.\n- `--clean` — list `~/.codewhale/sessions/checkpoints/latest.json` and\n `offline_queue.json` if they exist. Legacy\n `~/.deepseek/sessions/checkpoints/` files are not scanned automatically; set\n `CODEWHALE_HOME=~/.deepseek` for a one-off legacy cleanup. Pass `--force` to\n actually remove matched files. This never touches real session history or the\n task queue.\n\n`--status` and `--clean` are mutually exclusive with the scaffold flags.\n\n## Why the engine strips XML/`[TOOL_CALL]` text\n\ncodewhale sends and receives tool calls only over the API tool channel\n(structured `tool_use` / `tool_call` items). The streaming loop in\n`crates/engine/src/core/engine.rs` recognizes a fixed set of fake-wrapper start\nmarkers — `[TOOL_CALL]`, `<codewhale:tool_call`, `<tool_call`, `<invoke `,\n`<function_calls>` — and scrubs them from visible assistant text without ever\nturning them into structured tool calls. When a wrapper is stripped, the loop\nemits one compact `status` notice per turn so the user can see why their\nvisible text shrank. Treat any change that re-enables text-based tool\nexecution as a regression; the protocol-recovery tests in\n`crates/tui/tests/integration/protocol_recovery.rs` lock the contract.\n\n## Model-bound redaction (`[redaction] model_bound`)\n\nCodewhale masks credential-looking values in tool output **before it is sent\nto an upstream model** — the \"model boundary\". A file read by a tool can\ncontain a configured API key, a bare provider token, or a credential-shaped\nopaque string, and the model must not see those bytes. This backstop is\nseparate from the display/export scrubbers: it decides what the model itself\ncan quote back, and it is deliberately conservative (`CredentialShaped`\npolicy, see `crates/config/src/persistence.rs`), so ordinary code and config\nstay byte-exact while keys, JWTs, bearer tokens, PEM blocks, and long opaque\nruns are masked.\n\nTurning that masking **off** is a security decision, so it is not a plain\nboolean:\n\n```toml\n[redaction]\nmodel_bound = \"disabled\" # \"enabled\" (default) | \"disabled\"\n```\n\nSetting `\"disabled\"` only records a *request*. It takes effect only when all\nof these are true:\n\n1. You restart the interactive TUI.\n2. The startup gate appears and you press `1`/`Y` on its first stage\n (\"confirm and disable\"). This only advances to a second, final-confirmation\n stage - the gate repeats the red warning and asks \"are you really sure?\".\n3. On that second stage you press `1`/`Y` again. The gate is rendered with the\n same explicit-key discipline as workspace trust - `Enter` never confirms by\n reflex, and `2`/`U` on the second stage steps back.\n4. Only that second confirmation persists a receipt to\n `~/.codewhale/redaction-state.json` (next to `config.toml`) and rebuilds\n the engine with masking off for the rest of this launch and future ones.\n\nThe receipt is bound to the config it was made against and is valid only\nwhile that config still requests `\"disabled\"`. Setting `model_bound` back\nto `\"enabled\"` - or rewriting `config.toml` in any way after the\nconfirmation - invalidates it, so requesting `\"disabled\"` again later\nasks for a fresh confirmation on the next launch. The receipt checks both the\nconfig contents and modification time; missing, unreadable, or malformed config\nand older receipts without this binding keep masking enabled. Legacy-home\ninstalls store the receipt beside their resolved config file.\n\nUntil a confirmation exists, the effective mode is always `\"enabled\"`:\n\n- Choosing `2`/`U` (\"keep masking on\") leaves the config field untouched, so\n the next launch asks again. Edit the field back to `\"enabled\"` to stop being\n asked.\n- Non-interactive entry points (`codewhale exec`, hooks, automations, headless\n agents) never confirm anything and never apply an unconfirmed request.\n- Routing/classification summaries and durable goal-state text keep their own\n always-on redaction regardless of this switch; the opt-out exists so the\n model can quote file bytes for exact edits, not to relax stored state.\n\nThe config value itself is forgiving: `true`/`false`, `\"on\"`/`\"off\"`, and\n`\"enabled\"`/`\"disabled\"` (any casing) all parse, with `false`/`\"off\"` meaning\n`\"disabled\"`.\n\nA confirmed opt-out still sends your configured API keys to the provider you\nare already talking to. Only use it when the model must read and edit files\nthat contain real credentials.\n\n### Stored sessions\n\nThe masking runs once, when tool output enters the transcript, so saved\nsession files (`~/.codewhale/sessions/*.json` and their checkpoints) and\nRuntime API thread items (their text and tool metadata) do not store a\ncredential-shaped value from tool output. With the confirmed opt-out above,\ntool output is stored as the model saw it.\n\nNot yet masked: large outputs spilled to `~/.codewhale/tool_outputs`, shell\ncompletion evidence artifacts, and tool-call *inputs* (for example a\n`curl -H \"Authorization: Bearer …\"` command line). `doctor` and\n`scrub-secrets` do not check those either.\n\nSessions saved by builds before this change may still hold credentials in\ntheir tool output; nothing rewrites them automatically. `codewhale doctor`\nreports them under **Stored Sessions** (it checks the newest 50 files), and\nthis command finds and masks them across every saved session:\n\n```sh\ncodewhale sessions scrub-secrets # report only\ncodewhale sessions scrub-secrets --apply # rewrite the affected files\n```\n\n`--apply` rewrites each file under the same per-session lock a save takes,\nso it never loses a concurrent save. A session that is still open can write\nits in-memory copy back on its next save, so close open sessions first, and\nrotate any credential that was exposed — masking a stored copy cannot un-leak it.\n"
|
||
}
|
||
]
|
||
},
|
||
{
|
||
"id": "skills",
|
||
"slug": "skills",
|
||
"sources": [
|
||
{
|
||
"path": "docs/SKILLS.md",
|
||
"sha256": "d465564774757a3c4eeb7b266e5b50884f0a47365683f4b8b151b41466fdec9d",
|
||
"content": "# Skills Manager\n\nIn the terminal, `/skills` opens **Extensions → Skills**. Enter on a skill opens the dedicated manager, also available directly through `/skills manage`, for existing install, update, remove, and trust operations. `/skill <name>` still activates a skill, and the explicit inspection, remote, sync, and suggestion subcommands remain available.\n\n\n> 阅读简体中文版:[zh_hans/SKILLS.md](zh_hans/SKILLS.md)\n\nSkills are reusable `SKILL.md` instruction packs. Codewhale discovers them from\nseveral roots, but **only Codewhale-owned directories are writable**. The unified\n`/skills manage` manager is the interactive surface for audit and mutation; slash\naliases share the same write path.\n\nFor Claude Code plugin boundaries, see [CLAUDE_PLUGIN_COMPAT.md](CLAUDE_PLUGIN_COMPAT.md).\nFor `skills_dir` and `[skills]` config keys, see [CONFIGURATION.md](CONFIGURATION.md).\n\n## Architecture (four layers)\n\n| Layer | Role |\n| --- | --- |\n| **Root catalog** | Single source of precedence and ownership (`SkillRootCatalog`). |\n| **Audit** | Read-only, unmerged on-disk inventory (status, digest, actions). |\n| **Mutation controller** | Only writer for install / import / update / remove / trust. |\n| **Skills manager view** | TUI: emits events only; never writes files itself. |\n\nRuntime discovery (`SkillRegistry`) still merges skills for the model. Audit\nintentionally does **not** merge — it shows every on-disk copy so conflicts and\nshadowing stay visible.\n\n## Ownership and roots\n\n**Writable (Codewhale-owned)**\n\n| Scope | Path |\n| --- | --- |\n| Project | `<workspace>/.codewhale/skills/` |\n| Global | `~/.codewhale/skills/` |\n\n**Read-only compatible** (discover / import source only — never mutated in place)\n\nExamples: `<workspace>/.agents/skills`, `./skills`, `.claude/skills`,\n`.cursor/skills`, `.opencode/skills`, `~/.agents/skills`, `~/.claude/skills`,\nand similar harness layouts.\n\n**Audit-only (not runtime-active)**\n\n- Flat `<workspace>/skills` is an audit candidate until `[skills] flat_workspace_root = true`; an explicit `skills_dir` can also select it.\n- `.codex/skills` appears in **compatible** audit scans so operators can see it.\n It does **not** join the runtime discovery set.\n\nConfigured `skills_dir` that is not one of the owned Codewhale roots stays\nread-only. Discovery and the manager can list it; mutations still target owned\nproject/global roots only.\n\nWithin each scope, the owned `.codewhale/skills` root wins name collisions.\nProject order is `.codewhale`, `.agents`, `.claude`, `.opencode`, `.cursor`, then\nan opted-in flat `skills` root. Global order is `.codewhale`, `.agents`, `.claude`,\nthen legacy `.deepseek`. Project roots precede global roots. Shadowing warnings\nname both copies, so install and update select the owned copy in that scope.\n\nEvery root inside the workspace — owned, compatible, or a configured\n`skills_dir` that resolves there — loads only once the workspace is trusted\n(`/trust on --save`). Until then discovery names the skipped directories in its\nwarning, and the session's skills directory falls back to the global one.\n\n## Slash commands\n\n| Command | Behavior |\n| --- | --- |\n| `/skills` | Opens Extensions → Skills (owned-only scan, **no network**). |\n| `/skills manage` | Opens the dedicated Skills Manager for audit and mutation. |\n| `/skills <prefix>` | Text list filtered by name prefix. |\n| `/skills inspect` | Text discovery mode, searched directories, and source paths. |\n| `/skills --remote` | Explicit registry listing (network). |\n| `/skills suggest <task>` | Rank up to three remote skills for a task, with matching evidence and an explicit install command (network; no install). |\n| `/skills sync` | Explicit registry → local cache sync (network). |\n| `/skill <name>` | Activate a skill for the next turn. |\n| `/skill install [--project\\|--global] <spec>` | Install via mutation controller. |\n| `/skill update [--project\\|--global] <name>` | Update a managed skill from its registry provenance. |\n| `/skill uninstall [--project\\|--global] <name>` | Remove a managed skill. |\n| `/skill trust [--project\\|--global] <name>` | Write digest-bound advisory trust. |\n\nNotes:\n\n- There is **no** `/skills audit` subcommand. Use the manager (and `c` to toggle\n compatible roots) or `/skills inspect` for discovery details.\n- Bare `/skill install <spec>` (no scope flag) installs into the Codewhale\n **global** owned root.\n- `/skills suggest` only reads the curated registry through the existing\n network policy. It never downloads, trusts, enables, or activates a skill;\n each result gives a separate `/skill install <name>` command for the user to\n choose.\n- If the same name exists in both project and global owned roots, update /\n uninstall / trust require `--project` or `--global`.\n- If a name exists only under a compatible external root, writes are refused;\n import it through `/skills manage` instead of editing harness directories.\n\n## Skills Manager (TUI)\n\nDirect manager path: type `/skills manage` and confirm. The surface is zero-network on\nopen (owned-only audit).\n\n| Key | Action |\n| --- | --- |\n| `↑`/`↓` or `j`/`k` | Move selection |\n| `Enter` | Primary available action / confirm pending prompt |\n| `i` | Import (external → owned) |\n| `u` | Update (managed + registry provenance) |\n| `r` | Remove (managed; confirms first) |\n| `t` | Trust (managed; digest-bound) |\n| `s` | Toggle import target: project ↔ global |\n| `c` | Toggle scan: owned-only ↔ compatible (still local disk only) |\n| `Esc` | Cancel confirm, or close the manager |\n\nThe view never calls install helpers or touches the filesystem. It emits a\nmutation request; the host runs the controller, shows a receipt, and rebuilds\nthe inventory.\n\n## Bundled catalog tiers\n\nCodewhale presents its shipped skills in two compact tiers so agentic workflows\nare not buried under document and integration helpers:\n\n- **Core agentic** — planning, implementation, debugging, review, verification,\n delegation, Fleet, release, and `best-of-n` comparison workflows.\n- **Format & tooling** — document formats, data visualization, frontend and web\n testing, and skill/plugin/MCP authoring helpers.\n\nWorkspace, user, and compatible-harness skills stay labeled **custom**; Codewhale\ndoes not guess their intent from their name. The shipped pack also does not\nadvertise capabilities the runtime lacks. In particular, image understanding\nis available, but an image-generation skill is not bundled until a real\nimage-generation tool exists.\n\nRepository-maintenance and release-operator helpers (the `gh-*` skills and\n`codew-release-qa-sweep` under [`skills/`](skills/README.md)) are **not** part\nof the end-user starter pack and are never auto-installed; a catalog-matrix\ntest pins that boundary. Shipping them as an optional bundle is plugin-delivery\nwork tracked separately in\n[#4836](https://github.com/codewhale-hq/CodeWhale/issues/4836).\n\n### Invocation and alias metadata\n\nBundled and user skills may declare two runtime-routing fields in frontmatter:\n\n| Field | Meaning |\n| --- | --- |\n| `invocation: model+user` | The default; the skill appears in the model's compact catalogue and can be loaded by the model or user. |\n| `invocation: explicit-only` | The skill remains loadable by an explicit name, but is omitted from the model catalogue so opt-in instructions do not become ambient context. |\n| `aliases-for: name, other-name` | Additional lookup names for the same canonical skill. Aliases are not separate catalogue entries and do not duplicate prompt content. |\n\n`disable-model-invocation: true` makes a skill explicit-only. `user-invocable:\nfalse` hides it from user menus and refuses explicit activation while preserving\nmodel selection. Setting both disables both paths. Boolean spellings `true/false`,\n`yes/no`, `on/off`, and `1/0` are accepted; invalid policy booleans fail closed.\nThe model's catalog, list/query, and `load_skill` enforce model eligibility; the\nuser's slash and command palettes enforce user eligibility. `argument-hint` is\nshown beside user-facing descriptions. `when_to_use` joins the routing description\nas `Use when:`.\n\nThe runtime and installer share one frontmatter validator. Runtime accepts missing\ndescriptions and heading-only Markdown with warnings; installation requires a\nfrontmatter block, a nonempty description, and a path-safe name. A name/directory\nmismatch warns without renaming the file. Nested `metadata` stays nested; flow\nand block lists share the same interpretation. `license`, `compatibility`,\n`metadata`, localized descriptions, and `x-*` extension keys are accepted silently.\nUnknown keys warn once. `allowed-tools` / `disallowed-tools`, `model`, `context`,\nand `agent` warn because they grant no tool, approval, provider, or fork authority.\nThe existing workspace-trust and reviewed-plugin byte/hash gates still apply.\n\nMissing or unknown invocation values retain the historical `model+user`\nbehavior. Canonical names win over aliases when a collision exists. Loading a\nskill reports its canonical invocation and aliases so receipts remain\ninspectable.\n\n### Non-ASCII names and saved activation\n\nASCII names keep their existing command spelling. A name containing non-ASCII\ncharacters gets a stable ASCII ID: a shortened old slug plus 32 hexadecimal\nSHA-256 digits, at most 64 characters per skill-name segment. The hash uses\ntrimmed UTF-8 with ASCII case folding; it does not transliterate or merge Unicode\nnormalization forms. Unqualified raw names and those IDs select the same body.\nPackage directories stay in place. Qualified lookup requires the declared canonical\nnamespace, with ASCII case folding and no punctuation folding: `Team.Plugin:技能`\ncannot select a skill in `team-plugin`.\n\nPreviously disabled lossy names such as `skill` or `pdf` continue to suppress\nevery corresponding renamed skill. Enabling one exact catalog ID enables only\nthat identity, including a literal ASCII skill named `skill`; it does not enable\nits formerly colliding siblings. Toggle requests use the exact ID returned by\n`GET /v1/skills`. Plugin bundle trust remains a separate gate.\n\nActivation still uses one `skills_state.toml` file and its `disabled` array.\nReserved `!codewhale-skill-state:1:*` entries preserve legacy veto history and\nexact enable choices through older writers, using the same lock and atomic\nwrite. Listing/discovery do not rewrite the file. Unknown versions or malformed\nreserved entries are errors and are left untouched; existing recovery behavior\nkeeps native skills available but hides reviewed plugin skills when policy\ncannot be read.\n\nThis is **not simultaneous-version activation compatibility**. v0.10.0 readers\ncannot enforce new per-identity disables, and their lossy or no-op toggles cannot\nexpress every new choice. Upgrade every runtime sharing the state directory\nbefore relying on consistent controls. Retaining marker strings through an old\nwrite does not give that old binary the new identity semantics.\n\n### Starter-pack parity decisions\n\nThe v0.9.2 parity audit in [#4698](https://github.com/codewhale-hq/CodeWhale/issues/4698)\ncompared the five `xai-grok-memory` / `xai-grok-shell` reference skills with\nthe actual Codewhale bundle. This is a decision matrix, not a request to copy\nreference text or advertise unsupported tools:\n\n| Reference skill | Codewhale decision | Runtime grounding |\n| --- | --- | --- |\n| `check-work` | Canonical alias/compatibility mapping to `verify` | `verify` is the shipped evidence-collection workflow. |\n| `code-review` | Canonical alias/compatibility mapping to `review` | `review` is the shipped read-only correctness workflow. |\n| `create-skill` | Canonical alias/compatibility mapping to `skill-creator` | `skill-creator` is the shipped authoring workflow. |\n| `help` | Bounded `invocation: explicit-only` router, not an ambient manual | Routes to `/help`, `/skills`, `/config`, `doctor`, and the installed `docs/` tree; it embeds no manual text. |\n| `imagine` | Intentionally out of scope | Codewhale has no image-generation/edit tool, so the starter pack must not advertise one. |\n\nNotes on the two non-alias decisions:\n\n- **`help`** ships as a bundled skill but is `explicit-only`, so\n it never appears in the model catalogue and costs zero ambient prompt budget.\n Its body is a routing card — which surface owns which fact — and explicitly\n forbids pasting a command list or settings table into context. A checked\n invariant keeps it under 80 lines and requires it to name the `/help`,\n `/skills`, `/config`, and `doctor` surfaces.\n- **`imagine`** stays out. The shipped runtime exposes image *understanding*,\n not image generation or edit, so no bundled skill may advertise it. The\n catalog matrix asserts that `imagine`, `image`, and `image-gen` are absent\n from the bundle and resolve to nothing.\n\nNo reference skill body is copied by this compatibility slice. The explicit\naliases and invocation metadata are bounded routing facts; the full skill body\nstill enters context only through `load_skill`.\n\n### Catalog fixture matrix (provider-free)\n\n[`crates/tui/assets/skills-catalog-matrix.json`](../crates/tui/assets/skills-catalog-matrix.json)\nis an **authored** expectation table covering every bundled skill: canonical\nname, tier, invocation, aliases, whether it renders as an ambient catalogue\nentry, and which of its aliases are shadowed by another canonical name. The\ntests in `crates/skills/src/catalog_matrix.rs` assert a bijection between\nthat fixture and `BUNDLED_SKILLS`, so the shipped pack cannot change without an\nexplicit fixture update.\n\nWhat those tests do and do not claim:\n\n- They validate **deterministic registry / catalog / resolver behavior**:\n install, parse, eligibility, explicit load, non-activation, alias resolution,\n explicit-only exclusion, collision precedence, and prompt budget.\n- They validate **nothing about semantic LLM routing**. Whether a model chooses\n `debug` for a stack trace is a live-provider question; see\n [LIVE_SMOKE.md](LIVE_SMOKE.md).\n\nCollision and prompt-budget invariants asserted today:\n\n| Invariant | Meaning |\n| --- | --- |\n| Canonical wins | A canonical bundled name always beats another skill's alias (`docx` → `docx`, never `documents`). |\n| Single alias owner | No two bundled skills may claim the same alias. |\n| No duplicate entries | Each canonical name renders at most one catalogue line; aliases render zero. |\n| Budget headroom | The shipped pack alone renders under the window-scaled skills budget (2,400 characters at the default 128k window, the index floor) with **no** \"additional skills omitted\" line, so user skills are never silently displaced. |\n| No context poisoning | Descriptions stay single-line and respect `[skills] listing_max_description_chars` (default 120) before entering the prompt. |\n\n### Locale-aware routing metadata\n\n`description_<tag>` frontmatter is supported (exact tag, then primary subtag,\nthen the canonical description — with Traditional Chinese excluded from the\nSimplified `zh` fallback). **No bundled skill ships a localized routing\ndescription**, and none is fabricated. The shipped contract is therefore an\nexplicit, tested fallback:\n\n- For every skill in the bundle × every locale in `Locale::shipped()` — all 15\n of `en`, `ja`, `zh-Hans`, `zh-Hant`, `pt-BR`, `es-419`, `vi`, `ko`, `ca`,\n `de`, `fr`, `id`, `hi`, `ru`, `uk` (`crates/localization/src/lib.rs:70-88`) —\n `description_for_locale` returns the canonical English description.\n- The rendered catalogue block is byte-identical across all shipped locales.\n- Exact-tag match, primary-subtag fallback (`pt-BR` → `description_pt`), and\n English fallback are covered against a synthetic authored fixture, so the\n resolution paths stay tested even while the bundle itself is English-only.\n\nIf a bundled skill later ships localized routing metadata, the parity test\nfails until source-backed coverage is added for it — the fallback contract\ncannot silently absorb a translation.\n\n## Audit statuses\n\nEach audited row carries precedence and relationship flags:\n\n| Status | Meaning |\n| --- | --- |\n| **Active** | Highest-precedence copy for that canonical name in the scan. |\n| **Shadowed** | Same name exists at a higher-precedence root. |\n| **Duplicate** | Same canonical name and same package digest as another copy. |\n| **Conflict** | Same canonical name, different package digest. |\n\nExternal skills with no owned peer (and a valid digest) are **import\ncandidates**. Externals that conflict with or exactly duplicate an owned copy\ncan still offer Import — duplicate → already present; conflict → confirm replace\nin the selected import scope.\n\n## Provenance and markers\n\nManaged installs write schema **v2** metadata under the skill directory:\n\n**`.installed-from` (v2)** — written last on successful install/import:\n\n```json\n{\n \"schema_version\": 2,\n \"spec\": \"github:owner/repo\",\n \"url\": \"https://…\",\n \"source_checksum\": \"…\",\n \"content_digest\": \"…\",\n \"installed_name\": \"my-skill\",\n \"registry_version\": null\n}\n```\n\n- `content_digest` is a bounded package tree hash (not SKILL.md alone).\n- Display of URLs strips userinfo, query, and fragment.\n- Imports use a local `import:…` provenance and **cannot** be updated from a\n registry; re-import or remove them instead.\n- Legacy v1 markers are recognized as managed with\n `LegacyMetadataUnknown` integrity until refreshed.\n\n**`.trusted` (v2)** — advisory, digest-bound:\n\n```json\n{\n \"schema_version\": 2,\n \"content_digest\": \"…\"\n}\n```\n\nTrust records review intent. It does **not** sandbox the skill or auto-approve\ntools. Content updates clear trust so a stale marker cannot outlive the bytes.\n\nManual skills (owned root, no managed marker) are visible but not\nupdate/remove/trust through the managed actions.\n\n## Package digest and safety\n\nAudit and mutation share a bounded package digest:\n\n- Regular files only; symlinks that escape the skill root or cycle → fail closed.\n- Caps on total size, file count, and depth.\n- Mutations re-check an expected digest before write (TOCTOU).\n- Import/replace keeps a `.bak` until digest + marker finalize succeed; failure\n restores the previous owned package.\n\n## Readiness\n\nThe audit model has a readiness field and optional provider hook for a future\nreadiness cache ([#4407](https://github.com/codewhale-hq/CodeWhale/issues/4407)).\nToday, when no cache is wired, readiness is always **`Unknown`**. The manager\ndoes not run readiness probes and does not block mutations on readiness.\n\n## Config knobs\n\nParents, fresh and nested children, and `/context` use the same ambient-index renderer. `[skills] listing_max_description_chars` controls the per-entry ceiling (1–40,000; default 120). `listing_budget_fraction` controls the share of the resolved route window (greater than 0 and at most 1; default 0.002), estimated at four characters per token. The total index retains a 2,400-character floor and 40,000-character ceiling. Explicit profile values override inherited values independently. Changing a route recomputes its budget; it does not silently change the description setting.\n\nThe renderer favors `Use when:` triggers, shortens descriptions before falling back to names, and reports shortened/omitted entries. Descriptions exceeding the configured cap without a trigger also produce a load warning. `/skills inspect` shows a localized note with the same effective cap, and `/context` repeats the loss count. `load_skill name=\"list\"` and exact-name loading retain full text. These limits govern ambient routing metadata, not skill bodies or external MCP resource descriptions.\n\n```toml\n# Optional override for discovery preference (not automatically a write target\n# unless it is the Codewhale project/global owned path).\nskills_dir = \"/path/to/skills\"\n\n[skills]\n# When true, runtime discovery skips cross-tool roots (.claude, .agents, …).\n# Owned Codewhale roots and an explicit skills_dir override still apply.\nscan_codewhale_only = false\nlisting_max_description_chars = 120\nlisting_budget_fraction = 0.002\n\n# Optional registry / install size overrides used by --remote, sync, and install.\n# registry_url = \"https://…\"\n# max_install_size_bytes = 5242880\n```\n\nSee [CONFIGURATION.md](CONFIGURATION.md) for the full config surface.\n\n\n### Bundled skills\n\nUse `[skills.bundled]` independently of the discovery directory:\n\n```toml\n[skills.bundled]\ninstall = \"auto\"\nselected = []\ndisabled = [\"debug\"]\nexplicit_only = [\"handoff\"]\n```\n\n`auto` preserves the generation-marker and deletion-as-opt-out behavior. Setting top-level `skills_dir` disables automatic bundle installation in this mode. `all` installs enabled missing names even with a current marker or a discovery override, writing only to the owned global root (`$CODEWHALE_HOME/skills`, normally `~/.codewhale/skills`). `none` writes no files and leaves the marker untouched. Discovery continues in every mode.\n\nAn empty `selected` list enables the whole bundle; a nonempty list limits installation and discovery. `disabled` wins and ignores the bundled copy before name resolution, so a compatible root can supply the same name. `explicit_only` removes an enabled bundled copy from the model index and model `load_skill`, while retaining explicit `/skill <name>` invocation. Canonical names and their compatibility aliases share policy: `documents` maps to `docx`, `presentations` to `pptx`, and `spreadsheets` to `xlsx`.\n\nThese lists apply only to recognized shipped copies in the owned global root. Edited files, managed installs with `.installed-from`, project copies and compatible-root copies keep their own behavior. Bundle settings never delete skill files, including retired pack entries. `all` preserves edited and managed bodies. A marker that is a directory or symlink is refused without deleting its contents. Profile fields merge independently; an explicit empty list clears the inherited list.\n\n`codewhale doctor` and its JSON output report mode, marker generation, installed and missing names, disabled and explicit-only counts, shadowing, unknown names and ignored explicit-only entries. `/skills inspect` adds each bundled path and its selection reason. Unknown config names produce a doctor warning; they do not name a write target.\n\n## Operator checklist\n\n1. Prefer `/skills manage` for day-to-day management; keep `--remote` / `sync` explicit.\n2. Never hand-edit `.claude` / `.agents` / `.cursor` trees to “install” for\n Codewhale — import into `.codewhale/skills` instead.\n3. Treat `.trusted` as advisory documentation of review, not a security boundary.\n4. After registry updates that change content, re-trust if you still want the\n advisory marker.\n5. Dual project+global copies of the same name need an explicit scope flag on\n CLI mutations.\n"
|
||
},
|
||
{
|
||
"path": "docs/CLAUDE_PLUGIN_COMPAT.md",
|
||
"sha256": "d1c66e79af9ca257d883c503f36a892a5d270326c7e1a79a5322e4b4a7057f24",
|
||
"content": "# Claude Plugin Compatibility\n\nCodewhale treats Claude Code skill folders as instruction bundles when they are\nplain `SKILL.md` directories. It does not run Claude Code plugin runtimes.\n\n## Supported\n\n- Workspace or global `.claude/skills/<name>/SKILL.md` directories discovered by\n the normal skill registry.\n- GitHub or tarball installs that contain one selected skill directory such as\n `skills/<name>/SKILL.md`, `.agents/skills/<name>/SKILL.md`,\n `.claude/skills/<name>/SKILL.md`, or a nested package layout ending in\n `skills/<name>/SKILL.md`.\n- Companion files inside the selected skill directory, such as `references/`,\n `examples/`, or scripts that are only used after the skill is explicitly\n loaded and trusted.\n\n## Compatible plugin bundles\n\nSince v0.9.13, `/plugin install` accepts the declarative subset described in\n[Installing plugins](PLUGINS.md): nested `.claude-plugin/plugin.json` metadata,\nroot skills, commands and agent profiles, plus inline MCP declarations or\n`.mcp.json`. Local sources in Claude marketplace catalogs resolve from the\nrepository root outside `.claude-plugin`.\n\nExact `${ENV_NAME}` MCP header references map to Codewhale's environment-backed\ncredentials, including `Authorization: Bearer ${ENV_NAME}`. Literal credential\nvalues and compound header templates are rejected; importing never reads the\nreferenced environment variables.\n\nThe importer uses Codewhale's existing adapters, review hashes and enablement.\nA Claude marketplace's labels or defaults grant no authority. Bundles are not\nscanned from another application's plugin roots or activated automatically.\n\n## Unsupported runtime features\n\nThe importer rejects hooks, LSP declarations, custom MCP file paths, and\n`${CLAUDE_PLUGIN_ROOT}` expansion. It does not run plugin build steps,\nTypeScript agents, dashboard servers, shared plugin state, or token-gated\nservice processes. Claude-specific frontmatter behavior such as `model: inherit`\nis not an additional runtime contract. Remote MCP authentication still follows\nCodewhale's existing plugin credential boundary; installation does not complete\nOAuth or borrow another application's credentials.\n\n`/skill install` remains a separate one-skill operation: it rejects multi-skill\nplugin archives rather than silently selecting one directory and dropping other\ncomponents. Use `/plugin install` for supported bundles, or migrate one explicit\nskill directory when the repository depends on unsupported runtime behavior.\n\nSee [PLUGIN_BUNDLES.md](PLUGIN_BUNDLES.md) for discovery, review and activation.\n"
|
||
}
|
||
]
|
||
},
|
||
{
|
||
"id": "claude-code",
|
||
"slug": "claude-code",
|
||
"sources": [
|
||
{
|
||
"path": "docs/CLAUDE_PLUGIN_COMPAT.md",
|
||
"sha256": "d1c66e79af9ca257d883c503f36a892a5d270326c7e1a79a5322e4b4a7057f24",
|
||
"content": "# Claude Plugin Compatibility\n\nCodewhale treats Claude Code skill folders as instruction bundles when they are\nplain `SKILL.md` directories. It does not run Claude Code plugin runtimes.\n\n## Supported\n\n- Workspace or global `.claude/skills/<name>/SKILL.md` directories discovered by\n the normal skill registry.\n- GitHub or tarball installs that contain one selected skill directory such as\n `skills/<name>/SKILL.md`, `.agents/skills/<name>/SKILL.md`,\n `.claude/skills/<name>/SKILL.md`, or a nested package layout ending in\n `skills/<name>/SKILL.md`.\n- Companion files inside the selected skill directory, such as `references/`,\n `examples/`, or scripts that are only used after the skill is explicitly\n loaded and trusted.\n\n## Compatible plugin bundles\n\nSince v0.9.13, `/plugin install` accepts the declarative subset described in\n[Installing plugins](PLUGINS.md): nested `.claude-plugin/plugin.json` metadata,\nroot skills, commands and agent profiles, plus inline MCP declarations or\n`.mcp.json`. Local sources in Claude marketplace catalogs resolve from the\nrepository root outside `.claude-plugin`.\n\nExact `${ENV_NAME}` MCP header references map to Codewhale's environment-backed\ncredentials, including `Authorization: Bearer ${ENV_NAME}`. Literal credential\nvalues and compound header templates are rejected; importing never reads the\nreferenced environment variables.\n\nThe importer uses Codewhale's existing adapters, review hashes and enablement.\nA Claude marketplace's labels or defaults grant no authority. Bundles are not\nscanned from another application's plugin roots or activated automatically.\n\n## Unsupported runtime features\n\nThe importer rejects hooks, LSP declarations, custom MCP file paths, and\n`${CLAUDE_PLUGIN_ROOT}` expansion. It does not run plugin build steps,\nTypeScript agents, dashboard servers, shared plugin state, or token-gated\nservice processes. Claude-specific frontmatter behavior such as `model: inherit`\nis not an additional runtime contract. Remote MCP authentication still follows\nCodewhale's existing plugin credential boundary; installation does not complete\nOAuth or borrow another application's credentials.\n\n`/skill install` remains a separate one-skill operation: it rejects multi-skill\nplugin archives rather than silently selecting one directory and dropping other\ncomponents. Use `/plugin install` for supported bundles, or migrate one explicit\nskill directory when the repository depends on unsupported runtime behavior.\n\nSee [PLUGIN_BUNDLES.md](PLUGIN_BUNDLES.md) for discovery, review and activation.\n"
|
||
},
|
||
{
|
||
"path": "docs/CONFIGURATION.md",
|
||
"sha256": "618dd41d6edfb50b884d8634663b9d7ab75b1526a3f3e9121903631804e75e43",
|
||
"content": "# Configuration\n\ncodewhale reads configuration from a TOML file plus environment variables.\nAt process startup it may also load literal built-in-provider credentials from\na workspace-local `.env` file. Use the tracked `.env.example` as the template;\ncopy it to `.env`, then add only credential values.\n\nA workspace is not configuration authority. Codewhale therefore ignores\nconfig/profile/home paths, provider/model/base-URL routing, MCP/plugin state,\napproval/sandbox/shell posture, executable paths, runtime settings, and every\nother non-credential `.env` entry. Variable expansion is rejected so a\nrepository cannot substitute an ambient secret into a credential value. Use\n`config.toml`, CLI flags, or values exported by the launching shell for those\nexplicit control-plane settings. `.env` is read from a stable regular-file\nhandle, is capped at 1 MiB, and symbolic links, reparse points, and multiply\nlinked files are rejected.\n\n## Reading and checking configuration from the CLI\n\n`codewhale config get <key>` reads scalar keys, whole tables such as `tools`,\nand nested paths such as `tools.user_input_timeout_seconds`. Displayed tables\nand nested values apply the same recursive credential redaction as `config dump`.\n\n`config set` supports its named scalar keys and the provider, route, and\nnotification commands. It refuses a key that nothing reads and suggests the\nnearest real key (`config set calm_mod on` names `calm_mode`). A settings.toml\nkey such as `calm_mode` or `tool_collapse` is validated and written to the\nuser-global settings.toml, not config.toml; `config get` reads it back from\nthere even when an old config.toml copy (which nothing reads) is still present,\nand names that copy so you can `config unset` it. Settings keys have no project\nscope, so `--project` refuses them. A config.toml key is checked against the\ntype its reader expects and stored as a TOML boolean or number where the\nreader needs one (`yolo = true`, `max_subagents = 4`); `reasoning_effort`\naccepts the same aliases as `/effort` (#6563). Other dotted writes fail before\nmodifying the file and name the TOML table to edit. For example, set a tools\ntimeout in the file as:\n\n```toml\n[tools]\nuser_input_timeout_seconds = 0\n```\n\n`codewhale config doctor` checks credential presence and endpoint shape, and\nwarns about each config.toml root key that nothing reads: a settings.toml key\nleft in config.toml is named as misplaced, and any other unread key gets a\ndid-you-mean. Keys read by any runtime reader are not reported. The warnings do\nnot fail the check, and a clean result does not validate every runtime setting's\nvalue (#6083, #6563).\n\n## Usage and execution statistics\n\n`codewhale stats` is an alias of `codewhale metrics`; both use the same local,\nread-only backend. Use `--json` for the structured `stats` report. For example:\n\n```sh\ncodewhale stats --days 7 --models 10 --tools 20 --roles 10\ncodewhale metrics --project /path/to/workspace --json\n```\n\n`--days N` accepts a positive number of days and conflicts with `--since`.\n`--since 24h` and the existing duration syntax remain available. `--models N`,\n`--tools N` and `--roles N` select the top positive N rows by known tokens,\nidentified tool executions and recorded Fleet tasks respectively. These limits\nonly shorten the display; they do not remove stored receipts.\n\nThe report separates saved-document and Runtime views, frozen native-source\nmoney and token subtotals by actual model, retained tool-result history\nestimates and outcomes, completed compaction observations, and recorded Fleet\nroles. USD and CNY stay separate. A cumulative correction replaces its proven\npredecessor; copied Session/Runtime receipts and inherited Runtime fork rows\nare not added as new provider work. Conflicting source versions are shown\nseparately and excluded from the subtotal. Saved legacy display totals remain\nvisible as document projections without being charged again globally.\n\nCoverage is explicit. A missing quote, usage receipt, tool outcome, output\nshape or recovery link is unknown, not zero. Empty scalar usage from older\ntransport records cannot prove a reported zero. Token counts describe known\nreported data; tool history tokens are estimates of retained text, not bytes\nproven to have been replayed on a provider request. Latest prepared-request\ncomponents and source-linked compaction/role economics are unavailable where\nthey were not captured. The report does not infer provider invoices or prove\ncomplete historical coverage.\n\n`--project WORKSPACE` filters current saved/thread/Fleet views. Original source\nworkspace is not yet captured, so native money and model subtotals are\nunavailable for that filter; mutable current metadata is not used to assign a\ncharge. Older receipt projections without reliable workspace scope are also\nunavailable under this filter. Without it, those legacy receipt views remain\nseparate from native subtotals and may overlap one another.\n\nInspection uses the existing session-size bound (16 MiB per record/frame and\nfor each retained query index). Unreadable, linked, oversized or incomplete\nrecords create visible gaps; an exhausted native index refuses the query\nwithout evicting evidence. Use a narrower time window where possible. Reading\nstatistics does not create a store, repair history, refresh pricing, start a\nmodel call or retire accounting receipts. Existing state-root overrides remain\nthe read boundary.\n\n`/cache calls [count]` shows recent retained native source receipts in the TUI;\nit defaults to 10 and displays at most 50. A source correction updates its row.\nThe view keeps frozen USD/CNY estimates and known subtotals separate from missing\nusage or prices; absent or contradictory cache classes show unknown. The header\ncounts retained sources, not physical requests or complete historical calls.\nThe command does not issue a model request or remove accounting receipts.\n\nPublished prices are frozen for DeepSeek's root, `/v1`, `/beta` and Anthropic\nendpoint variants, and for other providers' default endpoints. Each quote stays\nbound to the exact endpoint, identity, model and dispatch time when saved or replayed.\nOperator endpoint overrides do not become published pricing authorities, and\nsubscription or token-plan routes remain outside money coverage. Signed cloud\nfacts keep their separate route-scoping rules. Recognition as an official\ncredential endpoint alone does not authorize sharing prices across regions or plans.\n\nMissing request outcomes, omitted provider usage, unverified legacy usage and\nusage corrections have distinct cost reasons. Known limitation: saved receipts\nwithout a frozen price are not repriced from today's catalog; their complete\ncost remains unknown even after updating the binary.\n\n## Constitution, project instructions, and repo authority\n\nCodewhale has several instruction surfaces. They are deliberately separate so a\npersonal constitution, repo policy, project instructions, and runtime security\ncontrols do not blur together.\n\n- **Bundled global Constitution** — the compiled base law in the binary. It is\n the default floor for every session.\n- **User-global constitution** — the normal guided setup output. Manage it with\n `/constitution` or `/setup`; Codewhale stores structured data at\n `$CODEWHALE_HOME/constitution.json` (default `~/.codewhale/constitution.json`)\n and renders it into a separate `<codewhale_user_constitution>` prose block.\n This can express preferences and stop conditions, but it does not change\n runtime approval policy, sandbox, shell, network, trust, or MCP permissions.\n- **Repo-local constitution** — optional project policy in\n `.codewhale/constitution.json`, described below.\n- **`AGENTS.md`** — cross-agent **project instructions** (prose). This is the\n canonical file for \"how should an agent work in this repo.\" Run `/init` to\n scaffold one. Instruction files written for other tools (`CLAUDE.md`,\n `.claude/instructions.md`, `.cursorrules`, `.github/copilot-instructions.md`\n and similar) are read only when you opt in with the top-level\n `project_instruction_imports` key, for example `[\"claude\"]`, `[\"cursor\"]` or\n `[\"all\"]` (see `config.example.toml`); Codewhale warns when it finds one it\n did not load.\n\n Codewhale reads one instruction file per directory, from the repository\n root down to the workspace, and puts your global instruction file (such as\n `~/.codewhale/AGENTS.md`) in front of them. The nearest scope reads last\n and wins: the workspace file overrides its parents, and project files\n override the global file. Global and project instructions are budgeted\n separately in estimated tokens (see [Instruction budget](#instruction-budget-instruction_budget-6526)\n below): a scope over its budget drops whole files from the broadest end\n first, and a single file larger than the remaining budget keeps its head\n with a marker that names the bytes and tokens dropped. A file keeps at most\n its first 100 KiB, and a file larger than `file_max_bytes` (8 MiB, lowerable\n only) is skipped with a notice. Every loss — trimmed, skipped, or refused —\n reaches the model and `/context report`.\n- **Memory and handoffs** — recalled state. Useful, but lower authority than\n constitutions and project instructions.\n\n### Managing the user-global constitution (`/setup` and `/constitution`)\n\nThe bundled **working agreement** is the safe default and no longer adds a\nrequired first-run screen. Customize it later through `/constitution` or the\nprogressive `/setup` guide. Provider/model readiness, workspace trust, and\nruntime posture stay separate from this guidance.\n\nOn the **Working agreement** (constitution) step:\n\n- **`1`–`6`** tune the guided draft. **`G`** previews it, and **`G`** again\n ratifies and saves a fresh structured `constitution.json`.\n- **`A`** (shown only when a provider is configured) asks your first configured\n model to draft the constitution. Drafting is **not** saving: the draft is\n rendered through the same preview and you still press **`G`** to ratify\n before anything persists.\n- **`K`** keeps your existing loaded constitution unchanged (shown only when a\n valid file is already present).\n- **`U`** (or `/constitution bundled`) records the bundled/default law.\n\n`/constitution` (alias `/law`) is the primary management surface once you are\nset up. Subcommands: `status` (the default), `preview`, `review`, `repo` (the\nrepo-local law block), `explain`, `base`, `suggestions`, `ratify <id>`,\n`migrate`, `rollback`, `edit`/`guided`, `repair`, `posture`, and `bundled`.\nManaging the constitution never changes runtime approval, sandbox,\nshell, network, trust, default mode, or MCP authority — those stay in runtime\nposture/config.\n\nEach repo can carry two distinct, complementary files:\n\n- **`AGENTS.md`** — ordinary project working instructions.\n- **`.codewhale/constitution.json`** — Codewhale-specific **repo authority /\n prioritization policy**: when local sources conflict, which should Codewhale\n trust first, and what to verify before claiming a task is done. `.codewhale/`\n lives inside the repo (like `.github/`). Example:\n\n ```json\n {\n \"schema_version\": 1,\n \"authority\": [\n \"current user request\",\n \"live code and tests\",\n \"GitHub issue/PR details\",\n \"AGENTS.md\",\n \"memory\",\n \"old handoffs\"\n ],\n \"protected_invariants\": [\n \"do not break old-session transcript replay\"\n ],\n \"branch_policy\": \"PRs target the integration branch, not main\",\n \"verification_policy\": {\n \"before_claiming_done\": [\"run focused tests\", \"read changed files back\"]\n },\n \"escalate_when\": [\n \"a destructive action was not explicitly authorized\"\n ]\n }\n ```\n\n All fields are optional. When present, the file is rendered into the system\n prompt as concise prose in a higher-authority block. Legacy `WHALE.md` files\n are ignored and reported as migration-only diagnostics.\n\n Each `protected_invariants` entry may be either a plain string (advisory\n prose, the historical shape) or an object carrying path globs, which is\n additionally **mechanically enforced** in the tool gate. See\n [Enforced repo-law invariants](#enforced-repo-law-invariants) below.\n\n This is the **repo-local law** layer. When guidance conflicts, the\n **Whose word wins** section of the effective base constitution owns the\n ordering; the bundled source is\n [`BASE_PROMPT`](../crates/engine/src/prompts/text.rs). Use `/constitution base`\n to inspect the effective base, including any opted-in expert override.\n The order in which these files are described or assembled is not an\n authority ranking. Runtime policy (permissions, sandbox and cost limits\n enforced in code) is separate from prompt guidance; editing a constitution\n does not grant permissions.\n\n> **`WHALE.md` is deprecated.** It overlapped confusingly with `AGENTS.md`.\n> Codewhale no longer reads `WHALE.md` as project or global context. If one is\n> present, setup/context diagnostics report it as ignored so you can migrate it.\n> Move ordinary instructions to `AGENTS.md` and Codewhale-specific authority\n> policy to `.codewhale/constitution.json`. Personal standing guidance belongs\n> in `/constitution` / `$CODEWHALE_HOME/constitution.json`. (The global\n> Codewhale Constitution shipped in the model prompt is a separate thing and is\n> unaffected.)\n\n### Enforced repo-law invariants\n\nBy default a `protected_invariants` entry is advisory prose: it is rendered into\nthe prompt as guidance the agent should honor, but nothing stops a write. An\nentry written as an **object with `paths`** is different — it compiles into a\nmechanical write hold that the engine's tool gate evaluates before the write\nruns. The law becomes mechanism, not just a request.\n\nAn enforced entry has this shape:\n\n```json\n{\n \"schema_version\": 1,\n \"protected_invariants\": [\n \"Keep DeepSeek support first-class.\",\n {\n \"text\": \"The wire format is frozen; protocol changes need a human.\",\n \"paths\": [\"crates/protocol/**\"],\n \"action\": \"block\"\n },\n {\n \"text\": \"Release notes need human review.\",\n \"paths\": [\"CHANGELOG.md\"],\n \"action\": \"ask\"\n }\n ]\n}\n```\n\n- `text` — required. The reason surfaced on the hold. An empty `text` is skipped.\n- `paths` — workspace-relative globs (globset syntax, e.g. `crates/protocol/**`,\n `**/secrets.toml`, `CHANGELOG.md`). An object with no usable `paths` stays\n advisory-only despite the object shape.\n- `action` — optional, defaults to `ask`. `ask` force-prompts in Ask and\n Auto-Review; in Full Access it denies the protected write without opening a\n modal. `block` **denies the write outright** in every posture.\n\nSemantics:\n\n- **Tighten-only.** The schema has no allow/widen shape, so law can only *add*\n holds — a crafted constitution can never grant authority or weaken a gate\n above it.\n- **Not bypassable by mode.** Like the built-in safety floor, an `ask` hold\n force-prompts in Ask and Auto-Review. Full Access never opens approval\n modals, so the same hold fails closed as a hard block; `block` always denies.\n Mode cannot turn a hold off.\n- **Repo-local only.** Only the repo's `.codewhale/constitution.json`\n participates. The user-global constitution stays advisory prose and never\n reaches this mechanism.\n- **Fails safe.** A missing file, parse error, or invalid glob degrades to\n fewer or zero rules — never a hold on unprotected paths and never a poisoned\n gate. Across matches the strongest action wins, so `block` outranks `ask`.\n- **Leaves a receipt.** Every hold emits a `tool.repo_law_decision` tool-audit\n event naming the invariant, the matched path, and the source file; the\n approval/denial reason names the invariant too.\n\n**Coverage is deliberately limited.** Holds are evaluated only for the write\ntools `write_file`, `edit_file`, `apply_patch`, and `fim_edit`, and only\nagainst the filesystem targets named in their inputs (`path`/`target`/\n`destination`/`file_path`, `changes[].path`, and unified-diff /\n`apply_patch`-envelope headers). A shell command that writes a protected path is **not** held by\nrepo law — those writes are still governed by the ordinary approval, sandbox,\nand shell-write gates, not by this mechanism.\n\n### Expert full base-prompt override (#3638)\n\nThe global Constitution (the base system prompt, normally compiled in from\n`crates/engine/src/prompts/text.rs` as `BASE_PROMPT`) can be replaced per-user\nwithout rebuilding. This is\nan expert escape hatch, not the normal `/constitution` guided setup output.\nBecause this is a prompt trust boundary, it takes **two deliberate steps** — a\nfile alone is not enough:\n\n1. Drop the replacement at `~/.codewhale/prompts/constitution.md` (under\n `$CODEWHALE_HOME` when set).\n2. Set the explicit opt-in flag `CODEWHALE_ALLOW_BASE_PROMPT_OVERRIDE=1`\n (`true`/`on`/`yes` also accepted).\n\nIf the file exists but the flag is unset, the override is **ignored** (with a\nlog line pointing to the flag) and the bundled Constitution stays in place.\nThis is intended for repurposing the TUI beyond software engineering — e.g.\nlong-form writing or document review — where the engineering-oriented base\nprompt is a poor fit. It is loaded once at startup; a **missing or empty file\nis a no-op**, so existing installs keep the bundled prompt.\n\nScope is deliberately narrow: only the byte-stable **base prompt segment** is\noverridable. Mode deltas, the approval policy, the tool taxonomy, Context\nManagement, and the Compaction Relay are still owned by Codewhale's runtime\nassembly, so an override **cannot remove safety-relevant guidance** (sandbox,\napprovals) — it only swaps the task/voice framing. To customize ordinary\npersonal behavior, prefer `/constitution`; to customize per-repo behavior,\nprefer `AGENTS.md` + `.codewhale/constitution.json` above.\n\n## Where It Looks\n\nDefault config path:\n\n- `~/.codewhale/config.toml`\n- Legacy fallback: `~/.deepseek/config.toml`\n\nOverrides:\n\n- CLI: `codewhale --config /path/to/config.toml`\n- Env: `CODEWHALE_CONFIG_PATH=/path/to/config.toml`\n- Legacy env alias: `DEEPSEEK_CONFIG_PATH=/path/to/config.toml`\n\nIf both are set, `--config` wins. Environment variable overrides are applied after the file is loaded.\n\n### TUI editability audit\n\nInside the TUI, run `/config audit` to see which documented keys can be changed\nfrom the current session, which ones can also be persisted, and which ones stay\nfile-only or restart-only. The audit includes current values for the high-impact\nruntime controls such as `approval_policy`, `allow_shell`,\n`stream_chunk_timeout_secs`, `base_url`, `mcp_config_path`, and the\n`[subagents]` concurrency/depth/timeout keys.\n\nUse the command's \"Command / reason\" column as the source of truth before\nediting by hand. For example, `/config approval_mode on-request --save` writes\ntop-level `approval_policy = \"on-request\"`, while provider base URLs are saved\nbut still require restarting the model client.\n\n### User workspace entries\n\nInteractive Agent sessions expose shell tools by default with approval gating\nunless you explicitly disable them. For a shell opt-in that should live in the\nuser's global config for noninteractive or durable-task profiles rather than in\nthe repository, add a workspace-scoped entry:\n\n```toml\n[workspace.'/absolute/path/to/project']\nallow_shell = true\n```\n\nThe entry applies only when the launched workspace path matches the table key.\nThe legacy `[projects.\"/absolute/path/to/project\"]` table is also accepted for\nthis user-owned override.\n\nIn interactive mode, the per-project overlay\n`<workspace>/.codewhale/config.toml` is applied after this user entry. A\nproject-level `allow_shell = false` can still tighten the session; project-level\n`allow_shell = true` is ignored.\n\n### Per-project overlay (#485)\n\nWhen the TUI starts in a workspace that contains a regular-file\n`<workspace>/.codewhale/config.toml`, the safe values declared in that file are\nmerged on top of the global config. Legacy\n`<workspace>/.deepseek/config.toml` files are still read when the Codewhale path\nis absent. Symlinked project config files are rejected. This lets a repo suggest\na model or tighten local safety posture without touching the user's\n`~/.codewhale/config.toml`. Pass `--no-project-config` to skip the overlay for\none launch.\n\nSupported keys in the project overlay (top-level fields only):\n\n| Key | Effect |\n|---|---|\n| `model` | override `default_text_model` |\n| `reasoning_effort` | force `\"high\"` / `\"max\"` for a complex repo |\n| `approval_policy` | only values that tighten the user's current permission posture |\n| `sandbox_mode` | only values that tighten the user's current sandbox posture |\n| `max_subagents` | clamp sub-agent concurrency for a constrained repo (clamped to 1..=128) |\n| `allow_shell` | `false` can disable shell access; `true` is ignored |\n\nThe overlay is intentionally narrow — it covers the fields a repo\nmaintainer is most likely to want to standardize across contributors.\nCredential, endpoint, provider-selection, MCP config, hooks, skills,\nretry, hotbar bindings, and `instructions = [...]` settings stay user-global.\nIf a repo-local config declares `api_key`, `base_url`, `providers`, `provider`,\n`mcp_config_path`, `notes_path`, `hotbar`, `allow_shell = true`, or `instructions`,\nCodewhale ignores that key and keeps the user's global setting.\n\nThe consolidated `codewhale` runtime uses `~/.codewhale/config.toml` for\nprovider and model settings. `codewhale auth set --provider deepseek` saves\nthe key in the local secret store (a private file under `~/.codewhale/secrets/`\nby default; the OS keychain is an explicit option), rather than writing it\ninto that config file. Legacy `~/.deepseek/config.toml` is migrated on first\nlaunch when needed. `codewhale --model deepseek-v4-flash` is forwarded to the\nTUI as `CODEWHALE_MODEL`. The dispatcher no longer writes `DEEPSEEK_*` twins;\na value you set yourself remains a legacy alias when its `CODEWHALE_*`\nequivalent is unset.\n\n### Account provider keys\n\n`/connect` offers Continue with Codewhale or Connect on this computer. The\naccount path signs in and lists usable model connections reported by the\naccount service. When none are ready, it offers retry or on-computer setup.\nSign-in alone does not supply model access or change the selected model.\nLocal provider keys and local models never need an account; use `/provider`\nor `codewhale auth set --provider <provider>` to manage them.\n\nFor an account that supports saved provider keys, these commands manage them:\n\n```bash\ncodewhale account keys list # configured status, never secret values\ncodewhale account keys remove deepseek # remove the account's saved key\n```\n\nLogin never uploads local keys, and removing an account key does not change\nlocal copies or revoke the key at the provider.\n\nThe account session is separate from provider keys. `/login` signs in inside\nthe TUI; `codewhale login` does so from the shell. `codewhale account status`\ninspects it. `/logout`, `codewhale logout` and `codewhale account logout` remove\nonly the account session for the selected `--profile`. The explicit\n`codewhale logout --all` action also clears saved model credentials.\nAccount sessions use the private `0600`\nCodewhale secrets file, scoped to the profile and account API origin; they do\nnot use the OS keychain. The older `codewhale cloud ...` spelling remains an\nalias for account commands.\n\n### Portable config bundles\n\n`codewhale config export --portable [--project] [--out FILE]` writes a\nportable, secret-free bundle of your configuration: sorted TOML with\ncredential and machine-specific keys (API keys, base URLs, socket paths)\ndropped, never a redacted placeholder in their place. Without `--out` the\nbundle goes to stdout. Typed tables, arrays, numbers, booleans, and datetimes\nremain typed. Machine-bound authority is deliberately non-portable: project\ntrust overlays, credential readers, auto-running hooks, executable LSP\ndefinitions, and local path bindings are omitted rather than copied to a new\nhost. So is trust posture (`yolo`, `allow_shell`, `approval_policy`,\n`sandbox_mode`, `sandbox_network_access`, and the sandbox read-denylist\npaths) and local endpoints or executables (`[lifecycle_outbox]`,\n`[extension_host]`, `[control_socket]`). URLs that carry a credential —\nuserinfo, a token-named query parameter, or a Slack/Discord/Teams webhook\npath — are treated as credentials.\n\n`codewhale config import <FILE|HTTPS_URL|-> [--dry-run] [--yes] [--project]`\napplies a bundle. The envelope is strict (`schema_version = 1`, kind\n`codewhale.portable-config`; unknown fields fail). Import prints a\ndeterministic plan — added / changed / skipped / conflicting / rejected —\nthen asks for consent unless `--yes` is given; headless use requires it.\nCredential-shaped entries are rejected by key name and by value shape;\nrejections name the field, never a value. Remote bundles come from HTTPS\nonly (loopback http excepted) with a 5 MiB cap. Application backs up the\ntarget document to `<config>.bundle-backup-<timestamp>-<random>`, rolls back on any\nfailure, and re-importing an applied bundle changes nothing.\n\nImport also rejects the non-portable authority classes omitted by export,\nincluding nested/camel/dotted credential keys and cookie headers. This keeps a\nhand-authored or remote bundle from reintroducing machine trust, local\ncredential access, or automatically executable commands that a local export\nwould refuse to carry. Structured tables are deep-merged: a portable model or\npreference update does not erase target-local provider credentials, endpoints,\nhooks, or executable definitions that were deliberately omitted from the\nbundle. Arrays and scalar values still replace the corresponding portable\nvalue.\n\nSections map to scope: `[project]` entries land only in the workspace\ndocument (`--project`, which must target an actual workspace config),\n`[global]` only in the user-global one; `preferences`, `profiles`, and\n`plugins` apply at either scope. A global bundle operation refuses a workspace\ndocument just as a project operation refuses the user-global document.\n\n### Credential read precedence (#5197)\n\nCredential reads are **folder-independent by default**: every layer below is\nuser-global or process-scoped, so a key saved in one repo resolves identically\nin every other repo. Repo-local config never carries credential material — the\nproject overlay above reads only its allowlisted keys and ignores `api_key`,\nand a credential write aimed at a workspace-scoped config is rescoped to the\nuser-global `~/.codewhale/config.toml` (#5045, #5193).\n\nFor the active provider, the runtime resolves the API key in this exact order\n(first match wins):\n\n1. **Route-specific auth contract.** Routes whose `auth_mode` disables API\n keys stop here with no credential. OAuth routes use their explicitly\n granted token: the official `openai-codex` plan route accepts only\n Codewhale-owned credentials from `codewhale auth chatgpt`, bound to its\n issued registration and verified account. Imported Codex CLI and process\n tokens cannot authorize this route;\n `[providers.xai] auth_mode = \"oauth\"` reads Codewhale's own xAI\n device-login store (or a consent-granted Grok CLI file).\n2. **Explicit CLI key.** `--api-key` forwarded with its source marker wins\n over every saved slot.\n3. **Config file `api_key`.** The `[providers.<name>] api_key` table slot for\n the active provider. `deepseek-CN` also reads `[providers.deepseek]`. An\n older top-level `api_key` is read as `[providers.deepseek] api_key` (see\n [Legacy top-level `base_url` and `api_key`](#legacy-top-level-base_url-and-api_key)).\n File-owned keys stay bound to their file-owned endpoint: when the\n environment replaces the route's base URL with a custom host, the saved\n key is not sent there.\n4. **`api_key_env` binding.** `[providers.<name>] api_key_env = \"VAR\"` reads\n the named environment variable. For custom providers an unset or empty\n binding is a loud error, not a silent fallback (#5104).\n5. **Secret store.** The durable per-provider slot written by\n `codewhale auth set` (file-backed under `~/.codewhale/secrets/` by\n default; the OS keyring only when explicitly selected). Skipped for named\n custom routes, self-hosted providers, custom endpoints other than an\n explicitly authenticated loopback, and routes whose `auth_mode` needs no\n key.\n6. **Ambient environment.** The provider's own variable\n (`DEEPSEEK_API_KEY`, `OPENROUTER_API_KEY`, `MOONSHOT_API_KEY`, …).\n Ambient keys are only ever sent to the provider's official endpoint and\n are skipped under the same conditions as the secret store.\n7. **Keyless fallback.** Self-hosted providers and loopback endpoints may run\n with no credential; every other route fails with provider-specific setup\n guidance.\n\nLegacy compatibility: `~/.deepseek/config.toml` is migrated into\n`~/.codewhale/config.toml` on first launch, `DEEPSEEK_*` environment\nvariables remain accepted aliases for the `CODEWHALE_*` forms, and\n`DEEPSEEK_SECRET_BACKEND` is the legacy alias for `CODEWHALE_SECRET_BACKEND`.\n\nRun `codewhale auth status` to inspect the active provider's config\nfile, OS keyring backend, environment variable, winning source, and last-four\nlabel without printing the key itself. The command only probes the active\nprovider's keyring entry.\n\nFor hosted, generic OpenAI-compatible, self-hosted, OpenAI Responses, or native\nAnthropic providers, set `provider = \"<id>\"` or pass\n`codewhale --provider <id>`. The canonical provider IDs are `deepseek`,\n`nvidia-nim`, `openai`, `atlascloud`, `wanjie-ark`, `volcengine`,\n`openrouter`, `orcarouter`, `xiaomi-mimo`, `novita`, `fireworks`,\n`siliconflow`, `arcee`, `siliconflow-CN`, `moonshot`, `sglang`, `vllm`,\n`ollama`, `ollama-cloud`, `huggingface`, `modelscope`, `together`, `qianfan`, `openai-codex`,\n`anthropic`, `openmodel`, `zai`, `stepfun`, `minimax`, `deepinfra`,\n`sakana`, `longcat`, `opencode-go`, `opencode-zen`, `meta`, `xai`,\n`mistral`, `telecomjs`, `modelstudio-token-plan`, `google`,\n`edenai`, `concentrate`, `codewhale`, and `custom` (a user-defined OpenAI-compatible endpoint via\n`[providers.<name>]`).\nFor the provider-by-provider registry, including wire protocol, auth variables,\ndefault base URLs, model IDs, and capability metadata, see\n[PROVIDERS.md](PROVIDERS.md).\nThe facade saves provider credentials in the local secret store and forwards\nthe resolved key, base URL, provider, and model to the TUI process. Use\n`codewhale auth set --provider nvidia-nim --api-key \"YOUR_NVIDIA_API_KEY\"` or\n`codewhale auth set --provider openai --api-key \"YOUR_OPENAI_COMPATIBLE_API_KEY\"` or\n`codewhale auth set --provider atlascloud --api-key \"YOUR_ATLASCLOUD_API_KEY\"` or\n`codewhale auth set --provider wanjie-ark --api-key \"YOUR_WANJIE_API_KEY\"` or\n`codewhale auth set --provider xiaomi-mimo --api-key \"YOUR_XIAOMI_KEY\"` or\n`codewhale auth set --provider fireworks --api-key \"YOUR_FIREWORKS_API_KEY\"` or\n`codewhale auth set --provider siliconflow --api-key \"YOUR_SILICONFLOW_API_KEY\"` or\n`codewhale auth set --provider arcee --api-key \"YOUR_ARCEE_API_KEY\"` or the\nmatching provider ID from [PROVIDERS.md](PROVIDERS.md) to save provider keys\nthrough the facade. The generic `openai` provider defaults\nto `https://api.openai.com/v1`, accepts `OPENAI_BASE_URL`, and defaults to\n`gpt-5.6`. A custom OpenAI-compatible gateway can still select its own model\nexplicitly. `atlascloud` defaults to\n`https://api.atlascloud.ai/v1`, accepts `ATLASCLOUD_BASE_URL`, and uses\n`deepseek-ai/deepseek-v4-flash` as its default model. `wanjie-ark` targets\nWanjie Ark's OpenAI-compatible endpoint at\n`https://maas-openapi.wanjiedata.com/api/v1`, defaults to `deepseek-reasoner`,\nand passes model IDs through unchanged because Wanjie model access is\naccount-scoped. SGLang, vLLM, and Ollama are\nself-hosted and can run without an API key by default. Ollama defaults to\n`http://localhost:11434/v1` and sends model tags such as `codewhale-coder:1.3b`\nor `qwen2.5-coder:7b` unchanged. Self-hosted providers and loopback custom\nURLs (`localhost`, `127.0.0.1`, `[::1]`, `0.0.0.0`) do not read the secret store\nunless API-key auth is explicitly requested; use an env var or config-file key\nwhen a local server does require bearer auth.\nOllama Cloud is the separate hosted `ollama-cloud` provider. It defaults to\n`https://ollama.com/v1` and `gpt-oss:120b`; save its key with\n`codewhale auth set --provider ollama-cloud`. Ambient auth reads\n`OLLAMA_CLOUD_API_KEY` first, then Ollama's official `OLLAMA_API_KEY`.\nSiliconFlow defaults to `https://api.siliconflow.com/v1`, accepts\n`SILICONFLOW_BASE_URL`, and uses `deepseek-ai/DeepSeek-V4-Pro` by default.\n`provider = \"siliconflow-CN\"` selects the China regional default\n`https://api.siliconflow.cn/v1` with the `[providers.siliconflow_cn]` table and\n`SILICONFLOW_API_KEY` credential slot.\nArcee AI defaults to `https://api.arcee.ai/api/v1`, accepts `ARCEE_BASE_URL`,\nand uses `trinity-large-thinking` by default for Codewhale agent work.\n`trinity-large-preview` is also listed as a direct Arcee API model; OpenRouter's\n`arcee-ai/trinity-large-thinking` remains the OpenRouter namespaced form, while\nthe direct Arcee provider uses the bare `trinity-large-thinking` ID. Direct\nArcee large-model API calls are tracked as 256K-context BF16 serving; Thinking\nis reasoning-capable, while Preview is not marked as a thinking model.\n\n### OpenRouter vendor pinning\n\nOpenRouter serves each model through several upstream vendors, and Codewhale\ncan pin requests to a vendor with `[providers.openrouter] vendor` (#6007):\n\n```toml\nprovider = \"openrouter\"\n[providers.openrouter]\nmodel = \"deepseek/deepseek-v4-pro\"\nvendor = \"deepinfra\" # copy the vendor slug from the model's OpenRouter page\n```\n\nThis sends `\"provider\": {\"order\": [\"deepinfra\"], \"allow_fallbacks\": false}`\non OpenRouter requests. A base slug can match multiple endpoint variants;\ncopy a full slug such as `deepinfra/turbo` to select one variant. An unavailable\npin fails at OpenRouter. Codewhale's separate `fallback_providers` setting can\nstill switch the whole route after a recoverable error.\n\nThe pin applies across OpenRouter models, including auxiliary requests on that\nroute. Set `vendor = \"\"` to clear it. Reload config or restart to apply edits;\nrequests already in flight keep their captured route. Other providers do not\ninherit the pin. `/preview-request` shows the primary request's routing fields.\n\nModel strings still pass through verbatim: `:floor` sorts by price, `:nitro`\nsorts by throughput, and `@preset/my-team-preset` references an account preset.\nSee OpenRouter's [provider routing](https://openrouter.ai/docs/guides/routing/provider-selection)\nand [presets](https://openrouter.ai/docs/guides/features/presets) documentation.\nCodewhale does not fetch per-vendor endpoint prices or availability; pinned\nusage reports a routing-dependent unknown cost instead of a catalog estimate.\n\n### Custom OpenAI-Compatible Gateways\n\nFor a single third-party service that implements the OpenAI Chat Completions\nAPI, the simplest setup is the built-in `openai` provider name pointed at the\ngateway:\n\n```toml\nprovider = \"openai\"\ndefault_text_model = \"your-model-id\"\n\n[providers.openai]\napi_key = \"YOUR_OPENAI_COMPATIBLE_API_KEY\"\nbase_url = \"https://your-gateway.example/v1\"\n```\n\nPut the endpoint under `[providers.openai]`, not the legacy top-level\n`base_url`, so the OpenAI-compatible provider receives it. `default_text_model`\nis the model ID sent to the gateway; `[providers.openai].model` can be used as\nthe OpenAI-provider-specific override.\n\nIf you keep several OpenAI-compatible gateways, or need a stable name for an\nAgentProfile provider pin, define a user-named custom provider table:\n\n```toml\nprovider = \"lm-studio\"\n\n[providers.lm-studio]\nkind = \"openai-compatible\"\nbase_url = \"http://127.0.0.1:1234/v1\"\napi_key = \"lm-studio\"\nmodel = \"qwen-2.5-7b\"\n```\n\nCustom provider names may be selected with `provider = \"<name>\"`,\n`--provider <name>`, or an AgentProfile `provider = \"<name>\"` when the matching\n`[providers.<name>]` table exists.\n\nA custom table sends Chat Completions requests by default. Add\n`wire = \"responses\"` to send OpenAI Responses requests to\n`<base_url>/responses` instead, or `wire = \"anthropic\"` for Anthropic Messages:\n\n```toml\n[providers.my-gateway]\nkind = \"openai-compatible\"\nbase_url = \"https://gateway.example/v1\"\napi_key_env = \"MY_GATEWAY_API_KEY\"\nmodel = \"gpt-5.6\"\nwire = \"responses\"\n```\n\nStepFun has a first-class provider entry, so keep Coding Plan credentials and\nbase URL scoped to `[providers.stepfun]`:\n\n```toml\nprovider = \"stepfun\"\n\n[providers.stepfun]\napi_key = \"YOUR_STEPFUN_API_KEY\"\nbase_url = \"https://api.stepfun.ai/step_plan/v1\"\nmodel = \"step-3.7-flash\"\n```\n\n`/provider` setup asks which StepFun billing route the key belongs to —\npay-as-you-go (`https://api.stepfun.ai/v1`) or a Step Plan subscription\n(`https://api.stepfun.ai/step_plan/v1`) — and validates the key against the\nendpoint you pick before saving it. The answer is written to\n`[providers.stepfun].base_url` and nowhere else. If that key already holds a\nbase URL Codewhale does not recognize as one of those two routes, the question\nis skipped and your value is left untouched.\n\nAlibaba Bailian / Model Studio DashScope Qwen routes use the same OpenAI\nprovider shape:\n\n```toml\nprovider = \"openai\"\n\n[providers.openai]\napi_key = \"YOUR_DASHSCOPE_API_KEY\"\nbase_url = \"https://dashscope-intl.aliyuncs.com/compatible-mode/v1\"\nmodel = \"qwen-plus\"\ncontext_window = 1000000\n```\n\nUse the regional DashScope `compatible-mode/v1` base URL that matches the\nregion of your API key. Codewhale keeps `qwen-plus` scoped to the `openai`\nprovider route and does not infer a different provider from the model prefix.\nThe same rule applies to all provider-prefixed model strings: a prefix such as\n`deepseek-ai/...` or `deepseek/...` is a provider-owned wire ID under the\nselected provider, not an automatic switch to the DeepSeek provider.\nSet `context_window` to the gateway/model's real total context window when it\ndiffers from Codewhale's static model metadata. See\n[Context length (context window)](#context-length-context-window) for the full\nresolution order and for how to check which value is in effect.\n\nIf the gateway accepts `POST /chat/completions` but rejects\n`/v1/chat/completions`, set a provider-local `path_suffix`:\n\n```toml\n[providers.openai]\nbase_url = \"https://your-gateway.example/v1\"\npath_suffix = \"/chat/completions\"\n```\n\nThe suffix applies only to chat-completion requests. Model listing and\nDeepSeek beta paths keep their built-in routing so a generic gateway override\ndoes not accidentally rewrite `/models` or `/beta/completions`.\n\nFor private gateways with broken or intercepted certificates, use\n`SSL_CERT_FILE` with a trusted CA bundle. The legacy provider-table key\n`insecure_skip_tls_verify = true` is still parsed so `codewhale doctor` can\nreport stale configs, but provider clients reject it instead of disabling TLS\ncertificate verification.\n\nLocal HTTP endpoints such as Ollama, SGLang, and vLLM are allowed by default\nwhen they use localhost or loopback addresses. For a non-local `http://`\ngateway, launch with `DEEPSEEK_ALLOW_INSECURE_HTTP=1` only on a trusted network:\n\n```bash\nDEEPSEEK_ALLOW_INSECURE_HTTP=1 codewhale\n```\n\nThird-party OpenAI-compatible gateways that need extra request headers can set\n`http_headers = { \"X-Model-Provider-Id\" = \"your-model-provider\" }` at the top\nlevel or under a provider table such as `[providers.deepseek]`. When configured,\ncodewhale sends those custom headers on model API requests. The equivalent\nenvironment override is `DEEPSEEK_HTTP_HEADERS`, using comma-separated\n`name=value` pairs such as\n`X-Model-Provider-Id=your-model-provider,X-Gateway-Route=dev`. `Authorization`\nand `Content-Type` are managed by the client and are not overridden by this\nsetting.\n\n### Vision Model\n\nCodewhale's chat provider and `image_analyze` tool are configured separately.\nThe main chat path remains the selected text/tool provider; image analysis runs\nthrough `[vision_model]` when the `vision_model` feature is enabled.\n\nXiaomi's current image-understanding docs include `mimo-v2.5` for image input.\nTo use MiMo for `image_analyze`, configure the vision model explicitly:\n\n```toml\n[features]\nvision_model = true\n\n[vision_model]\nmodel = \"mimo-v2.5\"\napi_key = \"YOUR_XIAOMI_KEY\"\nbase_url = \"https://api.xiaomimimo.com/v1\"\n```\n\nThe example above uses Xiaomi MiMo's pay-as-you-go OpenAI-compatible endpoint.\nIf you are using a Token Plan key (`tp-...`) for `[vision_model]`, you must set\n`base_url` explicitly because this generic OpenAI-compatible block does not\nauto-select MiMo endpoints. Use\n`https://token-plan-sgp.xiaomimimo.com/v1` for Singapore accounts,\n`https://token-plan-cn.xiaomimimo.com/v1` for China-region accounts, or\n`https://token-plan-ams.xiaomimimo.com/v1` for Europe/Amsterdam accounts.\n\n### Auto Model Routing (`[auto]`, `[auto.router]`)\n\nWith `model = \"auto\"`, each turn runs on your **declared default model** unless\nyou have opted into something else. Auto never guesses a cheaper or stronger\nmodel from how a request is worded; the old keyword-and-length heuristic was\nremoved (`auto_route_declared_fallback` in `crates/engine/src/model_routing.rs`).\nTwo optional layers change that:\n\n- an `[auto.router]` classifier, which you write down, that picks a model per\n turn; and\n- `[auto] cost_saving`, which prefers the active provider's fast sibling.\n\nWith neither set, Auto is local and free: the turn uses the default model and\nno classifier call is made.\n\n**There is no default classifier.** With `[auto.router]` unset, no classifier\ncall happens, whatever keys you hold. Holding a DeepSeek key used to elect\n`deepseek-v4-flash` automatically. That was removed because it spent tokens on\na route the user never chose and privileged one provider over the rest\n(`AutoRouterConfig` in `crates/tui/src/config.rs`). Electing a network\nclassifier is now something you write down.\n\nPoint the classifier at any configured provider with `[auto.router]`:\n\n```toml\n[auto.router]\nprovider = \"zai\"\nmodel = \"glm-5-turbo\"\nthinking = \"off\" # optional; defaults to off\ntimeout_secs = 4 # optional; default 4, 0 = default, capped at 300\n```\n\nA classifier call happens only when `[auto.router]` names both `provider` and\n`model` *and* that provider has a key:\n`router_available = router_configured && has_api_key_for(...)` in\n`ModelInventory::from_config` (`crates/engine/src/model_inventory.rs`). If either\ncondition fails, or the classifier call errors or times out, the local\nfallback decides: the default model, or the fast sibling under `cost_saving`.\nThe turn's route receipt (Turn Inspector, Ctrl+Alt+O or `/turn inspect`, \"Model\nroute + tokens/cost\") records which path was taken, and a\nrouter you configured that cannot run or fails (missing key, HTTP error,\ntimeout, invalid answer) is shown as `Auto router: failing — …` rather than\nsilently ignored.\n\n#### Set up model routing\n\n`/router` (also `/model router`) opens one Router setup view with these\npresets. Each writes only `[auto.router]`; none is ever chosen for you.\n\n| Preset | What it writes | Cost and privacy |\n| --- | --- | --- |\n| `/router jev` | Jev, TypeSafe's decision model, over OpenRouter (`typesafe/jev-1.13`) or TypeSafe direct, whichever key you have | About $0.00002 per turn ($0.042 per million input tokens, output free). Your latest request and up to six recent context lines go to OpenRouter → TypeSafe (or TypeSafe). |\n| `/router fast` | The active provider's runnable fast tier with thinking off | Your existing key; the classifier sees the same request text. |\n| `/router off` | Removes `[auto.router]` | No router call; Auto turns use the default model, or the fast tier while `[auto] cost_saving = true` (Off leaves that setting alone). |\n| `/router custom` | Nothing; prints the TOML to edit | — |\n\nChoosing Jev or Fast makes **one test call** with a fixed sample request and\nshows the tier it picked, the probabilities and confidence, the latency and the\nprovider-reported cost. `Enter` (or `/router save <preset>`) then saves through\nthe normal config writer; `Esc` discards it. TypeSafe paused new signups on\n2026-09-22, so OpenRouter is the default route for new users. A TypeSafe key is\nread from `TYPESAFE_API_KEY`, the `typesafe` secret-store entry, or\n`[providers.typesafe] api_key` / `api_key_env`.\n\n#### Decision routers (`kind = \"decision\"`)\n\nA decision router asks a non-generative decision model typed Choice questions\nfor the active provider's `fast` and `strong` tiers and thinking level, then\nreads calibrated probabilities. No prose is parsed.\n\n```toml\n[auto.router]\nkind = \"decision\" # default \"chat\"\nprovider = \"openrouter\" # or \"typesafe\"\nmodel = \"typesafe/jev-1.13\" # \"~typesafe/jev-latest\" also works; TypeSafe direct: \"jev-latest\"\ntimeout_secs = 2\nmin_confidence = 0.5 # default 0.5, clamped to 0..1\n```\n\n- The router is called only when the active provider has a runnable strong/fast\n pair; otherwise there is no call and no spend.\n- An answer below `min_confidence` takes the local fallback. Under\n `[auto] cost_saving`, a `strong` answer also needs a probability of at least\n 0.75, or the turn stays on the fast tier.\n- An unknown `kind`, or a decision `provider` other than `openrouter` /\n `typesafe`, leaves the router unconfigured and shown as failing.\n- `[auto.router] thinking` is ignored for decision routers. Both routes settle tokens through\n the originating session's routed-usage ledger. TypeSafe is a named Custom route\n with unknown billing; its price is never borrowed from the active chat provider.\n Provider-reported cost remains verbatim on the decision receipt.\n- OpenRouter uses `POST /api/alpha/decisions`; TypeSafe uses `POST /v1/systemone`.\n The shared client validates Choice, Noul and Score against the offered questions\n and bounds responses to 256 KiB. Malformed answers fail open with usage retained.\n\n#### Shadow Decision Gate (experimental, off by default)\n\nThe Superfast Decision Gate asks a decision model three typed questions about\nthe latest user message — does it need a tool, can it be answered from the\nconversation, and what is its intent — and logs a conservative recommendation.\nIt is shadow-only: it never changes routing, never skips or delays the model\ncall, and fails open on any error, timeout or malformed answer. It uses the same\nSystem One client as the decision router above; there is no separate HTTP\nclient. It is configured from the environment and reads it when a turn starts:\n\n```sh\nSUPERFAST_ENABLED=1 # off unless set\nSUPERFAST_PROVIDER=typesafe # or openrouter; required when enabled\nSUPERFAST_BASE_URL=http://localhost:8000/v1 # optional TypeSafe-route base, e.g. self-hosted\nSUPERFAST_MODEL=jev-latest # default jev-latest / ~typesafe/jev-latest\nSUPERFAST_TIMEOUT_MS=150 # 1..=10000, default 150\n```\n\n- Enabling the gate never picks an endpoint by itself: without\n `SUPERFAST_PROVIDER` nothing is sent and a warning is logged.\n- The key comes from the same place the decision router reads it. The TypeSafe\n route always authenticates, so a self-hosted server that ignores auth still\n needs a placeholder `TYPESAFE_API_KEY`.\n- Only the latest user message is sent, truncated to 4,000 characters and\n redacted of configured secrets. The log (target `superfast`) carries the\n route, failure class and latency, never prompt text.\n- The gate retains the originating turn's accounting owner and cancellation.\n Its tokens settle through the shared ledger; missing usage or a cancelled/timed\n out request after dispatch creates an explicit coverage gap. Unknown TypeSafe\n pricing is recorded as unpriced rather than free.\n- Provider-reported cost survives rejected answers and late responses in bounded\n receipts on the originating turn or session. These are diagnostic evidence;\n unknown pricing never becomes an authoritative dollar total. Incomplete token\n counters record a coverage gap while preserving the reported raw evidence.\n\nThe wire contracts are documented in [TypeSafe's OpenAPI schema](https://api.typesafe.ai/openapi.json)\nand [OpenRouter's Decisions examples](https://openrouter.ai/blog/insights/what-is-jev/).\n\nThe Decision Gate concept and reference implementation are by Andrea Bruno,\nreleased under CC BY 4.0:\n[harness-superfast](https://github.com/Andrea-Bruno/harness-superfast).\n\nTwo `[auto]` keys shape routing (`AutoConfig` in `crates/tui/src/config.rs`):\n\n```toml\n[auto]\ncost_saving = false # default false\ncross_provider = false # default false\n```\n\n- **`cost_saving`** (default `false`). Without a classifier, Auto pins the\n active provider's validated fast sibling instead of the default model. A\n provider with no runnable fast sibling stays on the default. With a\n classifier, the classifier is told to prefer the fast tier for routine or\n ambiguous work and to pick the strong tier only for clearly agentic,\n multi-step, architecture, security or debugging work. Cost-saving never\n switches provider just to save money.\n- **`cross_provider`** (default `false`). Auto stays on the provider the session\n is configured to use. The classifier is only shown that provider's models,\n and the fallback never leaves it. Setting `cross_provider = true` lets the\n classifier choose among every runnable provider. There is no interactive\n toggle for `cross_provider`; it has to be set in config. A decision router\n always chooses within the active provider.\n\nTo bootstrap MCP and skills directories at their resolved paths, run `codewhale setup`.\nTo only scaffold MCP, run `codewhale mcp init`.\n\nNote: `setup`, `doctor`, `mcp`, `features`, `sessions`, `resume`/`fork`, `exec`,\n`review`, and `eval` are all available from the installed `codewhale` command.\nThe consolidated dispatcher also provides `auth`, `config`, `model`, `thread`, `sandbox`,\n`app-server`, `mcp-server`, `completions`, `login`/`logout`, `account`,\n`metrics`, `update`, `lane`, `workflow`, and `web`. Plain prompts enter the\nin-process TUI runtime. Release installers expose the same bytes as `codew`.\n\n### Startup Update Checks\n\nBy default, the TUI starts a background check for the latest stable Codewhale\nrelease and shows a short toast only when a newer release is available and the\nofficial release assets are complete. The check never blocks startup, never\nblocks a turn, and fails silently when offline.\n\nDisable the startup check entirely for air-gapped, corporate-proxy, or managed\ndesktop environments:\n\n```toml\n[update]\ncheck_for_updates = false\n```\n\n#### Automatic updates\n\n`mode` decides what happens when a newer release is found (release-binary\ninstalls only; package-manager and desktop-app copies always just notify):\n\n```toml\n[update]\nmode = \"auto\" # auto (default): download in the background, apply at next start\n # notify: tell me only; off: no checks\n```\n\n`codewhale config set update.mode notify` writes it. `CODEWHALE_UPDATE_MODE`\noverrides it, and an organization policy file overrides both; see\n[INSTALL.md](INSTALL.md#automatic-updates) for the full flow, signing, the\npolicy file and `codewhale update --rollback`.\n\n#### Throttling\n\nThe answer is cached in `~/.codewhale/update-check.json` and reused for\n`check_interval_hours` (default `6`). Only the *network request* is throttled —\nthe notice still appears on every launch while an update is outstanding. Set `0`\nto check on every launch.\n\n```toml\n[update]\ncheck_interval_hours = 6\n```\n\nA failed check keeps the last answer (so the notice stays) and retries about an\nhour later rather than on every launch, which keeps offline and rate-limited\nmachines quiet.\n\n#### Automatic suppression\n\nChecks are skipped, without contacting the network, when any of these is set to\na non-falsey value:\n\n| Variable | Why |\n| --- | --- |\n| `CODEWHALE_NO_UPDATE_CHECK` | Explicit opt-out. |\n| `NO_UPDATE_NOTIFIER` | The cross-CLI convention, honored for compatibility. |\n| `CI`, `CONTINUOUS_INTEGRATION`, `GITHUB_ACTIONS`, `GITLAB_CI`, `BUILDKITE`, `CIRCLECI`, `JENKINS_URL`, `TEAMCITY_VERSION`, `TF_BUILD` | Automated build; nobody is at the terminal. |\n\nValues of `\"\"`, `0`, `false`, `no`, and `off` do not count as set, so a\n`CI=false` export does not disable checks for ordinary users.\n\n#### Which update command is offered\n\nCodewhale never installs anything on its own — it only tells you an update\nexists. The command it names depends on how the running binary was installed,\ndetected from its path:\n\n| Install | Command offered |\n| --- | --- |\n| GitHub release binary (including Termux) | `codewhale update` |\n| npm (`node_modules` on the path) | `npm install -g codewhale@latest` |\n| Homebrew (`Cellar` / `linuxbrew` prefix) | `brew upgrade codewhale` |\n| `cargo install` (`~/.cargo/bin`) | `cargo install codewhale-cli --locked --force` |\n\nFor package-managed installs the notice also warns against `codewhale update`:\nreplacing a binary Homebrew or npm owns leaves the manager describing a version\nthat is no longer on disk, and the next upgrade silently reverts you.\n\nOverride the detection with `CODEWHALE_INSTALL_METHOD=npm|homebrew|cargo|binary`\nif you relocated the binary somewhere the path heuristics cannot read.\n\nTo redirect the startup check, set `update_uri` to an internal endpoint that\nreturns GitHub-compatible latest-release JSON. Minimal mirror metadata with a\n`tag_name` field is accepted; if `assets` are present, Codewhale requires the\nsame uploaded asset set as the official release before showing the toast.\n\n```toml\n[update]\ncheck_for_updates = true\nupdate_uri = \"https://internal.mirror.example/codewhale/releases/latest\"\n```\n\nWhen `update_uri` is not set, startup checks honor release mirror environment\nvariables such as `CODEWHALE_RELEASE_BASE_URL` before falling back to the\nofficial GitHub API endpoint. If a configured `update_uri` cannot be fetched or\nparsed and a release mirror env var is set, the TUI falls back to that mirror\ninstead of failing startup.\n\n#### What's new after an update\n\nThe first time Codewhale starts after its version goes up, it says so once and\nlinks that version's release notes on codewhale.net:\n\n```text\nUpdated to 0.10.2. Got 12 features, 31 fixes and 8 other changes.\nRelease notes: https://codewhale.net/en/changelog/0.10.2\n```\n\nThe counts come from the changelog compiled into the binary. Entries under\nAdded or New count as features; Fixed, Security and Latest fixes count as\nfixes; Contributors, Thanks, Community and Known limits or issues are not\ncounted; every other section counts as other changes. The link uses your UI\nlanguage where the site has it (`zh` for Chinese) and English otherwise.\n\nThe TUI shows the notice on the startup card and moves it into the transcript\nwhen the card leaves (at once when the session opens from `--resume`,\n`--continue` or a prompt), adding `/change <version>` for reading the notes in\nplace. The `doctor`, `models`,\n`sessions`, `receipts`, `setup`, `init`, `install`, `features`,\n`integrations`, `mcp`, `config`, `model`, `metrics`, `providers`, `account`,\n`login`, `logout` and `update` commands print it to stderr instead, when\nstdout and stderr are both terminals and no JSON output is requested. `exec`,\nservers, JSON output and non-terminal runs never show it; the next start that\ncan show it does.\n\nNothing is shown on a fresh install or after a downgrade. The version that\nlast started is recorded in `~/.codewhale/last-launch.json`. The notice reads\nonly local files, so it is independent of `check_for_updates`. To turn it off:\n\n```toml\n[update]\nshow_whats_new = false\n```\n\n## Workshop output budgets\n\nBy default an oversized tool result uses bounded spillover: a result\nunder the byte threshold stays inline, a larger one gets a head/tail\npreview plus a session artifact the model can read back. There is no\nsynthesis sub-agent and no per-call `raw = true` escape.\n\n`[workshop] large_output_threshold_tokens` and\n`[workshop.per_tool_thresholds]` (exact model-visible tool names such as\n`bash`) only take effect when the process opts in to adaptive evidence\nrouting with `CODEWHALE_ADAPTIVE_OUTPUT_ROUTING=1`; they set where a result\nstops being inline and becomes handle-only evidence.\n\nTwo optional byte ceilings (#5367) apply either way. They raise the\nmodel-visible floor and never lower it:\n\n- `read_result_max_bytes` — cap for a single `read` / `read_file`\n result. Absent keeps the compile-time defaults (100000 bytes for\n `read`, which has no line cap; 16KiB / 500 lines for `read_file`).\n For `read` this is the middle of a three-layer budget: the model's\n own per-call `max_bytes` (hard maximum 500000) raises the budget for\n one call, this setting raises the floor for the whole process, and\n either way 2MiB is the absolute ceiling. Highest wins; neither layer\n can lower a budget the other granted.\n- `tool_result_max_bytes` — cap for a generic tool result after\n spillover. Absent keeps the 12K-character compact floor (48K on\n windows ≥500K tokens). Hard cap is 2MiB.\n\n## Context length (context window)\n\nAlso called context size, context limit, max context, or window. This is the\ntotal token window Codewhale budgets against, and it drives the header/footer\ncontext percent, the auto-compaction trigger, context-pressure checks, and the\nrequest output cap. If Codewhale compacts at 128K on a model you know serves a\n1M window, this is the setting to change (#5134).\n\n**See what is in effect, and where the value came from.** Every one of these\nprints the resolved window *and* its source:\n\n- `/status` — a `Context window:` row with the percent and token counts, and a\n `Window source:` row naming the provenance and the exact key that overrides\n it.\n- `/config` → Provider — `Context window` (your override, or `(not set)`) and\n `Effective context window` (`1048576 tokens · configured`). Typing\n `context length` in the `/config` filter jumps straight to them.\n- `/context report` — `Window: 1048576 tokens (12.4% used, ...; source: configured)`.\n- `/context json` — machine-readable `context_window_tokens` and\n `context_window_source`.\n\n**Change it** with the provider-table key `context_window`:\n\n```toml\n[providers.moonshot]\ncontext_window = 1048576\n```\n\nor from the CLI:\n\n```bash\ncodewhale config set providers.moonshot.context_window 1048576\ncodewhale config unset providers.moonshot.context_window # back to automatic\n```\n\nUse the table for the provider you are actually on (`providers.openai`,\n`providers.deepseek`, `providers.moonshot`, …); `/status` names it for you. The\nvalue is a positive token count for the route's *total* window.\n\nWhen one gateway fronts models with heterogeneous windows, scope the override\nto an exact wire model id with `[providers.<name>.model_context_windows]`:\n\n```toml\n[providers.command_code]\ncontext_window = 204800\n\n[providers.command_code.model_context_windows]\n\"MiniMaxAI/MiniMax-M2.5\" = 204800\n\"google/gemini-3.1-flash-lite\" = 1000000\n```\n\nKeys are the exact wire model ids the route sends (dotted and `org/model`\nspellings both work as TOML keys when quoted); each value must be a positive\ntoken count. A matching entry beats the provider-level `context_window` for\nthat model only — every other model on the provider still resolves against\n`context_window` and the rungs below. From the CLI:\n\n```bash\ncodewhale config set 'providers.command_code.model_context_windows.\"MiniMaxAI/MiniMax-M2.5\"' 204800\ncodewhale config unset 'providers.command_code.model_context_windows.\"MiniMaxAI/MiniMax-M2.5\"'\n```\n\n### How the effective window is resolved\n\nFirst match wins, and the source label each surface prints is exactly this\nrung:\n\n1. `configured (per-model)` — a `[providers.<name>.model_context_windows]`\n entry keyed by the route's exact wire model id. A hard override for that\n model only; it never rewrites another model's window.\n2. `configured` — `[providers.<name>] context_window` in `config.toml`. A hard\n override for every model on the provider: nothing below it can raise or\n lower the result. Read-time aliases:\n `contextWindow`, `context_window_tokens`, `contextWindowTokens`,\n `context_length`, `contextLength`.\n3. `provider-reported` — route-scoped 1M metadata a provider actually reported\n for the Kimi Code `k3` route, when it was observed within the last 24 hours.\n4. `static Kimi Code safe floor` — 262,144 tokens for Kimi Code memberships,\n because 1M access is plan-gated (Allegretto and above).\n5. `catalog` — the bundled route catalog (hand-curated offerings first, then\n the bundled Models.dev rows). The official `openai-codex` roster supplies\n account-specific model choices, not context-window metadata. Refresh it\n with `codewhale models --update --provider openai-codex`; Codewhale does\n not read `$CODEX_HOME` for this route.\n6. `model-name hint` — an `_Nk` suffix parsed from the model name itself\n (`qwen3-32b-256k` → 256,000), vendor-agnostic. A naming convention the\n serving engine may not honor is not a fact about the route, so this rung\n sits *below* the catalog: any catalog row for the same id beats it (#5441).\n7. `fallback` — the static per-provider capability table: 200,000 for\n Anthropic-wire routes, 128,000 for `openai-codex`, 8,192 for Ollama,\n otherwise Codewhale's static per-model metadata, and finally 128,000 when\n the model is unknown.\n\n### What \"(unverified)\" means\n\nThe `model-name hint` and `fallback` rungs still drive real budgets — the\ncompaction trigger, the context meter, and the output reservation all use the\nnumber — but they are guesses, not capabilities anyone checked. Every surface\nthat renders one of these windows appends `(unverified)` to its source label\n(the status line, the context-pressure message, `/status`, `/config`, and the\nmodel picker chip), so a window you did not configure and no provider reported\ncan never read as a verified limit (#5239, #5441). The `context_window` and\n`model_context_windows` provider-table keys above are the fix: a configured\nwindow is a hard override and renders as `configured` (or\n`configured (per-model)`) with no marker.\n\nOutput ceilings follow the same rule (#5440): an Anthropic-family model the\ncatalog does not describe keeps the 64K Messages floor as its clamp, labeled\n`unverified` (or an \"assumed floor\") instead of `documented`. The official\nChatGPT plan preview does not accept `max_output_tokens`; Codewhale sends no\noutput-token cap on that route. A local request budget is not an upstream\nlimit or a guarantee about the completed response length.\n\nThere is no environment variable for the context window; the provider-table\n`context_window` and per-model `model_context_windows` keys are the user\nknobs. They are the right ones to set when a gateway or self-hosted runtime\nserves a window Codewhale's catalog does not model — per-model when only some\nof a provider's routes differ, provider-wide when they all do. Codewhale will\nnot invent a window it cannot justify — it falls back to a conservative value,\nlabels it `fallback`, and marks it `(unverified)` at every surface that shows\nit.\n\n### Adjacent knobs\n\n- `auto_compact_threshold_percent` (settings.toml; also accepted as\n `auto_compact_threshold`; `10`–`100`, default `80`): the share of the full\n route context window at which auto-compaction fires, clamped so it can never\n cross the spendable input ceiling after output reservation and headroom.\n Editable from `/config`. Raising the window without touching this raises the\n absolute compaction point along with it.\n- `auto_compact` (settings.toml, on/off): turns automatic compaction off\n entirely; `/compact` and Ctrl+L stay available.\n- `[compaction] summary_instructions` and\n `[compaction] retained_user_message_tokens` (config.toml): standing\n summarizer instructions and the verbatim user-message retention budget. See\n the `compaction.*` entry in the config-key reference below.\n- `CODEWHALE_MAX_OUTPUT_TOKENS` (environment variable; legacy alias\n `DEEPSEEK_MAX_OUTPUT_TOKENS`): overrides the requested output cap. Without an\n override, Codewhale starts at the safe `65536` request cap and intersects it\n with any smaller documented model or route ceiling; a catalog `max_output`\n such as DeepSeek V4's 384K remains a capability ceiling, not the amount every\n response requests. Explicit overrides are preserved within the resolved\n route context window and any route output ceiling, and preflight/emergency\n budgeting reserves the same effective value that can reach the wire. A\n separately documented route input ceiling also clamps preflight and\n compaction even when the total context window is larger. A blank canonical\n variable falls through to a nonblank legacy value; a nonblank invalid or\n zero canonical value is authoritative and falls back to the safe automatic\n default instead of activating a stale legacy setting. There is no\n `max_output_tokens` key in `config.toml`.\n\nBefore compaction replaces conversation history, Codewhale durably saves the\noriginal messages to the session's `artifacts/context-transfer-<id>.json` and,\nwhen a model summary is produced, its handoff to the matching `.md` file.\nThese use the existing session artifact store and persistence redaction.\nA failed write aborts compaction without replacing context. Pruning-only passes\nsave the original messages without making an extra model call. The model can\nread them back through `context_archive_search` (ASCII case-insensitive\nsubstring search returning short snippets and an archive id) and\n`context_archive_get` (paged read of one message by id); the handoff note names\nboth so the model knows the archive exists. Pressure metadata\nshows estimated input tokens and the configured trigger; it is an estimate, not\nan exact promise about a provider's remaining context.\n\nCompaction history is available through `codewhale metrics` (or `--json`) and\n`audit.log` in the Codewhale home. Completed passes record their trigger,\nsummary/pruning path, message and estimated-token counts, effective threshold,\nand summarizer token usage. Automatic refusals are recorded once per turn with\ntheir reason. These are local diagnostics, not provider invoice totals; earlier\nartifacts are not retroactively counted. A text-mode `exec` that attempts\ncompaction saves its owning session at turn completion so its recovery artifacts\nremain discoverable. A killed process may leave artifacts without that final\nsession snapshot; the audit writer reports I/O failures instead of inventing data.\n\nSee [Settings File](#settings-file-persistent-ui-preferences) for the\ncompaction settings and [Token Quantities and\nDrivers](#token-quantities-and-drivers) for what each displayed token number\nactually measures.\n\n## Profiles\n\nYou can define multiple profiles in the same file:\n\n```toml\ndefault_text_model = \"deepseek-flash\"\n\n[providers.deepseek]\napi_key = \"PERSONAL_KEY\"\n\n[profiles.work.providers.deepseek]\napi_key = \"WORK_KEY\"\nbase_url = \"https://api.deepseek.com/beta\"\n\n[profiles.nvidia-nim]\nprovider = \"nvidia-nim\"\ndefault_text_model = \"deepseek-ai/deepseek-v4-pro\"\n\n[profiles.nvidia-nim.providers.nvidia_nim]\napi_key = \"NVIDIA_KEY\"\nbase_url = \"https://integrate.api.nvidia.com/v1\"\n\n[profiles.fireworks]\nprovider = \"fireworks\"\ndefault_text_model = \"accounts/fireworks/models/deepseek-v4-pro\"\n\n[profiles.siliconflow]\nprovider = \"siliconflow\"\ndefault_text_model = \"deepseek-ai/DeepSeek-V4-Pro\"\n\n[profiles.siliconflow.providers.siliconflow]\nbase_url = \"https://api.siliconflow.com/v1\"\n\n[profiles.openai-compatible]\nprovider = \"openai\"\n\n[profiles.openai-compatible.providers.openai]\nbase_url = \"https://openai-compatible.example/v4\"\nmodel = \"glm-5\"\n\n[profiles.atlascloud]\nprovider = \"atlascloud\"\n\n[profiles.atlascloud.providers.atlascloud]\nbase_url = \"https://api.atlascloud.ai/v1\"\nmodel = \"deepseek-ai/deepseek-v4-flash\"\n\n[profiles.sglang]\nprovider = \"sglang\"\n\n[profiles.sglang.providers.sglang]\nbase_url = \"http://localhost:30000/v1\"\nmodel = \"deepseek-ai/DeepSeek-V4-Pro\"\n\n[profiles.vllm]\nprovider = \"vllm\"\n\n[profiles.vllm.providers.vllm]\nbase_url = \"http://localhost:8000/v1\"\nmodel = \"deepseek-ai/DeepSeek-V4-Pro\"\n\n[profiles.ollama]\nprovider = \"ollama\"\n\n[profiles.ollama.providers.ollama]\nbase_url = \"http://localhost:11434/v1\"\nmodel = \"qwen2.5-coder:7b\" # an installed chat-capable tag\n\n[profiles.ollama-cloud]\nprovider = \"ollama-cloud\"\n\n[profiles.ollama-cloud.providers.ollama_cloud]\nbase_url = \"https://ollama.com/v1\"\nmodel = \"gpt-oss:120b\"\n```\n\nSelect a profile with:\n\n- CLI: `codewhale --profile work`\n- Env: `DEEPSEEK_PROFILE=work`\n\nIf a profile is selected but missing, codewhale exits with an error listing available profiles.\n\nFor local Ollama, omit `providers.ollama.model` to select a tag whose native\ncatalog confirms completion support. `/provider ollama`, exec, and Runtime API\ndefault-model admission refresh that catalog; embedding-only catalogs have no\nautomatic chat default. An explicit model keeps its exact tag.\n\nA loaded model's `/api/ps` context observation is usable for 60 seconds.\n`/api/show` reports the trained maximum, which does not establish the served\nwindow. An unloaded model or an expired observation uses the unverified 8192-token\nfallback; explicit context-window configuration still takes precedence.\n\n## Legacy top-level `base_url` and `api_key`\n\nOlder releases kept DeepSeek's endpoint and key at the top of `config.toml`,\nand each reader decided for itself which other routes inherited them. They\nnow live in provider tables only. An older file keeps working unchanged:\nevery load reads the top-level keys as if they were already in their table,\nby one rule, per file and per `[profiles.<name>]`:\n\n1. `provider = \"custom\"` with no `[providers.custom]` table: the endpoint, key\n and a copy of the model become `[providers.custom]`.\n2. An endpoint on another vendor's official host (for example\n `integrate.api.nvidia.com`, `xiaomimimo.com`, `openrouter.ai`, or the\n ChatGPT Codex endpoint) belongs to that vendor's table, so a DeepSeek route\n never sends requests there.\n3. Anything else belongs to `[providers.deepseek]`, which DeepSeek-CN also\n reads for its endpoint and key.\n\nThe key goes with DeepSeek (or the literal custom route). It follows the\nendpoint to another vendor only when the same table sets `provider` to that\nvendor, the host is that vendor's own, and the vendor's table has no key. When\nno `provider` is set, a NIM host still selects `nvidia-nim` and\n`api.deepseeki.com` still selects `deepseek-cn`. A `[vision_model]` without a\nkey of its own keeps using the top-level key. A profile's top-level\n`base_url` now overrides the base file's `[providers.deepseek] base_url`\n(before, the base table silently won).\n\nWhen a top-level value and its table disagree, the table's `base_url` and the\ntop-level `api_key` are used, which is what the runtime sent before.\n\nLoading never rewrites the file. The next save Codewhale makes (for example\n`codewhale config set`, `/config ... --save`, or `auth set`) moves the keys,\nkeeps comments, writes a one-time credential-free copy of the old file to\n`config.toml.pre-migrate.bak`, and prints one line saying what moved. A\ndisagreeing pair is never resolved in the file on its own; both values stay\nuntil you choose, and `codewhale config doctor` / `codewhale doctor` report\nit (sources only, never values). `codewhale config get|set|unset base_url`\n(and `api_key`) address the active provider's table.\n\nTo tidy the file yourself:\n\n```bash\ncodewhale config migrate --dry-run # show what would move\ncodewhale config migrate # move it (backup first)\ncodewhale config migrate --prefer top-level # resolve a conflict: keep the top-level value\ncodewhale config migrate --prefer table # resolve a conflict: keep the table value\n```\n\n## Environment Variables\n\nMost runtime environment variables override config values. API-key variables are\nfallbacks after saved config and keyring credentials.\n\nThe three user-facing slots — provider, model, base URL — expose `CODEWHALE_*`\naliases. When both forms are set the `CODEWHALE_*` value wins; the\n`DEEPSEEK_*` form is kept for older shells:\n\n- `CODEWHALE_PROVIDER` (preferred) / `DEEPSEEK_PROVIDER` (legacy alias) —\n `deepseek|deepseek-anthropic|nvidia-nim|openai|atlascloud|wanjie-ark|volcengine|openrouter|xiaomi-mimo|novita|fireworks|siliconflow|arcee|siliconflow-CN|moonshot|sglang|vllm|ollama|ollama-cloud|huggingface|modelscope|together|qianfan|openai-codex|anthropic|openmodel|zai|stepfun|minimax|deepinfra|mistral`\n- `CODEWHALE_MODEL` (preferred) / `DEEPSEEK_MODEL` (legacy alias) — default model for the active provider\n- `CODEWHALE_BASE_URL` (preferred) / `DEEPSEEK_BASE_URL` (legacy alias) — base URL for the active provider\n\n`CODEWHALE_BASE_URL` applies to the **active** route only. A request pinned to\nanother provider — a subagent or fleet child, a routed tool, the per-turn\nauto-router, a picker preview — resolves its endpoint from that provider's own\n`[providers.<table>]`, then its provider-scoped variable (`MOONSHOT_BASE_URL`,\n`OPENAI_BASE_URL`, …), then that provider's default. It never inherits the\nactive session's host, and a custom route with no configured `base_url` fails\nclosed on a loopback placeholder rather than borrowing another provider's\nendpoint. The environment writes the value into the active identity's own\ntable: DeepSeek-CN falls back to `[providers.deepseek]` for its endpoint and\nkey, but never to a DeepSeek value the environment addressed to DeepSeek alone.\nA managed-config overlay that supplies or reselects the effective\nroute's endpoint takes the generic override away from every route.\n\nRemaining variables:\n\n- `CODEWHALE_LOCAL_PROXY_FALLBACK` (`0`) — keeps reqwest's build-time proxy snapshot; the loopback fallback is on by default. Read once when the first client is built.\n- `DEEPSEEK_API_KEY`\n- `DEEPSEEK_ANTHROPIC_BASE_URL`\n- `DEEPSEEK_HTTP_HEADERS` (custom model request headers, comma-separated `name=value` pairs)\n- `DEEPSEEK_DEFAULT_TEXT_MODEL` (extra legacy alias of `DEEPSEEK_MODEL`)\n- `DEEPSEEK_STREAM_IDLE_TIMEOUT_SECS` (stream idle timeout in seconds; default `900`, clamped to `1..=3600`)\n- `DEEPSEEK_STREAM_OPEN_TIMEOUT_SECS` (connection setup + response-header wait in seconds; default `45`, clamped to `5..=300`; distinct from the per-chunk idle timeout; `stream.open_timeout_secs` (legacy `tui.stream_open_timeout_secs` fallback) wins when positive)\n- `CODEWHALE_CACHE_MAXIMAL` (`1`/`true`/`on`/`yes`) — cache-maximal context mode (#528). When on, the Repo Working Set block materializes the **full current contents** of the top active files into the system prompt each turn (deterministic order, byte-bounded), instead of only listing their paths. The block stays byte-stable while those files are unchanged so DeepSeek's KV prefix cache keeps hitting; editing a file cache-misses from its block onward. Off by default (path list only). Byte caps default to 24 KB per file / 96 KB total.\n- `NVIDIA_API_KEY` or `NVIDIA_NIM_API_KEY` (when provider is `nvidia-nim`)\n- `NVIDIA_NIM_BASE_URL`, `NIM_BASE_URL`, or `NVIDIA_BASE_URL`\n- `NVIDIA_NIM_MODEL`\n- `OPENAI_API_KEY`\n- `OPENAI_BASE_URL`\n- `OPENAI_MODEL`\n- `ATLASCLOUD_API_KEY`\n- `ATLASCLOUD_BASE_URL`\n- `ATLASCLOUD_MODEL`\n- `WANJIE_ARK_API_KEY`, `WANJIE_API_KEY`, or `WANJIE_MAAS_API_KEY`\n- `WANJIE_ARK_BASE_URL`, `WANJIE_BASE_URL`, or `WANJIE_MAAS_BASE_URL`\n- `WANJIE_ARK_MODEL`, `WANJIE_MODEL`, or `WANJIE_MAAS_MODEL`\n- `VOLCENGINE_API_KEY`, `VOLCENGINE_ARK_API_KEY`, or `ARK_API_KEY`\n- `VOLCENGINE_BASE_URL`, `VOLCENGINE_ARK_BASE_URL`, or `ARK_BASE_URL`\n- `VOLCENGINE_MODEL` or `VOLCENGINE_ARK_MODEL`\n- `OPENROUTER_API_KEY`\n- `OPENROUTER_BASE_URL`\n- `OPENROUTER_MODEL`\n- `XIAOMI_MIMO_TOKEN_PLAN_API_KEY`, `MIMO_TOKEN_PLAN_API_KEY`, `XIAOMI_MIMO_API_KEY`, `XIAOMI_API_KEY`, or `MIMO_API_KEY`\n- `XIAOMI_MIMO_BASE_URL` or `MIMO_BASE_URL`\n- `XIAOMI_MIMO_MODEL` or `MIMO_MODEL`\n- `XIAOMI_MIMO_MODE` or `MIMO_MODE` (`token-plan-sgp`, `token-plan-cn`,\n `token-plan-ams`, or `pay-as-you-go`)\n- `NOVITA_API_KEY`\n- `NOVITA_BASE_URL`\n- `NOVITA_MODEL`\n- `FIREWORKS_API_KEY`\n- `FIREWORKS_BASE_URL`\n- `FIREWORKS_MODEL`\n- `HUGGINGFACE_API_KEY` or `HF_TOKEN` (`HF_TOKEN` is a fallback alias accepted when provider is `huggingface`)\n- `MODELSCOPE_API_KEY`\n- `HUGGINGFACE_BASE_URL` or `HF_BASE_URL`\n- `HUGGINGFACE_MODEL` or `HF_MODEL`\n- `SILICONFLOW_API_KEY`\n- `SILICONFLOW_BASE_URL`\n- `SILICONFLOW_MODEL`\n- `ARCEE_API_KEY`\n- `ARCEE_BASE_URL`\n- `ARCEE_MODEL`\n- `TOGETHER_API_KEY`\n- `TOGETHER_BASE_URL`\n- `TOGETHER_MODEL`\n- `QIANFAN_API_KEY` or `BAIDU_QIANFAN_API_KEY`\n- `QIANFAN_BASE_URL` or `BAIDU_QIANFAN_BASE_URL`\n- `QIANFAN_MODEL` or `BAIDU_QIANFAN_MODEL`\n- `OPENAI_CODEX_ACCESS_TOKEN` or `CODEX_ACCESS_TOKEN` (legacy process tokens; ignored by the official ChatGPT plan route)\n- `OPENAI_CODEX_BASE_URL` or `CODEX_BASE_URL`\n- `OPENAI_CODEX_MODEL` or `CODEX_MODEL`\n- `OPENAI_CODEX_ACCOUNT_ID` or `CODEX_ACCOUNT_ID` (legacy identity hints; ignored by the official ChatGPT plan route)\n- `CODEWHALE_CHATGPT_NEW_ACCOUNT=1` explicitly replaces the single selected ChatGPT registration after a validated browser sign-in\n- `ANTHROPIC_API_KEY`\n- `ANTHROPIC_BASE_URL`\n- `ANTHROPIC_MODEL`\n- `ZAI_API_KEY` or `Z_AI_API_KEY`\n- `ZAI_BASE_URL` or `Z_AI_BASE_URL`\n- `ZAI_MODEL` or `Z_AI_MODEL`\n- `STEPFUN_API_KEY` or `STEP_API_KEY`\n- `STEPFUN_BASE_URL` or `STEP_BASE_URL`\n- `STEPFUN_MODEL` or `STEP_MODEL`\n- `MINIMAX_API_KEY`\n- `MINIMAX_BASE_URL`\n- `MINIMAX_MODEL`\n- `DEEPINFRA_API_KEY` or `DEEPINFRA_TOKEN`\n- `DEEPINFRA_BASE_URL`\n- `DEEPINFRA_MODEL`\n- `MISTRAL_API_KEY`\n- `MISTRAL_BASE_URL`\n- `MISTRAL_MODEL`\n- `MOONSHOT_API_KEY` or `KIMI_API_KEY`\n- `MOONSHOT_BASE_URL` or `KIMI_BASE_URL`\n- `MOONSHOT_MODEL`, `KIMI_MODEL_NAME`, or `KIMI_MODEL`\n- `SGLANG_BASE_URL`\n- `SGLANG_MODEL`\n- `SGLANG_API_KEY` (optional; many localhost SGLang servers do not require auth)\n- `VLLM_BASE_URL`\n- `VLLM_MODEL`\n- `VLLM_API_KEY` (optional; many localhost vLLM servers do not require auth)\n- `OLLAMA_BASE_URL`\n- `OLLAMA_MODEL`\n- `OLLAMA_API_KEY` (optional; many localhost Ollama servers do not require auth)\n- `OLLAMA_CLOUD_BASE_URL`\n- `OLLAMA_CLOUD_MODEL`\n- `OLLAMA_CLOUD_API_KEY` (preferred Cloud key; `OLLAMA_API_KEY` is the official fallback)\nFor every product-level `CODEWHALE_*` variable below, the matching legacy\n`DEEPSEEK_*` name is still read as a compatibility fallback; when both are set,\nthe `CODEWHALE_*` value wins.\n\n- `CODEWHALE_TIMING=1` emits local JSON phase durations on stderr for startup, model preparation, tool planning, approval waits and postprocessing. Nested intervals overlap. The records contain only phase, process ID, end time and elapsed microseconds; stream-json stdout is unchanged.\n- `CODEWHALE_LOG_LEVEL` or `RUST_LOG` (`info`/`debug`/`trace` enables lightweight verbose logs)\n- `CODEWHALE_SKILLS_DIR`\n- `CODEWHALE_MCP_CONFIG`\n- `CODEWHALE_NOTES_PATH`\n- `CODEWHALE_MEMORY` (`1|on|true|yes|y|enabled` turns user memory on)\n- `CODEWHALE_MEMORY_PATH`\n- `CODEWHALE_TELEMETRY` / `DEEPSEEK_TELEMETRY` (legacy alias) — anonymous usage\n counting is on by default in the current 0.9.12 source, with a disclosure\n naming Codewhale and PostHog and an easy durable opt-out. Prior explicit\n declines remain off. Accepts `0|1|true|false|yes|no|on|off|enabled|\n disabled`. An explicit \"off\" is a **floor**: it beats `--telemetry true` and\n `telemetry = true` in config, and a value this list cannot read also resolves\n to off, because a typo in a kill switch must never resolve to \"on\". See\n [`TELEMETRY.md`](TELEMETRY.md).\n- `CODEWHALE_TELEMETRY_ENDPOINT` / `DEEPSEEK_TELEMETRY_ENDPOINT` (legacy alias)\n — `https://`, or plain `http://` only for loopback. Overrides the config file.\n Unset selects the shipped default,\n `https://telemetry.codewhale.net/v1/telemetry`; setting it to the **empty\n string** routes batches to a local dry-run file and contacts nobody. Either\n way it only decides where a session sends — it cannot override an opt-out.\n- `CODEWHALE_ALLOW_SHELL` (`1`/`true` enables)\n- `CODEWHALE_APPROVAL_POLICY` (`on-request|untrusted|never`)\n- `CODEWHALE_SANDBOX_MODE` (`read-only|workspace-write|danger-full-access|external-sandbox`)\n- `CODEWHALE_NO_NEW_PRIVS` (`0`/`false`/`no`/`off`/`disabled` opts out) — Linux only. The\n TUI process sets the kernel's irreversible no-new-privileges flag at startup\n as defense-in-depth, which blocks `sudo`/`su`/setuid helpers for Codewhale's\n whole process tree. The flag is already skipped when the startup sandbox\n mode resolves to `danger-full-access` (#5723), so this variable is the\n explicit override in the remaining cases: set it to a falsey value before\n launching if you administer through Codewhale as a wheel-group user under a\n narrower posture and need escalation to work (#5413), or set a truthy value\n to force the flag on even under `danger-full-access`. Unset, the posture\n decides; the other startup hardening (no ptrace, no core dumps) always\n stays on.\n- `CODEWHALE_MANAGED_CONFIG_PATH`\n- `CODEWHALE_REQUIREMENTS_PATH`\n- `CODEWHALE_MAX_SUBAGENTS` (clamped to `1..=128`)\n- `CODEWHALE_TASKS_DIR` (runtime task queue/artifact storage, default\n `~/.codewhale/tasks`, with legacy `~/.deepseek/tasks` fallback when only the\n legacy directory exists)\n- `CODEWHALE_RUNTIME_DIR` (override the runtime thread store root). Interactive\n sessions default to `$CODEWHALE_HOME/sessions/<session-id>/runtime` so each\n Codewhale process owns its own store (#5630). The store is single-owner: a\n second process on the **same** root fails at startup. Set this variable to\n share one store across processes, or when the runtime API server should use a\n stable non-session path. Unset, the API/server path remains\n `$CODEWHALE_HOME/tasks/runtime`. Legacy alias: `DEEPSEEK_RUNTIME_DIR`.\n- `CODEWHALE_ALLOW_INSECURE_HTTP` (`1`/`true` allows non-local `http://` base URLs; default is reject)\n- `CODEWHALE_FORCE_HTTP1` (`1|true|yes|on` pins the HTTP client to HTTP/1.1, disabling HTTP/2; useful on Windows or behind proxies that mishandle long-lived H2 streams)\n- `CODEWHALE_HOME` (override the base data directory; defaults to `~/.codewhale`).\n If you previously exported `DEEPSEEK_HOME`, rename it to `CODEWHALE_HOME`;\n the old env var is not used for new Codewhale state paths.\n- `CODEWHALE_RELEASE_BASE_URL` (release asset mirror used by `codewhale update`\n and by TUI startup update checks when `[update].update_uri` is not set, or as\n a fallback when that configured URI cannot be fetched)\n- `CODEWHALE_AUTOMATIONS_DIR` (override the automations storage directory; uses\n `~/.codewhale/automations` by default, with legacy `~/.deepseek/automations`\n fallback when only the legacy directory exists)\n- `NO_ANIMATIONS` (`1|true|yes|on` forces `low_motion = true` and\n `fancy_animations = false` at startup, regardless of the saved\n settings; see [`docs/ACCESSIBILITY.md`](./ACCESSIBILITY.md)).\n- `SSL_CERT_FILE` — corporate-proxy / TLS-inspecting MITM users\n point this at a PEM bundle (or single DER cert) and the cert(s)\n get added alongside the platform's system trust store. Failures\n log a warning and continue — the existing system roots still\n apply.\n\n### Instruction sources (`instructions = [...]`, #454)\n\nAdd a list of additional system-prompt sources that get\nconcatenated, in declared order, alongside the auto-loaded\n`AGENTS.md`:\n\n```toml\ninstructions = [\n \"./AGENTS.md\",\n \"~/.codewhale/global.md\",\n \"~/team/agents-shared.md\",\n]\n```\n\nRules:\n\n- Paths run through `expand_path` so `~` and env vars work.\n- These entries are **global-scope** instruction authority: they are charged\n to the global budget together with the global `AGENTS.md`/`instructions.md`\n fallbacks and `~/.claude/CLAUDE.md`.\n- A file keeps at most its first 100 KiB. A file larger than\n `[instruction_budget] file_max_bytes` (8 MiB by default) is skipped with a\n notice. Missing and empty files are skipped with a notice so a stale entry\n doesn't fail the launch.\n- Only user-owned config, profiles, and managed config may set this array.\n Project config (`<workspace>/.codewhale/config.toml`, or legacy\n `<workspace>/.deepseek/config.toml`) ignores `instructions` so a cloned repo\n cannot choose arbitrary local files to place into the prompt.\n\n### Instruction budget (`[instruction_budget]`, #6526)\n\nStanding instruction files are assembled into two independent scopes and\nbudgeted in **estimated tokens** — the same conservative estimator `/context`\nuses, never a `bytes/4` shortcut, which underestimates Cyrillic by 30–50%.\n\n- **global** — `~/.codewhale/AGENTS.md` (or the `.agents`/`.deepseek`\n fallbacks), the global `instructions.md` fallbacks, `~/.claude/CLAUDE.md`\n as a last resort when `claude_fallback` is on, and the\n `instructions = [...]` array.\n- **project** — the repository-root → workspace `AGENTS.md` chain, co-read\n foreign files, and the `.codewhale/rules`/`.claude/rules` block.\n\nEach scope is capped at `min(absolute_tokens, percent × usable_window)`. The\nusable window is the route window minus the output reservation the request\nwill actually carry and a safety reserve of 10% of the window (minimum 2,048\ntokens). A route whose context window is unknown has no usable window: the\npercentage ceilings and the guard are reported as `unknown` and only the\nabsolute caps apply.\n\n```toml\n[instruction_budget]\nglobal_budget_tokens = 100000\nproject_budget_tokens = 100000\nglobal_budget_percent = 25\nproject_budget_percent = 25\nstatic_prefix_guard_percent = 80\nfile_max_bytes = 8388608\npolicy = \"trim\"\nclaude_fallback = false\n```\n\nWith a 64K (65,536-token) output reservation:\n\n| window | usable | per scope | both scopes | guard 80% |\n| --- | --- | --- | --- | --- |\n| 128,000 | 49,664 | 12,416 | 24,832 | 39,731 |\n| 1,000,000 | 834,464 | 100,000 (absolute cap) | 200,000 | 667,571 |\n\nBehaviour:\n\n- **Nothing is trimmed while a source is within its scope budget.**\n- A scope over budget drops whole files from the broadest end first — the\n root-most ancestor goes first, the workspace file last — and a single file\n that alone exceeds the remaining budget keeps its **head** and drops its\n tail with a marker carrying bytes and estimated tokens.\n- Every instruction, rules, and configured file retains at most its first\n 100 KiB of text, ending in a marker that names the bytes kept and dropped.\n- Rules are charged to the project scope after the instruction chain and keep\n a head-first trim.\n- `file_max_bytes` is a hard ceiling that can only be lowered from its 8 MiB\n default: a larger file is **skipped**, never trimmed, and the read is\n bounded on the open file, so a FIFO or a growing file cannot exhaust memory.\n The same applies to a whitespace-only file and to files beyond the\n 50-per-directory rules cap.\n- `policy = \"trim\"` (default) trims and reports. `policy = \"refuse\"` stops the\n request before dispatch when a scope budget, the rules budget, or the\n 100 KiB retention would trim, or when the request prefix exceeds the guard;\n skips stay skips, because one bad file must not make the project unopenable.\n- `static_prefix_guard_percent` caps the request prefix: every system block\n plus the tool schemas the request carries. When it trips, the two scopes are\n narrowed largest first until the prefix fits, only the installed instruction\n block is replaced, and the change is recorded as a declared prefix change.\n If the prefix still exceeds the guard afterwards, the residual excess is\n reported rather than looped on. The guard must leave at least 10 points\n above the two scope percents. Prompts owned by a host (child agents,\n isolated chat, RLM) are measured and can be refused, but are never trimmed.\n- `claude_fallback = true` (opt in; default `false`): a directory with no\n `AGENTS.md` (or codewhale `instructions.md`) uses its `CLAUDE.md`. A skipped\n `AGENTS.md` counts as absent; a file already loaded from a wider scope (a\n symlink) is a reported duplicate, not a fallback trigger. Other tools'\n instruction files are never imported unless you opt in here or through\n `project_instruction_imports`.\n- Every loss — trimmed, skipped, refused — reaches the model as one\n runtime-provenance message whenever the list changes, and appears per source\n in `/context report`. `codewhale doctor`'s report is a preview built from the\n current files and config; it does not claim a request passed the tool-schema\n guard.\n- The table is **user config only**, like `instructions`: project config is\n ignored and a cloned repository cannot shrink the global budget or force\n `refuse`. An invalid table (unknown key, zero budget, percent outside\n 1–100, guard without room left, `file_max_bytes` outside 1–8388608) fails\n the launch.\n\n### Hooks\n\nHooks are a **TUI runtime feature**. They fire from the interactive TUI and\nthe engine turn loop it drives; `codewhale exec`, the CLI subcommands, the\napp-server / ACP surfaces, and the `workflow` tool do not fire them.\n\n[`docs/HOOKS.md`](HOOKS.md) is the authoritative reference for all eleven hook\nevents — their firing points, environment variables, stdin payloads, timeout\nand background semantics, and which three of them can steer Codewhale. The\nsections below cover the configuration surface and the steering contracts in\nmore depth.\n\nTwo contract points worth reading there before writing a hook:\n\n- `background = true` means **submitted and never awaited**. The hook still\n gets the documented stdin payload and the same timeout, but it has no exit\n code and cannot steer.\n- A condition that references context its event never carries (an `exit_code`\n condition outside `tool_call_after` / `on_error`, a `mode` condition on\n `shell_env`, a tool condition on a non-tool event) is **rejected at load**,\n logged, and shown in `/hooks list`. It does not silently never match.\n Rejection is per entry, so a broken hook never drops another one that merely\n shares its `name` or is likewise unnamed.\n\n### `/hooks` listing\n\nRun `/hooks` (or `/hooks list`) inside the TUI to see every\nconfigured lifecycle hook grouped by event, including each\nhook's name, command preview, effective timeout, and condition. When\n`[hooks].default_timeout_secs` is set it replaces every per-hook\n`timeout_secs`, and the listing shows that effective value and names the\noverride rather than echoing the per-hook number. A\n`default_timeout_secs = 0` is rejected at load — it would expire every hook\nin the config immediately — so the override is ignored, per-hook\n`timeout_secs` applies, the listing shows that per-hook value with no\noverride provenance, and the rejection appears under `configuration\nproblems`. The\n`[hooks].enabled` flag's state is shown at the top so it's\nobvious when hooks are globally suppressed, and any entry rejected\nat load is listed under `configuration problems` with the reason.\nHooks are configured under `[[hooks.hooks]]` entries — see\n[`docs/HOOKS.md`](HOOKS.md) for the full schema.\n\n### Mutable `message_submit` hooks\n\n`message_submit` hooks run before a submitted message is added to\nhistory or sent to the model. Unlike observer-only lifecycle hooks,\nnon-background `message_submit` hooks can replace or block the\nsubmitted text.\n\n```toml\n[[hooks.hooks]]\nevent = \"message_submit\"\ncommand = \"~/.codewhale/hooks/inject-context.sh\"\ntimeout_secs = 2\ncontinue_on_error = true\n```\n\nThe hook receives JSON on stdin:\n\n```json\n{\n \"event\": \"message_submit\",\n \"text\": \"original user text\",\n \"text_bytes\": 18,\n \"text_original_bytes\": 18,\n \"text_truncated\": false,\n \"session_id\": \"sess_12345678\",\n \"workspace\": \"/path/to/workspace\",\n \"mode\": \"agent\",\n \"model\": \"deepseek-chat\",\n \"total_tokens\": 1234\n}\n```\n\nThe entire serialized document is capped at 32 KiB. Codewhale retains the\nlargest UTF-8-safe `text` prefix that fits after JSON escaping and bounded\nmetadata, and the three `text_*` fields make truncation explicit. Immediate\nmessages, restored queue entries, merged steers, and prior-hook replacements\nall cross this same serialization boundary.\n\nIf the hook exits `0` and prints JSON with a non-empty string `text` field,\nthat value replaces the submitted text:\n\n```json\n{ \"text\": \"replacement user text\" }\n```\n\nExit `0` with empty stdout, or stdout JSON without `text`, leaves\nthe current text unchanged. A JSON `text` field must not be empty;\n`{\"text\":\"\"}` is treated as invalid stdout and ignored. Exit `2`\nblocks the submission before the turn starts; a structured `reason` field can\nprovide the bounded, redacted status message shown in the TUI. Raw stdout,\nstderr, and process-error text are not copied into denial receipts.\nOther non-zero exits follow the hook's `continue_on_error` setting.\nTimeouts and spawn failures are also surfaced as transient TUI status\nmessages when `continue_on_error = true` lets submission continue.\n\nMultiple `message_submit` hooks run in config order, and each hook\nreceives the text produced by the previous hook. Hooks marked\n`background = true` are observer-only and cannot transform or block\nthe message — they still receive the same stdin payload and the same\nenvironment, they are simply never awaited. Existing environment\nvariables remain available.\n`shell_env` hooks keep their existing `KEY=VALUE` stdout contract;\nJSON stdout contracts exist for `message_submit` (above) and\n`tool_call_before` (below).\n\n### `tool_call_before` decision hooks\n\n`tool_call_before` hooks run before each tool call executes. In\naddition to the legacy hard deny (exit code `2`, which always wins\nregardless of stdout), a foreground hook may print a JSON decision on\nstdout with exit code `0`:\n\n```json\n{\n \"decision\": \"allow\" | \"deny\" | \"ask\",\n \"reason\": \"human-readable explanation (used for deny)\",\n \"updatedInput\": { \"command\": \"ls -la\" },\n \"additionalContext\": \"text appended to the tool result for the model\"\n}\n```\n\nAll fields are optional. Empty stdout, non-JSON stdout, and JSON\nwithout a `decision` field behave exactly as before (allow). An\nunrecognized `decision` string logs a fixed warning without echoing the\nuntrusted value and is treated as allow.\n\n- `deny` blocks the tool; the model receives a permission-denied tool\n result containing `reason`.\n- `ask` forces the interactive approval prompt in Ask and Auto-Review even for\n tools that would otherwise auto-run. Full Access does not open tool-approval\n prompts, so hook `ask` does not downgrade that posture.\n- `updatedInput` must be a JSON object; it replaces the tool input\n before execution. When several hooks supply it, the last hook wins.\n- `additionalContext` is appended to the tool result sent back to the\n model as `[hook context] ...`. Multiple hooks' contexts are\n concatenated.\n\nWhen multiple hooks match, precedence is deny > ask > allow. Hooks\nmarked `background = true` cannot steer tool calls — they are\nsubmitted and never awaited, so they have no verdict to contribute.\n\nA foreground hook that produced no verdict at all — it hit its timeout, the\nprocess could not be started, or a strict process exited non-zero without an\nexplicit JSON decision — is not treated as permission. If\n*that* hook is configured with `continue_on_error = false`, the outcome\ndenies the tool call and the denial names the hook and a bounded reason.\nStrictness is read off the hooks that actually matched this call, so a\nstrict gate scoped to another tool cannot deny it, and a lenient hook's\ntimeout does not deny merely because a strict hook exists elsewhere in\nconfig. Under the default `continue_on_error = true` the outcome is\nlogged and the call proceeds.\n\n`reason` and `additionalContext` are capped (2 000 characters per field,\n8 000 for the concatenated context of one call) and stripped of control\ncharacters before they reach the TUI or the model.\n\nExample deny hook:\n\n```toml\n[[hooks.hooks]]\nevent = \"tool_call_before\"\ncommand = '''echo '{\"decision\":\"deny\",\"reason\":\"blocked by project policy\"}' '''\ncondition = { type = \"tool_name\", name = \"exec_shell\" }\n```\n\nExample ask hook (force approval for every MCP tool):\n\n```toml\n[[hooks.hooks]]\nevent = \"tool_call_before\"\ncommand = '''echo '{\"decision\":\"ask\"}' '''\ncondition = { type = \"tool_name\", name = \"mcp__*\" }\n```\n\nExample input rewrite:\n\n```toml\n[[hooks.hooks]]\nevent = \"tool_call_before\"\ncommand = \"~/.codewhale/hooks/clamp-shell-timeout.sh\"\ncondition = { type = \"tool_name\", name = \"exec_shell\" }\n```\n\nwhere the script reads the hook context, then prints\n`{\"updatedInput\": {...}}` with the adjusted arguments.\n\n`tool_name` conditions support `*` globs: `mcp__*` matches every MCP\ntool (e.g. `mcp__github__create_issue`) but not built-ins like\n`read_file`; exact names keep matching exactly. Other regex\nmetacharacters in the pattern are matched literally.\n\n### Project-local hooks\n\nRepositories can ship policy in `<workspace>/.codewhale/hooks.toml`,\nusing the same shape as the `[hooks]` table (top-level fields plus\n`[[hooks]]` entries). Project hooks are executable shell\nconfiguration, so Codewhale only loads them after the workspace has\nbeen trusted in user-owned config through the trust prompt or a\n`[projects.\"<workspace>\"] trust_level = \"trusted\"` entry. Session\n`/trust on` mode does not enable repo-supplied hooks by itself, and\nrepo-local legacy markers such as `.deepseek/trusted` do not enable\nproject hooks. Once trusted, project hooks are appended after global\nhooks from `config.toml`, so they run last and, for `updatedInput`,\nwin ties. A malformed trusted project file logs a warning and startup\nfalls back to global hooks only.\n\n```toml\n# .codewhale/hooks.toml\n[[hooks]]\nevent = \"tool_call_before\"\ncommand = '''echo '{\"decision\":\"deny\",\"reason\":\"no shell in this repo\"}' '''\ncondition = { type = \"tool_name\", name = \"exec_shell\" }\n```\n\n### Turn-end observer hooks\n\n`turn_end` hooks observe the end of each model turn after post-turn\nstate, usage totals, cost accounting, notifications, receipts, and\nqueue recovery have been updated. They receive JSON on stdin and are\nobserver-only: stdout is ignored, failures are logged as warnings, and\nthe hook cannot block user input, mutate the transcript, or change the\nnext queued follow-up.\n\nObserver-only UI events share one 32-entry queue and two persistent workers;\nthe terminal loop uses non-blocking submission and does not create a thread per\nevent. A full queue or unavailable dispatcher drops that observer event and is\nkept as an event-specific error toast, independent of ordinary agent/turn\nstatus text.\n\n```toml\n[[hooks.hooks]]\nevent = \"turn_end\"\ncommand = \"~/.codewhale/hooks/turn-audit.sh\"\ntimeout_secs = 2\ncontinue_on_error = true\n```\n\nThe payload includes common hook metadata plus post-turn accounting:\n\n```json\n{\n \"event\": \"turn_end\",\n \"session_id\": \"sess_12345678\",\n \"workspace\": \"/path/to/workspace\",\n \"mode\": \"agent\",\n \"created_at\": \"2026-07-12T10:30:00+00:00\",\n \"model_backed\": true,\n \"provider\": \"deepseek\",\n \"model\": \"deepseek-chat\",\n \"billing_surface\": null,\n \"turn_id\": \"turn_12345678\",\n \"status\": \"completed\",\n \"error\": null,\n \"duration_ms\": 1834,\n \"usage\": {\n \"input_tokens\": 1200,\n \"output_tokens\": 180,\n \"prompt_cache_hit_tokens\": 900,\n \"prompt_cache_miss_tokens\": 300,\n \"prompt_cache_write_tokens\": 0,\n \"reasoning_tokens\": null,\n \"reasoning_replay_tokens\": null\n },\n \"totals\": {\n \"session_tokens\": 1380,\n \"conversation_tokens\": 1380,\n \"input_tokens\": 1200,\n \"output_tokens\": 180\n },\n \"tool_count\": 2,\n \"queued_message_count\": 1,\n \"stop_hook_active\": false\n}\n```\n\n`created_at` anchors time-window pricing; `provider` and `model` identify the\neffective route used for model-backed turns. `billing_surface` is an optional,\nnon-secret classification derived from the endpoint that actually served the\nturn. Recognized StepFun routes emit `stepfun-payg` or `stepfun-plan`; the raw\nbase URL is never written to hook or runtime records. Runtime `TurnRecord`\nexports call the same field `effective_billing_surface`, which `scorecard`\naccepts as an alias. This keeps subscription quota separate from token-priced\nusage. Unrecognized and custom endpoints remain `null` and unpriced.\n\nShell-only lifecycle completions set `model_backed` to `false` and may report a\n`null` provider; offline scorecards exclude those records from model token and\ncost totals. Completion-only shell, manual-compaction, and purge events that do\nnot have a matching `TurnStarted` retain the observer notification with a\nsynthetic `lifecycle_<uuid>` turn id and the time the completion was observed.\n\nFor `interrupted` or `failed` turns, `status` reflects that terminal\nstate and `error` carries the engine error string when one is available.\n`stop_hook_active` is reserved for future re-entry protection and is\ncurrently always `false`.\n\n### Sub-agent lifecycle hooks\n\n`subagent_spawn` and `subagent_complete` hooks observe sub-agent lifecycle\nevents. They receive bounded JSON metadata on stdin and are observer-only:\nhook failures are logged as warnings and do not block sub-agent scheduling,\nchange prompts, or change results. For these observer events,\n`continue_on_error` has no effect: later matching hooks still run even when an\nearlier hook exits non-zero.\n\n```toml\n[[hooks.hooks]]\nevent = \"subagent_complete\"\ncommand = \"~/.codewhale/hooks/subagent-audit.sh\"\ntimeout_secs = 2\ncontinue_on_error = true\n```\n\n`subagent_spawn` receives:\n\n```json\n{\n \"event\": \"subagent_spawn\",\n \"agent_id\": \"agent_12345678\",\n \"session_id\": \"sess_12345678\",\n \"workspace\": \"/path/to/workspace\",\n \"mode\": \"agent\",\n \"model\": \"deepseek-chat\",\n \"total_tokens\": 1234,\n \"prompt_preview\": \"bounded prompt preview\",\n \"prompt_truncated\": false\n}\n```\n\n`subagent_complete` receives the same common fields plus terminal metadata:\n\n```json\n{\n \"event\": \"subagent_complete\",\n \"agent_id\": \"agent_12345678\",\n \"session_id\": \"sess_12345678\",\n \"workspace\": \"/path/to/workspace\",\n \"mode\": \"agent\",\n \"model\": \"deepseek-chat\",\n \"total_tokens\": 1234,\n \"status\": \"completed\",\n \"result_preview\": \"bounded result preview\",\n \"result_truncated\": false\n}\n```\n\nPreviews are capped before delivery so lifecycle hooks do not receive full\nsub-agent prompts, transcripts, or unbounded results. Use the transcript handle\nreturned by `agent` when full sub-agent details are needed.\n\n### Running-turn input\n\nComposer shortcuts keep the same role throughout a session:\n\n- **Enter** sends when idle and queues a next-turn follow-up while busy. The\n behavior does not change before versus after the provider's first token.\n- With an empty composer and queued follow-ups visible, **Enter** sends the\n oldest queued follow-up into the active turn now.\n- **Ctrl+Enter** (or **Cmd+Enter** when the terminal forwards it) explicitly\n steers the active turn. It sends normally when idle.\n- By default, **Shift+Enter**, **Alt+Enter**, and **Ctrl+J** insert a newline.\n- Set `composer_multiline_mode = true` to make **Enter** insert a newline and\n **Alt+Enter** send instead. **Shift+Enter** also sends when the terminal\n reports it distinctly; legacy terminals may report it as plain **Enter**.\n **Ctrl+J** still inserts a newline, and supported **Ctrl+Enter** /\n **Cmd+Enter** behavior is unchanged.\n- **Ctrl+G** and **Ctrl+S** only stash drafts; they never send or steer.\n\n### Composer stash (`/stash`, Ctrl+G / Ctrl+S)\n\nPress **Ctrl+G** in the composer to park the current draft to\n`~/.codewhale/composer_stash.jsonl`. `/stash list` shows parked\ndrafts with one-line previews and timestamps; `/stash pop`\nrestores the most recently parked draft (LIFO); `/stash clear`\nwipes the file. Capped at 200 entries; multiline drafts round-trip intact.\n**Ctrl+S** remains an alias in terminals that forward it; Cursor and VS Code\nreserve Ctrl+S for Save, so Ctrl+G is the portable default.\n\n## Settings File (Persistent UI Preferences)\n\ncodewhale also stores user preferences in:\n\n- `~/.codewhale/settings.toml` on new installs\n- `~/.deepseek/settings.toml` or the legacy platform config-dir\n `deepseek/settings.toml` when an existing settings file is present\n\nNotable settings include `auto_compact`, which uses a model-aware default-on\npolicy for known context windows up to the 1M-token V4 class. Automatic\ncompaction runs before the active model limit and carries the compacted summary\nforward into the next request. The trigger defaults to\n`auto_compact_threshold_percent = 80`. Users who prefer manual continuity can\npersist `auto_compact = false`; manual `/compact` / Ctrl+L remains available.\nYou can inspect or update these from the TUI with `/settings` and `/config`\n(interactive editor).\n\nCommon settings keys:\n\n- `theme` (`system`, `terminal`, `underwater`, `underwater-retro`,\n `shoreline`, `shoreline-light`, `dark`, `light`, `grayscale`,\n `catppuccin-mocha`, `tokyo-night`, `dracula`, `gruvbox-dark`, `claude`,\n `matrix`, `solarized-light`, `uwu`; default `underwater`): `underwater` is\n the dark navy fresh-install default, `shoreline` is its warm charcoal\n alternative and `shoreline-light` the light variant, `system` follows terminal\n background detection, `dark`/`light` use the Codewhale Whale pair,\n `terminal` inherits the host terminal, `grayscale` is the low-opinion\n black/white theme, and the named community presets apply across the TUI.\n Aliases such as `whale`, `mono`, `black-white`, `tokyonight`, and `gruvbox`\n are accepted. In Whale, cobalt blue owns action/focus, seafoam owns live\n work, Signal Gold owns human decisions and the whale, coral owns warnings,\n rose owns danger, violet owns Operate, and green remains completed/verified.\n Text labels, markers, and motion policy carry the same states when color is\n unavailable; color is never the only cue.\n User-authored overlays live only at `~/.codewhale/themes/<name>.json` (or\n `$CODEWHALE_HOME/themes/<name>.json`) and are selected with\n `/theme custom:<name>`. The filename is a bounded slug, symlinks and files\n over 64 KiB are refused, colors must be `#RRGGBB`, and unknown fields fail\n validation. `/theme schema` prints the embedded JSON Schema and `/theme path`\n shows the exact directory. An overlay names one compiled `base` theme and\n changes only listed semantic colors; it cannot include or read another file.\n Open `/theme` to browse valid overlays, preview them live, and keep the\n active `custom:<name>` selector when the picker is opened without moving.\n- `auto_compact` (on/off, model-aware default on for known context windows\n unless explicitly configured)\n- `auto_compact_threshold_percent` (10-100, default `80`): pre-send\n auto-compaction threshold used only when `auto_compact` is enabled.\n- `paste_burst_detection` (on/off, default on): fallback rapid-key paste\n detection for terminals that do not emit bracketed-paste events. This is\n independent of terminal bracketed-paste mode.\n- `work_surface_placement` (`bottom`, `top`, `left`, `right`, or `off`;\n default `bottom`): places the workbar — Tasks / To-do / Workers — under the\n composer (the default bottom workbar), above the transcript, in a side\n workbar, or hides it entirely (`off`). Side choices fall back to the top\n layout on narrow terminals without changing the saved preference. Set it\n live with `/config work_surface_placement right --save` (or `left` / `top` /\n `bottom` / `off`).\n- `rail_panel` (`tasks`, `agents`, `background`, `files`, `notepad`,\n `context`, `git`, `price`; default `tasks`, alias key `rail`): which panel\n the workbar shows. Panel selection is orthogonal to placement. `tasks` is\n the full live work list (to-dos, then sub-agents); `agents` narrows to the\n sub-agent rows; `background` lists background shells and automations;\n `files` lists touched files; `notepad` shows the workspace notes; `context`\n is a read-only session-facts list; `git` shows branch status; `price`\n shows cost. In every panel except `context`, rows are selectable and\n clickable and open their detail surface. `Alt+!`/`Alt+@`/`Alt+#`/`Alt+$`\n switch panels live.\n- `work_surface_top_height` (2–16) and `work_surface_side_width` (26–80):\n ceilings for the top strip's height and the side workbar's width. Both are\n normally persisted by dragging the divider rather than edited by hand; the\n strip still auto-fits its content below the ceiling.\n- `focus_texture` (`off`, `scrim`, or `grain`; default `off`): focus-context\n texture for modal views. `scrim` dims the already-rendered background\n outside the focused modal toward the theme surface; `grain` sprinkles\n sparse dots over blank cells there. The texture is static (no time\n component, so it is unaffected by `low_motion`), never writes over a cell\n that carries text, and preserves the 4.5:1 body-text contrast floor\n wherever both colors are resolvable. It is skipped entirely on frames\n below the ambient-life minimum size and when the focused modal already\n covers 90% or more of the frame. Set it live with\n `/config focus_texture scrim --save`.\n- `mention_menu_limit` (integer, default `128`): maximum number of\n `@`-mention popup candidates retained before the composer renders the\n visible window. The visible rows still depend on terminal height.\n- `mention_walk_depth` (integer, default `10`): maximum workspace depth for\n `@`-mention completion walks. Set to `0` for unlimited depth in deeply\n nested workspaces; keep the default in very large repos unless needed.\n- `mention_menu_behavior` (`fuzzy`, `browser`; default `fuzzy`): controls how\n `@`-mention completions are populated. `fuzzy` searches the workspace and\n applies mention frecency. `browser` lists only the immediate children of the\n currently typed directory segment in deterministic alphabetical order.\n- `show_thinking` (on/off)\n- `thinking_default_expanded` (on/off, default off): renders thinking blocks\n expanded initially when `show_thinking` is enabled. Space still toggles the\n selected block, and it decides only the blocks you have not touched: a block\n you expanded or collapsed yourself keeps that state if you change this\n setting later. This is useful in SSH/tmux environments where the Space\n binding may be intercepted.\n- `thinking_preview_lines` (integer, default `2`): how many body rows a\n **collapsed** completed thought still shows. `0` is header-only; `10` is\n the older dump. Live streaming preview is unchanged. Expand a block with\n Space, or set `thinking_default_expanded` to open every block.\n- `help_expand_groups` (on/off, default off): start Help/shortcuts with every\n group expanded. Default folds the long tail (Grok-style); type-to-filter\n still unfolds matches.\n- `pin_last_prompt` (on/off, default on): pin the last user prompt at the top\n of the transcript viewport after it scrolls off.\n- `show_tool_details` (on/off)\n- `inline_diffs` (`full`, `summary`, or `off`; default `full`): controls the\n inline presentation of successful structured File mutations. `full` shows a\n bounded red/green diff and semantic statistics, `summary` keeps only the\n statistics, and `off` keeps the calm changed-file outcome. All three retain\n the exact applied change in the selected File receipt's Alt/Option+V detail.\n Failure and cancellation never render a successful diff. Save the choice\n with `/config inline_diffs <mode> --save`.\n- `locale` (`auto`, `en`, `ja`, `zh-Hans`, `zh-Hant`, `pt-BR`, `es-419`, `vi`,\n `ko`; default `auto`): UI chrome locale. `auto` checks `LC_ALL`,\n `LC_MESSAGES`, then `LANG`; unsupported locale selections resolve to English.\n Every shipped pack holds full `en.json` parity, so no string falls back\n to English. The runtime also exposes the resolved locale in the system\n prompt as the fallback natural language for V4 reasoning and replies when the\n latest user message is ambiguous. Clear user language still takes priority;\n Chinese turns should produce Chinese `reasoning_content` and Chinese final\n replies even when the resolved locale is English.\n- `background_color` (`#RRGGBB`, `RRGGBB`, or `default`): optional main TUI\n background color applied to the root, header, transcript, and footer\n surfaces while preserving panel contrast.\n- `cost_currency` (`usd`, `cny`; default `usd`): currency used by the footer,\n context panel, `/cost`, `/tokens`, and long-turn notification summaries. The\n aliases `rmb` and `yuan` normalize to `cny`.\n- `default_mode` (`agent`, `plan`, or `operate`; legacy values are accepted for migration but are not live mode vocabulary)\n- `launch_screen` (legacy, migration-only): this historical `on`/`off` value\n is still accepted when reading an existing settings file, but it no longer\n changes behavior and is omitted from new saves. A fresh interactive launch\n always opens Tideline Startup; only an explicit resume or an explicit\n initial prompt enters the live session directly.\n- `sidebar_focus` (legacy, migration-only): the classic right sidebar this key\n configured was removed in the 0.9.4 rail unification. The key is still read\n once so old settings carry forward, then folds into the live keys:\n `pinned`/`work`/`plan`/`todos` become `rail_panel = \"pinned\"`,\n `agents`/`subagents` become `rail_panel = \"agents\"`, `context`/`session`\n become `rail_panel = \"context\"`, `tasks`/`auto` (the old default) become the\n `tasks` panel, `sessions` enables `sessions_rail`, and `hidden` turns the\n workbar off via `work_surface_placement = \"off\"`. An explicit `rail_panel`\n in the file always wins over the migrated value. Configure the workbar with\n `rail_panel` and `work_surface_placement`, not this key.\n- `sessions_rail` (`on`/`off`; default `off`): show the persistent Sessions\n list in the workbar. Rows list this workspace's recent\n non-archived sessions, newest first, with the active one marked; activating a\n row opens the session picker preselected on it (`/sessions open <id>`), so\n resume keeps its single implementation. Rows are projected from cached\n session metadata — the list never reads a transcript per frame, and never\n contacts a provider.\n- `session_auto_resume` (`on`/`off`; default `off`): reattach to this\n workspace's most recent session when Codewhale starts. Off by default so\n plain `codewhale` keeps starting fresh. `--resume`, `--continue`, and\n `--fresh` always take precedence. When it is on, startup still refuses to\n resume a session that is archived, fails to load, or is recorded against a\n different workspace; each of those falls back to a fresh transcript and says\n which session was skipped and why. It applies to the interactive launch only\n — `codewhale \"<prompt>\"` and `codewhale exec` are never silently prefixed\n with a prior conversation.\n- `max_input_history` (number of submitted input history entries; cleared\n drafts are also kept locally for composer history search). Note the spelling:\n the serde field on disk is `max_input_history`\n (`crates/tui/src/settings.rs:426`, default 100). `max_history` is the key\n name accepted by `/config set` and `settings.set()` (`settings.rs:1388`), not\n a settings.toml key — writing `max_history` into the file is silently\n ignored.\n- `default_model` (model name override)\n\n`/task digest` (alias `/tasks digest`) renders the canonical Work Graph\noperations and four-state To-do list as plain text, running work first. It\nreads the same snapshots as the styled Work surface and owns no parallel\nprogress state.\n\nPlan and Work are the everyday visible modes in the UI; Operate is an explicit\npreview entry while its Workflow control surface is still being built. Switch\nbetween them with `/mode`. For compatibility, older settings files with\n`default_mode = \"normal\"` still load as `agent`.\n\nLocalization scope is tracked in [LOCALIZATION.md](LOCALIZATION.md). The v0.7.6\ncore pack covers high-visibility TUI chrome only; provider/tool schemas,\npersonality prompts, and full documentation remain English unless explicitly\ntranslated later.\n\nReadability semantics:\n\n- Selection uses a unified style across transcript, composer menus, and modals.\n- Footer hints use a dedicated semantic role (`FOOTER_HINT`) so hint text stays readable across themes.\n\n### Token Quantities and Drivers\n\nDeepSeek V4 prefix caching makes token labels matter. These quantities are kept\nseparate:\n\n| Quantity | Meaning | Allowed to drive |\n|---|---|---|\n| Active request input estimate | Conservative estimate of the next request's live system prompt and transcript payload. | Header/footer context percent, auto-compaction trigger, opt-in Flash seam trigger, and emergency overflow preflight. |\n| Reserved response headroom | The effective request cap plus `1024` safety tokens. Normal no-override requests start at `65536`; a smaller route/provider ceiling narrows that value, and an explicit output override raises it only within the resolved route window and output ceiling. The identical cap reaches the wire and drives preflight, except on a route that states an output limit above half its known context window with no explicit output override: there compaction aims to keep a quarter of the window free (at most `65536`, and never more than the request cap), preflight refuses a request only when fewer than `128` output tokens would fit, and every request's `max_tokens` is what the window has left after that request's estimated input and the `1024` safety tokens, never above the request cap. Reasoning effort does not add a second hidden reservation. A separately published route input ceiling independently clamps the spendable input budget. | Emergency overflow budget checks only. |\n| Cumulative API usage | Provider-reported input plus output tokens summed across completed API calls; multi-tool turns may count the same stable prefix more than once. | Session usage and approximate cost telemetry only. |\n| Prompt cache hit/miss | Provider cache telemetry for the most recent call when available. | Cache-hit display and cost estimation only; never compaction or seam triggers. |\n| Context percent | Active request input estimate divided by the model context window. | Display only; it mirrors the active-input basis used by context safeguards. |\n| Cost estimate | Approximate spend from provider usage and configured DeepSeek rates. | Display only. |\n\nFor known context-window models, including 1M-class V4 models, replacement\ncompaction is enabled by default unless the user explicitly configures\n`auto_compact = false`. It fires at the active model's compaction threshold and\nreplaces old history with recent user context followed by one ordinary\ncheckpoint message. The standing system prompt remains unchanged. Unknown model\nids remain opt-in.\n\n### Command Migration Notes\n\nIf you are upgrading from older releases:\n\n- Old: `/codewhale`\n New: `/links` (aliases: `/dashboard`, `/api`)\n- Old: `/set model deepseek-reasoner`\n New: `/config` and edit the `model` row to `deepseek-v4-pro` or `deepseek-flash`\n- Old: visible `Normal` mode or `default_mode = \"normal\"`\n New: use `Agent` / `default_mode = \"agent\"`; legacy `normal` still maps to `agent`\n- Old: discover `/set` in slash UX/help\n New: use `/config` for editing and `/settings` for read-only inspection\n\n## Key Reference\n\n### Kimi Code membership model IDs\n\nThe exact `https://api.kimi.com/coding/v1` endpoint accepts `k3`, `k3-256k`,\n`kimi-for-coding`, and `kimi-for-coding-highspeed`. Use `k3-256k` for a fixed\n262,144-token K3 window; use bare `k3` with `context_window = 1048576` only\nwhen the membership plan includes the 1M entitlement. Both K3 ids use the same\nreasoning contract, and all four membership ids omit generic sampling fields.\n\n### Core keys (used by the TUI/engine)\n\n- `provider` (string, optional): `deepseek` (default), `deepseek-anthropic`, `nvidia-nim`, `openai`, `atlascloud`, `wanjie-ark`, `volcengine`, `openrouter`, `xiaomi-mimo`, `novita`, `fireworks`, `siliconflow`, `arcee`, `siliconflow-CN`, `moonshot`, `sglang`, `vllm`, `ollama`, `ollama-cloud`, `huggingface`, `modelscope`, `together`, `qianfan`, `openai-codex`, `anthropic`, `openmodel`, `zai`, `stepfun`, `minimax`, `deepinfra`, `sakana`, `longcat`, `opencode-go`, `meta`, `mistral`, `telecomjs`, `xai`, `orcarouter`, `modelstudio-token-plan`, `google`, `edenai`, or `custom`. Legacy `deepseek-cn` configs are still accepted as an alias for `deepseek`; DeepSeek uses the same official host [`https://api.deepseek.com`](https://api-docs.deepseek.com/) worldwide. `deepseek-anthropic` targets DeepSeek's Anthropic Messages-compatible endpoint at `https://api.deepseek.com/anthropic` using `DEEPSEEK_API_KEY`; `nvidia-nim` targets NVIDIA's NIM-hosted DeepSeek endpoints through `https://integrate.api.nvidia.com/v1`; `openai` targets a generic OpenAI-compatible endpoint, defaulting to `https://api.openai.com/v1`; `atlascloud` targets AtlasCloud's OpenAI-compatible endpoint at `https://api.atlascloud.ai/v1`; `wanjie-ark` targets Wanjie Ark's OpenAI-compatible endpoint at `https://maas-openapi.wanjiedata.com/api/v1`; `volcengine` targets Volcengine Ark's OpenAI-compatible coding endpoint at `https://ark.cn-beijing.volces.com/api/coding/v3`; `openrouter` targets `https://openrouter.ai/api/v1`; `xiaomi-mimo` targets Xiaomi MiMo's OpenAI-compatible endpoint, using `https://token-plan-sgp.xiaomimimo.com/v1` by default for Token Plan keys (`tp-...`) and `https://api.xiaomimimo.com/v1` for pay-as-you-go keys. For Token Plan accounts outside the Singapore default, set `base_url` explicitly or use `mode = \"token-plan-cn\"` for China and `mode = \"token-plan-ams\"` for Europe/Amsterdam; `novita` targets `https://api.novita.ai/openai/v1`; `fireworks` targets `https://api.fireworks.ai/inference/v1`; `siliconflow` targets SiliconFlow, defaulting to `https://api.siliconflow.com/v1`; `arcee` targets Arcee AI's OpenAI-compatible endpoint at `https://api.arcee.ai/api/v1`; `siliconflow-CN` targets the SiliconFlow China regional endpoint through `[providers.siliconflow_cn]`; `moonshot` targets Moonshot/Kimi, defaulting to `https://api.moonshot.ai/v1`; `sglang` targets a self-hosted OpenAI-compatible endpoint, defaulting to `http://localhost:30000/v1`; `vllm` targets a self-hosted vLLM OpenAI-compatible endpoint, defaulting to `http://localhost:8000/v1`; `ollama` targets Ollama's OpenAI-compatible endpoint, defaulting to `http://localhost:11434/v1`; `huggingface` targets Hugging Face Inference Providers at `https://router.huggingface.co/v1`; `modelscope` targets ModelScope's OpenAI-compatible inference API at `https://api-inference.modelscope.cn/v1`; `together` targets Together AI at `https://api.together.xyz/v1`; `qianfan` targets Baidu Qianfan at `https://api.baiduqianfan.ai/v1`; `openai-codex` targets official ChatGPT plan inference with Codewhale-owned OAuth at `https://api.openai.com/v1`; `anthropic` targets Claude's native Messages API; `openmodel` targets OpenModel's Anthropic-compatible Messages API at `https://api.openmodel.ai`; `zai` targets Z.ai at `https://api.z.ai/api/coding/paas/v4`; `stepfun` targets StepFun at `https://api.stepfun.ai/v1`; `minimax` targets MiniMax at `https://api.minimax.io/v1`; `deepinfra` targets DeepInfra at `https://api.deepinfra.com/v1/openai`; `sakana` targets Sakana AI Fugu at `https://api.sakana.ai/v1`; `longcat` targets Meituan LongCat at `https://api.longcat.chat/openai/v1`; `opencode-go` targets the subscription-backed OpenCode Go model-aware route (Chat Completions, Responses, or Messages according to the documented model) at `https://opencode.ai/zen/go/v1`; `meta` targets Meta Model API; `mistral` targets Mistral AI's OpenAI-compatible endpoint at `https://api.mistral.ai/v1`; `telecomjs` targets TelecomJS TokenHub at `https://aigw.telecomjs.com/v1`; and `xai` targets xAI's API-key or OAuth route.\n- `opencode-zen` (string provider value): selects the model-aware OpenCode Zen gateway through `[providers.opencode_zen]`. The default base URL is `https://opencode.ai/zen/v1`, the default model is `gpt-5.6`, and credentials come from `api_key`, `OPENCODE_ZEN_API_KEY`, or fallback `OPENCODE_API_KEY`—never ChatGPT/Codex OAuth. `OPENCODE_ZEN_BASE_URL` and `OPENCODE_ZEN_MODEL` are accepted. The selected model's wire comes from the curated Zen snapshot, then from the AI SDK package its Models.dev `opencode` row names (refresh with `codewhale models --update`): GPT, Grok, and Muse Spark use Responses; Claude and most Qwen rows use Anthropic Messages; DeepSeek, MiniMax, GLM, Kimi, `qwen3.8-max`, and the free rows use Chat Completions. Gemini, Models.dev rows marked `deprecated`, and models no loaded catalog lists fail closed because Codewhale has no proven supported wire contract for them. See the exact current model groups in [`PROVIDERS.md`](PROVIDERS.md#opencode-zen-protocol-catalog).\n- `minimax-anthropic` (string provider value): selects MiniMax's Anthropic-compatible Messages route through `[providers.minimax_anthropic]`. The default Base URL is `https://api.minimax.io/anthropic`; set `https://api.minimaxi.com/anthropic` for China. Keep the `/anthropic` suffix because Codewhale appends `/v1/messages`. The route uses `MINIMAX_API_KEY` and defaults to `MiniMax-M3`; `MiniMax-M2.7` is also registered. Official M3 input modalities are text, image, and video, with adaptive or disabled thinking. M2.7 is text-only and always keeps thinking enabled.\n- `api_key` (string, required for hosted providers): must be non-empty for DeepSeek/hosted providers (or set the provider API key env var). Self-hosted SGLang, vLLM, and local `ollama` can omit it. `ollama-cloud` requires a key saved for that provider or supplied by `OLLAMA_CLOUD_API_KEY`, then `OLLAMA_API_KEY`.\n- `auth_mode` (string, optional provider-table key): selects a provider-specific authentication contract. Kimi Code membership uses `auth_mode = \"api_key\"` (or omit the field), a key created in the [Kimi Code console](https://www.kimi.com/code/console), `base_url = \"https://api.kimi.com/coding/v1\"`, and bare `model = \"k3\"` for K3. Codewhale gives that route a safe 262,144-token baseline; set `context_window = 1048576` only when the Kimi Code plan includes 1M access (Allegretto and above). `k3[1m]` is a Claude Code-only convention, not an API model ID, and Codewhale rejects it instead of silently changing the wire model or assuming an entitlement. `model = \"kimi-for-coding\"` remains the valid K2.7 compatibility route available to all Kimi Code members. Legacy `auth_mode = \"kimi_oauth\"` fails closed with API-key guidance and never probes, reads, refreshes, or rewrites `kimi_cli`/`kimi_code_cli` credential files. First-class OAuth requires Codewhale's own vendor-registered client identity and remains tracked in #4417.\n- `base_url` (string, optional, `[providers.<name>]` key; see [Legacy top-level `base_url` and `api_key`](#legacy-top-level-base_url-and-api_key) for the older top-level spelling): defaults to `https://api.deepseek.com/beta` for DeepSeek's OpenAI-compatible Chat Completions API, including legacy `provider = \"deepseek-cn\"` configs. Other defaults are `https://api.deepseek.com/anthropic` for `deepseek-anthropic`, `https://integrate.api.nvidia.com/v1` for `nvidia-nim`, `https://api.openai.com/v1` for `openai`, `https://api.atlascloud.ai/v1` for `atlascloud`, `https://maas-openapi.wanjiedata.com/api/v1` for `wanjie-ark`, `https://ark.cn-beijing.volces.com/api/coding/v3` for `volcengine`, `https://openrouter.ai/api/v1` for `openrouter`, `https://token-plan-sgp.xiaomimimo.com/v1` for `xiaomi-mimo` when the API key starts with `tp-...` and `https://api.xiaomimimo.com/v1` otherwise, `https://api.novita.ai/openai/v1` for `novita`, `https://api.fireworks.ai/inference/v1` for `fireworks`, `https://api.siliconflow.com/v1` for `siliconflow`, `https://api.siliconflow.cn/v1` for `siliconflow-CN`, `https://api.arcee.ai/api/v1` for `arcee`, `https://api.moonshot.ai/v1` for `moonshot`, `https://api.minimax.io/v1` for `minimax`, `https://api.openmodel.ai` for `openmodel`, `https://api.z.ai/api/coding/paas/v4` for `zai`, `https://api.stepfun.ai/v1` for `stepfun`, `https://api.deepinfra.com/v1/openai` for `deepinfra`, `https://api.sakana.ai/v1` for `sakana`, `https://router.huggingface.co/v1` for `huggingface`, `https://api-inference.modelscope.cn/v1` for `modelscope`, `https://api.together.xyz/v1` for `together`, `https://api.baiduqianfan.ai/v1` for `qianfan`, `https://api.openai.com/v1` for `openai-codex`, `https://api.anthropic.com` for `anthropic`, `https://api.mistral.ai/v1` for `mistral`, `http://localhost:30000/v1` for `sglang`, `http://localhost:8000/v1` for `vllm`, `http://localhost:11434/v1` for `ollama`, and `https://ollama.com/v1` for `ollama-cloud`. Set `base_url = \"https://token-plan-cn.xiaomimimo.com/v1\"` for China-region Xiaomi MiMo Token Plan accounts or `base_url = \"https://token-plan-ams.xiaomimimo.com/v1\"` for Europe/Amsterdam accounts. Mistral-specific reasoning fields and polymorphic replay are enabled only on the documented first-party HTTPS `/v1` hosts; a custom Mistral base URL keeps generic Chat semantics. Set `https://api.deepseek.com` or `https://api.deepseek.com/v1` explicitly to opt out of DeepSeek beta features.\n- `ollama-cloud` route: select `provider = \"ollama-cloud\"`, configure `[providers.ollama_cloud]` when overriding the default `https://ollama.com/v1` / `gpt-oss:120b` tuple, and save a key from [Ollama account settings](https://ollama.com/settings/keys) with `codewhale auth set --provider ollama-cloud`. Ambient precedence is `OLLAMA_CLOUD_API_KEY`, then `OLLAMA_API_KEY`; arbitrary Ollama model IDs pass through unchanged.\n- Legacy Ollama Cloud migration: a released `provider = \"ollama\"` config whose normalized `[providers.ollama].base_url` is exactly `https://ollama.com/v1` is upgraded to the `ollama-cloud` runtime identity in memory. Only that exact tuple may read its old `ollama` provider table and secret slot. The config and secrets are never rewritten, and neighboring paths, HTTP downgrades, lookalike hosts, or an explicit `ollama-cloud` selection never consume the fallback.\n- `telecomjs` base URL and catalog: `[providers.telecomjs]` defaults to `https://aigw.telecomjs.com/v1`; `TELECOMJS_BASE_URL` overrides it. With `TELECOMJS_API_KEY`, `/models` refreshes a key-scoped catalog without mixing rows into another provider.\n- `edenai` gateway: select `provider = \"edenai\"`; `[providers.edenai]` defaults to `https://api.edenai.run/v3` and `deepseek/deepseek-v4-pro`. `EDENAI_API_KEY`, `EDENAI_BASE_URL`, and `EDENAI_MODEL` are accepted. Use `EDENAI_BASE_URL = \"https://api.eu.edenai.run/v3\"` for Eden AI's documented EU endpoint; the default `deepseek/deepseek-v4-pro` is only listed on the global catalog, so pair the EU endpoint with an EU-listed model such as `qwen/deepseek-v4-pro` via `EDENAI_MODEL` or `model`. The provider refreshes Eden AI's `/models` catalog, but leaves model-specific reasoning controls untouched because the gateway spans multiple model families.\n- `codewhale` (Codewhale API): select `provider = \"codewhale\"`; `[providers.codewhale]` defaults to `https://api.codewhale.net/v1` and `deepseek/deepseek-v4-pro`. The credential is a Codewhale account API key (`cwc_key_…`) with the `models:infer` scope, read from `CODEWHALE_API_KEY` or the `codewhale` secret-store slot; `codewhale account api-keys create --name <name> --use` mints one and saves it locally. `CODEWHALE_API_BASE` overrides the origin and must be HTTPS except on loopback. The model catalog is the account's own authenticated `GET /v1/models`: ids are `provider/model` and each row states its wire (`chat-completions` → `/v1/chat/completions`, `anthropic-messages` → `/v1/messages`, `responses` → `/v1/responses`). Connect the underlying provider keys with `codewhale account keys set <provider>`.\n- `concentrate` gateway: select `provider = \"concentrate\"`; `[providers.concentrate]` defaults to `https://api.concentrate.ai/v1` and `deepseek-v4-pro` over the OpenAI Responses wire. `CONCENTRATE_API_KEY`, `CONCENTRATE_BASE_URL`, and `CONCENTRATE_MODEL` are accepted. Model ids pass through verbatim (`gpt-5.6-sol`, `openai/gpt-5.6-sol`, or `concentrate/auto` for the gateway router). BYOK only; see [PROVIDERS.md](PROVIDERS.md#concentrate-notes).\n- `mistral` model and reasoning contract: `[providers.mistral]` defaults to `mistral-code-latest`; `MISTRAL_MODEL` overrides it and the generic `CODEWHALE_MODEL` override wins when both are set. The current picker also lists `mistral-medium-latest`, `mistral-small-latest`, and `mistral-large-latest`. On exact first-party HTTPS `/v1` routes, Medium and Small accept only `reasoning_effort = \"none\" | \"high\"` and replay polymorphic thinking blocks. Deprecated native Magistral IDs may still be configured explicitly, remain always-reasoning, and never receive the adjustable effort field.\n- `context_window` (integer, optional provider-table key): override the total context window for the active `[providers.<name>]` route when an OpenAI-compatible gateway, hosted model alias, or self-hosted runtime has a different limit than Codewhale's static model table. For example, `[providers.openai] context_window = 1000000` lets an OpenAI-compatible DashScope/Qwen route budget against a 1M-token window instead of the conservative fallback. For Kimi Code K3, keep `model = \"k3\"` and set `[providers.moonshot] context_window = 1048576` only when the membership plan includes 1M access; otherwise omit it to retain the 262,144-token safe baseline. The value must be greater than 0 and affects prompt context notes, compaction thresholds, context-pressure checks, and request output caps. Full resolution order, and how to see which rung produced the current window: [Context length (context window)](#context-length-context-window).\n- `path_suffix` (string, optional provider-table key): override the chat-completions path for OpenAI-compatible gateways that do not serve `/v1/chat/completions`. For example, `[providers.openai] path_suffix = \"/chat/completions\"` sends chat requests to the unversioned base URL plus `/chat/completions`; `models` and `beta/*` requests keep their normal routing.\n- `reasoning_stream_style` (string, optional provider-table key): override how streaming reasoning is separated from answer text for the active provider route. Use `separate_field` for `reasoning_content` / `reasoning` deltas, `inline_tags` for gateways that stream `<think>...</think>` inside `delta.content`, or `none` to render incoming content exactly as answer text. When unset, every Chat Completions route uses `separate_field` (Mistral's first-party route uses its typed thinking blocks); set `none` only for a gateway that streams its answer inside `reasoning_content`.\n- `[providers.<name>.auth]` (table, optional): provider-scoped auth source metadata. `source = \"command\"` stores a command argv plus optional `timeout_ms`; `source = \"secret\"` stores a `secret_id`. This slice lets provider readiness, `/provider`, and doctor JSON report the auth source class without exposing command argv output or secret values; executing commands and resolving external secret material is handled by the follow-up resolver work.\n- `insecure_skip_tls_verify` (bool, optional provider-table key): legacy compatibility key, disabled by default. When true on the active provider table, provider clients reject the configuration instead of skipping TLS certificate verification. Use `SSL_CERT_FILE` for corporate or private CA bundles; `codewhale doctor` reports stale uses of this setting.\n- `default_text_model` (string, optional): defaults to `deepseek-flash` for DeepSeek and `deepseek-anthropic`, `gpt-5.6` for OpenAI, `grok-4.6` for xAI, `deepseek-ai/deepseek-v4-pro` for NVIDIA NIM, `deepseek-ai/deepseek-v4-flash` for AtlasCloud, `deepseek-reasoner` for Wanjie Ark, `DeepSeek-V4-Pro` for Volcengine Ark, `deepseek/deepseek-v4-pro` for OpenRouter and Novita, `mimo-v2.6-pro` for Xiaomi MiMo, `accounts/fireworks/models/deepseek-v4-pro` for Fireworks, `deepseek-ai/DeepSeek-V4-Pro` for SiliconFlow and DeepInfra, `trinity-large-thinking` for Arcee AI, `kimi-k2.7-code` for Moonshot, `MiniMax-M3` for MiniMax, `GLM-5.3` for Z.ai, `step-3.7-flash` for StepFun, `ernie-4.0-turbo-8k` for Qianfan, `fugu` for Sakana AI, `deepseek-ai/DeepSeek-V4-Pro` for SGLang/vLLM, no fixed id for local Ollama (Codewhale adopts a chat-capable tag from the live local catalog — coder or tool-capable tags first, then the largest context — and never an embedding or reranker model), and `gpt-oss:120b` for Ollama Cloud. Hugging Face and Together AI both default to `deepseek-ai/DeepSeek-V4-Pro`; `openai-codex` defaults to `gpt-6.1-sol`; `anthropic` defaults to `claude-sonnet-5-5`; `openmodel` defaults to `deepseek-v4-flash`. Current public DeepSeek IDs include `deepseek-v4-pro` and `deepseek-flash` (V4.1 Flash, shipped as the unversioned id), both with 1M context windows, 384K max output, and thinking mode enabled by default. DeepSeek's live pricing/model page now labels the Pro backend `DeepSeek-V4-Pro-0813`; the callable API ID remains `deepseek-v4-pro`, so Codewhale does not send the backend label or the Claude Code-specific `deepseek-v4-pro[1m]` selector. DeepSeek retired `deepseek-chat` and `deepseek-reasoner` on July 24, 2026; direct first-party routes migrate both to `deepseek-flash`, with omitted reasoning settings preserving their former non-thinking (`off`) and thinking (`high`) intent. DeepSeek has also retired the `deepseek-v4-flash` and `deepseek-v4-flash-vision-exp` models: it still accepts both names, serves them with V4.1 Flash, and bills them at the `deepseek-flash` price, which Codewhale applies to turns from 2026-09-10 04:00 UTC. The `flash`, `deepseek-v4flash`, `flash-vision`, and `deepseek-v4flashvisionexp` shorthands select `deepseek-flash`. Explicit `reasoning_effort` wins, and provider-owned ids on Wanjie Ark, aggregators, self-hosted runtimes, and custom endpoints are not globally rewritten. SiliconFlow retains its own mapping: `deepseek-reasoner` and `deepseek-r1` select its Pro model while `deepseek-chat` and `deepseek-v3` select Flash. Provider-specific mappings translate `deepseek-v4-pro` / `deepseek-v4-flash` to each provider's model ID where supported. OpenRouter also recognizes recent large IDs such as `arcee-ai/trinity-large-thinking`, `minimax/minimax-m3`, `minimax/minimax-m2.7`, `xiaomi/mimo-v2.5-pro`, `qwen/qwen3.6-flash`, `qwen/qwen3.6-35b-a3b`, `qwen/qwen3.6-max-preview`, `qwen/qwen3.6-27b`, `qwen/qwen3.6-plus`, `qwen/qwen3.7-max`, `google/gemma-4-31b-it`, `moonshotai/kimi-k2.7-code`, `moonshotai/kimi-k2.6`, `nvidia/nemotron-3-nano-omni-30b-a3b-reasoning:free`, and `nvidia/nemotron-3-ultra-550b-a55b`; direct Arcee uses bare IDs such as `trinity-large-thinking` and `trinity-large-preview`; direct Moonshot recognizes `kimi-k3`, `kimi-k2.7-code`, and `kimi-k2.6`. The exact Kimi Code endpoint recognizes bare `k3` for K3 and `kimi-for-coding` for K2.7; those membership IDs are distinct from the direct Moonshot IDs and are never rewritten across routes. Direct MiniMax recognizes `MiniMax-M3` and the documented M2.x chat model IDs; direct Z.ai recognizes `GLM-5.3` (the default), `GLM-5.2`, `GLM-5.1`, and `GLM-5-Turbo`, and OpenRouter recognizes the matching `z-ai/glm-5.1`, `z-ai/glm-5.2`, `z-ai/glm-5.3`, and `z-ai/glm-5-turbo` IDs — `GLM-5.3` has been live on the Z.ai Coding Plan since 2026-08-13; it inherits its catalog metadata from `GLM-5.2` until Z.ai publishes distinct 5.3 numbers and carries no price, and an explicit `GLM-5.2` selection keeps its own id; direct Sakana recognizes `fugu` and `fugu-ultra-20260615`; direct Xiaomi MiMo recognizes chat IDs `mimo-v2.6-pro`, `mimo-v2.6-flash`, and `mimo-v2.5-pro-ultraspeed` (the retired `mimo-v2.5-pro` and `mimo-v2.5` are sent as `mimo-v2.6-pro` and `mimo-v2.6-flash`), while TTS IDs are selected through `codewhale speech` / `tts`. Generic `openai`, `atlascloud`, `wanjie-ark`, `xiaomi-mimo`, `arcee`, `moonshot`, `minimax`, `openmodel`, `zai`, `stepfun`, `qianfan`, `sakana`, local Ollama, and Ollama Cloud model IDs are passed through unchanged after known aliases are normalized. OpenRouter and SiliconFlow provider configs with a custom `base_url` also preserve explicit model values, which lets OpenAI-compatible gateways accept bare model IDs. Use `/models` or `codewhale models` to discover live IDs from your configured endpoint. `CODEWHALE_MODEL` overrides this for a single process; `DEEPSEEK_MODEL` is the legacy alias.\n- TelecomJS uses `deepseek-v4-pro` only as a conservative pre-refresh fallback. Once its key-scoped `/models` catalog is available, the picker uses those live rows; Codewhale omits unsupported reasoning request fields on this route.\n- `reasoning_effort` (string, optional): `auto`, `off`, `minimal`, `low`, `medium`, `high`, `xhigh`, `ultra` (`ultracode` is an alias of `ultra`), or `max`; defaults to the configured UI tier. DeepSeek Platform receives top-level `thinking` / `reasoning_effort` fields. Ollama Cloud's OpenAI-compatible Chat Completions route preserves its documented `none` / `low` / `medium` / `high` / `max` ladder (`off` is sent as `none`; `xhigh` and `ultracode` normalize to `max`). Direct xAI `grok-4.7` and `grok-4.6` on exact `https://api.x.ai/v1` receive top-level `reasoning_effort = \"low\" | \"medium\" | \"high\" | \"xhigh\"` (the ladder comes from the bundled catalog row; `grok-4.5` maps `xhigh` to `high`, and rows without a documented effort such as `grok-4.3` get no field); Grok reasoning cannot be disabled, so `off` normalizes to `high`, `max`/`ultracode` to `xhigh`, and `auto` leaves the field omitted so xAI's documented default `high` applies. A custom xAI-compatible `base_url` does not inherit that dialect. Direct Moonshot `kimi-k3` on exact `https://api.moonshot.ai/v1` is always-thinking and receives only top-level `reasoning_effort = \"low\" | \"high\" | \"max\"`; `off` normalizes to `low`, and `medium` to `high`. Kimi Code membership `k3` on exact `https://api.kimi.com/coding/v1` instead receives nested `thinking.effort`, and its `off` setting also normalizes to enabled `low`. Normal dispatched `auto` uses Codewhale's auto-reasoning selector and sends a concrete route-normalized tier; only an omitted reasoning setting leaves the provider default in control. Neighboring gateways and model/endpoint combinations retain the generic Moonshot contract. OpenAI Codex normalizes stale `off` to `low` and sends `max` / `ultracode` as Responses `xhigh`. Z.ai receives documented `thinking` controls and treats enabled thinking as the GLM coding high/max lane. NVIDIA NIM receives equivalent settings through `chat_template_kwargs`.\n- `verbosity` (string, optional): `normal` or `concise`. `normal` keeps the\n default conversational prompt. `concise` appends a prompt discipline block\n for direct, low-chatter output; CLI noninteractive commands (`exec` and\n `eval`) default to `concise` unless config/env/CLI overrides it.\n Override per process with `CODEWHALE_VERBOSITY` or the legacy\n `DEEPSEEK_VERBOSITY` alias.\n- `telemetry` (bool, optional): anonymous usage counting, **`true` by default\n in the current 0.9.12 source**. Notice version `5` names Codewhale and PostHog\n and describes the opt-out policy; no acceptance is invented for a default\n user. Existing explicit declines remain off. An explicit `false` here is the\n durable *opt-out*: it deletes the random install id, truncates buffered and\n dry-run events, and leaves a tombstone reasserted while the setting is false.\n It is a floor: `--telemetry true` and `CODEWHALE_TELEMETRY=1` lose to it.\n Use `/settings` or `codewhale config set telemetry true` to turn counting\n back on explicitly for new sessions through the existing privacy transition. `CODEWHALE_TELEMETRY`\n (legacy alias `DEEPSEEK_TELEMETRY`) and `--telemetry false` provide a run-scoped\n kill switch that stops collection and delivery without erasing the owner's\n state. A repo-local `.codewhale/config.toml` cannot set this preference.\n `codewhale config telemetry` shows the disclosure; `codewhale config get\n telemetry` reports preference and privacy status. Full schema and opt-out\n behavior: [`TELEMETRY.md`](TELEMETRY.md).\n- `telemetry_endpoint` (string, optional): where batches are POSTed. Leaving it\n unset selects the shipped default,\n **`https://telemetry.codewhale.net/v1/telemetry`** — the first-party ingest\n service described in [`TELEMETRY.md`](TELEMETRY.md), whose source is in\n `telemetry-ingest/`. This key decides only *where* a permitted session sends;\n it cannot override an opt-out. Setting it\n to the **empty string** is how you stay enabled and contact nobody: each batch\n is then written to `$CODEWHALE_HOME/telemetry/dryrun.jsonl` and no HTTP client\n is constructed at all, so you can read exactly what would have been sent. Any\n other value replaces the default outright. `https://` is required; plain\n `http://` is accepted only for loopback hosts, and there is no environment\n variable that overrides that refusal. A rejected endpoint turns telemetry off\n for the run rather than falling back to plaintext or to the default. Override\n per process with `CODEWHALE_TELEMETRY_ENDPOINT` (legacy alias\n `DEEPSEEK_TELEMETRY_ENDPOINT`), where an empty value means the same \"contact\n nobody\". A repo-local `.codewhale/config.toml` cannot set it.\n- `allow_shell` (bool, optional): in interactive TUI Agent sessions, omitting\n this keeps shell tools available with approval prompts; setting it to `false`\n hides shell tools. Headless, durable-task, and other noninteractive profiles\n keep the conservative omitted-field default and require `allow_shell = true`\n to expose shell. Plan mode always hides shell; Full Access enables shell and\n auto-approval.\n- `approval_policy` (string, optional): `on-request`, `untrusted`, or `never`. Runtime `approval_mode` editing in `/config` also accepts `on-request` and `untrusted` aliases.\n- `[approval] default_selection` (string, optional): which option an approval\n card highlights when it first appears — `deny` (default) or `allow_once`.\n `deny` means a reflexive Enter on a card you have not read refuses the call.\n The highlighted row is tagged `(Enter)`; press `y` to allow once.\n Set `allow_once` to restore the pre-v0.9.6 Enter-to-approve muscle memory\n (#5293). It moves the highlight only: which calls are prompted for is still\n `approval_policy` plus the rules in `permissions.toml`.\n\n ```toml\n [approval]\n default_selection = \"allow_once\"\n ```\n- `[approval] timeout_seconds` (integer, optional): bound how long an\n approval may wait — the TUI card and the Runtime API approvals the desktop\n app and web use alike. When the window elapses the call is refused\n (fail-closed) and recorded as a timeout, not as the operator's denial.\n Omitted or `0` waits indefinitely, which is the default everywhere: no\n approval is ever denied on your behalf unless you set this. Values above\n 24h clamp with a warning (#6101).\n\n ```toml\n [approval]\n timeout_seconds = 300\n ```\n- `sandbox_mode` (string, optional): `read-only`, `workspace-write`, `danger-full-access`, `external-sandbox`.\n The value can only narrow the sandbox the permission mode picks (Full\n Access: no wrapper; Ask and Auto-Review: `workspace-write`; Plan:\n `read-only`), so `danger-full-access` and `external-sandbox` take effect\n only in Full Access (`yolo`).\n Platform support is not identical. macOS uses Seatbelt when its runtime\n probe succeeds. Linux uses bubblewrap by default whenever `/usr/bin/bwrap`\n is installed and a probe proves it can confine a child; `prefer_bwrap =\n false` opts out and reports no OS command sandbox. Windows does not currently advertise an OS sandbox; its planned helper contract starts\n with process-tree containment only and must not be described as read-only\n filesystem isolation, workspace-write enforcement, network blocking,\n registry isolation, or AppContainer isolation until those are implemented.\n- The cross-layer relationship between mode admission, hooks, registered tool\n requirements, typed rules, auto-review, repo law, human approval, and the\n execution sandbox is defined in\n [Authorization order](AUTHORIZATION_ORDER.md).\n- **Read deny-list.** Every sandbox posture — `read-only` included — grants\n read access to the whole filesystem; the postures differ in what they may\n *write* and whether they may reach the network. The read deny-list narrows\n that, in two scopes. Codewhale's own file tools (`read`, `grep`,\n `file_search`, `read_media`) refuse every listed path. A shell command\n refuses only the *key material and other-principal* entries — SSH and GPG\n keys, `/etc/shadow`, `/etc/sudoers`, system and login keychains, browser\n profiles, other agents' credential stores, Codewhale's own secret stores,\n runtime daemon sockets, and anything in `sandbox_denied_read_paths`. A\n tool's own auth config is refused to the model but stays readable to the\n tool: `gh` cannot start without `~/.config/gh/hosts.yml`, so denying it to\n the process would disable the CLI without keeping its token out of reach.\n - `sandbox_read_denylist_defaults` (bool, default `true`): apply the built-in\n credential-store set — `~/.ssh`, `~/.gnupg`, cloud credential directories\n (`~/.aws`, `~/.config/gcloud`, `~/.azure`, `~/.kube`, …), `~/.netrc`,\n `~/.npmrc`, `~/.git-credentials`, macOS keychains, browser profiles,\n Codewhale's own secret stores, and `.env` files (but not `.env.example`\n and friends). Ordinary source, `Cargo.toml`, `~/.gitconfig`, `~/.cargo`,\n and `~/.npm` stay readable so builds and tests still work. Set `false` to\n restore the pre-0.9.12 full-disk-read behavior.\n - `sandbox_denied_read_paths` (list of paths): additional denied subpaths.\n `~` expands. These can never be exempted.\n - `sandbox_read_denylist_exempt` (list of paths): subtract a path from the\n *built-in defaults* when a project genuinely needs it. Deny wins over\n allow: this never reopens anything in `sandbox_denied_read_paths`.\n\n Exemption granularity is **whole-rule**, not per-file. An exempt path\n removes a built-in rule only when the rule's own path is at or below it,\n so exempting `~/.ssh/config` does nothing: the `~/.ssh` rule still denies\n it, because `~/.ssh` does not lie within `~/.ssh/config`. To reopen that\n one file you must exempt `~/.ssh` itself — which also reopens the private\n keys next to it. That is the documented tradeoff: there is no shipped way\n to narrow a built-in rule to \"everything except one file\"; copy what you\n need out of the denied tree instead. The one name-shaped rule, `.env`\n files, is exempted by *name* rather than by path: any exempt entry whose\n file name is exactly `.env` — bare `.env`, `~/.env`,\n `some/project/.env` — disables the entire `.env` filename rule, i.e.\n every `.env` and `.env.<name>` on disk rather than one project's.\n (`.env.example` and friends are never denied, so they need no exemption.)\n\n Enforced at two points, with different reach. Codewhale's own in-process\n tools, which the OS sandbox never wraps, refuse every rule — `read_file` /\n `read` / `read_media` for contents, `list_dir` / `file_search` for\n *enumeration* (listing a denied directory, or searching one, is refused just\n as Seatbelt blocks its readdir; a name search rooted above a denied tree\n skips entries inside it). Sandboxed shell commands (Seatbelt last-match-wins\n `deny file-read*` rules; bubblewrap masks each path) refuse only the\n key-material and other-principal rules, so an authenticated CLI can still\n read its own config. A refused read is always an explicit error, never an\n empty result, and the error names the path as the caller spelled it rather\n than a symlink target's real location.\n\n **This is defense-in-depth, not a security boundary.** It does not stop a\n hardlink to a denied file, a secret already copied into the workspace, an\n indirect read (`ssh-agent`, `security find-generic-password`, `aws sts …`),\n reads under `danger-full-access` shell commands, reads by MCP servers or\n other unwrapped child processes, or exfiltration of anything that *was*\n read. Keep least-privilege credentials and short-lived tokens doing the real\n work.\n- `permissions.toml` (sibling file, optional): typed permission rule records\n loaded next to `config.toml`, for example `~/.codewhale/permissions.toml`.\n This active user file is the only permission-rule source today; project\n config overlays do not load a project-local `permissions.toml`. A rule's\n optional `workspace` field is its repository scope, not a second source.\n Manually authored `[[rules]]` entries accept `tool`, optional `command` or\n `path`, optional absolute `workspace`, optional `command_exact = true`, and\n optional `action = \"deny\" | \"ask\" | \"allow\"`; omitted `action` defaults to\n `\"ask\"`. `workspace` limits a rule to that repository, while\n `command_exact = true` changes a command rule from the historical\n arity-aware prefix match to a complete-command match. `deny` blocks matching\n invocations before mode-based\n approval handling, `allow` skips approval for matching invocations, and\n `ask` forces approval only in modes that can prompt. Outside the TUI\n auto-approve path, a matching `ask` rule under `approval_policy = \"never\"`\n is rejected because no prompt can be shown. In Full Access / auto-approval sessions,\n `ask` rules do not downgrade the session into prompting or blocking; explicit\n `deny` rules still block according to the current execution-policy logic.\n\n In a supported approval card, press `S` to allow the request once and append\n exact `action = \"ask\"` rules to this file. For eligible safe requests, choose\n **Always allow this exact rule in this repo** (shortcut `P`) to append an\n `action = \"allow\"` rule with the current absolute `workspace` scope.\n Remembered shell grants set `command_exact = true`, so later commands with\n extra arguments do not inherit the grant. File and patch grants retain the\n exact workspace-relative paths produced by the existing validation path.\n Supported saves are intentionally narrow:\n `exec_shell` stores the exact approved command string; `write_file` and\n `edit_file` store the exact workspace-relative file path; `apply_patch`\n stores one exact workspace-relative `path` rule per validated touched file\n from apply-patch preflight. Existing exec command matching remains\n arity-aware for manually authored prefix rules; approval-card allow grants\n use complete-command matching. File paths are normalized to the same\n workspace-relative form used by runtime matching.\n\n `read_file` rules can still be authored manually when you want future reads\n of a specific path to ask, allow, or deny, but the approval UI does not save\n `read_file` rules. Commands classified as requiring approval or dangerous,\n critical approval cards, and repo-law prompts cannot save allow grants and\n continue to require review.\n\n `/permissions` (or `/permissions list`) is the narrow rule-management\n surface. It lists each numbered rule with the active user-file source, its\n exact effective matcher (tool-wide, command prefix, exact command, or exact\n normalized path), global or repository scope, and whether that scope\n applies in the current workspace. `/config ask-rules` remains a compatibility\n entry to the same list.\n\n Deletion is review-gated: `/permissions remove <number>` only previews the\n selected rule and prints a confirmation command. That command carries an\n opaque token bound to the exact file bytes and rule index; if another writer\n changes `permissions.toml`, confirmation fails instead of deleting a rule\n that moved into the old position. Confirmed removal and approval-card appends\n share the adjacent `permissions.toml.lock`, preserve unrelated TOML comments\n and formatting, and atomically replace the file. The running TUI reloads the\n user ruleset without clearing session-only approvals.\n\n This editor intentionally does not create or rewrite rules, persist deny\n choices from approval cards, expand globs, or create broad\n directory/recursive rules. Author those supported exact/prefix records\n manually when needed.\n- `[[hotbar]]` (array of tables, optional): user-owned 1-8 slot bindings for\n the TUI hotbar. Each entry has `slot`, `action`, and optional `label`.\n The Hotbar is hidden by default (#3807): omitting `hotbar` and setting\n `hotbar = []` both mean no hotbar. `/hotbar on` writes the eight default\n slots (`slash.workflow`, `slash.goal`, `slash.auto`, `mode.plan`,\n `mode.agent`, `mode.operate`, `palette.open`, `sidebar.toggle`) as explicit\n `[[hotbar]]` tables. When one or more `[[hotbar]]` tables are present, that\n list is the whole bar; missing slots stay empty. Invalid slots outside `1..=8` are skipped with a warning, duplicate\n slots use the later entry, and unknown action IDs are kept so the UI can show\n a disabled/unknown cell instead of silently deleting user config. Trusted\n user config, profiles, and managed config replace the whole list; project\n overlays cannot change hotbar bindings. Setup or wizard flows that persist\n hotbar bindings write this same schema to the resolved `~/.codewhale/config.toml`\n path, preserving legacy `~/.deepseek/config.toml` only when that fallback file\n is already the active config.\n\n ```toml\n [[hotbar]]\n slot = 1\n action = \"mode.plan\"\n label = \"Plan\"\n\n [[hotbar]]\n slot = 2\n action = \"session.compact\"\n ```\n- `[auto_review]` (table, optional): tool-call review policy — a deterministic floor plus a model guardian tier.\n This layer sits on top of the existing permission posture; it can hold or block a\n tool call, but it is not an auto-push, auto-merge, or hosted review service.\n Block rules are checked first, then the built-in safety floor, then allow\n rules. In Ask, a safety hold opens approval that no session grant or allow\n rule can answer; in Auto-Review or a non-interactive `never` posture it\n fails closed as a hard block. Full Access skips the built-in floor. The\n safety floor covers publish-like actions, destructive background/headless\n actions, and `curl`/`wget` uploads whose request body carries a credential\n file or a secret-named variable, or reads stdin fed from one of those or\n from `env`/`printenv`, even if an allow rule matches; a shell command,\n shell input or verifier over 64 KiB or 4,096 words whose text contains\n `curl` or `wget` anywhere, or one nested too deeply to read, is held too.\n Outside Full\n Access it also refuses, in every posture, a\n shell command, shell input or verifier containing a hidden Unicode character\n (bidirectional controls, zero-width and other invisible characters, tag\n characters, stray variation selectors, and terminal control characters\n except ASCII control keys in shell input), naming the code point and its\n position.\n\n ```toml\n [[auto_review.allow]]\n id = \"read-only-inspection\"\n action_kind = \"read\"\n reason = \"Read-only inspection is safe to run automatically.\"\n\n [[auto_review.block]]\n id = \"no-release-publish\"\n action_kind = \"publish\"\n reason = \"Release and publish actions require maintainer review.\"\n ```\n\n Rule matchers are exact `tool` and/or `action_kind`. At least one matcher is\n required. `action_kind` accepts the six decision-relevant kinds `read`,\n `write`, `shell`, `external`, `publish`, and `destructive`. Invalid names\n fail config validation instead of silently broadening into another policy\n class. In block rules, the old names remain conservative compatibility\n aliases: `network`, `git`, `mcp_action`, `browser`, and `unknown` map to\n `external`; `secret` maps to `destructive`; and `mcp_read` maps to `read`.\n Retired narrow kinds in allow rules fail validation rather than widening to\n a broader class. The retired `text_contains` matcher likewise fails\n validation instead of silently broadening an old intent-dependent rule.\n Fallback holds in interactive Auto-Review escalate to one stateless guardian\n request. The request contains the exact held call and deterministic\n observations as separate JSON fields. Conversation history, skill\n instructions, attached file contents, and other expanded model context are\n excluded. The guardian does not infer user intent or compute an authorization\n score. It exposes no tools and returns a risk level, allow/deny, and a\n rationale. High or critical risk cannot auto-run even if the model says\n allow. An oversized exact call is denied rather than truncated. Exactly one\n reviewer request is made; incomplete or malformed output, timeout,\n cancellation, provider failure, or an empty rationale all fail closed. The\n deterministic floor is never model-reviewed, and headless adapters use the\n deterministic-only tier. The pinned Codex, Kimi, and DeepSeek source\n boundaries are linked from [Permission Posture](MODES.md#permission-posture).\n Reviewer outcomes emit `tool.auto_review` audit events with\n `gate = \"guardian\"`.\n\n Auto-review decisions emit `tool.auto_review` audit events with\n `gate = \"deterministic\"` when tool\n audit logging is enabled. Future PreToolUse/PostToolUse hooks can add\n observer input around this layer, but the configured auto-review policy is\n evaluated before a tool call is allowed to proceed.\n- `managed_config_path` (string, optional): managed config file loaded after user/env config.\n- `requirements_path` (string, optional): requirements file used to enforce allowed approval/sandbox values.\n- `max_subagents` (int, optional): defaults to `64` and is clamped to `1..=128`.\n- `subagents.*` (optional compatibility table): manual per-role model pins\n for direct and Workflow `agent` starts. An explicit saved profile wins,\n then a manual role pin, then a unique saved role pin. Conflicting tool\n `model` or `model_strength` choices are refused before admission. Unpinned\n roles allow task model/strength choices before inherited defaults.\n `[subagents.roles.<role>] model = \"provider/model\"` folds into the existing override\n map and wins over `[subagents.models]`, then the convenience keys. Structured\n canonical role keys win over legacy aliases. Only this structured syntax\n separates the explicit provider from the model suffix; unknown providers\n fail before admission. Bare structured model ids inherit the active provider.\n Legacy scalar/map values preserve namespaced provider-owned ids unchanged.\n Supported convenience keys are\n `default_model`, `worker_model`, `scout_model`, `planner_model`,\n `reviewer_model`, `custom_model`, `max_concurrent`, `max_admitted`,\n `launch_concurrency`, `token_budget`, `api_timeout_secs`, and\n `heartbeat_timeout_secs`. The v0.9.x keys `explorer_model`, `awaiter_model`,\n and `review_model` remain accepted as aliases. The `[subagents]\n max_concurrent` value overrides\n top-level `max_subagents` and is also clamped to `1..=128`. `[subagents]\n max_admitted` (aliases: `max_total`, `admission_limit`) is the bounded total\n of queued plus running sub-agents; it defaults to `1024`\n (`MAX_SUBAGENT_ADMISSION`, `crates/tui/src/config/subagent_limits.rs:21`,\n applied at `config.rs:6400`) so high-fanout turns can queue and drain while\n runtime launch pressure remains bounded, and is clamped to\n `max_concurrent..=1024`. `[subagents]\n launch_concurrency` sets how many direct children start at once before the\n rest queue for a launch slot; it defaults to the resolved `max_subagents` cap\n and is clamped to `1..=max_subagents` (the deprecated\n `interactive_max_launch` key is accepted as an alias, with the new key\n winning when both are set). `[subagents] token_budget` is an optional\n aggregate token ceiling for each root `agent` run and its descendants; unset\n or `0` preserves unlimited legacy behavior. `[subagents] api_timeout_secs`\n controls the per-step API timeout for sub-agent model calls and is clamped to\n `1..=3600`, with `0` or unset preserving the 600 second default. A timeout\n after possible dispatch interrupts with a preserved checkpoint and unknown\n outcome. Only a transient connection timeout proven to precede dispatch uses\n exponential backoff, up to 5 retries.\n `[subagents] heartbeat_timeout_secs` controls stale running agent cleanup,\n defaults to `300`, and is clamped to `30..=3600` while staying above the\n resolved API timeout. `[subagents.providers.<provider>]` accepts the same\n fanout, depth, budget, and timeout knobs (`enabled`, `max_concurrent`,\n `max_admitted`, `launch_concurrency`, `max_depth`, `token_budget`,\n `api_timeout_secs`, `heartbeat_timeout_secs`) and inherits the global\n `[subagents]` value for any key you omit. Provider keys accept canonical\n names such as `deepseek`, `zai`, `openrouter`, `anthropic`, plus convenience\n aliases such as `glm` for Z.ai and `deepseek_api` for direct DeepSeek:\n\n ```toml\n [subagents]\n max_concurrent = 20\n launch_concurrency = 20\n max_admitted = 200\n max_depth = 6\n\n [subagents.providers.deepseek]\n max_concurrent = 20\n launch_concurrency = 20\n max_admitted = 200\n\n [subagents.providers.glm]\n max_concurrent = 4\n launch_concurrency = 3\n max_admitted = 12\n max_depth = 2\n\n [subagents.providers.openrouter]\n max_concurrent = 5\n launch_concurrency = 3\n max_admitted = 20\n ```\n\n `/config subagents status` prints both global values and the active\n provider's resolved profile so rate-limit tuning is visible in the TUI.\n `[subagents.models]` accepts lower-case Fleet role keys such as `worker`,\n `scout`, `planner`, `reviewer`, `builder`, and `verifier`; legacy type keys\n remain accepted during v0.9.x. Values are validated\n against the active provider at spawn time; direct DeepSeek requires DeepSeek\n IDs, while OpenAI-compatible/custom provider routes pass explicit model IDs\n through to that provider. To route a child to a different provider than the\n parent session, save a Fleet/AgentProfile with explicit `provider` and\n `model` fields (including user-named custom providers such as `lm-studio`)\n and call `agent(profile: \"...\")`; see [SUBAGENTS.md](SUBAGENTS.md).\n- `skills_dir` (string, optional): defaults to `~/.codewhale/skills` (each skill is\n a directory containing `SKILL.md`). After workspace trust, project skills\n take precedence, with `.codewhale/skills` ahead of compatible roots such as\n `.agents/skills`. Flat `./skills` requires explicit opt-in below. The runtime\n also discovers global agentskills.io-compatible `~/.agents/skills` and the\n broader Claude-ecosystem\n `~/.claude/skills`. With the default bundle policy and no `skills_dir` override, first launch installs versioned bundled skills for common\n workflows including skill creation, delegation, MCP/plugin scaffolding,\n documents, presentations, spreadsheets and PDFs. Feishu/Lark is outside the\n default pack. Only\n Codewhale-owned roots (`<workspace>/.codewhale/skills` and\n `~/.codewhale/skills`) are writable install/import targets; compatible harness\n roots stay read-only. Bare `/skills` opens Extensions → Skills (owned-only,\n zero network); `/skills manage` opens the manager. See [SKILLS.md](SKILLS.md)\n for the manager, audit statuses, provenance markers, and mutation rules, and\n [CLAUDE_PLUGIN_COMPAT.md](CLAUDE_PLUGIN_COMPAT.md) for the supported boundary\n between portable `SKILL.md` bundles and Claude Code plugin runtimes.\n- `[skills.bundled].install` (`\"auto\"` default, `\"all\"`, `\"none\"`): controls bundle writes independently of discovery. `auto` keeps deletion-as-opt-out and disables installation when `skills_dir` is set. `all` repairs enabled missing skills into `$CODEWHALE_HOME/skills` (normally `~/.codewhale/skills`) even with a discovery override. `none` writes nothing and leaves the marker untouched. Modified and managed bodies are preserved in every mode.\n- `[skills.bundled].selected` / `disabled` / `explicit_only` (arrays of names, default `[]`): selection limits installation and discovery; empty selects the whole bundle. Disabled wins and reveals lower-root copies. Explicit-only retains user invocation while removing the shipped copy from the model index and model loading. Aliases share canonical policy. These settings affect recognized global bundle copies only and never delete files. Profile fields merge separately; `[]` clears an inherited list. `doctor` warns about unknown/ignored entries and `/skills inspect` reports per-path reasons. See [bundled skills](SKILLS.md#bundled-skills).\n- `[skills].listing_max_description_chars` (integer, default `120`, range `1..=40000`): ambient-index description cap, shared by parent and child prompts and `/skills inspect`.\n- `[skills].listing_budget_fraction` (finite number, default `0.002`, greater than `0` and at most `1`): share of the route window for that same index, estimated at four characters per token and clamped to `2400..=40000` characters. Explicit profile fields override inherited values independently; `/context` uses the same resolution. Full skill loading is unaffected.\n- `[skills].scan_codewhale_only` (bool, default `false`): when `true`, session\n skill discovery ignores cross-tool roots such as `.claude/skills`,\n `.opencode/skills`, `.cursor/skills`, and `~/.agents/skills`. Codewhale still\n scans `<workspace>/.codewhale/skills`, `~/.codewhale/skills`, and any explicit\n `skills_dir` override. The Skills Manager can still toggle a local compatible\n audit scan independently of this runtime knob — see [SKILLS.md](SKILLS.md).\n- `[skills].flat_workspace_root` (bool, default `false`): opt in to the flat\n `<workspace>/skills` compatibility root after workspace trust. Without this\n opt-in it is an audit candidate only; an explicit `skills_dir` remains an\n alternative. `scan_codewhale_only = true` excludes the flat compatibility\n root regardless of this flag, unless it is the explicit `skills_dir`.\n- `[skills].registry_url` / `[skills].max_install_size_bytes` (optional): used by\n `/skills --remote`, `/skills suggest <task>`, `/skills sync`, and `/skill\n install|update`. The default manager open path does not contact the registry.\n- `[verifier].enabled` (bool, default `false`): enables automatic\n claim-of-done verifier preview once that runtime trigger is active. The\n manual `run_verifiers` tool is still available when this is false.\n- `mcp_config_path` (string, optional): defaults to `~/.codewhale/mcp.json`, with\n legacy `~/.deepseek/mcp.json` fallback when the Codewhale path is absent.\n Custom paths must be absolute; a relative value falls back to the user-global\n path so changing the launch directory cannot silently change the MCP pool.\n It is visible in `/config` and can be changed from the TUI. The new path is\n used immediately by `/mcp`; run `/mcp reload` to rebuild the model-visible\n MCP tool pool without restarting the TUI. A headless runtime still needs a\n restart because its `ConfigReload` request does not refresh MCP connections.\n- `notes_path` (string, optional): defaults to `~/.codewhale/notes.txt`, with\n legacy `~/.deepseek/notes.txt` fallback when the Codewhale path is absent, and\n is used by the model-visible `note` tool.\n- `[memory].enabled` (bool, optional): defaults to `false`. When `true`,\n the TUI loads the user memory file into a `<user_memory>` prompt block,\n enables `# foo` quick-capture in the composer, surfaces the `/memory`\n slash command, and registers the `remember` tool. The same toggle is\n available via `DEEPSEEK_MEMORY=on`.\n- `memory_path` (string, optional): anchors the native memory store. The\n configured filename is **not** the file that is written. Under the Native\n backend (the only backend) the store is re-rooted to\n `<parent-of-memory_path>/memory/global/MEMORY.md` — so the default\n `~/.codewhale/memory.md` yields `~/.codewhale/memory/global/MEMORY.md`\n (plus workspace-scoped files and a rebuildable SQLite FTS5 index). See\n [`MEMORY.md`](MEMORY.md) for the full feature surface (`# foo` composer\n prefix, `/memory` slash command, `remember` tool, opt-in toggle).\n- `snapshots.*` (optional): side-git workspace snapshots for file rollback:\n - `[snapshots].enabled` (bool, default `true`)\n - `[snapshots].max_age_days` (int, default `7`)\n - snapshots live under\n `~/.codewhale/snapshots/<project_hash>/<worktree_hash>/.git`, with legacy\n `~/.deepseek/snapshots/...` fallback when only the legacy state exists, and\n never use the workspace's own `.git` directory\n- `context.*` (optional):\n - `[context].project_pack` (bool, default `false`): include a deterministic\n project context pack (a large pretty-printed directory listing) in the\n stable prompt prefix (#4781). Useful for weak tool-calling models; the\n model can rebuild the same information with one `File` call.\n - The removed seam-manager keys (`enabled`, `verbatim_window_turns`,\n `l1_threshold`, `l2_threshold`, `l3_threshold`, `seam_model`) are\n ignored if an older config still carries them; they no longer load.\n- `compaction.*` (optional, config.toml): how a compaction pass behaves once\n it fires. `auto_compact` / `auto_compact_threshold_percent` (settings.toml)\n still decide *when* it fires. Both keys are absent by default, and absent\n means the built-in behaviour, unchanged:\n - `[compaction].summary_instructions` (string, default empty): standing\n operator instructions appended to the summarizer prompt as a clearly\n delimited \"Additional instructions from the operator\" section on **every**\n pass, manual and automatic — the effort-free counterpart to a one-off\n `/compact <focus>`, which still composes after this text. Useful for\n \"always list exact file paths and line numbers\", \"always restate open\n decisions and their trade-offs\", \"always write a TL;DR first\". Truncated\n at 4 000 characters with a warning naming the key; whitespace-only reads\n as unset. The summarizer still runs with no system prompt and no tools —\n this suffix is the only operator-authored input it sees.\n - `[compaction].retained_user_message_tokens` (int, default `20000`, clamped\n to `2000`–`200000`; also accepted as `retained_user_message_max_tokens`):\n token budget for the recent plain user messages kept **verbatim** in the\n replacement history. Raising it keeps more of the user's own earlier\n messages instead of only whatever the lossy summary captured; the\n last-round survival contract still applies on top. The `/compact` receipt\n names the effective budget and whether operator instructions were applied,\n so you can tell the knob took effect.\n- `retry.*` (optional): HTTP retry settings. The following backoff fields govern\n ordinary transport retries; HTTP 429 uses the independent policy below:\n - `[retry].enabled` (bool, default `true`)\n - `[retry].max_retries` (int, default `3`): ordinary transport retries\n - `[retry].initial_delay` (float seconds, default `1.0`)\n - `[retry].max_delay` (float seconds, default `60.0`)\n - `[retry].exponential_base` (float, default `2.0`)\n - `[retry].jitter` (bool, default `true`): randomize ordinary transport backoff\n - `[retry].jitter_factor` (float, default `0.1` = ±10%; clamps to `0.0..=1.0`)\n - `[retry].respect_retry_after` (bool, default `true`): wait for a server\n `Retry-After` preference retained for legacy retry configuration; it does not\n disable HTTP 429 server minima\n\n HTTP 429 recovery has its own validated `[retry.rate_limit]` fields:\n\n - `max_attempts` (integer, default `20`, range `1..=1000`): total attempts,\n including the first\n - `recovery_budget_secs` (integer seconds, default `600`, range `1..=86400`)\n - `initial_delay_secs` (integer seconds, default `5`)\n - `max_delay_secs` (integer seconds, default `60`), with\n `1 <= initial_delay_secs <= max_delay_secs <= 86400`\n\n This policy always honors server minima and uses equal jitter, independently\n of the legacy `initial_delay`, `max_delay`, `exponential_base`, `jitter`,\n `jitter_factor` and `respect_retry_after` fields. `max_retries = 0` does not\n disable 429 recovery; set `retry.rate_limit.max_attempts = 1` to disable only\n automatic 429 retries, or `retry.enabled = false` to disable this HTTP retry\n loop. A safe pre-dispatch HTTP/1.1 fallback inside a stream-opening operation\n remains possible. The one-hour limit on actual provider pauses and automatic sleeps\n still applies. See [HTTP rate-limit retries](rate-limit-retries.md) for the\n budget, server-minimum and active-time boundaries.\n\n `[retry]` schedules HTTP-request retries inside the client. Each physical\n streaming request gets its own `stream.open_timeout_secs` response-header\n wait. Provider backoff and honored `Retry-After` waits happen between\n attempts, outside that timer, so the configured retry count remains\n reachable. Inference retries require an ordinary rate limit or a proven\n transient connection failure before dispatch. Only that connection failure\n can switch once to HTTP/1.1; a header stall cannot. Both inference clients\n disable redirects and reqwest protocol retries. Explicit plan-window\n exhaustion is a terminal quota error. Server failures, uncertain timeouts\n and interrupted streams preserve unknown coverage and stop without\n automatically reissuing inference. Read-only catalog GETs retain bounded\n retries at their actual method owner. The `[stream]` keys below bound safe\n pre-dispatch recovery and stream consumption; legacy `tui.stream_max_*`\n keys remain configuration fallbacks.\n- `[notifications]`: notification delivery, attention, categories and audio share one\n policy. `quiet = true`, `method = \"off\"`, `condition = \"never\"` and disabled\n categories suppress both the banner and Codewhale's selected sound.\n- `notifications.method`: `auto` (default), `osc9`, `kitty`, `ghostty`, `bel`, `off`.\n- `notifications.condition`: `unfocused` (default), `always`, `never`. When absent,\n the legacy `tui.notification_condition` remains the fallback. `always` also\n bypasses the duration threshold; `unfocused` requires two seconds away.\n- `notifications.threshold_secs`: nonnegative integer, default `30`.\n- `notifications.include_summary`: boolean, default `false`.\n- `notifications.sound`: optional `off`, `whale`, `bell`, `beep`, `file`.\n A selected value controls audio across enabled categories. Absent keeps legacy\n `completion_sound` and `event_sound` choices; `off` overrides both.\n- `notifications.sound_file`: custom local WAV path for `sound = \"file\"` or legacy\n `completion_sound = \"file\"`.\n- `notifications.subagent_completion`: `always`, `final-only` (default), `off`.\n Covers all background work that finishes: sub-agents, background shells and\n durable tasks. `final-only` sends one notice naming everything that finished\n once no agent, workflow or durable task is still running; a running shell\n (a dev server, a watcher) never holds it back. `always` sends one per item.\n- `notifications.quiet`: boolean, default `false`.\n- `notifications.events`: six boolean categories, all enabled by default; see below.\n- `notifications.completion_sound`: legacy completion cue, default `off`, with the\n same values as `sound`. Used only when `sound` is absent.\n- `notifications.event_sound`: legacy `enabled` (default `false`), `events`\n (default `[\"turn-complete\", \"approval-needed\"]`), and `quiet` (default `false`).\n `min_interval_ms` (default `2000`) applies to each category's audio in both modes.\n- `tui.alternate_screen` (string, optional, default `auto`): which screen an interactive session starts on. `auto` and `always` start on the TUI-owned alternate screen; `never` starts in inline mode — a ratatui viewport the full height of the terminal with no alternate screen, so the shell's scrollback survives the session and stays scrollable after exit. `/fullscreen` and `/inline` switch it in-process; a switch that the terminal refuses rolls back and says why. Inline mode paints the whole transcript inside its viewport — nothing is written into the host scrollback while the session runs.\n- `tui.mouse_capture` (bool, optional, default `true` on non-Windows terminals and on Windows Terminal/ConEmu/Cmder when the alternate screen is active; `false` on legacy Windows console and inside JetBrains JediTerm — PyCharm/IDEA/CLion/etc. — where mouse-event escapes leak into the input stream as garbled text, see #878 / #898): enable internal mouse scrolling, transcript selection, right-click context actions, and transcript scrollbar dragging. TUI-owned drag selection copies the intersected cells, removes visual wrap-column line breaks from paragraphs, and keeps selection scoped to the transcript pane; the payload is Markdown source by default, see `tui.selection_copy_markdown` below. Set this to `false` or run with `--no-mouse-capture` for raw terminal selection; set it to `true` or run with `--mouse-capture` to opt in anywhere it's defaulted off. On raw terminal selection, especially on legacy Windows console or when mouse capture is disabled, selection may cross the right workbar and include visual wraps because the terminal, not the TUI, owns the selection.\n On Linux, finishing a transcript or composer selection quietly copies text to\n PRIMARY, leaving the regular clipboard unchanged. Middle-click inside the\n composer pastes PRIMARY at the pointer without submitting it. This uses native\n X11 or Wayland data control; compositors must support PRIMARY selection. Over\n SSH without a forwarded graphical display, use your terminal's selection/paste\n gestures or `--no-mouse-capture`. Explicit Copy still uses the regular clipboard.\n\n- `tui.selection_copy_markdown` (bool, optional, default `true`): copy TUI-owned\n transcript selections (drag release, context-menu Copy, and `Cmd+C`/`Ctrl+C`\n on an active selection) as Markdown source instead of rendered text. Every\n intersected cell serializes through the same canonical projection `Ctrl-Y`\n and `/copy` use — user and assistant cells keep their authored Markdown,\n other cells keep their full transcript form — partial intersections round out\n to whole cells, cells join with blank lines, and a toast names the copied\n cell count. Set `false` to copy the rendered text as displayed. Composer\n selections and the Linux PRIMARY auto-copy are unchanged; PRIMARY always\n carries rendered text.\n\n- `tui.stream_chunk_timeout_secs` (int, optional, default `900`): per-SSE-chunk idle timeout for streamed model responses. Slow local or compatible servers can raise this with `/config stream_chunk_timeout_secs <seconds>` (add `--save` to write canonical `stream.chunk_timeout_secs`); `0` maps to the default and explicit values must be `1..=3600`. The legacy `DEEPSEEK_STREAM_IDLE_TIMEOUT_SECS` env var is still honored when this key is omitted.\n- `tui.osc8_links` (bool, optional, default on for macOS/Linux, off for Windows): emit OSC 8 escape sequences around URLs in transcript output so supporting terminals (iTerm2, Terminal.app 13+, Ghostty, Kitty, WezTerm, Alacritty, recent gnome-terminal/konsole) can open them with the terminal's link gesture—usually Cmd-click on macOS and Ctrl-click on Linux/Windows. Terminals without OSC 8 support render the plain label and ignore the escape. The escapes are emitted out-of-band (not inside buffer cells), so column corruption is not a concern; set `false` only for terminals that misrender the OSC 8 terminator itself. Windows legacy consoles default off; opt in with `true`.\n- `tui.max_model_steps` (int, optional, default uncapped): optional model-step ceiling for one ordinary turn. Omission or `0` leaves model steps uncapped; explicit positive values are preserved (`4294967295` is the compatibility value for no limit). Headless `exec` and Fleet workers also have no implicit model-step ceiling; `exec --max-turns N` and positive worker budgets still apply. At exhaustion the turn ends `Failed` with `Maximum model steps reached before completion (limit: N)` after one bounded final-report response when needed. Cumulative wall-clock and per-stream limits remain independent. Active interactive goal turns use `goal.max_steps` instead (also uncapped by default); see the Goal loop section below.\n- `tui.turn_wall_clock_secs` (int, optional, default: no limit): cumulative per-turn wall-clock budget in seconds, measured across every model step of one turn (not per request). Time blocked on a human approval is excluded. Omitted or `0` means no limit; positive values are preserved without an added runtime ceiling. When exhausted the turn stops before authorizing another billable request with a message naming the limit and the key to raise.\n- `tui.stream_max_resumes` (int, optional, default `3`): turn-level recovery after a proven transient connection failure before request dispatch. `0` disables this additional recovery; values clamp to `0..=10`. Missing response headers, empty or partial streams, sleep and network drops never authorize inference replay.\n- `tui.stream_max_transparent_retries` (int, optional, default `2`): retained for configuration compatibility, clamped to `0..=10`. Interrupted streams no longer trigger transparent inference reissues, including before visible content.\n- `tui.stream_max_errors` (int, optional, default `5`): retained for configuration compatibility; `0` selects the default and positive values clamp to `1..=50`. Core ends an uncertain stream on its first read error or error frame regardless of this value.\n- `tui.stream_open_timeout_secs` (int, optional, default `45`): wait for a streaming request's response headers (connection setup included). A header stall ends with an unknown outcome and does not replay over HTTP/1.1. Omitted or `0` falls back to `CODEWHALE_STREAM_OPEN_TIMEOUT_SECS`, then the default; values clamp to `5..=300`.\n- `tui.connect_timeout_secs` (int, optional, default `30`): TCP/TLS connect timeout for the model HTTP client. Omitted or `0` uses the default; values clamp to `1..=300`.\n- `tui.force_http1` (bool, optional, default `false`): pin the model HTTP client to HTTP/1.1, for provider edges or proxies that mishandle long-lived HTTP/2 streams. `CODEWHALE_FORCE_HTTP1=1` does the same; either one pins.\n- `tui.stream_max_content_mb` (int, optional, default `10`) and `tui.stream_max_duration_secs` (int, optional, default `1800`): per-step caps on one stream's accumulated content and wall-clock duration. `0` selects the default; values clamp to `1..=512` MB and `10..=86400` seconds.\n- `transcript.prose_measure` (positive integer, optional, default absent = full width): wrap cap, in columns, for prose cells — user messages, assistant answers, and reasoning/thinking blocks — in the live transcript (#5436). Absent (or `0`) spends the full content width, consistent with tool/status cells and the #5322 wide-frame decision; the former 105-column prose rail is gone. Set a positive whole number (e.g. `prose_measure = 120` under `[transcript]`) to restore a bounded reading measure on ultrawide terminals. Narrow terminals always keep their content width — the cap clamps from above only. Tool, diff, and status cells never inherit this cap. Invalid values (negative or non-integer) are rejected at startup with a `transcript.prose_measure` config error. Resolved once per render pass, so the main transcript cache and the full-screen overlay always agree on the effective width.\n- `hooks` (optional): lifecycle hooks configuration (see `config.example.toml`).\n- `features.*` (optional): feature flag overrides (see below).\n\n### Stream and transport settings\n\n`[stream]` is the canonical table for model-stream policy and its HTTP clients.\n`codewhale config dump` and `codewhale config get stream` show effective values\nfrom the runtime resolver, including environment fallbacks and clamps; this does\nnot persist defaults. Set or unset individual fields with, for example,\n`codewhale config set stream.open_timeout_secs 120` and\n`codewhale config unset stream.open_timeout_secs`. Per-run\n`--set stream.open_timeout_secs=120` uses the same validation.\n\n| `[stream]` key | Default | Accepted behavior | Legacy `[tui]` fallback |\n| --- | --- | --- | --- |\n| `open_timeout_secs` | 45 | Positive values clamp to 5–300; 0 or omission falls through to the environment/default | `stream_open_timeout_secs` |\n| `chunk_timeout_secs` | 900 | 0 selects default; positive values clamp to 1–3600 | `stream_chunk_timeout_secs` |\n| `max_resumes` | 3 | Proven transient pre-dispatch recovery only; 0 disables, maximum 10 | `stream_max_resumes` |\n| `max_transparent_retries` | 2 | Compatibility value; interrupted streams never reissue inference | `stream_max_transparent_retries` |\n| `max_stream_errors` | 5 | Compatibility value; Core ends on the first uncertain stream error | `stream_max_errors` |\n| `max_duration_secs` | 1800 | Per-stream wall clock; 0 selects default, positive values clamp to 10–86400 | `stream_max_duration_secs` |\n| `max_content_mb` | 10 | Per-stream content; 0 selects default, positive values clamp to 1–512 MiB | `stream_max_content_mb` |\n| `connect_timeout_secs` | 30 | TCP/TLS setup; 0 selects default, positive values clamp to 1–300 | `connect_timeout_secs` |\n| `force_http1` | false | Boolean; a truthy environment pin always enables HTTP/1.1 | `force_http1` |\n| `tcp_keepalive_secs` | 30 | Idle time before TCP keepalive probes; 0 disables, positive values clamp to 1–3600 | none |\n| `http2_keep_alive_interval_secs` | 15 | PING interval on active HTTP/2 connections; 0 disables, positive values clamp to 1–3600 | none |\n| `http2_keep_alive_timeout_secs` | 20 | PING acknowledgement deadline; 0 selects default, positive values clamp to 1–3600 | none |\n\nFor each field, an explicit canonical value wins over the legacy `[tui]` field,\nincluding zero or false; an absent canonical field preserves the legacy value.\nThe existing environment precedence is unchanged: a positive configured header\nwait wins over `CODEWHALE_STREAM_OPEN_TIMEOUT_SECS` (then its `DEEPSEEK_` alias);\nzero falls through to those variables. An omitted chunk timeout uses\n`CODEWHALE_STREAM_IDLE_TIMEOUT_SECS` (then its `DEEPSEEK_` alias), whereas an\nexplicit zero uses 900. `CODEWHALE_FORCE_HTTP1` (legacy `DEEPSEEK_FORCE_HTTP1`)\nis ORed with the selected config flag, so a false config flag cannot defeat a\ntruthy environment pin. Profile overlays merge canonical fields independently.\n\nHTTP transport values apply to all newly constructed model, catalog and HTTP/1\nfallback clients. They do not rebuild an active client, govern MCP/other network\nservices, or send HTTP/2 PINGs on idle pooled connections. HTTP/2 knobs have no\neffect when HTTP/1.1 is pinned. The operating system controls TCP probe details;\nthese settings do not disable certificate validation. `[retry]` still owns the\nHTTP-request backoff schedule independently of stream-level budgets.\n\n### Workspace notes\n\n`/note` manages a simple notes file in the current workspace at\n`.codewhale/notes.md` (legacy `.deepseek/notes.md` is the fallback path when\nno `.codewhale/notes.md` exists yet).\nExisting `/note <text>` usage still appends a note.\nThe management forms are:\n\n| Command | Action |\n|---|---|\n| `/note <text>` | Append a note (legacy shorthand) |\n| `/note add <text>` | Append a note explicitly |\n| `/note list` | List notes with temporary 1-based numbers |\n| `/note show <n>` | Show the full note at number `n` |\n| `/note edit <n> <text>` | Replace note `n` with new text |\n| `/note remove <n>` | Delete note `n`; `rm` and `delete` are aliases |\n| `/note clear` | Empty the workspace notes file |\n| `/note path` | Show the resolved workspace notes path |\n\nThe numbers shown by `/note list` are not stored in the file; they are derived\nfrom the current order each time notes are read. This keeps the file format\ncompatible with the existing `---`-separated notes.\n\n### User memory\n\nUser memory is split across one top-level path setting and one opt-in\ntoggle table:\n\n```toml\n# Anchors the store only — actual writes go to\n# ~/.codewhale/memory/global/MEMORY.md (see MEMORY.md).\nmemory_path = \"~/.codewhale/memory.md\"\n\n[memory]\nenabled = true\n```\n\nNotes:\n\n- `memory_path` stays at the top level beside `notes_path` and\n `skills_dir`; it is not nested under `[memory]`.\n- The configured path is an **anchor**: its parent directory gains\n `memory/global/MEMORY.md`, workspace-scoped files, and `index.db`.\n Pointing `memory_path` at the native layout path itself would double-nest\n (`…/memory/global/memory/global/MEMORY.md`); keep the legacy-style\n anchor filename.\n- `DEEPSEEK_MEMORY_PATH` overrides the anchor path from the environment.\n- `DEEPSEEK_MEMORY=on` (also `1`, `true`, `yes`, `y`, or `enabled`)\n flips the feature on without editing `config.toml`.\n- The feature is inert when disabled: no file is injected, `# foo`\n falls through to normal message submission, and the model does not\n see the `remember` tool.\n- See [`MEMORY.md`](MEMORY.md) for examples and the full `/memory`\n command surface.\n\n### Goal loop (`[goal]`)\n\nOperate-mode goals run to their completion gate with no default token, time, or\ncontinuation ceiling (#5052). Token/time budgets, when explicitly supplied,\nare telemetry only and do not stop a goal. Users who want a circuit breaker can\nopt into one:\n\n```toml\n[goal]\n# Optional safety backstop on automatic goal continuation passes.\n# Default: 0 (unlimited). Set a positive value to opt into a ceiling.\nmax_continuations = 100\n\n# Optional cancellable quiet period between successful turns. This is useful\n# for coordinator goals that should poll on a cadence instead of keeping one\n# provider turn open. Default: 0 (continue immediately).\ncontinuation_delay_seconds = 300\n\n# Optional per-turn model-step ceiling while a goal is active (#6512).\n# Default: uncapped (0 or absent). Positive values are preserved.\n# This bounds each provider turn, never the number of continuation passes.\nmax_steps = 0\n\n# Optional independent judges (#6013). Default: false (the model's own\n# update_goal call is final). When true, one adversarial critique of the claim\n# runs before `update_goal` completes (complete_judge) or blocks (blocked_judge)\n# the goal. Each judged call is billed as auxiliary usage.\ncomplete_judge = false\nblocked_judge = false\n```\n\nThe effective delay is capped at 86,400 seconds (24 hours); use an automation\nfor schedules that are less frequent than once per day.\n\nWhen an explicit backstop fires, the goal pauses with a status message naming\n`[goal] max_continuations` and a warning is logged; resume the goal after\ninspecting progress, or raise/disable the backstop.\n\n`[goal] max_steps` governs one engine turn at a time. Like ordinary interactive\nturns, omitted or `0` leaves model steps uncapped. Explicit per-invocation\nceilings — `exec --max-turns N`, child-worker caps — always win over it. At exhaustion\nit gets one bounded final report and the turn classifies as budget-exhausted. An\nunfinished goal then pauses with the BudgetLimit reason instead of re-arming\nanother goal turn — resume it explicitly after reviewing the report. Wall-clock\nand stream protections are separate and still apply.\n\nWith `[goal] complete_judge = true`, a refuted completion claim keeps the goal\nactive and records the judge's findings as critical gaps. A missing,\nunparseable, uncertain or failed verdict never completes the goal; it records\none fixed gap, so a persistent judge outage ends in the existing no-progress\npause rather than a silent pass. With `[goal] blocked_judge = true`, a refuted\nblocker keeps the goal active, and an unverifiable one hands the goal back as a\nyield that your next message resumes. The judge reads only the objective, the\nevidence and the verification receipt, not the transcript or the diff. The\noperator `POST /v1/threads/{id}/goal/complete|block` routes and `/goal`\ncommands stay ungated.\n\nThe delay starts only after a successful turn while an explicitly created goal\nis still active. `/goal pause`, `/goal done`, `/goal blocked`, `/goal clear`,\nEsc, or Ctrl+C cancels a pending continuation before another provider request\nstarts. Failed turns and policy/route failures never schedule another turn.\nOnly the numeric cadence is stored in config; no prompt, credential, or secret\nis persisted for the loop.\n\n### Reasoning-only recovery (`[reasoning_only]`)\n\nWhen a model finishes with only reasoning and no answer or tool call, Codewhale\ncan request an answer again. A clean stop permits two retries by default:\nthe first keeps the request unchanged, and later retries carry a request-scoped\nnudge. The default nudge is `Continue: give your answer, or make the next tool call.`\n\n```toml\n[reasoning_only]\nmax_reprompts = 2\non_output_limit = false\n```\n\nSet `on_output_limit = true` to also retry `length`/`max_tokens` stops that\nproduced no answer or tool call, including providers that did not stream their\nhidden reasoning. This option is off by default. Output-limit retries carry the\nnudge immediately and share the same `max_reprompts` allowance. Set\n`max_reprompts = 0` to disable both reasoning-only and output-limit recovery;\ntransport and empty-stop recovery have their own existing limits.\n\nThis applies to ordinary interactive, exec and Runtime API engines and Fleet\nworkers. Workers use the captured parent configuration. RLM keeps its separate\ncompletion/FINAL/REPL stop protocol and does not use this option.\n\nEvery retry is a fresh request and may repeat the model's reasoning. It does not\ncarry hidden reasoning forward, raise output limits or bypass route, context,\nturn, cancellation or tool-admission controls. Each request can incur provider\ncost. Existing output-limit overrides still apply, and request preparation may\nreduce or refuse an allowance when input grows. Restart existing engines after\nchanging these settings.\n\nThe nudge appears in one outbound request without joining conversation history\nor later resumed model context. Retry status retains its stable protocol prefix\nand localizes its human text. Its receipt is visible in the transcript; Fleet\nalso stores an owner-bound prepared-request receipt in its existing private\nartifact before dispatch. A prepared receipt does not claim a completed model\ncall. Existing version-1 artifacts remain readable.\n\nTo customize or disable the nudge:\n\n```toml\n[reasoning_only]\nreprompt_message = \"Give your answer, or make the next tool call.\"\n```\n\nAn empty `reprompt_message = \"\"` disables the nudge, including on output-limit\nretries; the retry still counts against `max_reprompts` and uses the unchanged\nrequest. Configuration with recovery enabled does not guarantee the model will\ncomplete a long proof within its allowance.\n\n### Notifications\n\nNotification controls are available in the existing `/config` settings view,\nfrom terminal commands, and through the CLI. All write the same `config.toml`\nkeys. Terminal changes apply immediately; add `--save` to keep them. CLI writes\napply when the next process loads its configuration.\n\n```sh\ncodewhale config set notifications.sound whale\ncodewhale config set notifications.events.approval-needed false\ncodewhale config set notifications.quiet true\ncodewhale config get notifications\ncodewhale config unset notifications.quiet\n```\n\n```text\n/config notifications sound whale --save\n/config notifications condition unfocused --save\n/config notifications quiet true\n/config notifications status\n```\n\nNested CLI edits preserve TOML types, unrelated keys and comments. Unset removes\nonly the selected leaf. `notifications.sound legacy` in the TUI, or CLI unset of\n`notifications.sound`, restores previous sound choices. Invalid values are\nrejected before file or session changes. In an active TUI profile, saved edits\nupdate its notification table when it owns one; otherwise they update the\ninherited root table. The settings detail keeps saved and current values distinct.\n\n```toml\n[notifications]\nmethod = \"auto\" # auto | osc9 | kitty | ghostty | bel | off\ncondition = \"unfocused\" # unfocused | always | never\nthreshold_secs = 30\ninclude_summary = false\nsound = \"whale\" # optional; sound is opt-in, not enabled by default\nquiet = false\n\n[notifications.events]\nturn-complete = true\nsubagent-terminal = true\napproval-needed = true\ninput-needed = true\nelevation-needed = true\nmodel-notify = true\n```\n\n`quiet = true` mutes every category without changing saved choices. `method =\n\"off\"` also stops both banner and selected audio. Disabling a category stops its\nsound. Attention and duration gates apply before any sink runs. Title animation\ncompletion is silent; only the authorized notification event can request audio.\nSuccessful turn completion is a notification category; failed/cancelled turns do\nnot create a success notification.\n\n`auto` chooses a recognized terminal protocol or the existing macOS native\nfallback; unknown terminals remain unsupported and never invent a bell. `bel`\nis an audio-only transport: one selected cue is dispatched, without a second\ntransport bell. With `sound = \"off\"`, that transport is silent. `osc9`, `kitty`\nand `ghostty` use their terminal notification protocols; tmux passthrough is\npreserved. Terminal/OS notification preferences still govern display, attribution\nand any sound the host itself adds.\n\nBy default the terminal must stay unfocused for two seconds. `condition =\n\"always\"` allows foreground notifications and bypasses the duration threshold;\n`\"never\"` suppresses all delivery. The canonical condition takes precedence over\nlegacy `[tui].notification_condition`.\n\nThe bundled `whale` is a 1.55-second original whale-inspired cue, with no\nthird-party recording. It remains an opt-in candidate pending listening approval.\nWAV playback uses a background worker: macOS `/usr/bin/afplay`, Linux `aplay`\nfrom [ALSA utilities](https://github.com/alsa-project/alsa-utils), or Windows\n`PlaySoundW`. A missing player/file or unsupported platform has no fallback bell.\nOnly one WAV plays at a time. A worker-start receipt is a dispatch attempt, not\nproof of audible playback or OS acceptance. The existing macOS `osascript`\nbanner retains Script Editor attribution; this Core change does not provide a\nbranded native Apps banner.\n\n#### Previous sound settings\n\nWhen `notifications.sound` is absent, completion uses a non-off\n`completion_sound` selection, and other events use the existing event allow-list.\nThese are compatibility inputs to the same audio decision, not separate playback\npaths. When the global sound is selected, it takes precedence over the previous\ncompletion/event choices. The default remains silent unless a legacy sound or\nexplicit `bel` transport was already selected.\n\n```toml\n[notifications.event_sound]\nenabled = false\nevents = [\"turn-complete\", \"approval-needed\"]\nmin_interval_ms = 2000\nquiet = false\n```\n\nLegacy event cues use one bell for completion, subagent completion, input and\nmodel notices; approval/elevation cues use two. The per-category repeat interval\nsurvives settings refreshes. Old unknown event names are ignored on load; new\nCLI/TUI edits require names from the six categories above.\n\nLocal approval/input/elevation prompts and error receipts remain available when\nexternal notifications are muted. Action prompts use the selected UI language\nand retire when that request settles. Repeated live notices keep their first\nexpiry; a later routine update does not hide an unresolved warning at completion.\nOptional plugin suggestion toasts and contextual tips share one session guidance\nbudget. `/config contextual_tips off` hides those toasts while preserving\nrequired notices and explicit plugin review requests.\n\n#### What a notification can contain\n\nA desktop notification is a glance surface: on macOS it can appear on the\nlock screen, and on every platform it is visible to anyone near the machine.\nCodewhale therefore builds notifications from a typed payload with a fixed\nper-event disclosure policy rather than from whatever text was on hand:\n\n| Event | Shown | Never shown |\n|---|---|---|\n| Turn complete | status line (+ elapsed/cost when `include_summary`), preview of the assistant's reply | — |\n| Sub-agent finished | status line, agent id, preview of the child's summary line | — |\n| Approval needed | the tool name | the tool description, the command, the arguments |\n| Input needed | \"Answer the question in the terminal to continue\" | the question |\n| Sandbox elevation needed | the tool name and the denial reason | the command |\n| `notify` tool | model-supplied title and body | — |\n\nEvery field is capped (80 characters for the status line, 120 for the\nidentifier, 200 for the preview), stripped of control bytes and escape\nsequences, and passed through a redactor that replaces credential-shaped\nstrings with `[redacted]`, reduces absolute local paths to `…/basename`,\nand replaces raw tool JSON with `[details hidden]`. The redactor is\ndeliberately over-eager: an unbroken 40-character run has no word\nstructure, so it is redacted even when it is not a secret.\n\n#### macOS: why the banner says \"Script Editor\"\n\nOn macOS terminals that provide no notification escape of their own —\nApple Terminal, the VS Code and JetBrains embedded terminals, plain tmux\nwithout `LC_TERMINAL` — `method = \"auto\"` falls back to `osascript`'s\n`display notification`. That command posts on behalf of the *bundled*\nhost process, and `/usr/bin/osascript` is unbundled, so macOS attributes\nthe banner to `com.apple.ScriptEditor2`. That attribution supplies the\nScript Editor icon and owns the System Settings → Notifications entry\n(alert style, previews, Do Not Disturb). `display notification` has no\nicon parameter, so this cannot be fixed from the notification code; it\nneeds Codewhale to ship a real `.app` bundle. Tracked in\n[#4834](https://github.com/codewhale-hq/CodeWhale/issues/4834). In the meantime,\niTerm2, WezTerm, Ghostty, and kitty are matched first and use their own\nnotification protocols, and `method = \"osc9\"` / `\"bel\"` / `\"off\"` opt out\nof the `osascript` path explicitly.\n\n## Automations in the terminal\n\nOpen `/automation`, then choose **New automation** (`n`) or **Edit automation**\n(`e`). The form edits the name, multiline prompt, schedule, model, workspace,\nand enabled status. `Tab` moves between fields; `Enter` inserts a newline in\nthe prompt. Schedule presets include daily, weekly, hourly, once, and a custom\nRRULE. Time fields accept `HH:MM`; their arrow controls change the time by\n15 minutes. Weekly day buttons and model search support mouse and keyboard.\n\nThe next-run preview uses the scheduler's local time zone, shown with its UTC\noffset. An enabled automation can run after **Save** (`Ctrl+S`); a paused one\nhas no scheduled next run. **Cancel** (`Esc`) discards the draft. Editing keeps\nexisting permission settings, custom schedules, and additional workspace\nentries unless the corresponding supported field is explicitly changed.\n\nSchedules are evaluated in the machine's local time zone against the wall\nclock. A wall time that does not exist on a spring-forward day is skipped and\nan ambiguous fall-back time fires once. Occurrences missed while Codewhale was\nclosed, asleep, or still running the previous occurrence are coalesced: the\nnext start runs one catch-up occurrence and then continues from the next\nfuture slot, never replaying every missed slot. An occurrence never starts\nwhile an earlier run of the same automation is still queued or running. Each\nrun is recorded durably with its status, timing and error; a run that needs a\ntool approval has no operator to ask and fails once the approval wait expires.\n\nChoosing a concrete model pins both the model and its exact configured\nprovider, including named custom routes. Later changes to the active provider\ndo not move that automation's pin. The default-model choice and legacy\ndefinitions without a provider pin keep the runtime's existing default\nbehavior. New automation records use schema v2 and tasks use v3 so older\nruntimes reject records whose provider pins they cannot preserve.\n\n## Lifecycle Outbox (`[lifecycle_outbox]`)\n\nThe lifecycle outbox is an opt-in, machine-readable stream of session,\nturn, and sub-agent lifecycle events. With a path configured, Codewhale\nappends one JSON line per event to that file — for interactive TUI\nsessions *and* headless `codewhale exec` runs — so a supervisor\n(terminal multiplexer wrapper, automation harness, alerting setup) can\nreact to what happened without scraping the screen or installing per-hook\nshell commands. Unset or empty `path` = the feature is **off** and\nbehavior is unchanged.\n\n```toml\n[lifecycle_outbox]\npath = \"~/.codewhale/notifications/outbox.jsonl\" # unset/empty = OFF\nwebhook_url = \"\" # optional; POSTs events as JSON when set\nwebhook_token = \"\" # optional bearer token for webhook_url\n```\n\n### Events emitted\n\n| Event | Kind | Fired at |\n|---|---|---|\n| `turn_start` | `turn.started` | a new turn begins (TUI TurnStarted; `exec` at message dispatch) |\n| `turn_end` | `turn.completed` / `turn.failed` / `turn.interrupted` | turn completion, kind projected from the turn status |\n| `turn_stalled` | `turn.stalled` | the stall watchdog recovers a wedged turn |\n| `subagent_spawn` | `subagent.spawned` | a sub-agent is spawned |\n| `subagent_complete` | `subagent.completed` | a sub-agent reaches a terminal state |\n| `session_start` | `session.started` | interactive session start |\n| `session_end` | `session.ended` | interactive session end |\n\n### File contract\n\nEach line is a `RuntimeEventEnvelope`:\n\n```json\n{\"schema_version\": 1, \"seq\": 3, \"event\": \"turn_start\", \"kind\": \"turn.started\",\n \"thread_id\": \"…\", \"turn_id\": \"…\", \"item_id\": null, \"timestamp\": \"…\",\n \"created_at\": \"…\", \"payload\": {…}}\n```\n\n- `seq` is monotonic per outbox file and recovers from the last written\n line when a new process opens the file.\n- Lines are written one complete JSON line per append, serialized by an\n internal writer task and flushed before the next event; concurrent\n sessions writing the same path do not interleave bytes mid-line, but\n separate processes each continue from their own recovered `seq`, so seqs\n can repeat across processes sharing one file — prefer one file per\n process for strict uniqueness.\n- Parent directories are created lazily on the first event.\n- Payloads are constructed from bounded, pre-redacted fields only — never\n raw tool arguments, environment, or full transcript text. Free-form\n fields (error messages, previews) are capped at the notification limits\n (80 headline / 120 detail / 200 preview characters) and stripped of\n control bytes.\n\n### Webhook delivery\n\nWith `webhook_url` set, every event is additionally POSTed as\n`{\"at\": \"<ISO 8601 timestamp>\", \"event\": {…}}` with\n`Authorization: Bearer <webhook_token>` when a token is configured.\nDelivery is best-effort: failures are logged and dropped, never retried\ninto the agent loop, and a failing webhook never blocks the local file\nappend.\n\n## Control Socket (`[control_socket]`)\n\nPer-session control surface for supervised operation: with the feature\nenabled, the interactive TUI binds one unix domain socket per *running*\nsession at `<sessions-dir>/<session-id>/control.sock` (mode `0600`;\n`<sessions-dir>` is the same directory the session store uses, typically\n`~/.codewhale/sessions`). The socket is removed with the session's\nartifact directory, and a stale socket left by a crashed process is taken\nover by the next launch. Unset or `enabled = false` = the feature is\n**off** (the default) and behavior is unchanged. Unix-only; on other\nplatforms the key parses but no socket is bound.\n\n```toml\n[control_socket]\nenabled = false # default: OFF\n```\n\nThe socket speaks newline-framed JSON-RPC, one request per connection:\nwrite one request line, read one response line, close.\n\n```json\n{\"id\":\"1\",\"method\":\"message\",\"params\":{\"text\":\"hello\"}}\n{\"id\":\"2\",\"method\":\"interrupt\",\"params\":{}}\n{\"id\":\"3\",\"method\":\"status\",\"params\":{}}\n```\n\n- `message` — delivers `text` as a structured user message through the\n ordinary composer dispatch path; dispatched immediately when idle,\n queued when a turn is in flight (the response's `delivery` field says\n which).\n- `interrupt` — the Esc-shaped cancel of the active turn; `cancelled`\n reports whether active work was in flight.\n- `status` — answers `turn_state` (`idle` / `in_progress` / `waiting`) and\n `goal` (`objective`, `status`, `paused`).\n\nSuccess responses echo the request id with a `type`-tagged result;\nfailures carry `error.code` (`invalid_request`, `command_error`,\n`timeout`, `server_unavailable`). Requests are bounded at 1 MiB per line\nand a handler that does not answer within 5 s is reported as `timeout`.\n\n## Tool Catalog\n\nCodewhale loads a small core native tool catalog by default and leaves less\ncommon native tools discoverable through ToolSearch. To keep specific native\ntools loaded on every request, add them to `[tools].always_load`:\n\n```toml\n[tools]\nalways_load = [\"Git\", \"notify\"]\n```\n\n### Script tools and overrides\n\nScripts in `~/.codewhale/tools/` (or `[tools].plugin_dir`) that start with a\n`# name:` header become model-visible tools, and `/plugin tools` lists them.\nThe script reads the tool's JSON input on stdin and writes a JSON\n`ToolResult` (`{\"content\": \"...\", \"success\": true}`) on stdout.\n\n```sh\n#!/usr/bin/env sh\n# name: word_count\n# description: Count words in the given text\n# schema: {\"type\":\"object\",\"properties\":{\"text\":{\"type\":\"string\"}}}\n# approval: required\n```\n\n`# approval:` takes `suggest` (the default) or `required`; either way the\ntool follows the session's approval setting. A script cannot approve itself:\n`approval: auto` is no longer supported, so such a script gets the default,\nand the runtime log (`~/.codewhale/logs/`) and `/plugin tools` name it.\n\nA script cannot replace a built-in tool either. A script whose `# name:` is\nalready registered is not loaded. `[tools.overrides]` may disable a built-in,\nor add a script or command tool under a name of its own:\n\n```toml\n[tools.overrides]\n\"Web\" = { type = \"disabled\" } # turn a built-in off\n\"audited_shell\" = { type = \"script\", path = \"audit-shell.sh\" } # a new tool\n\"Bash\" = { type = \"script\", path = \"audit-shell.sh\" } # refused: Bash is built in\n```\n\nA `script` or `command` override keyed by a built-in is refused, and the\nbuilt-in stays active. A status line names the key once per session, and the\nruntime log records it. To route a\nbuilt-in through your own wrapper, disable the built-in and register the\nwrapper under a new name. An override keyed by a drop-in script's name still\nreplaces that script. Relative `path` values resolve against the plugin\ndirectory.\n\n### `request_user_input` limits\n\n`request_user_input` asks the user a short batch of multiple-choice questions.\nBoth ceilings are configurable (#5949): raise `user_input_max_questions` when a\nresearch or planning workflow legitimately needs more clarifications, lower it\nwhen interactive triage should stay terse.\n\n```toml\n[tools]\nuser_input_max_questions = 6 # default 6, clamped to 1..=10\nuser_input_max_options = 4 # default 4, clamped to 2..=10\n```\n\nThe effective values are applied in three places at once: the tool's JSON\nschema (`minItems` / `maxItems`), its model-visible description, and the\npayload validator. A rejected payload names the key to raise, so the model can\neither resize the batch or tell the user which setting to change.\n\n### User-input wait timeout\n\nQuestions from `request_user_input` wait until answered or canceled by default\n(#6003). An omitted setting or `0` leaves the wait unbounded; a positive value\ncancels the question when that many seconds pass, capped at 86,400 (24 hours).\nHeadless `exec` runs have no\nresponder, so `request_user_input` is withheld there by default:\nthe model reports the tool absent and finishes instead of stalling.\n\n```toml\n[tools]\nuser_input_timeout_seconds = 300 # opt into 5 minutes; omitted or 0 waits indefinitely; maximum 86400\n```\n\nThis key governs question waits only. Approvals have their own clock,\n`[approval] timeout_seconds`, which waits indefinitely unless you set it.\nWall-clock and stream protections elsewhere are unaffected.\n\n## Feature Flags\n\nFeature flags live under the `[features]` table and are merged across profiles.\nDefaults are enabled for built-in tooling, so you only need to set entries you\nwant to force on or off.\n\n```toml\n[features]\nshell_tool = true\nsubagents = true\nweb_search = true # enables deferred Web; the flag name is retained for config compatibility\napply_patch = true\nmcp = true\nexec_policy = true\ncode_mode = true # execute_tools composes MCP/plugin/native calls; false defers it behind tool_search\nverify_tool = true # agent-callable `verify` self-critique; false removes it from the tool catalog\nvision_model = false # true routes image analysis to the [vision_model] model (see above)\nextension_host = false # experimental: run reviewed plugins' native code (see EXTENSIONS.md)\n```\n\n`code_mode` is on by default: `execute_tools` is advertised from the first\nrequest and nested calls go through the same permission gate as direct calls\n(see [Tool surface](TOOL_SURFACE.md#code-mode-execute_tools)). Set\n`code_mode = false` to defer it behind `tool_search` again.\n\n`extension_host` is experimental and off by default. Turning it on lets reviewed\nplugins run their `native` TypeScript/JavaScript tool and slash-command code in\na separate Node (or, opt-in, Bun) process; toggling it in either direction\nchanges the plugin activation policy, so every plugin is reviewed again after a\nrestart. Its `[extension_host]` table (`runtime`, `node`, `bun`) and what the\nhost's sandbox does on each platform are in\n[Writing an extension tool](EXTENSIONS.md).\n\n`[extension_host] mcp_backend` (user config only) chooses the MCP client\nindependently of the `extension_host` flag: `\"host\"` (default) uses the official\nMCP SDK behind the Rust broker; `\"rust\"` keeps the previous Rust client for one\nrelease. A host refusal never falls back to Rust. See\n[MCP connection lifecycle](MCP.md#connection-lifecycle).\n\nEvery flag has a row in [`docs/features.toml`](features.toml), the feature\nregistry; a test fails when a flag and its row disagree.\n\nYou can also override features for a single run:\n\n- `codewhale --enable web_search`\n- `codewhale --disable subagents`\n\nUse `codewhale features list` to inspect known flags and their effective state.\nThe native `/config` view also includes a read-only **Experimental** section\nfor experimental feature flags. It shows each flag's effective enabled/disabled\nstate and whether that state comes from the default or a configured override.\nChange feature flags in `[features]` or with `--enable` / `--disable`; the\n`/config` section is an audit surface, not a stability promise. Goal and\nWorkflow preview rows may appear there as reserved entries until those workflows\ngraduate behind real gated flags.\n\n## Web Search Provider\n\n`web_search` uses keyless Firecrawl by default. Runtime failure or an exhausted\nkeyless quota degrades visibly through DuckDuckGo and Bing. China deployments\ncan explicitly select Baidu, Metaso, Volcengine, or a trusted SearXNG endpoint;\nCodewhale does not guess geography from locale or model provider.\n\nConfigured API providers are attempted first. Runtime failure or an empty\nresult visibly degrades through DuckDuckGo and then Bing; the structured search\nreceipt records every hop. Missing configuration and network-policy denials\nfail closed without sending the query to another provider.\n\n**Provider-native search.** On routes whose provider offers its own web-search\ntool (OpenAI, xAI, Anthropic, DeepSeek, Kimi and others), that search can run\nahead of the configured provider. It is a separate model call on the active\nroute. `[search] native` decides the order:\n\n- unset (default): native search leads only when no search provider is\n configured; a provider chosen in `[search] provider`,\n `CODEWHALE_SEARCH_PROVIDER`, or a Tavily key wins;\n- `native = true`: native search leads even when a provider is pinned;\n- `native = false`: native search is never used.\n\nNative citations are link-checked before they are returned. Each cited URL is\nrequested through the same guarded fetch as `Web` fetch (SSRF, DNS and redirect\nchecks, `[network]` policy): a `HEAD`, then a one-byte ranged `GET` when `HEAD`\nis refused, at most four at a time and five seconds each. The request uses the\naddress as it will appear in `results`, without userinfo, fragment or\ncredential-like query parameters; an address whose query string repeats the\nsearch query is not requested and is removed. Only sources that\nanswer with a 2xx status stay in `results`; the receipt lists each removed one\nas `citation_unverified` with its reason. When no citation passes, or when the\nquery asks for `recency` (no native search tool can enforce a time window), the\nsearch falls through to the configured provider. A citation whose host the\n`[network]` policy denies is removed. A citation whose host the policy would\nask you about is not fetched: it stays in `results` and the receipt records it\nas `citation_unchecked` until you allow the host.\n\nThe provider's prose answer is returned whole in a separate\n`unverified_synthesis` field: it is the provider model's summary, and neither\nits claims nor its links are checked. Oversized tool output spills to a session\nartifact the model can page back.\n\n**Recency and locale.** `recency` and `locale` are forwarded where the\nbackend's API takes them, and the search receipt reports each as honored or\nignored:\n\n| Backend | Recency | Locale |\n| --- | --- | --- |\n| Firecrawl | `tbs=qdr:d/w/m/y` | `country` from the region (`de-DE` → `DE`); a bare language is ignored |\n| Tavily | `time_range` | not sent (Tavily takes country names) |\n| SearXNG | `time_range` | `language`, as given |\n| Serply | not sent (undocumented) | `hl` language, `gl` country |\n| DeepSeek | not sent | not sent (`localization` takes an IP address) |\n| Provider-native | declined; the search falls through to the configured provider | not sent |\n\nRecency is rounded up to the backend's nearest window (day, week, month,\nyear), so `recency = 10` searches the last month. Other backends ignore both\nknobs and say so in the receipt.\n\nFor a private/internal search service that serves DuckDuckGo-compatible HTML,\nkeep `provider = \"duckduckgo\"` and set `base_url`; Codewhale appends the `q`\nquery parameter to that endpoint and applies network policy to its host.\nCustom endpoints do not fall back to public Bing. `CODEWHALE_SEARCH_BASE_URL`\ncan override this per process; `DEEPSEEK_SEARCH_BASE_URL` remains accepted as\nthe legacy alias.\n\n**SearXNG** ([docs](https://docs.searxng.org/dev/search_api.html)) uses the\nconfigured instance's JSON API. Set `provider = \"searxng\"` and\n`base_url = \"https://your-searxng.example\"`; Codewhale calls\n`/search?q=...&format=json`. Codewhale does not use a public SearXNG instance\nby default because public instances often disable JSON output or rate-limit API\ntraffic.\n\nSelf-host it as a separate process (Docker is fine); Codewhale never bundles or\nmanages the search engine itself:\n\n- Enable JSON on the instance (`settings.yml`, `search.formats` must include\n `json`) and restart it. An HTML-only instance answers the API with HTTP 403;\n Codewhale reports that as a JSON/API-access problem on the SearXNG hop rather\n than silently returning no results.\n- Bind it to loopback, or to a host and port your network policy allows. The\n instance keeps its own engine list, limiter, and limits.\n- Point Codewhale at it with `[search] provider = \"searxng\"` and `base_url`\n (required; either the root URL or the `/search` endpoint). No instance ships\n as a default, and none is discovered automatically.\n- `codewhale doctor --probe-search` sends a transport-only `HEAD` to that\n origin — no `q=`, no credentials, no redirects, no audit receipt — so a green\n probe proves reachability and network-policy admission, not that JSON is on.\n\nConfirm the JSON API itself before assuming a Codewhale bug:\n\n```sh\ncurl -sS \"$BASE/search?q=codewhale&format=json\" | jq '.results[0] | {title,url,score}'\n```\n\nCodewhale ranks the returned rows by `score`, highest first, and applies\n`max_results` to that ranking; rows an instance reports without a usable score\nkeep their original relative order.\n\n**Metaso** ([metaso.cn](https://metaso.cn)) requires a user-supplied key. Set\n`METASO_API_KEY` or `[search] api_key`; Codewhale does not ship a shared key.\n\n**Firecrawl** ([docs](https://docs.firecrawl.dev/sdks/cli)) searches Firecrawl\nCloud without a key using its bounded per-IP daily quota. Set\n`FIRECRAWL_API_KEY` or `[search] api_key` for authenticated limits. Codewhale\nsends no `Authorization` header in keyless mode.\n\n**Baidu** uses Baidu AI Search at\n`https://qianfan.baidubce.com/v2/ai_search/web_search`. Set\n`BAIDU_SEARCH_API_KEY` or `[search] api_key`. This is a search-tool backend\nonly; it does not add a Baidu model provider.\n\n**Sofya** ([sofya.co](https://sofya.co)) returns full extracted page content\nrather than snippets. Set `[search] api_key` to your `ay_live_...` key, or the\n`SOFYA_API_KEY` env var. This is a search-tool backend only; it does not add a\nSofya model provider.\n\n**Serply** ([serply.io](https://serply.io)) returns Google organic results with\ntitle, URL, and snippet. Set `[search] api_key` to your Serply key, or the\n`SERPLY_API_KEY` env var. This is a search-tool backend only; it does not add a\nSerply model provider.\n\n**DeepSeek** ([docs](https://api-docs.deepseek.com/guides/search)) uses\n`POST https://api.deepseek.com/v1/tools/search`. Set `provider = \"deepseek\"`\n(`deep-seek` and `deepseek-search` are also accepted) and supply `[search] api_key`\nor the `DEEPSEEK_API_KEY` env var, in that order. The saved DeepSeek model-provider\nkey is not reused by this search backend. Queries are truncated to 128 Unicode\ncharacters. The backend clamps counts to 1–50; the shared search tool currently\ncaps requests at 10 results. Query operators such as `site:`, `lang:`,\n`after:`, and `before:` are passed through; the `recency` and `locale` knobs are\nignored. Results include title, URL, and snippet, falling back to summary when a\nsnippet is empty.\n\n**Tavily** ([tavily.com](https://tavily.com)) is selected automatically when a\nTavily key is present and no provider is pinned: `TAVILY_API_KEY` set, or\n`[search] api_key` / `CODEWHALE_SEARCH_API_KEY` in the `tvly-` family. Doctor\nreports that as `source: tavily key`. Autodetect is runtime-only — Codewhale\nnever writes `[search] provider` for it, and `TAVILY_API_KEY` is never merged\ninto `[search] api_key`. An explicit `[search] provider` or\n`CODEWHALE_SEARCH_PROVIDER` always wins, so `provider = \"firecrawl\"` keeps\nFirecrawl even with a Tavily key in the environment. Pinned `tavily` accepts\nany non-empty `[search] api_key` and is configured by that key or\n`TAVILY_API_KEY`; with both empty it fails closed.\n\n```toml\n[search]\nprovider = \"firecrawl\" # also duckduckgo | bing | tavily | bocha | metaso | searxng | baidu | volcengine | sofya | serply | deepseek\n# base_url = \"https://search.example/\" # optional with provider = \"duckduckgo\"; required with \"searxng\"\n# api_key = \"YOUR_KEY\" # optional for firecrawl; required by the other API providers\n# native = false # provider-native search: unset = only when no provider is configured\n```\n\n## Local Media Attachments\n\nUse `@path/to/file` in the composer to add local text file or directory context\nto the next message. Use `/attach <path>` for local image/video media paths, or\n`Ctrl+V` to attach an image from a local clipboard or an explicitly forwarded\nX11/Wayland clipboard. SSH terminal paste without a forwarded graphical display\nis text-only; use the local terminal's paste command (`Cmd+V` on macOS or\n`Ctrl+Shift+V` on Linux/Windows), and use `/attach <path>` for remote image\nfiles. OpenSSH loopback X11 displays are detected automatically. For an\nexplicitly forwarded Wayland or non-loopback X11 display, set\n`CODEWHALE_SSH_CLIPBOARD=graphical`; set it to `terminal` to force terminal\ntransfer instead of an ambient remote display. DeepSeek's public Chat\nCompletions API currently accepts text message\ncontent, so media attachments are sent as explicit local path references instead\nof native image/video payloads.\nAttachment rows appear above the composer before submit; move to the start of\nthe composer, press `↑` to select an attachment row, then press `Backspace` or\n`Delete` to remove it without editing the sample text by hand.\n\n## Managed Configuration and Requirements\n\ncodewhale supports a policy layering model:\n\n1. user config + profile + env overrides\n2. managed config (if present)\n3. requirements validation (if present)\n\nBy default on Unix:\n- managed config: `/etc/deepseek/managed_config.toml`\n- requirements: `/etc/deepseek/requirements.toml`\n\nRequirements file shape:\n\n```toml\nallowed_approval_policies = [\"on-request\", \"untrusted\", \"never\"]\nallowed_sandbox_modes = [\"read-only\", \"workspace-write\"]\n```\n\nIf configured values violate requirements, startup fails with a descriptive error.\n\n## Notes On `codewhale doctor`\n\n`codewhale doctor` follows the same config resolution rules as the rest of the\nTUI. That means `--config`, `CODEWHALE_CONFIG_PATH`, and the legacy\n`DEEPSEEK_CONFIG_PATH` are respected, and MCP/skills\nchecks use the resolved `mcp_config_path` / `skills_dir` (including env overrides).\n\nTo bootstrap missing MCP/skills paths, run `codewhale setup --all`. You can\nalso run `codewhale setup --skills --local` to create the owned project\n`.codewhale/skills` directory. Project skills load after workspace trust.\n\nBoth plain `codewhale doctor` and `codewhale doctor --json` are structural and\noffline by default. They do not check the release service, hosted provider\nAPIs, local provider endpoints, or MCP processes, and they do not load a\nworkspace credential `.env`. Use `--check-updates`,\n`--probe-api`, `--probe-local`, or `--probe-mcp` to opt into the corresponding\nlive boundary; `--probe-local` may start a desktop-managed service such as\nOllama. Only the explicit API/local probe paths may load workspace credential\n`.env` values. Live flags conflict with `--json`, so machine-readable doctor\noutput is always offline. Top-level keys include `version`, `paths`, `secret_backend`,\n`config_path`, `config_present`, `workspace`, `api_key.source`,\n`api_key.availability`, `base_url`,\n`default_text_model`, `mcp`, `skills`, `tools`, `plugins`, `sandbox`,\n`platform`, `api_connectivity`, and `capability`. CI consumers should rely on\n`api_key.source` (`config_declared`/`env_declared`/`external_auth_declared`/\n`secret_store_unprobed`/`secret_store_unavailable`/`oauth_unprobed`/\n`external_consent`/`none`/`local_runtime`/`unknown`) and\n`api_key.availability`\n(`present`/`not_required`/`not_probed`/`unavailable`/`unknown`) rather than parsing the\nhuman-readable `doctor` text. Source is declaration metadata, not proof that a\ncredential exists or works. Only a non-empty, non-sentinel literal config value\nis structurally `present`; no-auth and local routes are `not_required`. Environment,\nexternal-auth, OAuth, consent, and secret-store declarations remain `not_probed`\nand cannot make structural Setup or Fleet readiness true. A secret-store sentinel\non a named/custom endpoint that is prohibited from using the shared store is\n`secret_store_unavailable`/`unavailable`, while `unknown` remains reserved for\nthe absence of a supported structural conclusion. Exact and whitespace-wrapped\nlegacy sentinels are never treated as literal credentials. The structural\nloader still honors safe environment routing/model/policy fields, but it never\nmaterializes environment HTTP headers, sandbox API keys, or search API keys;\nonly an explicit API/local probe switches to the normal credential-loading\npath. An opted-in update check also emits only typed generic failures: untrusted\nrelease metadata and transport errors are not echoed.\n\nIf configuration loading or validation fails, `doctor --json` returns nonzero\nand prints a bounded JSON error envelope with\n`status = \"error\"` and `error.kind = \"config_validation\"`. It does not emit a\nnormal route or capability report—or the underlying possibly sensitive error—\nfor an invalid configuration.\n\nMCP entries are configuration diagnostics unless an explicit MCP command is\nrun. `mcp.probe_scope` is `configuration`, `mcp.live_health_checked` is false,\nand each server separates `checks.configuration` / `checks.command` from\n`checks.process_reachable`, `checks.protocol_initialized`, and\n`checks.backend_tool_health`. The latter three remain `not_checked` in doctor\noutput. Run `codewhale mcp validate` to explicitly start enabled servers and\nverify protocol initialization/discovery; backend health still requires an\nappropriate explicit tool call. Doctor reports only safe structural MCP fields:\nURL userinfo/path/query/fragment and raw command arguments, environment values,\nheader values, and token material are omitted. Provider URLs follow the same\nrule and expose only `scheme://host[:explicit-port]`.\n\nThe `capability` key contains per-provider capability info derived from\nstatic knowledge (release docs, API guides) rather than live API probes.\nTop-level sub-keys: `resolved_provider`, `resolved_model`, `context_window`,\n`max_output`, `thinking_supported`, `cache_telemetry_supported`,\nand `request_payload_mode`.\n\nUse `capability.context_window` and `capability.max_output` for model-limit\nchecks in CI scripts; do not treat `capability.max_output` as the per-turn\nrequest budget. Use `capability.thinking_supported` to decide whether to\nconfigure reasoning effort.\n\n## Setup status, clean, and extension dirs\n\n`codewhale setup` accepts a few flags beyond the existing `--mcp`,\n`--skills`, `--local`, `--all`, and `--force`:\n\n- `--status` — print a compact one-screen status (api key, base URL, model,\n MCP/skills/tools/plugins counts, sandbox, `.env` presence). Read-only and\n network-free; safe to run in CI. If `.env` is missing and `.env.example` is\n present in the workspace, the status output points at `cp .env.example .env`.\n- `--tools` — scaffold `~/.codewhale/tools/` with a `README.md` describing the\n self-describing frontmatter convention (`# name:` / `# description:` /\n `# usage:`) and an `example.sh` that follows it. The directory is\n intentionally not auto-loaded; wire individual scripts into the agent via\n MCP, hooks, or skills.\n- `--plugins` — scaffold `~/.codewhale/plugins/` with a `README.md` and an\n `example/plugin.toml` plus a namespaced example Skill. Bundles are discovered\n read-only, untrusted, and disabled; review them through `/plugin` before\n enabling. v0.9.1 activates only declared Skills and MCP servers. See\n [PLUGIN_BUNDLES.md](PLUGIN_BUNDLES.md).\n- `--all` now scaffolds MCP + skills + tools + plugins together.\n- `--clean` — list `~/.codewhale/sessions/checkpoints/latest.json` and\n `offline_queue.json` if they exist. Legacy\n `~/.deepseek/sessions/checkpoints/` files are not scanned automatically; set\n `CODEWHALE_HOME=~/.deepseek` for a one-off legacy cleanup. Pass `--force` to\n actually remove matched files. This never touches real session history or the\n task queue.\n\n`--status` and `--clean` are mutually exclusive with the scaffold flags.\n\n## Why the engine strips XML/`[TOOL_CALL]` text\n\ncodewhale sends and receives tool calls only over the API tool channel\n(structured `tool_use` / `tool_call` items). The streaming loop in\n`crates/engine/src/core/engine.rs` recognizes a fixed set of fake-wrapper start\nmarkers — `[TOOL_CALL]`, `<codewhale:tool_call`, `<tool_call`, `<invoke `,\n`<function_calls>` — and scrubs them from visible assistant text without ever\nturning them into structured tool calls. When a wrapper is stripped, the loop\nemits one compact `status` notice per turn so the user can see why their\nvisible text shrank. Treat any change that re-enables text-based tool\nexecution as a regression; the protocol-recovery tests in\n`crates/tui/tests/integration/protocol_recovery.rs` lock the contract.\n\n## Model-bound redaction (`[redaction] model_bound`)\n\nCodewhale masks credential-looking values in tool output **before it is sent\nto an upstream model** — the \"model boundary\". A file read by a tool can\ncontain a configured API key, a bare provider token, or a credential-shaped\nopaque string, and the model must not see those bytes. This backstop is\nseparate from the display/export scrubbers: it decides what the model itself\ncan quote back, and it is deliberately conservative (`CredentialShaped`\npolicy, see `crates/config/src/persistence.rs`), so ordinary code and config\nstay byte-exact while keys, JWTs, bearer tokens, PEM blocks, and long opaque\nruns are masked.\n\nTurning that masking **off** is a security decision, so it is not a plain\nboolean:\n\n```toml\n[redaction]\nmodel_bound = \"disabled\" # \"enabled\" (default) | \"disabled\"\n```\n\nSetting `\"disabled\"` only records a *request*. It takes effect only when all\nof these are true:\n\n1. You restart the interactive TUI.\n2. The startup gate appears and you press `1`/`Y` on its first stage\n (\"confirm and disable\"). This only advances to a second, final-confirmation\n stage - the gate repeats the red warning and asks \"are you really sure?\".\n3. On that second stage you press `1`/`Y` again. The gate is rendered with the\n same explicit-key discipline as workspace trust - `Enter` never confirms by\n reflex, and `2`/`U` on the second stage steps back.\n4. Only that second confirmation persists a receipt to\n `~/.codewhale/redaction-state.json` (next to `config.toml`) and rebuilds\n the engine with masking off for the rest of this launch and future ones.\n\nThe receipt is bound to the config it was made against and is valid only\nwhile that config still requests `\"disabled\"`. Setting `model_bound` back\nto `\"enabled\"` - or rewriting `config.toml` in any way after the\nconfirmation - invalidates it, so requesting `\"disabled\"` again later\nasks for a fresh confirmation on the next launch. The receipt checks both the\nconfig contents and modification time; missing, unreadable, or malformed config\nand older receipts without this binding keep masking enabled. Legacy-home\ninstalls store the receipt beside their resolved config file.\n\nUntil a confirmation exists, the effective mode is always `\"enabled\"`:\n\n- Choosing `2`/`U` (\"keep masking on\") leaves the config field untouched, so\n the next launch asks again. Edit the field back to `\"enabled\"` to stop being\n asked.\n- Non-interactive entry points (`codewhale exec`, hooks, automations, headless\n agents) never confirm anything and never apply an unconfirmed request.\n- Routing/classification summaries and durable goal-state text keep their own\n always-on redaction regardless of this switch; the opt-out exists so the\n model can quote file bytes for exact edits, not to relax stored state.\n\nThe config value itself is forgiving: `true`/`false`, `\"on\"`/`\"off\"`, and\n`\"enabled\"`/`\"disabled\"` (any casing) all parse, with `false`/`\"off\"` meaning\n`\"disabled\"`.\n\nA confirmed opt-out still sends your configured API keys to the provider you\nare already talking to. Only use it when the model must read and edit files\nthat contain real credentials.\n\n### Stored sessions\n\nThe masking runs once, when tool output enters the transcript, so saved\nsession files (`~/.codewhale/sessions/*.json` and their checkpoints) and\nRuntime API thread items (their text and tool metadata) do not store a\ncredential-shaped value from tool output. With the confirmed opt-out above,\ntool output is stored as the model saw it.\n\nNot yet masked: large outputs spilled to `~/.codewhale/tool_outputs`, shell\ncompletion evidence artifacts, and tool-call *inputs* (for example a\n`curl -H \"Authorization: Bearer …\"` command line). `doctor` and\n`scrub-secrets` do not check those either.\n\nSessions saved by builds before this change may still hold credentials in\ntheir tool output; nothing rewrites them automatically. `codewhale doctor`\nreports them under **Stored Sessions** (it checks the newest 50 files), and\nthis command finds and masks them across every saved session:\n\n```sh\ncodewhale sessions scrub-secrets # report only\ncodewhale sessions scrub-secrets --apply # rewrite the affected files\n```\n\n`--apply` rewrites each file under the same per-session lock a save takes,\nso it never loses a concurrent save. A session that is still open can write\nits in-memory copy back on its next save, so close open sessions first, and\nrotate any credential that was exposed — masking a stored copy cannot un-leak it.\n"
|
||
}
|
||
]
|
||
},
|
||
{
|
||
"id": "plugins",
|
||
"slug": "plugins",
|
||
"sources": [
|
||
{
|
||
"path": "docs/PLUGINS.md",
|
||
"sha256": "2b69ca39b3ecb8f02a2ba3454c24dd7e9d49f4c26b3a36407ef76b6ab1e4cb0c",
|
||
"content": "# Installing plugins\n\n> 阅读简体中文版:[zh_hans/PLUGINS.md](zh_hans/PLUGINS.md)。\n\nTo create a bundle, start with [Write your first Codewhale plugin](PLUGIN_AUTHORING.md)\nand its runnable Skills example.\n\nThis is the walkthrough for the `/plugin install` on-ramp (v0.9.4, #5182).\n[PLUGIN_BUNDLES.md](PLUGIN_BUNDLES.md) remains the contract for the bundle\nformat (`plugin.json`, compatible `kimi.plugin.json` or\n`.claude-plugin/plugin.json`, or legacy `plugin.toml`), discovery, validation, and\nthe trust/enable lifecycle — this document covers how bits get onto disk in\nthe first place.\n\n## Choose a plugin, skill, or MCP connection\n\n| What you need | Use | Starting point |\n| --- | --- | --- |\n| Reusable instructions that work through your existing tools | A skill | `/skills`, then `/skill <name>`; [Skills](SKILLS.md) |\n| A reviewed bundle of workflows, tools, commands or agent profiles | A plugin | `/plugin marketplace list`; [first-party catalog](https://github.com/codewhale-hq/codewhale-plugin-marketplace) |\n| Tools or context from a particular local process or remote service | A direct MCP connection | `/mcp`; [MCP setup](MCP.md) |\n\nA plugin can combine skills, Markdown commands, Fleet agent profiles, hooks,\nMCP servers and AI-provider declarations. Tools come from an MCP server or an\nexperimental native extension; there is no generic application-UI component in\nthe bundle manifest. Computer Use's Mac helper and Chromewhale's browser\nextension have their own setup after plugin review.\n\nMCP has two connection kinds: a local process over stdio, or a remote URL over\nStreamable HTTP. Legacy SSE is available when the server still supports it.\nUser-configured remote servers can use environment-backed credentials or the\nhost's OAuth login. Plugin-contributed remote servers require explicit\nenvironment-backed credentials when authentication is needed; their interactive\nOAuth path is not supported. Installing a skill or plugin does not connect an\naccount or establish that a service works.\n\nA local stdio server runs with your operating-system user's access. Requested\nfilesystem roots in a plugin manifest do not sandbox that process.\n\nFor example, configure [Cloudflare's public documentation server](https://developers.cloudflare.com/changelog/post/2026-10-22-cloudflare-mcp-servers-retire-sse/)\ndirectly, or install the `cloudflare-docs` plugin when you also want its skill:\n\n```sh\ncodewhale mcp add cloudflare-docs --url https://docs.mcp.cloudflare.com/mcp\ncodewhale mcp connect cloudflare-docs\ncodewhale mcp tools cloudflare-docs\n```\n\nChoose one setup for that server. `connect` is a diagnostic connection; it does\nnot attach tools to an already running session. In an existing terminal session,\nrun `/mcp reload` after changing configuration.\n\n## Bundle compatibility\n\n| Format | Supported boundary |\n| --- | --- |\n| Native `plugin.json` or legacy `plugin.toml` | Reviewed declarative adapters above; [manifest contract](PLUGIN_BUNDLES.md) |\n| Claude `.claude-plugin/plugin.json` | Skills, commands, agent profiles and MCP subset; hooks, LSP, custom MCP paths and plugin-root expansion are rejected; [compatibility details](CLAUDE_PLUGIN_COMPAT.md) |\n| `kimi.plugin.json` | Compatible skills, commands, agents and MCP declarations; unsupported runtime fields fail closed |\n| Local DSH package | Explicit preview and exact-hash import through `/plugin import dsh`; experimental native runtime, not blanket DSH compatibility or release qualification; [current evidence and limits](DSH_PLUGIN_COMPAT.md) |\n\nNative TypeScript extensions require `extension_host = true` under `[features]`\nand fresh capability review. LSP, requested filesystem roots and lifecycle mutation\nremain inventory-only. Current DSH adapters include work beyond the published\nv0.10.2 CLI; third-party DSH plugins' complete workflows are not established by\nan import succeeding. DSH plugin UI is unsupported. Other applications' plugin\ndirectories and credentials are not borrowed.\n\n`/plugin suggest <task>` is a local, read-only companion: it ranks already\ninstalled bundles (name, keywords, description, bundled skill names, declared\nhosts) and available marketplace catalogs, including the bundled snapshot and\ncatalogs you have added with `/plugin marketplace add`.\nIt explains the match and gives the next review, enable, or catalog-install\nstep, but never installs, trusts, or enables a bundle on its own.\n\n## How Codewhale offers plugins\n\nCodewhale is helpful about plugins, not pushy. The rules:\n\n- **One proactive surface.** Sending a task can show one quiet toast when the\n prompt matches an installed-but-idle plugin or a catalog candidate you do\n not have yet, for example `/plugin trust supabase` or\n `/plugin marketplace install <catalog> supabase`. Nothing appears while you\n type, and nothing is appended to your message to advertise plugins.\n- **One switch.** With `contextual_tips` off, no plugin guidance appears\n anywhere. Required notices and your own `/plugin` commands still work.\n- **One budget.** Plugin offers share the per-session guidance budget with\n other tips. In the interactive TUI the model can call\n `request_plugin_install` once per session to ask you to review a plugin the\n task needs; a second call fails. Exec, ACP, and runtime-API sessions do not\n get the tool.\n- **Built-ins are never advertised.** Inspect existing bundles in `/plugin list`\n and Extensions before installing another copy. The published v0.10.2 CLI\n includes Computer Use; its layout differs from the current source candidate.\n See [Computer Use across versions](PLUGIN_MARKETPLACE.md#computer-use-across-versions).\n- **Specific terms only.** Generic words (accessibility, browser, chrome,\n docs, screenshot, web, wiki, …) and Codewhale's own vocabulary (agent, code,\n data, MCP, model, session, tool) never trigger an offer, so asking why an\n MCP server is stuck does not suggest a vendor's plugin. Distinctive names,\n phrases and domains still do. The matcher and the marketplace's\n `check-marketplace.mjs` share one stoplist.\n- **Only what runs here.** Plugins whose `when.os` excludes this OS are not\n offered.\n- **Review before acting.** A model-requested row offers Review. Its button\n opens the existing Extensions inventory: Plugins for an installed bundle,\n Marketplace for a suggestion that is not installed. Nothing is installed,\n trusted, or enabled by that click. Choose the inventory's explicit action;\n trust still reviews the exact installed content before activation.\n- **Reversible dismissal.** Esc clears a non-empty draft first, then hides the\n row for this session only. \"Don't suggest again\" is the explicit,\n persisted choice. `/plugin dismissals` lists both kinds, and\n `/plugin dismissals reset [<name>]` lets suggestions offer a plugin again.\n- **Discovery is passive.** Find new plugins in these docs,\n `/plugin marketplace list`, Extensions, and the browser guide below.\n\nCodewhale does not invent a remote plugin URL; missing plugins are suggested\nonly from available catalogs. On-disk bundle changes still toast\n`/plugin reload` on send and between turns.\n\n## Browser: pick one\n\nSeveral options can drive a browser. They differ in whose browser it is and\nwhat it can see.\n\n| Option | Whose browser | Good for |\n| --- | --- | --- |\n| `chrome-devtools` MCP (`/mcp recommendations`) | A Chrome it drives, which can include signed-in pages | DevTools-level inspection and performance work |\n| Playwright MCP (`/mcp recommendations`) | A fresh, isolated profile with `--isolated` | Scripted flows and testing without your identity |\n| Computer Use browser tools (off until reviewed) | One it launches, in a profile of its own | Browser steps inside a wider desktop task |\n| Chromewhale (developer preview, `codewhale-hq/codewhale-plugin-marketplace`) | Yours, already open, in your own Chrome profile; load unpacked | Reading or acting on the tab you are looking at, one granted site at a time |\n\nNone of these is offered to you proactively. Add the one that fits the job.\n\n## Sources\n\n`codewhale install <spec>` and `/plugin install <spec>` use the same installer.\nThe shell entry does not start a model or require model credentials. Both keep\nbundles disabled and untrusted until you explicitly review and enable them.\n\n```sh\ncodewhale install git:github.com/example/[email protected]\ncodewhale install npm:@example/[email protected]\ncodewhale install ./local-plugin\n```\n\nGit shorthand supports GitHub archives only; refs must be a single safe segment.\nUse the existing HTTPS archive URL plus `#path=...` for a nested bundle.\nNpm requires an exact version and a native plugin manifest in the tarball.\nNo git hooks, npm lifecycle scripts, dependency installation or Pi executable\nextensions run. A ref names the requested revision, not an integrity signature;\nreview still binds the exact downloaded bytes. Tags can move: pin a commit for\nimmutable GitHub selection. Updates of version-pinned sources retain that version.\nNetwork approval and archive safety checks are unchanged.\n\nThe original source forms remain available:\n\n```text\n/plugin install ./path/to/bundle # local directory (copied)\n/plugin install github:owner/repo # GitHub archive of the default branch\n/plugin install https://example.com/x.tar.gz # direct tarball URL\n```\n\nThere is no registry index and no `git clone` in v1 — tarball-only fetching\nkeeps the size cap and no-symlink guarantees of the installer. Downloads are\ngated by the per-domain network policy: an unknown host returns a\n\"needs approval\" error naming the host (`/network allow <host>`, then retry),\na denied host aborts without touching disk.\n\nThe fetched tree must contain **exactly one** bundle root — a directory\nholding a `plugin.json`, compatible `kimi.plugin.json`,\n`.claude-plugin/plugin.json`, or legacy `plugin.toml` manifest. Kimi bundles are accepted when they use Codewhale-compatible Skills,\ncommands, agents, and MCP declarations; unsupported Kimi runtime fields fail\nclosed instead of being silently ignored. Bundles land in\nthe user plugins root at `~/.codewhale/plugins/<name>/`, where `<name>` is the\nmanifest's plugin name.\n\nClaude bundles keep their metadata in `.claude-plugin/plugin.json` and their\ncomponents at the bundle root. The importer supports skills, commands, agents,\nand MCP servers declared inline or in root `.mcp.json` (flat server map or an\n`mcpServers` wrapper). Claude `http` transport maps to Streamable HTTP. Relative\nsources in a `.claude-plugin/marketplace.json` catalog resolve from the marketplace\nrepository root. The whole bundle remains subject to the same review hashes and\npath checks as native plugins.\n\nRemote MCP headers can name credentials without embedding them: exact\n`Bearer ${ENV_NAME}` authorization values become `bearer_token_env_var`, and\nexact `${ENV_NAME}` header values become `env_headers`. Import reads no credential\nvalues. Literal credentials and compound templates are rejected.\n\nThis is a compatible subset: hooks, LSP declarations, custom MCP file paths, and\n`${CLAUDE_PLUGIN_ROOT}` expansion are rejected with an explanation; no partial\nplugin is installed. Installing a remote MCP declaration does not complete its\nauthentication. Plugin-contributed remote servers retain the existing explicit\ncredential requirements; this importer does not enable plugin OAuth.\n\n## The guided flow\n\nInstalling never activates anything. The command places the bits, then drops\nyou straight into the standard capability review:\n\n```text\n/plugin install github:someone/neat-plugin\n→ Installed plugin 'neat-plugin' to ~/.codewhale/plugins/neat-plugin.\n It is disabled and untrusted. Review its requested authority below…\n <full inventory, permissions, MCP authority render>\n /plugin trust neat-plugin <content-hash>.<capability-hash>\n\n/plugin trust neat-plugin <paste the token> # records the hash-bound receipt\n/plugin enable neat-plugin # activates for this workspace\n```\n\nThis is the same review render and confirmation token as `/plugin trust\n<name>` — trust is the strict hash-bound receipt flow, not an advisory marker.\nIf the bundle's content or declared capabilities change, the receipt stops\nmatching and the plugin goes inactive until you review again.\n\n## Update and uninstall\n\n```text\n/plugin update <name> # re-download, byte-compare, atomic swap if changed\n/plugin disable <name> # required before uninstall\n/plugin uninstall <name> # deletes the bundle and prunes its state entry\n```\n\n- `update` re-downloads the recorded source. Identical bytes are a no-op; a\n changed bundle is swapped atomically and its trust receipt is automatically\n invalidated (the hash no longer matches), so re-review is forced before the\n plugin can activate again. For a local install, it stages a fresh copy of the\n recorded source directory; keep that authoring directory available. Editing\n the installed copy does not update its source. See the\n [local authoring loop](PLUGIN_AUTHORING.md#4-iterate-and-review-changes).\n- `update` retains the recorded repository revision, npm version, archive URL\n and bundle selector. Refreshing a catalog does not retarget that source. To\n select a newer catalog pin, follow\n [Changing an installed pin](PLUGIN_MARKETPLACE.md#changing-an-installed-pin).\n- `uninstall` refuses enabled plugins (disable first), deletes the bundle\n directory, and removes its persisted trust/enablement entry.\n\n## Safety rules\n\n- Every install carries an `.installed-from` provenance marker. The installer\n **refuses to overwrite or delete** a bundle that lacks it — hand-placed\n bundles under `~/.codewhale/plugins/` are never clobbered.\n- Tarballs are size-capped and extracted into a private staging directory\n first; path traversal (`..`, absolute paths) and symlinks/hard links inside\n the bundle are rejected, and the destination only appears via an atomic\n rename after every check passes.\n- Install pre-checks the name against builtin and workspace bundles so a\n higher-precedence bundle cannot silently shadow (or be shadowed by) the\n install.\n- Newly installed bits are always **disabled and untrusted**; enablement only\n ever happens through the explicit trust review above.\n"
|
||
},
|
||
{
|
||
"path": "docs/PLUGIN_BUNDLES.md",
|
||
"sha256": "bc178f6e0c60792f135476ea2e301796125089c5cae164f69af906212a56b9ce",
|
||
"content": "# Plugin bundles\n\n> 阅读简体中文版:[zh_hans/PLUGIN_BUNDLES.md](zh_hans/PLUGIN_BUNDLES.md)。\n\nCodewhale supports a deliberately small plugin-bundle boundary. The boundary\nwas drawn in v0.9.1 and is extended deliberately in v0.9.10: a bundle may\ncontribute declarative Skills, MCP configuration, Commands, Agent profiles,\nand Hooks through Codewhale's existing engines. Unsupported declarations stay\ninventoried instead of disabling a mixed bundle. Discovery alone never\nexecutes, enables, trusts, downloads, updates, or installs anything.\n\nThis document owns the bundle formats, discovery,\nvalidation, and the trust/enable/runtime contract. [PLUGINS.md](PLUGINS.md)\nowns how bits get onto and off disk — the `/plugin install`, `update`,\n`uninstall`, and `suggest` on-ramp added in v0.9.4 (#5182). Compatible Claude Code bundles use the same native adapters; their supported subset is\n[CLAUDE_PLUGIN_COMPAT.md](CLAUDE_PLUGIN_COMPAT.md).\nFor a runnable native example and explicit OpenCode/DSH data conversion, see\n[Write your first plugin](PLUGIN_AUTHORING.md).\n\n## Discovery and precedence\n\nCodewhale scans only its own roots, looking in each `<name>/` directory for a\nmanifest named `plugin.json` (the native Agent Plugins v1.0.0 format, since\nv0.9.4), `kimi.plugin.json` (the compatible Kimi Skills/MCP subset, since\nv0.9.8), `plugin.toml` (the legacy Codewhale format, still fully readable), or\n`.claude-plugin/plugin.json` (the compatible Claude subset, since v0.9.13):\n\n- User: `~/.codewhale/plugins/<name>/`\n- Workspace: `<workspace>/.codewhale/plugins/<name>/`\n\nA bundle that publishes multiple formats is read through `plugin.json` first,\nthen `kimi.plugin.json`, then the legacy `plugin.toml`, then\n`.claude-plugin/plugin.json`.\nComputer Use installs from the first-party catalog as a normal user plugin.\nIt updates independently of the Engine and requires review and enablement.\nThe native Mac helper is installed separately for permission setup and the\ncomputer-control indicator. See the [Computer Use guide](https://github.com/codewhale-hq/codewhale-cu-plugin).\n\nUser plugins take precedence over workspace plugins. Symbolic-link roots,\nmanifests, component paths, and nested component files fail closed.\nHistorical built-in snapshots and receipts remain untouched for older binaries;\nthe current Engine neither creates nor discovers a built-in Computer Use copy.\nInstall and review the catalog plugin once when upgrading from that layout.\n\nNew user and workspace bundles are always untrusted and disabled. Discovery is\nread-only and does not inspect any other application's extension or credential\ndirectories: ambient roots such as `.claude/plugins` or `.cursor/plugins` are\nnever scanned.\n\nPre-v0.9.1 `overrides.json` enablement was intentionally not imported as\ntrust; every bundle activates only through the content-hash and\n`codewhale-plugin-capabilities-v3` activation-policy review below.\n\n## Manifest\n\nAll supported encodings parse into the same internal manifest, so validation, hashing,\nreview, and runtime behavior are identical downstream. On-disk auto-migration\nbetween them is deliberately not performed; `/plugin export <name>\n<target-dir>` publishes a loaded bundle as a spec-valid Agent Plugins v1.0.0\ndirectory without modifying the installed one.\n\n### `plugin.json` (Agent Plugins v1.0.0)\n\nThe standard's manifest root is closed: `$schema`, `name`, and the optional\nwell-known fields (`version`, `description`, `author`, `homepage`,\n`repository`, `license`, `keywords`, `extensions`). An unknown root key is a\nparse error. Client-specific data lives under `extensions`, keyed by\nreverse-domain namespace: unknown vendor namespaces are ignored, never\nrejected — that is what lets a bundle authored for another client load here —\nwhile unknown keys inside Codewhale's own `extensions[\"net.codewhale\"]`\nnamespace are rejected rather than silently dropped.\n\nNames follow the standard's rule: 1–64 lowercase ASCII letters, digits, or\ninternal single `-`/`.`, starting and ending alphanumeric, never `--` or `..`.\nA `skills/` directory in the bundle root is picked up automatically; other\ncomponent locations, `capabilities`, `when`, and `display_name` ride in\n`extensions[\"net.codewhale\"]`.\n\nMCP servers cannot live in `plugin.json` (the root is closed); they live in a\nsibling `mcp.json` under `mcpServers`, with `stdio`, `streamable-http`, or\n`sse` transports (`type` may be omitted and is inferred from `command` vs\n`url`). Codewhale-only server options — timeouts, tool filters, env-backed\ncredentials, enablement — ride per-server under `extensions[\"net.codewhale\"]`.\nThe `env` names `PLUGIN_ROOT` and `PLUGIN_DATA` are reserved by the standard\nfor the host runtime and are rejected in plugin definitions.\n\n### `plugin.toml` (legacy Codewhale format)\n\n```toml\nschema_version = 1\n\n[plugin]\nname = \"example\"\nversion = \"0.1.0\"\ndescription = \"Example instruction and MCP bundle\"\nauthor = \"Example Author\"\n\n[skills]\npath = \"skills\"\n\n[commands]\npath = \"commands\"\n\n[agents]\npath = \"agents\"\n\n[hooks]\npath = \"hooks\"\n\n[mcp_servers.local]\ncommand = \"node\"\nargs = [\"server.js\"]\ncwd = \"mcp\"\n\n[mcp_servers.remote]\nurl = \"https://example.invalid/mcp\"\n\n[capabilities]\nnetwork_hosts = [\"example.invalid\"]\n\n[when]\nos = [\"macos\", \"linux\", \"windows\"]\nbinaries = [\"node\"]\n```\n\nLegacy TOML names are 1–64 lowercase ASCII letters, digits, or internal\nhyphens. A pre-versioned manifest without `schema_version` still parses, with\na migration warning from `/plugin validate` (and `0.0.0` displayed when\n`[plugin].version` is missing). An unknown top-level table or field is a\nparse error (`deny_unknown_fields`), reported by byte offset without echoing\nmanifest values.\n\n### Validation (both formats)\n\nComponent paths must be relative, contained, present, and free of symbolic\nlinks or Windows reparse points (including junctions and mount points). The v1\nschema rejects unknown MCP fields, ambiguous local/remote\ntransport combinations, unbounded lists/timeouts, and overlapping tool\nfilters.\n\nRemote MCP URLs must use HTTPS, except for explicit loopback HTTP endpoints.\nThey cannot contain user information, a query, or a fragment. Literal headers\nare rejected: authentication must name a source environment variable through\n`env_headers` or `bearer_token_env_var`. A remote bundle must declare exactly\nthe normalized host set used by its endpoints in\n`capabilities.network_hosts`; endpoint scheme, normalized host, port, and path\nremain bound to the review. Redirects are limited and must retain that exact\nnormalized origin. Reviewed remote transports use an explicit no-proxy HTTP\nclient: plugin bundles never read or use ambient `HTTP_PROXY`, `HTTPS_PROXY`,\nor `NO_PROXY` values, because proxy credentials and proxy observation are\noutside the reviewed authority. User-authored MCP configuration keeps its\nexisting explicit proxy support.\n\nLocal stdio environment entries must use exact `${SOURCE_ENV}` references.\nThe review shows destination and source names, but never reads or prints their\nvalues. Plugin children inherit only Codewhale's base secret-scrubbed child\nenvironment plus those reviewed mappings; credential-capable proxy variables\nand the broader compatibility environment used by user-authored MCP\nconfiguration are not inherited ambiently. Absolute arguments and parent\ntraversal are rejected; contained bundle entrypoints are frozen to their\nstaged paths before spawn.\n\nEvery stdio argument is shown losslessly as a JSON string during review.\nCommon credential-bearing flags and known literal token shapes are rejected\nfrom argv; credentials must instead use a reviewed environment mapping.\nPlugin-contributed MCP OAuth has been disabled since v0.9.1 and remains\ndisabled as of v0.9.6, including discovery, login, refresh, and token storage;\na manifest declaring OAuth fields on a plugin MCP server fails validation.\n\n### Active and inactive component surfaces\n\nCodewhale 0.9.10 activates declarative `[skills]`, `[mcp_servers.*]`,\n`[commands]`, `[agents]`, and `[hooks]` components from its content-addressed\nruntime snapshot. Commands use markdown command files, Agents use Fleet TOML\nprofiles, and Hooks use `HooksConfig` TOML files. A component may name one file\nor a directory of the corresponding files. Ordinary user/workspace commands\nand Agent profiles keep precedence over plugin contributions; trusted project\nhooks run after plugin hooks.\n\nThe manifest can additionally inventory the following inactive surfaces.\nThose declarations stay hashed, reviewed, and displayed, but do not activate\nand no longer disable the whole bundle:\n\n```toml\n[lsp] # TOML alias: [lsp_servers]\npath = \"lsp\"\n\n[native] # TOML alias: [native_extension]\npath = \"native/index.mjs\"\n\n[capabilities]\nfilesystem_roots = [\"workspace\"]\nnetwork_hosts = [\"api.example.invalid\"]\nlifecycle_mutation = true\n```\n\n(In a `plugin.json` bundle the same tables ride under\n`extensions[\"net.codewhale\"]`.)\n\nThe accept/reject behavior is deliberately loud, never silent:\n\n- Compatibility is per-component: `full` when every declared surface has an\n adapter (or the bundle is empty), `partial` when supported components can\n activate beside named inactive surfaces, and `unsupported` when the bundle\n only declares surfaces Codewhale cannot activate yet. The same versioned\n activation policy (v3) drives those labels, the runtime adapters, and the\n capability hash. Executing LSP or native code must change that policy,\n which changes the capability hash and forces re-review. v1 and v2 trust\n receipts fail closed as `capabilities-changed`.\n- **`native` under the experimental extension host.** With\n `[features] extension_host` on, the policy becomes v4 and `native` is an\n active adapter: each entry is one `.mjs`, `.js` or `.mts` ES module file that the\n TypeScript extension host imports. A directory or any other file reports\n an error in `/plugin validate` and review and prevents activation. Its tools\n always use `Required` approval;\n Full Access, Bypass, or an exact session grant for the reviewed build can\n satisfy that gate without a prompt. Toggling the flag\n re-reviews every plugin. See\n [the design](design/TS_EXTENSION_HOST.md#as-built-phase-1-2026-09-25).\n The [extension author guide](EXTENSIONS.md) includes a tested typed example,\n diagnostic workflow and Node's erasable-TypeScript restrictions.\n- A **recognized-but-inactive** declaration (`lsp`, `native`, a non-empty\n `capabilities.filesystem_roots`, or\n `capabilities.lifecycle_mutation = true`) parses and is validated like any\n component (contained, present, link-free). It is counted in the inventory,\n hashed into the capability receipt, shown in review and `/plugin show` as\n inactive, and never executed (for `native`, only while the extension host\n flag is off). A reviewed, trusted, applicable mixed bundle\n can still be enabled: supported declarative components become active, and\n the inactive surfaces stay named as inactive.\n- An **all-unsupported** bundle can be reviewed and trusted, but `/plugin\n enable` fails closed and names the inactive surfaces. There is nothing\n Codewhale can honestly activate.\n- An **unrecognized** section or field is a validation failure, not an\n inventory entry: unknown top-level TOML tables, unknown MCP server fields,\n unknown `plugin.json` root keys, and unknown keys inside\n `extensions[\"net.codewhale\"]` are all rejected outright. The single\n ignore-without-error case is another vendor's `extensions` namespace in the\n Agent Plugins format, which the standard requires clients to skip.\n- `capabilities.network_hosts` is not a future surface: it is enforced today,\n and must exactly match the normalized host set of the bundle's remote MCP\n endpoints (so it cannot be declared without them, or omitted with them).\n\nA successful environment or health check is never treated as trust.\n\n## Review, trust, and enablement\n\nUse the in-session command surface:\n\n```text\n/plugin list\n/plugin validate example\n/plugin show example\n/plugin enable example\n```\n\nThe first `enable` opens a review showing source, component inventory,\nrequested permissions, sanitized MCP endpoints, full content and capability\nhashes, and inactive declarations. It also prints an exact confirmation:\n\n```text\n/plugin trust example <full-content-sha256>.<full-capability-sha256>\n```\n\nRun that exact command only after reviewing the bundle. The confirmation token\nuses both complete SHA-256 receipts rather than display prefixes. The\ncapability receipt is the v3 digest: it still hashes the complete inventory\nand also binds this build's activation policy (which adapters are executable\nversus inventoried-only). Trust first\ncopies the complete reviewed tree into a Codewhale-owned, content-addressed\nruntime snapshot and records the matching receipt; it does not activate\nanything.\nThen run `/plugin enable example` again. Trust and enablement are separate:\n\n- `/plugin disable example` stops contribution while preserving trust.\n- `/plugin revoke example` removes trust while preserving the enablement bit;\n the bundle remains inactive until reviewed again.\n- `/plugin reload` rebuilds the current workspace registry when files have\n changed on disk.\n- `/plugin status [name]` shows the existing inventory or one bundle's state\n and diagnostics. `/plugin restage <name>` restores a missing runtime snapshot\n only when its source still matches the enabled review and current policy.\n It preserves the receipt and generation; an existing changed snapshot is\n refused and needs review.\n\nNormal runtime loading also attempts that verified recovery for missing\nsnapshots. A failed automatic attempt keeps its reason and requires an explicit\nretry for the same review. Recovery does not authorize changed source bytes.\n\nFrom a shell, `codew plugins status [name] [--json]` inspects the same captured\nplugin roots without carry-forward, cleanup or recovery writes. A built-in\nwhose review needs upgrade carry-forward can therefore appear unreviewed\nuntil normal runtime loading. `codew plugins restage <name> [--json]` performs\nan explicit verified repair. Use `/plugin reload` in an already running\nterminal session to refresh its registry after a repair from another process.\n\n(`/plugin install`, `update`, and `uninstall` place, replace, and remove the\nbits themselves and always drop into this same review — see\n[PLUGINS.md](PLUGINS.md). `/plugin suggest` ranks installed bundles and\nany locally added marketplace catalogs; sending a matching task can toast the\nsame next step without installing anything. Nothing is written into the\nmodel's request to advertise plugins; the full offering policy is in\n[PLUGINS.md](PLUGINS.md#how-codewhale-offers-plugins).)\n\nTrust, enable, restage, disable, revoke, and reload rebuild the current workspace's\nSkills, MCP, Commands, Agent profiles, and Hooks immediately. Each persisted\ntransition advances a per-bundle generation under a stable cross-process lock.\nA generation change cancels in-flight MCP work, removes cached catalog\nentries, terminates an idle plugin stdio child, and denies persisted queued\nSkills carrying the older authority receipt.\n\nThe review distinguishes remote MCP endpoints from local stdio MCP servers.\nA local stdio server is a child process running with the Codewhale user's host\nfilesystem and network authority; plugin trust is not an OS sandbox. The\nreview therefore shows the command, argument count, working directory,\nenvironment-variable names, and this host-authority warning without printing\nenvironment or header values. MCP tool approval still applies after the\nserver starts.\n\nTrust receipts live in `~/.codewhale/plugins/state.json`. Atomic owner-only\nwrites record the full content hash, capability hash, reviewed capability\ninventory, generation, and review time, with the latest 32 reviews retained as\na bounded audit trail. Malformed or unsupported state is not overwritten: all\nbundles fail closed until the state file is repaired or moved.\n\nThe content hash covers the manifest, complete bundle tree, and executable\nshape in deterministic path order, including local MCP entrypoints and\ncompanion assets. Staging is bounded, rejects symbolic links and unsupported\nfile kinds (plus every Windows reparse point and hard-linked files), uses an\natomic destination swap, and applies owner-only runtime permissions or ACLs\nthrough validated object handles on Windows. The capability hash covers the\nnormalized component and permission inventory. A source or staged-content\nedit, capability change, or unsafe runtime-root replacement invalidates the\nreceipt deterministically; an already-enabled bundle becomes inactive until\nit is reviewed again. This is the same invalidation `/plugin update` relies\non: replaced bytes stop matching the receipt, forcing re-review.\n\n## Runtime behavior\n\nAn active bundle must be enabled, trusted for its current hashes, applicable to\nthe host, and free of validation errors. A reviewed mixed bundle may be\nactive, but only supported components in the reviewed v3 activation mask are\nconsumable. Unsupported components remain listed, hashed, reviewed, and\ninactive.\n\n- Skills are exposed only as `<plugin>:<skill>`. The model-facing catalogue and\n `load_skill` use an in-memory snapshot bound to the reviewed staged tree,\n rather than reading a mutable source path at execution time. `load_skill`\n revalidates source, stage, receipt, workspace, and generation immediately\n before releasing content and fails closed on drift. Queued messages persist\n the same provenance and repeat that check at dispatch. `/skills inspect`\n identifies the reviewed bundle without exposing its mutable source path.\n- MCP server names are exposed as\n `plugin-<plugin-name-byte-length>-<plugin>-<server>` so hyphens in either\n component cannot create an authority collision. Disabled or untrusted\n bundles are denied again at the headless MCP adapter. Authority is checked\n before connection, immediately before every lazy stdio spawn, after\n transport construction, and before each tool/resource/prompt operation.\n Persisted generation/enablement/trust state is also watched while an\n operation is in flight, so disable, revoke, or another cross-process state\n transition cancels the operation and terminates a plugin stdio child. Full\n source and staged-tree hashes are revalidated at dispatch/catalogue\n boundaries; the runtime does not continuously re-hash those trees during an\n already-running MCP call. Source or stage drift therefore fails the next\n boundary and drops the stale connection/catalogue entry, but is not claimed\n to interrupt a call already executing. Every failure includes instructions\n to reload, review, trust, and enable the bundle again.\n- Commands load after ordinary user/workspace commands and saved workflows, so\n existing definitions keep precedence and collisions are visible. The\n palette hides a revoked command immediately; dispatch rechecks the full\n receipt before expanding its body and reports a visible denial on stale\n input.\n- Agent profiles join the Fleet roster below explicit config, personal, and\n workspace profiles but above built-ins. Roster collisions retain the\n existing visible shadow record. Every Agent spawn rebuilds from the current\n registry and rechecks the selected plugin profile's authority before its\n prompt or route can be used.\n- Hooks merge after global hooks and before trusted project hooks. Foreground\n Hooks recheck authority immediately before process spawn; background Hooks\n check before enqueue and again at dequeue so a queued, revoked Hook cannot\n start later.\n- Plain launch, resume, fork, exec, and serve each construct an immutable\n workspace-scoped registry before constructing their plugin-backed catalogues.\n- Constitution, repository instructions, permission rules, sandbox policy,\n and MCP tool approval continue to outrank plugin instructions.\n\n`/plugin list`, `show`, `suggest`, and `validate` perform no network requests,\nprocess launches, credential reads, or configuration writes. Reviews render\nstructural argv as lossless JSON strings and environment provenance without\nvalues. Credential-bearing argv is rejected at manifest validation;\nplugin-originated errors suppress URL query, authentication, argv, and\nenvironment material. Legacy executable tools under `[tools].plugin_dir`\nremain a distinct system and are listed under `/plugin tools`; they cannot\napprove themselves or replace built-in tools (see\n[CONFIGURATION.md](CONFIGURATION.md#script-tools-and-overrides)).\n\n## Explicit non-goals as of v0.9.10\n\nFederated marketplace catalogs (`/plugin marketplace add|list|show|remove|install`)\nparse local Kimi-, Claude-, Codex-, and Codewhale-format catalog documents; see\nthe marketplace section below (`/plugin install` fetches\none reviewed source, and `/plugin suggest` ranks only what is already\ninstalled), no ambient compatibility discovery, no automatic trust, no\nplugin-contributed MCP OAuth, no LSP adapter or MCP subscription adapter, no\nnative extension runtime outside the experimental `extension_host` flag, no\nforeign executable plugin runtime import, and no on-disk auto-migration of a\nlegacy `plugin.toml` to `plugin.json`. The explicit offline\n[OpenCode/DSH converter](PLUGIN_AUTHORING.md#convert-an-existing-plugin) supports\nselected portable Skills, static Streamable HTTP MCP declarations, and\nexplicitly packaged Node `.mjs`, `.js`, or `.cjs` MCP servers selected with `--stdio-root`.\nLocal source and dependencies are copied for the same native installation,\ncapability review, hash-bound trust and enable flow; conversion executes no\ncode or package manager. It does not migrate arbitrary bundles or reproduce\nanother client's runtime or policy.\nThe other capabilities above remain later work rather than implied support.\n\n## Marketplace catalogs (#5311)\n\n`/plugin marketplace` reads LOCAL catalog documents in the real published\nschemas (Kimi, Claude, Codex, Codewhale native; Codex via its policy markers)\nand renders every candidate with an honest install plan:\n\n```text\n/plugin marketplace add <name> <path> # parse a local catalog file (no network)\n/plugin marketplace list # catalogs + candidates + diagnostics\n/plugin marketplace show <name> # one catalog in detail\n/plugin marketplace remove <name> # forget a catalog (plugins unaffected)\n/plugin marketplace install <catalog> <candidate>\n```\n\n- `add` never fetches anything: it reads one local JSON file (≤4 MiB, regular\n files only, symlinks refused) and stores the parsed catalog next to the\n plugin state file.\n- Catalog tiers and provenance (`official`, `curated`, …) are **display\n only** — they never grant trust, enablement, or installation.\n- Foreign policies are visibly ignored: a Codex `INSTALLED_BY_DEFAULT` entry\n is listed with a `NO_AUTO_INSTALL` warning and nothing is installed until\n an operator runs the install verb.\n- Catalog npm package declarations, `command:` sources and non-tarball URLs\n are listed as `not installable` with the reason. Explicit\n `/plugin install npm:[email protected]` and `codewhale install npm:[email protected]`\n instead fetch an exact-version npm registry tarball containing a native plugin\n manifest. They do not run npm lifecycle scripts or install dependencies; see\n [install sources](PLUGINS.md#sources).\n- `install` routes through the same reviewed installer as `/plugin install`:\n the bundle lands disabled and untrusted, and enters the hash-bound trust\n review before anything activates.\n"
|
||
},
|
||
{
|
||
"path": "docs/PLUGIN_MARKETPLACE.md",
|
||
"sha256": "93e8eeb319da86064d8288940e5c13ccfc47c99e1c6a50e0d20604212651bfef",
|
||
"content": "# First-party plugin marketplace\n\nCodewhale includes an offline snapshot of the `codewhale` catalog in the same\nmarketplace store consumed by the terminal, Extensions, recommendations, and\nRuntime API. Browse the [public first-party marketplace](https://github.com/codewhale-hq/codewhale-plugin-marketplace)\nfor current entries and their prerequisites. Its contents can be newer than\nthe snapshot in your installed CLI. Browsing a catalog does not fetch or\nexecute a plugin.\n\n```text\n/plugin marketplace list\n/plugin marketplace show codewhale\n/plugin marketplace install codewhale whalewiki\n/plugin show whalewiki\n/plugin trust whalewiki\n```\n\nThe last command prints a review. Run its exact confirmation command containing\nboth full hashes, then `/plugin enable whalewiki`. Review the manifest and\ncapabilities before trust and enablement. Installation\nuses the existing size-limited, traversal-safe installer and starts disabled\nand untrusted. An update with changed bytes requires review again. Official\ncatalog provenance grants no execution or network permission. Removing the\ncatalog persists the choice and leaves installed plugins untouched; a locally\nadded catalog named `codewhale` takes precedence over the bundled snapshot.\n\nThe bundle source uses a gzip tarball URL with `#path=plugins/whalewiki` (or\n`#path=skills`). The fragment selects exactly one bundle inside the shared\nrepository archive. Only that subtree is installed. Empty paths, traversal,\nambiguous roots, links, oversized archives, and changed plugin identities are\nrejected. The install receipt preserves the source, including its selector,\nso `/plugin update` retains the same bundle selector and reviewed revision.\n\n## Catalog freshness and pinned installs\n\nThe published v0.10.2 CLI contains the 19-entry catalog from marketplace revision\n`2e3a6fba453c9b34d5b1444ab5bd6edc7b119189`. Current source and the public\nmarketplace include later additions, such as Office and Studio; that does not\nmake them part of the v0.10.2 snapshot. Use `/plugin marketplace show codewhale`\nto inspect the catalog your session actually has.\n\nThe bundled catalog is a build-time snapshot. It does not poll the public\nrepository. To use a separately reviewed catalog revision, obtain a local copy\nof the public marketplace, inspect its changes, then add its local document:\n\n```text\n/plugin marketplace add codewhale /absolute/path/codewhale-plugin-marketplace/marketplace.json\n/plugin marketplace show codewhale\n```\n\nRe-adding the same document refreshes its saved contents. If `codewhale` already\nnames a different local source, remove that catalog first with\n`/plugin marketplace remove codewhale`, then add the intended document. Catalog\nremoval does not remove installed plugins or change their trust.\n\nRepository entries in the bundled snapshot pin an immutable marketplace commit.\nRemote entries keep their declared URL and any archive checksum. The current\npublic Computer Use entry pins a versioned `computer-use.tar.gz` asset and its\nSHA-256; it does not follow GitHub's latest-release address. `/plugin update`\nre-reads the installed bundle's recorded source, including its pin. It does not\nselect a new source from a refreshed catalog.\n\n### Changing an installed pin\n\nFor a user-scope bundle installed by Codewhale, retain its original source and\nany local changes before replacing it: uninstall deletes the installed copy.\nAfter refreshing and reviewing the catalog, this example selects its current\nWhaleWiki entry:\n\n```text\n/plugin show whalewiki\n/plugin disable whalewiki\n/plugin uninstall whalewiki\n/plugin marketplace install codewhale whalewiki\n/plugin trust whalewiki\n```\n\nRun the exact hash-bound confirmation printed by the new review, then\n`/plugin enable whalewiki`. Do not apply this sequence to built-in or workspace\nbundles: update and uninstall accept only user-scope installed bundles. A local\ncatalog's `path:` source records that local directory, so keep it available for\nfuture `/plugin update` calls. A changed source still needs review and does not\nsilently inherit an earlier receipt.\n\n## Computer Use across versions\n\nFor the current helper with published CLI v0.10.2, follow the [direct MCP setup](https://github.com/codewhale-hq/codewhale-cu-plugin#codewhale-cli-setup), which uses the app’s bundled runtime. Disable an enabled older built-in before using that connection. Direct MCP supplies tools separately from plugin commands and skills.\n\nThe published v0.10.2 CLI embeds Computer Use 0.12.1. To inspect that older bundle, start with\n`/plugin show computer-use`, review with `/plugin trust computer-use`, run the\nprinted hash-bound confirmation, then `/plugin enable computer-use`. A catalog\ninstall of the same name reports that the built-in already exists; it does not\nreplace it with a newer release. Direct installation of a second bundle with\nthat name is refused. Built-in bundles cannot be updated or uninstalled through\nthe user-plugin commands.\n\nThe current Engine source candidate no longer creates or discovers the embedded\ncopy. On that layout, install `computer-use` from the catalog as an ordinary\nuser plugin, then review, trust and enable it. Its old snapshots and receipts\nremain intact; a built-in receipt cannot authorize the downloaded bundle.\nThis source transition does not establish a published CLI migration path.\n\nComputer Use has its own [published releases](https://github.com/codewhale-hq/codewhale-cu-plugin/releases)\nand [contribution guide](https://github.com/codewhale-hq/codewhale-cu-plugin/blob/main/CONTRIBUTING.md).\nInstall the native Mac helper separately using the\n[setup guide](https://codewhale.net/computer-use) for system permissions and\nPause/Stop controls. A helper release, a plugin bundle and an Engine release\nhave separate versions; their presence alone does not qualify a combination.\n\n## Chromewhale\n\nChromewhale (listed as \"Codewhale for Chrome\") is in the catalog bundled with\nCore from marketplace revision `ae3dd22` on, where its checks pass on macOS,\nLinux and Windows. It is a developer preview: installing the plugin does not\ninstall the browser side, so you load its bundled Chrome extension unpacked\nyourself. It then reads and acts on the tab you are looking at, one granted\nsite at a time. Like every catalog entry, it installs disabled and untrusted\nuntil you review it.\n\n## Keeping the repositories current\n\n| Content | Authoritative source | Copies to check |\n| --- | --- | --- |\n| Catalog, WhaleWiki, Whalesong, Cloudflare Docs | `codewhale-hq/codewhale-plugin-marketplace` | Core catalog snapshot |\n| Bundled skills | Core active catalog and `crates/tui/assets/skills` | Marketplace `skills` and `skills/upstream.json` |\n| Computer Use | `codewhale-hq/codewhale-cu-plugin` | Versioned release archive and checksum in the catalog; no current runtime mirror |\n\nThe `Marketplace connection` workflow validates the exact catalog revision on\ncatalog changes. Its weekly and manual runs compare the current public\nmarketplace, bundled snapshot, and skills. Drift fails\nthe check with a maintenance instruction; it does not rewrite user installs\nor grant new permissions. It uses read-only repository access.\n\nFor each intentional update, review the upstream diff, synchronize the\nsource-owned copies, and run the marketplace checks:\n\n```sh\n# From codewhale-plugin-marketplace, with sibling source checkouts:\n# After committing canonical skill changes, when intentionally updating skills:\nnpm run sync:skills\nnpm run check -- --core ../codewhale\nnpm test && npm run check:web\n```\n\nCommit the reviewed marketplace changes, then update Core from that committed\nrevision (the generator never copies an uncommitted marketplace document):\n\n```sh\n# From codewhale:\npython3 scripts/sync-marketplace.py --marketplace ../codewhale-plugin-marketplace\npython3 scripts/sync-marketplace.py --marketplace ../codewhale-plugin-marketplace --check\nnpm test && npm run check:web\n```\n\nReview the generated snapshot and rebuild Core. Its provenance records the\nexact marketplace commit. Local bundle URLs pin that immutable revision; remote\nentries retain their declared source. Updating the bundled catalog requires a\nnew Core snapshot; users may explicitly add a reviewed local catalog without\nrebuilding Core. Neither change retargets an existing pinned install. Push the\nreviewed marketplace revision before publishing a Core release that references it. Hosted CI must be green for the\nactual published revisions; local checks do not prove a public URL works.\n\nComputer Use source and checks belong to its canonical repository. Publish its\nqualified `computer-use.tar.gz` release asset before publishing a catalog that\nreferences that address. Do not copy its runtime or native helper into Core.\n\nSkill wording changes also need behavioral evaluation before claiming better\noutcomes. See [Skill evaluation](SKILL_EVALUATION.md).\n"
|
||
},
|
||
{
|
||
"path": "docs/EXTENSIONS.md",
|
||
"sha256": "46d2443e6068f17c6c904a4d2fe1ffcec393d7c86b708bb467ec68423ecba1ee",
|
||
"content": "# Writing an extension tool or command\n\nThe experimental TypeScript extension host runs reviewed plugin code in a\nshared Node process (or, as an opt-in, Bun). Rust still owns sessions, tool admission, approval and\nexecution. Extensions currently contribute tools and slash commands; they\ncannot provide an approval service, run a second agent loop or replace\nbuilt-in tools or commands.\n\n## Quick start\n\nStart with [hello-extension](examples/plugins/hello-extension/hello.mts) and its\n[plugin.json](examples/plugins/hello-extension/plugin.json). The example is\nexecuted unchanged by the host tests. It returns a greeting and does not read\nfiles or use the network.\n\n1. Explicitly enable `[features] extension_host = true` in your configuration,\n or start Codewhale with `codewhale --enable extension_host` (the flag goes\n before any subcommand).\n2. Install the example's directory using `/plugin install <local-directory>`.\n3. Run `/plugin validate hello-extension`, then `/plugin show hello-extension`.\n4. Run `/plugin enable hello-extension` to open its content/capability review.\n Read the review and personally run its exact `/plugin trust <token>` command,\n then enable the plugin. Trust alone does not execute the code.\n5. Ask Codewhale to use `hello_greet`. The tool follows the normal approval gate.\n6. Run `/hello-greet Codewhale`. The command runs when you type it, and shows\n its answer in the transcript with its origin, `extension:hello-extension`.\n\nAn authoring agent should stop after installation and validation and present\nthe review to the person. Do not trust or enable a bundle automatically.\n\n## Locations, installation and trust\n\nUser bundles live under `~/.codewhale/plugins/`; workspace bundles live under\n`<workspace>/.codewhale/plugins/`. New bundles use Agent Plugins v1.0.0\n`plugin.json`. Declare `extensions[\"net.codewhale\"].native.path` as one regular\n`.mjs`, `.js` or `.mts` file inside the bundle. Directories and other extensions\nfail validation when the host is enabled. With the flag off, native entries\nremain inventory-only.\n\nA plugin may split its code across several entries with `native.paths` (up to\n64, alone or beside `path`). They are activated in the order declared, as the\nfibers of one plugin: they share one owner, so one disable, review change or\ncrash tears all of them down together. Each entry is a separate module with its\nown `apply`, and tool and command names must be unique across them. If an entry\nfails to activate (throws, requires a service the host does not provide, or has\na registration refused), only that entry is withdrawn: entries that already\nactivated stay registered, later entries still activate, and the plugin fails\nonly when no entry activates. Listing the same file twice is one entry.\n\n## Runtime\n\n`[extension_host] runtime` selects what runs the host. Node is the default.\nBun is an opt-in: it is qualified on macOS only, and becomes the default only\nafter an explicit, recorded cutover.\n\n```toml\n[extension_host]\nruntime = \"node\" # default: Node only\n# runtime = \"bun\" # Bun only; fails rather than falling back to Node\n# runtime = \"auto\" # Bun >= 1.4.0 if one is found and starts, otherwise Node\n# bun = \"~/.bun/bin/bun\" # the only Bun tried when set\n# node = \"/opt/homebrew/bin/node\" # the only Node tried when set\n# mcp_backend = \"rust\" # temporary opt-out; default is \"host\"\n```\n\nThe default `mcp_backend = \"host\"` uses the pinned MCP SDK independently of\n`[features] extension_host`, which controls optional Native extensions. Existing\nstdio and HTTP servers use modern discovery when supported; explicitly configured\nlegacy SSE servers use their legacy handshake. Rust retains process and network\nauthority, credential resolution, catalog admission, tool approval and cancellation.\nThe SDK receives opaque session selectors and exact operation grants. Modern\nrequests carry Rust-approved protocol metadata and headers without a session id.\n\nA typed discovery refusal can admit a legacy handshake. Only an explicit HTTP\ntransport refusal during that handshake can admit legacy SSE with a fresh exact\nRust grant. Legacy HTTP retains its GET preflight and bounded OAuth refresh;\nmodern HTTP does not inherit those session or resend behaviors. Incomplete result\nflows are reported without automatically repeating the operation. Credentials,\nconfigured URLs, OAuth browser/login operations and Computer Use decision keys\nstay in Rust.\n\nBoth backends share recorded semantic MCP goldens and retain separate transport\ngoldens, including discovery, authentication and session evidence. The Host default\nrequires those fixtures and the actual broker acceptance checks, plus the platform\nisolation and measured gates in Ops CURRENT_DECISIONS §26. An explicit\n`mcp_backend = \"rust\"` remains available for one release after the qualified default\nflip, before the native protocol adapters retire. Missing or unsupported Node uses\nthe existing doctor runtime diagnostic. Host failures never select Rust automatically.\n\nThe stock finance, data and speech adapters have independent experimental flags:\n`[features] finance_host = true`, `data_host = true` and `speech_host = true`.\nEach defaults to Rust. Speech preparation is shared by CLI `speech`/`tts` and\nthe model tools; clone samples, provider requests and output writes stay in Rust.\nSelecting either uses the pinned Builtin harness and its existing operation\nbroker, even when Native extensions are disabled. Rust retains network/file\naccess, parsing diagnostics and permissions. A selected Host failure is reported;\nit never silently switches to the Rust adapter.\n\nNode must satisfy `^22.19 || >=24`; Bun must be 1.4.0 or newer. Leaving\n`runtime` unset means `node`, except that a table which sets only `bun` means\n`bun`. A configured `node` or `bun` path is the only candidate for that\nruntime: if it does not run or is below the floor, the host fails with that\nreason instead of searching `PATH`. Without one, every `node` (or `bun`, then\n`$BUN_INSTALL/bin`, default `~/.bun/bin`) on `PATH` is tried in order, and one\ninside a `node_modules` directory or the working directory is skipped without\nbeing run; set the path explicitly to use such a runtime. The working-directory\ncheck does not apply when Codewhale starts in a directory that contains your\nhome directory, such as `~` or `/`.\n\nUnder `auto`, if the Bun it found fails to start the host, `/plugin` reports\nwhy once and Node runs the host for the rest of the session. The runtime is\npinned once a host on it completes its handshake, and restarts reuse it. A\nruntime binary replaced at that path mid-session is refused; restart Codewhale\nto use it. `codewhale doctor` and `/plugin` show which runtime runs the host,\nits version and path, how its memory cap is enforced, and, when `auto` used\nNode, why Bun was not used.\n\nWhen the host cannot take a call, `/plugin` and the failing extension tool\ncall give the same reason: not started, starting, unresponsive, restarting\nafter a crash (with how it exited), failed to start or out of crash budget\n(with the reason; change or reload a plugin to retry), or disabled by config.\nEvery call into the host has a deadline, 120 s for a tool call; past it\nCodewhale cancels the call, reports a timeout, and the host keeps serving\nother calls. A plugin that ignores cancellation keeps running inside the host\nuntil the host is torn down.\n\nWrite extensions for both runtimes. Use Node's APIs (Bun implements them) and\nonly erasable TypeScript in `.mts`. Enums, decorators and syntax that needs\ntransformation require a separate author build to JavaScript. Bun would accept\nmore, but Node would not. Node never reads a `tsconfig.json` for the host. Bun\ndoes: a `tsconfig.json` next to or above an extension's files applies its\n`paths`, `baseUrl` and JSX settings to that extension's imports, so an import\nthat resolves under Bun can fail under Node. Do not rely on it. Known limit:\nthat includes a `tsconfig.json` in a directory above the reviewed bundle,\nwhich is not part of what you reviewed. See\n[Node's TypeScript rules](https://nodejs.org/docs/latest-v22.x/api/typescript.html).\nUnder Bun the host runs with `--no-install`: a missing package fails the import;\nit is never downloaded.\n\nReviewed DSH compositions admit every local module against its recorded path\nand SHA-256 before importing it. Node checks runtime resolution; Bun prepares\nthe reviewed JavaScript syntax and checks computed imports before they run.\nUnreviewed files and ambient package imports are refused. Bun currently cannot\npreserve query/fragment module identity, and computed CommonJS resolution also\nrequires Node. These cases stop with a diagnostic: select\n`[extension_host] runtime = \"node\"` for that composition. Ordinary UTF-8 source\nand reviewed computed file/JSON imports work on both runtimes.\n\nExtensions cannot run native code inside the host. On both runtimes\n`process.dlopen`, `process.execve` and Worker threads are unavailable (a Worker\nis a new JavaScript realm that would start without these restrictions). Under\nBun, `bun:ffi`, `Bun.FFI`, `bun:sqlite`, `node:sqlite` and `ShadowRealm` are\nunavailable too; under Node, `node:sqlite` and `node:ffi` are switched off\n(SQLite extensions and FFI load native libraries). The host refuses to start\nwhen one of these restrictions does not hold on the installed runtime. Known\nlimit: these are the native-code entry points found so far (Bun 1.4, Node 22\nand 26); one a newer runtime adds is not covered until it is added. A\nprocess an extension starts is outside this policy; on macOS and Linux it runs\nunder the same sandbox as the host.\nInclude local imports in the bundle; trust stages reviewed content, and the\nentry is rehashed before import. Changes to reviewed bytes or capabilities\nrequire another review. See [bundle rules](PLUGIN_BUNDLES.md).\n\n## Imports and services\n\nExport a Cordis plugin function or an object with `apply`. The host supplies\none shared Cordis and the supported DSH compatibility services. The example's\n`inject = ['tools', 'commands']` asks for the tool and command registries. The\nsupplied service names are `tools`, `commands`, `prompt`, `storage`, `skills`, `shellHooks`, `avatars`, `mcp`, `logger`,\n`events`, `reflect` and `registry`. `ctx.avatars.registerPack({path})` registers a\nreviewed bundle-relative avatar pack; see\n[the whale-avatar example](examples/plugins/whale-avatar/README.md). A tool can ask the core to run a core tool through\n`exec.core` (see [Asking the core to run a tool](#asking-the-core-to-run-a-tool));\nReviewed Native entries can propose MCP definitions through `ctx.mcp` as\ndescribed below. Programmable pre-execute listeners use `ctx.on` as described below. A required\nservice that is unavailable fails activation with a diagnostic.\n\nPackage runtime dependencies and local imports within the reviewed bundle.\nDo not install packages or fetch code during activation. Register cleanup\nthrough `ctx.effect`; asynchronous disposers are awaited with a deadline.\n\n`ctx.prompt.registerSection({ id, text })` contributes an attributed plain-text\nsection and returns its disposer. Rust delivers the complete current snapshot\nas a user-role runtime message; changes replace earlier snapshots and an empty\nsnapshot explicitly withdraws earlier sections. Rust admits\nsections only for the caller's live reviewed owners, sorts them by owner/id,\nand withdraws them on unregister, disable, revoke or host exit. Each section\nis limited to 4 KiB UTF-8 text, each owner to 32 KiB and 128 sections, and the\nhost to 128 KiB and 1,024 sections. The final attributed prompt has the same\n128 KiB limit. IDs use lower-case letters, digits, `_` and `-`, start with a\nletter, and contain at most 64 characters. Duplicate owner-local IDs require\ndisposing the earlier section first. The author cannot replace the core\nsystem prompt or select another session's sections.\n\nTo interpolate Core's accepted turn facts, register\n`{id, text, interpolate: 'model-cwd'}`. Only `{{model}}` and `{{cwd}}` are\nsupported; Rust expands them once when it captures the turn's prompt. Literal\nsections retain braces unchanged. Both source and expanded text must fit the\nsection and snapshot limits. Scoped contributions keep their exact selected\nentry identity through capture and withdrawal. The fixed DSH persona bridge\nuses this path for additive prefix/suffix text; complete prompt replacement and\nruntime-context suppression are refused.\n\nReviewed DSH compositions can register Claude Code and Codex command hooks\nthrough `shellHooks`. Codewhale's existing fifteen firepoints use the same\npinned Builtin runner and Rust-owned process driver when the host feature is\nenabled. Rust retains hook approval, process environment and final verdicts;\nShellEnv values remain in Rust. Forced continuation, observer steering,\nnoncommand hooks and asynchronous dialect commands are unsupported and\nreported explicitly. A Native hook's `allow` cannot approve a tool.\n\n`ctx.storage.get(key)`, `set(key, json)` and `delete(key)` persist owner-local\nJSON under the `dataDir` Rust assigned. The API refuses access once owner\ndisposal begins, symlinked storage, corrupt data and writes that exceed its\nbounded key/value/owner limits. It does not expose session history or secrets.\nTool and command invocations also expose frozen `sessionId`, `agentId` and\n`originTurnId` strings when Rust supplies them for that particular call. Tool invocations\nwith an authoritative session and workspace also expose frozen `exec.agent.id`\nand `exec.agent.session.header.cwd`; root identity uses the session id and a\nchild uses its supplied agent id. The projection has no methods and retaining it\ngrants no operations after the invocation ends.\n\nAn entry injecting `sessions` can call `ctx.sessions.flush(exec.agent.session)`\nwhile its directly invoked root tool holds an Engine checkpoint ticket. The\nargument must be that invocation's exact session object. The Engine captures\nits committed prefix and waits for the owning Runtime, terminal, or exec store\nto acknowledge persistence. Runtime retains its existing receipt-secret\nredaction policy; success describes that persisted projection. It does not\ninclude the running tool's future result or the plugin's separate journal.\nRuntime recovery retains later tool results without replaying the tool.\nCheckpoint-bearing Runtime turns require schema version 5; older readers\nrefuse them rather than silently reconstructing incomplete history.\n\n`false` is reserved for a deliberately ephemeral host with no persistence\nbackend. Missing authority, foreign or expired handles, child checkpoints,\nsuperseded writes, storage failure, cancellation and unavailable consumers\nreject. Cancellation can race a disk write, so a rejected call does not prove\nnothing was written. Other `sessions` methods remain unsupported.\n\nAn entry injecting `subagents` can start one independent child agent from its\ndirectly invoked root tool. `ctx.subagents.getProvider('spawn')` describes the\nonly provider: a fresh child with no inherited conversation.\n`ctx.subagents.startActivation({ provider: 'spawn', childId, request: { parent:\nexec.agent, prompt, agentOptions: { model } }, signal, delivery })` runs the\nentry's own `agent/created` listeners first. There the child's\n`ctx.systemPrompt.section({ name: PERSONA_PREFIX_SECTION, ... })` sets a persona\nof at most 32 KiB, appended to that child's own system prompt and recorded in\nits launch manifest. `ctx.tools.restrict({ deny })` adds to the tools the child\nmay not use. Then the Engine starts the child as an ordinary `agent` start\nthrough this turn's approval gate. The plugin's call needs its own approval and\nnever reuses the model's grants. The child runs in Codewhale's existing\nsub-agent manager inside the parent's permission and budget limits. The\nreturned activation resolves once the child accepted its first message.\n`result` waits for the child to finish and returns its text output and stop\nreason. `dispose()` stops that exact child and its own descendants.\n`delivery: 'caller'` hands the result only to the plugin, not to the parent\nmodel. A child the plugin never disposes is stopped when the invocation ends.\n`ctx.tools.get(name, exec.agent)` reports whether the calling agent has a tool.\nPrompts are text only. Only the `model` agent option is accepted. The service\nis unavailable inside a child agent's own tool calls. Non-text output and the\nother DSH agent APIs are not supported, and\n`agents` and root-level `systemPrompt` offer no methods.\n\n## MCP definitions\n\n`ctx.mcp.registerServer({ serverName, server })` returns an idempotent disposer\nand proposes a literal MCP definition to the existing Rust catalog. Include\n`mcp` in the entry's `inject` list. Rust owns transport connections, tool\nadmission, permissions and authentication; this service does not expose\ncredentials or grant extensions direct process or network access.\n\nDefinitions retain their exact reviewed, selected Native entry receipt.\nThe limits are 64 servers per owner, 256 per host and 64 KiB per definition.\nLiteral stdio, streamable HTTP and SSE definitions are supported by the bridge;\ncredential values, endpoint query data and unsupported startup/reconnect\ncontrols are refused. Disposing an entry, changing its caller selection,\ndisabling its plugin or changing reviewed bytes withdraws its definitions and\ncancels affected calls. An uncertain write is never replayed.\n\nThe raw DSH skill-filesystem bridge uses the same reviewed skill-root service\nbelow. Its configuration must explicitly set `includeDefaultRoots: false` and\n`watch: false`, and list bundle-relative `customSkillDirs`. Ambient filesystem\nroots and independent watchers remain unsupported.\n\n## Skill roots\n\n`ctx.skills.registerRoot({ path: 'profiles/review-skills' })` returns an\nidempotent disposer and contributes reviewed instructions to Rust's existing\nskill catalog. Include `skills` in the entry's `inject` list. For example:\n\n```js\nexport const inject = ['skills']\nexport function apply(ctx) {\n ctx.skills.registerRoot({ path: 'profiles/review-skills' })\n}\n```\n\nPlace each skill in a child package directory such as\n`profiles/review-skills/quick-check/SKILL.md`. Paths are bundle-relative,\nat most 512 UTF-8 bytes, with normal slash-separated components. Absolute\npaths, links, empty components, `.` and `..` are refused. Rust parses only\nfiles covered by the reviewed bundle inventory, through the existing\n[frontmatter and invocation contract](SKILLS.md#invocation-and-alias-metadata).\nThe existing nesting rules apply: hidden child directories are skipped, and\na package containing `SKILL.md` claims its nested examples. An invalid skill\nor an empty root refuses admission. Native review covers this registration;\nthe entry does not need a separate declarative Skills component.\n\nPending and admitted roots count toward the host shim's limits: 8 roots per\nowner and 64 per host. Rust stops each root proposal at 128 candidate\n`SKILL.md` files or 4 MiB of raw input. Retained instruction fields are also\nlimited to 128 skills / 4 MiB per owner and 1,024 skills / 32 MiB per host.\nThese are logical catalog limits. Duplicate owner-local root paths require\ndisposing the earlier root first.\n\nDisposal, disable, revocation and host exit remove the root from later\ndiscovery. Loading a selected skill rechecks the reviewed Native receipt and\nthe live registration. Queued selections also recheck it; selections saved\nbefore a process or host restart require selecting the skill again. Completed\nsession history remains the record of instructions already used. Roots have\nno watcher, companion-file access or tool permission grant; core approval and\nsandbox policy continue to apply.\n\n## Tool rules\n\nRegister a unique name with `ctx.tools.register`, a description, a JSON object\ninput schema and an `execute(input, exec)` function. Names start with a letter,\ncontain only letters, digits, `_` and `-`, and are at most 64 characters. Use a\nplugin-specific prefix, such as `hello_greet`. Core names, core approval-name\nfamilies, and `mcp_`/`ext_` prefixes are reserved. Refusals explain the rule.\n\nEvery extension tool is `Required` and never treated as read-only based on a\nplugin's claim. Full Access, Bypass or an exact session grant for the reviewed\nplugin receipt may satisfy that requirement without another prompt. Tools are\ndeferred by default; `[tools].always_load` can pin a tool through the normal\ntool configuration. Registration never grants permission to execute it.\n\nThe core checks every call's input against that schema (JSON Schema, draft\n2020-12 unless the schema names another) before it asks for approval and again\nbefore anything is sent to the host, so `additionalProperties: false`,\n`required`, types and bounds hold even when `execute` does not check them. A\ncall that fails is returned to the model as an invalid-input error naming what to\ncorrect, and the host never sees it. A schema that cannot be compiled (an\ninvalid keyword value, or a `$ref` to anything outside the schema itself;\nnothing is fetched) is refused at registration, which fails activation with the\nreason. Declare the narrowest schema you can: it is both what the model sees\nand what is enforced.\n\nReturn a JSON value or text; the host renders it into ordinary tool output.\nAvoid secrets in descriptions, logs and results. An approval card's wording\nand identity come from Rust, never from plugin-supplied labels.\n\nFor DSH definitions with `output`, declare both `output.schema` and\n`output.render`. The host validates the canonical JSON result before rendering\nit. This adapter currently accepts plain text blocks only; unsupported rich\ncontent or extra block fields produce an explicit error instead of disappearing.\nA boolean `deferLoading` is accepted as a presentation hint; the Engine\nkeeps its existing tool-loading policy. Non-boolean values are refused.\nNonempty registration options, `when`, `projectContent`, `finalizeContent`\nand `timeoutMs` remain unsupported and are refused. The shared\n[`compatibility.json`](../crates/engine/extension-host/src/dsh/compatibility.json)\npins the definition API to DSH 0.2.1-rc.5 and records service, row-bridge and\nmodule mappings. Its bytes participate in Native content and capability review,\nso changing the compatibility contract invalidates prior trust.\n\nSuccessful and soft-failure tool results carry Rust-stamped `extension`\nmetadata identifying the plugin, reviewed content, entry, generation, caller\nselection, handle and process boot. Plugin-supplied output cannot choose this\nidentity.\n\n## Command rules\n\nRegister a slash command with `ctx.commands.register`, which returns an\nidempotent disposer (the registration is also removed when the plugin unloads):\n\n```ts\nctx.commands.register({\n name: 'hello-greet', // /hello-greet\n description: 'Greet someone.', // one line, shown in the palette and /help\n argumentHint: '[name]', // optional; a command with a hint waits in\n // the composer for arguments\n handler({ args, signal }) {\n return { kind: 'success', text: `Hello, ${args || 'world'}!` }\n },\n})\n```\n\nNames are lower case, start with a letter and use only `a-z`, `0-9`, `_` and\n`-` (at most 64 characters). A command can never take the name of a built-in\ncommand (or one of its aliases) or of another plugin's command: the\nregistration is refused, activation fails, and the reason is in `/plugin`.\nA user, workspace or plugin-manifest markdown command with the same name wins\nthe spelling and the extension command is left out of the registry. Descriptions\n(1 KiB) and hints (256 bytes) are single-line text.\n\nA handler runs only when the **user** types the command; invoking it is the\nuser's own action and needs no approval. It can only return an answer:\n\n- a string, or `{ kind: 'success', text? }`: shown in the transcript;\n- `{ kind: 'error', text }` or a thrown error: shown as a failure;\n- `{ kind: 'submit', prompt, text? }`: `prompt` enters the ordinary turn with\n Runtime provenance and visible plugin attribution. Every model tool call\n is gated as usual. `text` is shown beside the prompt.\n\nThe handler cannot call the model, a tool or approval. Output is stripped of\nterminal escape sequences and cut at 64 KiB; a prompt over 128 KiB is refused,\nnot truncated. A command has 30 seconds: past it Codewhale cancels the call\n(`signal` aborts) and reports a timeout. If the host is down, the command\nfails at once with why.\n\n`args` is what follows the command name, trimmed. The invocation also carries\n`signal`, a `commandId` and an empty `attachments`. DSH plugins using\n`ctx.commands.register({ name, description, input: { hint }, handler })` and\nreturning `{ kind: 'success' | 'error', text }` run unchanged, including\n`import { CommandDefinitionId } from '@deepseek-ai/dsh-commands/brand'` (the\nonly `dsh-commands` import the host resolves; `definitionId` is accepted and\nignored). DSH's `rawInput` is provided (it keeps the leading separator,\n`' name'`). When Rust supplies the caller session, `invocation.agent` exposes\nits read-only id and `followup(message)`. A handler may capture exactly one\nJSON user message with a bounded id, a source containing only `kind`, and one\nplain text content block. Successful completion converts it to the same Submit\nresult; an error result, cancellation or exception never submits it. Explicit\nSubmit plus followup, multiple messages, attachments, empty text and text above\n128 KiB are refused. The handle closes when the invocation settles. Core owns\nthe resulting turn identity and provenance; this is not a general agent service\nor persistence of the supplied DSH message identity. Missing caller context\nrefuses access.\n\nNot provided: attachments (`input.attachments: true` fails activation), a `list`/`find`/\n`execute` surface, and DSH command lifecycle events. The Runtime API records\nCodewhale command admission, completion and cancellation in its existing event\nstore.\n\nThread snapshots also include bounded `command_receipts` for the most recent\n100 terminal commands that did not submit a turn. Each receipt has a stable\nidentity, completion time, status, text and extension attribution. This is a\nprojection of the existing event log; reopening a native App conversation can\ndisplay the result without rerunning a command or adding it to model history.\n\n### Runtime API and native App\n\nRead `GET /v1/commands?thread_id=<id>` to discover commands for that thread's\nworkspace and plugin selection. Extension entries have `kind` and `binding`\nset to `extension`, an `origin` and an opaque `registration_id`. A listing\nwithout a thread has no invocable registration identity.\n\nSubmit `name`, `args`, `registration_id`, `operation_key` and\n`expected_workspace` to `POST /v1/threads/<id>/commands`. Reuse the same\noperation key only for the exact same request. A completed retry returns the\nstored outcome; a pending or uncertain invocation is never executed again.\n`GET /v1/threads/<id>/commands/<operation_key>` reads the result. If a recorded\ninvocation has lost its worker, lookup settles an explicit uncertain outcome\nand recovers any admitted turn without rerunning the callback. `DELETE`\nrequests cancellation, including cancellation before admission and interruption\nof an associated active turn. A stale\nregistration or changed workspace requires discovering and selecting again.\n\nThe Engine admits a returned prompt through its ordinary turn lifecycle with\nRuntime provenance. TUI and App present it as attributed plugin input and exclude\nit from ordinary human recall, edit and retry. Clients must not resubmit that prompt. Results include the\naccepted turn when present and the exact extension receipt. The native App\nuses these routes for its command palette and slash input, preserves edited\ndrafts and refreshes the catalog after plugin changes and reconnects.\n\n## Execution context\n\nA tool's `execute(input, exec)` gets, in `exec`:\n\n- `signal`, the call's cancellation signal; check it and propagate it to\n asynchronous operations;\n- `callId`, which identifies the call, and `args`, its input (also the first\n argument);\n- `workspace`, the root path of the workspace of the session that made the\n call, and no other (it is absent only if that path is not valid UTF-8);\n- `dataDir`, the plugin's own directory;\n- `core`, only while the call runs under the turn's permission gate:\n [`exec.core.call`](#asking-the-core-to-run-a-tool).\n\nA command's handler gets `workspace` (where the user ran it) and `dataDir` in\nits invocation beside `args`, `signal` and `commandId`. All of these are\nread-only strings (`exec` and the invocation are frozen). Nothing else about the\nmachine is passed: no home directory, no other workspace, no credential path.\nA call id or plugin trust grants no new privileges.\n\nThe workspace is where the *call* comes from, so read it per call: one host\nserves every session in the Codewhale process, and a tool called from another\nworkspace sees that one. The host process's working directory is its data\ndirectory, not the caller's workspace; do not use `process.chdir` in this\nshared process, and resolve relative paths against `exec.workspace` yourself.\nReading a workspace file is your code's own filesystem access, under the host's\nsandbox, not something the core checks per file.\n\n`dataDir` is `~/.codewhale/extension-host/data/plugins/<name>-<id>`, created by\nCodewhale (mode 0700 on Unix) before the plugin activates, and the same\ndirectory for every generation and every entry of that plugin, so it keeps\nfiles across restarts and updates. It sits inside the host's one writable\nroot, which is where the sandbox allows writes. It is not a boundary between\nplugins: they share a process, and one can write into another's directory.\nCodewhale never deletes it, including on uninstall.\n\n## Configuration\n\nA user configures a plugin in their own `config.toml`, keyed by the plugin's\nmanifest name:\n\n```toml\n[plugins.\"hello-extension\".config]\ngreeting = \"Howdy\"\n```\n\nThat table is delivered as the second argument of `apply(ctx, config)`\n(`{}` when there is none). If the entry module exports a `Config` schema\n(`import Schema from '@deepseek-ai/schemastery'`; `export const Config =\nSchema.object({ ... })`), the host validates the table against it before\n`apply` runs, applies its defaults, and a mismatch fails activation with the\nreason, so the plugin only ever sees a config its own schema accepts. Every\nentry of a multi-entry plugin gets the same table and checks it against its own\n`Config`. [hello.mts](examples/plugins/hello-extension/hello.mts) reads one\nsuch setting.\n\n- It is the user's data, not part of what was reviewed: the user can change\n it without a new trust review, which is why only the user's config can set it\n (a project's `.codewhale/config.toml` cannot).\n- Plain TOML values only (a date-time is refused), at most 16 KiB serialized\n and 16 levels deep. A table over a limit fails that plugin's activation with\n the reason, in `/plugin show`; it is never delivered cut short.\n- `/plugin show <name>` lists the configured keys (not the values) and says\n when the config is refused. Do not put a secret in it: the plugin's code\n reads every value, and so does any other plugin in the shared process.\n- Codewhale reads `[plugins]` at start, and again at `/plugin reload` (or any\n plugin command that changes plugins). A plugin whose table changed is revoked\n and activated again as a new generation, with the new values; one whose table\n did not change is left alone. A file that cannot be read keeps the previous\n settings.\n\n## Programmable tool admission\n\nA reviewed native mod may register `ctx.on('tools/pre-execute', async (exec, next) => ...)`. The frozen call view carries `name`, `callId`, `arguments`, `signal`, `workspace`, `mode`, and `model`. It has no session, agent, tool, or approval handle. `next()` returns an abstention; Rust evaluates the remaining listeners and gates.\n\nReturn `{kind:'deny', reason}`, `{kind:'ask', reason?}`, `{kind:'revise', input}` (a JSON object), `{kind:'annotate', text}`, or `{kind:'abstain'}`. `undefined` also abstains. DSH-shaped `allow` is an abstention and logs a warning once per owner. A malformed answer, thrown error, timeout, or withdrawn owner fails the call closed. The listener batch has a five-second deadline and observes cancellation through `exec.signal`.\n\nNative hooks run first, then mod listeners in core registration order. Denial always wins; the last accepted input revision wins. Rust re-prepares revised arguments and reruns the existing authority, policy, and approval checks. Input revisions use the existing 32 KiB hook limit; context and reasons use the existing sanitizers. Unloading a fiber removes its listener, and disabling or revoking an owner retires its admitted handles before host teardown. Other workspaces do not receive the call.\n\nThis bridge runs on Engine sessions, including model tools, gated code-mode calls, and `exec.core` calls. The standalone ACP execution path has no TypeScript host attachment. Prepend/global listener ordering, DSH runtime/agent handles, around-execution wrappers, and post-result rewriting are not provided. Native `tool_call_after` remains an observer contract.\n\n## Asking the core to run a tool\n\nA tool can ask the core to run one of the core's tools for it:\n\n```ts\nasync execute({ path }, exec) {\n if (!exec.core) return { error: 'not run under the turn gate' }\n try {\n const { content, isError, structured } = await exec.core.call('read', { path })\n return { content, isError }\n } catch (error) {\n // error.name === 'CoreCallError'; error.code is 'refused' | 'denied' |\n // 'cancelled' | 'unavailable' | 'failed'\n return { error: error.code, message: error.message }\n }\n}\n```\n\nThe call is planned and approved exactly like a call the model makes: the\nallow and deny lists, hooks, Auto-Review, repo law, the worker authority\nenvelope and the approval card all apply. **You never decide any of that.**\nThere is no way to approve, to supply a card's text, an argv, a URL or a\nticket. Rust composes the card (\"Requested by `extension:<plugin>` from inside\nits tool `<tool>`\") and decides whether one is shown.\n\n**When `exec.core` exists.** Only when the model called your tool directly and\nthe turn loop is serving its permission gate for that call. It is absent for a\ncommand, a timer, activation code, and a tool run from inside\n`execute_tools` (code mode gives nested tools no gate); the core refuses\na `core/call` from any of them. It also ends with the call: when your `execute`\nreturns, fails, times out or is cancelled, or your plugin is disabled, or the\nhost exits, pending core calls are cancelled and a waiting approval card is\nwithdrawn (recorded as cancelled; an answer given afterwards changes nothing).\nCancelling your `exec.signal` cancels them too, and `call(name, input, {signal})`\ncan cancel one.\n\n**Refused outright** (`error.code === 'refused'`, nothing runs, no card): every\ntool code mode refuses (`execute_tools`, the interpreters, `agent`, `workflow`,\n`rlm`, `request_user_input`, interactive shells, sandbox escalation, Computer\nUse consent and scripts, MCP sign-in); any extension tool, yours included (no\nrecursion); tool search and tool-result retrieval; the memory writer\n(`remember`) and tools that change what the session may do or schedule work\n(`request_plugin_install`, goals, automations, `send_later`, starting MCP\nservers); and **every MCP tool, Computer Use included** (not in v1). Names match\ncase-insensitively and after the core resolves aliases and after any hook\nrewrites the call.\n\n**What prompts.** Every call needs approval except a read-only, workspace-local\ntool from a short list (`read`, `read_file`, `list_dir`, `file_search`,\n`grep_files`) when nothing else asks for one. Your plugin's own approvals are separate: the user approved your\ntool, not what it asks the core to do. Approval keys are scoped to your plugin\nbuild: a grant the user gave the model for a tool never covers your call of it,\nand a grant for your call never covers the model's. **Shell and network calls\nforce a prompt**: a session grant is not consulted. Ask and Full Access\nboth ask the user every time; explicit session denials still refuse the call.\nAuto-Review and Never refuse it without opening a card. In Full Access every\nother call an extension makes is auto-approved, as it is for the model.\n`error.code === 'denied'` is the user's \"no\": do not retry it.\n\n**Limits**, per invocation: 50 core calls in all, 4 at once, and one approval\ncard at a time; per host, 256 requests in flight. A host that presents invalid\ntickets in a burst is ended as a protocol violation. Your `tool/call` deadline\n(120 s) stops while a core call waits on an approval card. The result is the\ntool's text (`content`) and, when it was JSON, the parsed value\n(`structured`); a long one is cut with a note, and images are dropped. What your\ntool asked the core to run is recorded (bounded) in your tool result's\n`core_calls` metadata, with each call's decision and outcome.\n\n## Lifecycle, diagnostics and restarts\n\nUse `/plugin show <name>` for that plugin's owner state, live tool names and\nup to 20 recent retained diagnostic messages. The host retains a bounded 64-entry\nshared diagnostic ring; this view is not a persistent per-plugin log. Warnings,\nerrors, activation/refusal/fault messages and teardown outcomes are attributed\nto the plugin when known. Display text is escaped. `/plugin list` includes\noverall host health and bounded diagnostics.\n\nDo not block the event loop. Heartbeats mark an unanswered host Unresponsive\nafter 3 seconds and kill it after 10 seconds. Unexpected exits restart with a\nshort backoff; the third crash within 5 minutes stops automatic recovery.\nOpening another engine does not reset that budget. Explicit plugin changes or\nreload retry deliberately. Launch failures also allow a newly attached engine\nto retry after a one-minute cooldown.\n\nRecovery verifies current attachments and persisted trust again, then creates\nfresh owner tokens and tool handles. Outstanding calls fail and are never\nreplayed; failed/faulted receipts remain suppressed until explicit retry or\nchanged authority. Two dirty teardowns within 10 minutes request maintenance\nwhen active calls finish. This restart preserves the unexpected-crash budget.\nDispose within 2 seconds and avoid leaving background work behind.\n\n## Sandbox\n\nTrust is not a complete security boundary. Native plugins share one process\nand data-access cohort and can interfere with each other. Owner labels and\nJavaScript method locks do not isolate mutually untrusted plugins.\n\nNative hosts use a positive filesystem-read policy on macOS Seatbelt and\nLinux bubblewrap (`/usr/bin/bwrap`). Grants cover the selected runtime and\nrequired system files, the host bundle, Core-managed reviewed snapshot stores,\nand the Native data/private temporary directories. Ordinary workspace files,\nincluding project `.env` files, are outside those grants. Child processes\ninherit the same boundary. Direct network access is denied; writes stay in\nthe Native data and private temporary directories. The general shell profile's\nCargo, npm and Darwin cache write grants do not apply to this Native profile.\n\nSnapshot-store grants include other reviewed snapshots in the shared Native\ncohort, not just the currently selected entry. They do not grant repository\naccess. A plugin that directly runs `git` over the user's project therefore\nneeds a separately scoped operation bridge before it can work in this profile.\n\nNative extensions require a verified OS wrapper at each launch. On Linux,\nmissing bwrap or a failed namespace probe refuses Native activation and reports\nthe concrete error. Windows uses the existing LPAC filesystem/network wrapper\nand refuses Native activation when that wrapper cannot be established. Source\nsupport is not evidence of platform or bundled-artifact qualification. The pinned Builtin tier retains\nan explicitly diagnosed unsandboxed exception; every effect still requires\nRust operation tickets. That exception does not qualify Native extensions or\nthe mandatory-host/default-runtime cutover. Planning creates the sibling\nBuiltin data directory before masking it for Native launch. Review the\n[current design limits](design/TS_EXTENSION_HOST.md) before enabling\nthird-party code.\n\n## Limits\n\nThe host process has a 1 GiB memory cap. On Linux it is `RLIMIT_DATA` (or a\nlower hard limit Codewhale itself inherited) and on Windows the Job Object's\nper-process limit, so an allocation past it fails; both also apply to each\nprocess an extension starts. Those two have been tested with a Node host only.\nOn macOS the Bun host applies a jetsam limit to itself before any extension\nloads, and the kernel kills it past the cap; processes it starts are not\ncovered. A Node host on macOS has no kernel limit: Codewhale checks its\nresident size at each 3-second heartbeat and kills it past the cap. Under Node\nthe JavaScript heap is also limited to 256 MiB. The host has a\n32 MiB frame limit, 256 in-flight request\nlimit (plus a reserved heartbeat), 128 tools per owner and 1024 per host, and\n64 commands per owner and 256 per host. Tool\ndescriptions are at most 4 KiB and schemas 64 KiB. Tool calls have a 120-second\ndeadline and commands a 30-second one. The feature stays Experimental and off by default; local fixture\ntests do not establish sandbox parity, provider behavior or release readiness.\n"
|
||
}
|
||
]
|
||
},
|
||
{
|
||
"id": "runtime-api",
|
||
"slug": "runtime-api",
|
||
"sources": [
|
||
{
|
||
"path": "docs/RUNTIME_API.md",
|
||
"sha256": "f4aa394d8b349d7dcecbb85f55c3af4a08f77339728a8aec39b76b03b794f3ce",
|
||
"content": "# Codewhale Engine Runtime API & Integration Contract\n\n> 阅读简体中文版:[zh_hans/RUNTIME_API.md](zh_hans/RUNTIME_API.md)。\n\n`codewhale app-server --http` exposes the [Codewhale Engine](ARCHITECTURE.md) through\nits canonical local Runtime API and control plane.\nLocal SDKs, mobile/remote-control clients, and editor integrations talk to it\ninstead of screen-scraping terminal output. It serves the full HTTP/SSE runtime\nAPI (`/v1/*`), a JSON-RPC control transport over stdio, and the phone-friendly\nmobile page. `codewhale doctor --json` provides machine-readable health, and\n`codewhale serve --acp` speaks the Agent Client Protocol over stdio for editors\nsuch as Zed.\n\n`codewhale serve --http` / `serve --mobile` remain as **compatibility aliases**\nfor `codewhale app-server --http` / `--mobile`; both launch the identical\nserver. Desktop, web, SDK and custom integrations should target\n`app-server --http`. `--stdio` and `--socket` expose compatibility controls.\nBare `app-server` selects initialized JSONL control over stdio. Local stdio,\nsocket and ACP owners do not bind a hidden TCP listener. Explicit `--http`\nretains HTTP/SSE and compatibility routes on the same Runtime owner.\n\n`codewhale exec` is the separate one-shot headless worker path (stream-json,\nfleet worker subprocess, CI primitive). It is not part of this API, but it\nshares the same runtime, provider/model resolution, permission profiles, and\nevent vocabulary.\n\nThis document is the stable integration contract for native workbench\napplications (and other local supervisors) that embed the Codewhale Engine.\n\n## Architecture\n\n```\nlocal supervisor / SDK / automation harness\n │\n ├─ codewhale app-server --http → HTTP/SSE runtime API (/v1/*) [canonical]\n ├─ codewhale app-server --mobile → runtime API + mobile control page\n ├─ codewhale app-server --stdio → JSON-RPC control transport over stdio\n ├─ codewhale app-server --socket → same JSON-RPC over a unix domain socket (desktop daemon)\n ├─ codewhale doctor --json → machine-readable health & capability\n ├─ codewhale serve --acp → ACP stdio agent for editors such as Zed\n ├─ codewhale serve --mcp → MCP stdio server\n ├─ codewhale serve --http/--mobile → legacy aliases for `app-server --http/--mobile`\n └─ codewhale exec [args] → one-shot headless worker (stream-json)\n```\n\nThe engine runs as a local-only process. All APIs bind to `localhost` by\ndefault. No hosted relay, no provider-token custody, no secret leakage.\n\nFor the read-only record of what a thread or turn did, see\n[`docs/RECEIPTS.md`](RECEIPTS.md): `codewhale receipts` on the CLI and the\n`/receipt` routes under **Threads** below.\n\n## Runtime API entrypoints\n\n| Entry | Transport | Use |\n|---|---|---|\n| `codewhale web [--port 7878]` | HTTP/SSE on `127.0.0.1:7878` + embedded client | First-class loopback-only browser client; opens the default browser |\n| `codewhale app-server --http` | HTTP/SSE on `127.0.0.1:7878` | Full `/v1/*` runtime API (canonical) |\n| `codewhale app-server --mobile` | HTTP/SSE on loopback + `/mobile` | Runtime API + local mobile control page |\n| `codewhale app-server --stdio` | Initialized JSONL over stdio | Compatibility controls and explicit tool-free turns through the held manager; no hidden TCP listener or full Codex interaction channel. |\n| `codewhale app-server --socket [--socket-path P]` | RPC text messages over WebSocket on a `0600` Unix domain socket (macOS/Linux) or an owner-only named pipe (Windows) | Desktop daemon: multi-client, kernel peer identity checked, `daemon/attach` claim handshake. Windows native-app qualification remains open. |\n| `codewhale app-server` | Initialized JSONL over stdio | Same selection as `--stdio`; HTTP requires explicit `--http`. |\n| `codewhale serve --http` / `--mobile` | same server as `app-server --http`/`--mobile` | Compatibility aliases |\n\n`app-server --http` and `--mobile` launch the same mature runtime API server\nhistorically reached through `serve --http` — no routes or behavior changed, so\nevery endpoint documented below is identical across both entrypoints. The\nruntime API token is read from `--auth-token`, then `CODEWHALE_RUNTIME_TOKEN`,\nthen `DEEPSEEK_RUNTIME_TOKEN`; use `--insecure-no-auth` only with a loopback\nbind. The `serve` compatibility aliases keep their `--insecure` flag.\nBare `app-server` does not open an HTTP port. Selected attached HTTP listeners\nretain their explicit bearer and non-loopback security checks.\n\nInitialized stdio/private-socket controls support `thread/loaded/list` with\n`{cursor?: string | null, limit?: number | null}`. The result is\n`{data: string[], nextCursor: string | null}` from the held manager's loaded\nengine snapshot, including idle and archived engines still in memory. It does\nnot enumerate saved history or only running turns. Omit the limit for the full\nsnapshot. A zero limit is clamped to one entry when entries remain, so an\nempty page cannot retain an unchanged continuation. Pass the opaque\n`nextCursor` back unchanged; loading or\nunloading between pages can change the snapshot. The\n`app/thread_loaded_list`, `app/thread-loaded-list` and `app/request`\n`ThreadLoadedList` compatibility forms retain their thread-object wrapper and\nnow use that same loaded-engine authority. Full-tool `turn/start`, interaction\nresponses and subscription/reconnect parity remain unavailable.\n\nThe pinned `thread/name/set` control takes `{threadId: string, name: string}`\nand returns `{}` after the existing held owner persists the canonical title.\nThe legacy `thread/set_name`, `thread/set-name` and typed compatibility request\nretain their current parameter and response shapes. An empty or whitespace-only\nname clears the title through the same manager. The durable Runtime journal\ncontinues to publish `thread.updated`; the pinned `thread/name/updated` fan-out\nand per-connection subscriptions remain part of the unfinished event contract.\n\nDevice tokens minted by the master through `POST /v1/auth/client-tokens`\nhave an immutable `intent`: `watch` (the default when omitted) or explicit\n`drive`. Labels do not grant authority. Watch permits ordinary GET/HEAD reads,\nComputer display and one-use display tickets; it cannot mutate Runtime state,\nupgrade a protected HTTP read into a write channel, acquire/release control,\nor forward display input. Drive retains the existing Runtime/control authority\nbut cannot mint, list or revoke device tokens. Display tickets retain the\nissuing principal's intent; input still requires its current, live control lease.\n\n`GET /v1/runtime/info` advertises `capabilities.client_token_intents: true`,\nand the mint receipt returns `device_id`, `intent` and `expires_at`. A relay\ngrant issuer must require this capability before minting and validate that\nreceipt against the requested device/intent before exposing a token. Older\nEngines lack enforcement and must refuse relay grants through this issuer;\nan intent-like label or a successful legacy mint is insufficient. This change\ndoes not add account/Computer ownership scopes to Engine-local device tokens.\n\n### Workspace file suggestions\n\n`GET /v1/workspace/files/search?query=runtime&limit=20` returns\n`{\"paths\":[\"src/runtime.rs\"]}` through the existing authenticated `/v1/*`\nrouter. It searches only the server's configured workspace, not a thread's\nworkspace or the process's current directory. No workspace/path override is\naccepted. The response contains workspace-relative file paths with `/`\nseparators, never file contents, absolute paths, or directories.\n\n- `query` is a literal partial filename/path, without an `@` prefix, at most\n 256 UTF-8 bytes. Missing, empty, or whitespace-only queries return an empty\n list without walking the filesystem. No match also returns an empty list.\n- `limit` defaults to 20; accepted values are 1–100. Invalid limits, oversized\n queries, and unknown query parameters return HTTP 400.\n- Matching reuses TUI fuzzy `@file` discovery/ranking: case-insensitive path\n prefix matches first, then substring matches, alphabetically within each\n group. This is not glob, subsequence, content, or semantic search, and does\n not apply the TUI's personal frecency boosts.\n- Discovery shares the composer's ignore policy, including `.ignore` and\n `.deepseekignore`, always-discoverable AI directories, and the bounded\n hidden/gitignored local-reference fallback. The special `.agents`, `.claude`,\n `.cursor`, and `.deepseek` walks intentionally bypass ignore rules, as in\n the TUI. Ignore files are not confidentiality boundaries.\n- Directory symlinks are not traversed. Files are canonicalized and filtered\n for containment in the workspace before applying the result limit; external\n and broken file symlinks are omitted. In-workspace file symlinks may appear\n by their relative names. Suggestions are a filesystem snapshot, not\n authorization to read a file later; consumers must revalidate when opening it.\n\nDiscovery runs off the async executor, with the shared default depth of 10,\nat most 20,000 candidates, and a cooperative two-second discovery budget.\nResults are best-effort, not an exhaustive listing; a slow filesystem operation\ncan finish after that budget. Each request scans anew; there is no new index or\ncache. This read-only endpoint does not alter sessions or the pinned model\nprompt/tool prefix.\n\n### Workspace files and session artifacts\n\nNative clients browse and edit\nthe server's configured workspace through three authenticated routes. They\nread and write the workspace directly; there is no second file store, cache\nor index, and no path override: the workspace root is the only root.\n\n- `GET /v1/workspace/files?path=<dir>&limit=<1-2000>` lists one directory.\n `path` is workspace-relative with `/` separators; empty or `.` is the root.\n Each entry carries `name`, `path`, `kind` (`file`, `directory`, `symlink`,\n `other`), and for files `size` and `modified` (RFC 3339). Directories sort\n first, then names case-insensitively. `limit` defaults to 200; `truncated`\n reports a cut. `.git` is never listed or served, and symlinks are listed by\n name only: they are never followed, so `path=<link>` returns 403.\n- `GET /v1/workspace/files/read?path=<file>&offset=<bytes>&limit=<1-4194304>`\n returns one byte window of a regular file with `size`, `revision` (the\n SHA-256 hex of the **whole** file, not of the window), `modified`,\n `offset`, `bytes`, `truncated`, `encoding` and `content`. Text windows are\n `utf-8`; a window with a NUL byte, invalid UTF-8, or a split multi-byte\n character is `base64`. `limit` defaults to 256 KiB. Files above 16 MiB are\n refused with 413; a directory is 400; a link is 403; a missing file is 404.\n- `PUT /v1/workspace/files` with `{\"path\", \"content\", \"encoding\"?,\n \"expected_revision\"?}` writes one file atomically through the same confined\n opener Fleet artifacts use. `encoding` is `utf-8` (default) or `base64`;\n bodies above 4 MiB are 413. Creating a new file requires **no**\n `expected_revision` (and creates missing parent directories inside the\n workspace); overwriting requires the `revision` from the read that the\n edit was based on, and a stale or missing one is 409 with the current\n revision in the error message so the client can re-read and merge. This is\n optimistic concurrency, not a lock: two writers racing between the check and\n the write can still interleave. The response carries `path`, `size`,\n `revision`, `created` and `written_at`; 201 for a new file, 200 otherwise.\n Writes through a link, into `.git`, or to a directory are refused.\n\nEvery path is validated before any filesystem access: absolute paths,\nbackslashes, `.` or `..` components are 400, and each directory on the way is\nopened without following links (`O_NOFOLLOW` per component on Unix, reparse\npoint checks on Windows). These routes use the runtime bearer token like every\nother `/v1/*` route; they do not consult the model's tool permission posture,\nbecause the caller is the authenticated operator, not the model.\n\nSession artifacts are the oversized tool outputs a session recorded as\n`ArtifactRecord`s (`crates/tui/src/artifacts.rs`), stored under\n`sessions/<id>/artifacts/`:\n\n- `GET /v1/sessions/{id}/artifacts` lists the records a saved session carries:\n `id`, `kind`, `tool_call_id`, `tool_name`, `created_at`, `byte_size`,\n `preview` and the session-relative `path`.\n- `GET /v1/sessions/{id}/artifacts/{artifact_id}?offset=&limit=` reads one\n artifact with the same window, `revision` and `encoding` contract as the\n workspace file read. A record whose stored path is absolute or leaves the\n session directory is 403; a record whose file is gone is 404.\n\nThese routes only serve what a SavedSession indexes (plus immutable image\nevidence). A runtime turn's spills are read through the turn route below. An\nunbound runtime thread's engine has no SavedSession index at all, so for its\nspills the turn record is the only way in.\n\n#### Turn artifacts\n\nA turn records what it produced as typed references on its items and on the\nturn itself. Nothing is scanned to build them. Each fact is recorded where the\nbytes were written:\n- file tools and `apply_patch` report `size`/`sha256` in `mutation.files[]`;\n- spills report `artifact_digest`;\n- tool media reports `sha256`.\n\nThe workspace-level half comes from the turn's own restore points: the\n`pre_turn` and `post_turn` receipts in `TurnRecord.workspace_snapshots` (see\n\"Workspace restore points\" below), diffed by tree id in the existing side\nrepo. No second store, snapshot or event is involved.\n\nA reference (`TurnArtifactRef`) carries:\n\n| Field | Meaning |\n| --- | --- |\n| `id` | Stable within the turn. A file's id is `file_` plus the first 32 hex digits of SHA-256(path). A spill's is `art_<call>`. Media's is `art_image_<sha256>`. |\n| `kind` | `file`, `tool_output` or `media`. |\n| `path` | For `file`: workspace-relative with `/` separators. For `tool_output` and `media`: session-relative (`artifacts/...`). |\n| `change` | For `file` only: `created`, `updated`, `deleted` or `renamed`. A rename also has `previous_path`. |\n| `size` | Byte size. Absent when the file was deleted. |\n| `revision` | SHA-256 hex of the whole content. This is the value `GET /v1/workspace/files/read` reports as `revision`, and file-revert's `expected_hash` is `sha256:` + `revision`. Absent when the file was deleted, or for a delta blob over 16 MiB. |\n| `content_type` | For `media`: the exact media type. |\n| `session_id` | For `tool_output` and `media`: the artifact session that owns the bytes. |\n| `item_id`, `tool_call_id`, `tool_name` | The tool call that wrote it. Absent for a change seen only in the workspace delta. |\n| `source` | `tool_mutation`, `tool_output_spill`, `tool_media`, or `workspace_changed_during_turn`. |\n| `restore_snapshot_id` | A restore point `POST /v1/threads/{id}/file-revert` accepts for this path on this thread: the `tree_id` of a receipt recorded on this turn's `workspace_snapshots`, so the thread owns it whether or not it is bound to a saved session. For a tool write it is the call's `tool` receipt; for a delta change it is the turn's `pre_turn` receipt. Absent when the turn recorded no such receipt. |\n| `recorded_at` | When the reference was recorded. |\n\nWhere references appear:\n- **Items.** `TurnItemRecord.artifacts` lists what one tool call produced. It\n is set when the call completes, succeeded or failed, so `item.completed` and\n `item.failed` carry it live.\n- **Legacy projection.** `artifact_refs` is derived from `artifacts`. It holds\n only the workspace-relative paths of files that still exist: never spills,\n media or deleted files.\n- **Turns.** `TurnRecord.artifacts` is the turn aggregate. It is computed by\n one merge and ordered most recent first.\n - Spill and media refs are always kept.\n - File refs compose in item order: created then deleted drops the file,\n created then updated stays `created`, and a rename folds its origin.\n - Once the workspace delta settles, it is authoritative for the net change,\n `size` and `revision` of every path the snapshots can see. It adds files\n no tool receipt named, such as shell and sub-agent writes. It drops an item\n path the snapshots track but that ended the turn unchanged.\n - The aggregate is capped at 1000 refs, and `workspace.truncated` /\n `workspace.omitted` report the cut.\n\n`TurnRecord.workspace` follows the delta's lifecycle. It is `null` while the\nturn runs. `turn.completed` carries it with one of these states:\n- `pending`: the turn recorded both a `pre_turn` and a `post_turn` receipt,\n and their diff is still running. When it finishes, the runtime publishes\n `turn.artifacts`\n (`{turn_id, workspace, artifacts}`). That event may arrive after the next\n turn's `turn.started`, so key it by `turn_id`.\n- `settled`: the delta is merged. `pre_turn_snapshot_id` and\n `post_turn_snapshot_id` are the tree ids of the pair.\n- `unavailable`: no delta will come. `reason` says why:\n - `snapshots_disabled`, `workspace_too_large`, `too_many_files`,\n `unsafe_location`, `snapshot_failed`: the snapshot gates. A turn with a\n `pre_turn` receipt but no `post_turn` one keeps `pre_turn_snapshot_id`.\n - `not_captured`: the turn recorded no restore point: a compaction or purge\n operation, or a turn whose engine ended before taking one.\n - `runtime_restarted`: the process stopped before the delta settled. This is\n reconciled at startup and never recomputed.\n - `delta_failed`: the diff itself failed.\n\n `artifacts` still holds what the tool receipts recorded. `turn.artifacts` is\n published for every settlement outcome, so a client waiting on `pending`\n always hears back.\n\nWhat the delta means, and what it cannot see:\n- **It is a workspace diff, not attribution.** A delta change is everything\n that changed in the workspace while the turn ran. That includes an editor,\n another thread, or a background job writing the same workspace at the same\n time. `source: workspace_changed_during_turn` says exactly that.\n- **Excluded paths are invisible to snapshots.** This covers the built-in\n excludes (for example `node_modules/`, `target/`, `dist/`, `build/`,\n `.next/`, and binary and media extensions) and the workspace's `.gitignore`. A file tool's write to\n such a path is still reported from its receipt. A shell command's write to\n one is not reported at all.\n- **Shell writes have no per-call record.** A shell command's writes are\n never attributed to its item, only to the turn, and only when snapshots are\n enabled.\n\nRoutes:\n\n- `GET /v1/threads/{id}/turns/{turn_id}/artifacts` returns\n `{thread_id, turn_id, workspace, artifacts}` from the runtime store's turn\n record. While the turn runs, `artifacts` is merged from its items on the fly\n and `workspace` is `null`. An unknown turn, or a turn of another thread, is\n 404.\n- `GET /v1/threads/{id}/turns/{turn_id}/artifacts/{artifact_id}?offset=&limit=&revision=`\n reads one reference.\n - It uses the workspace file read's window contract (`size`, `revision`,\n `offset`, `bytes`, `truncated`, `encoding`, `content`) and adds:\n - `artifact`: the reference;\n - `source`: `workspace`, `snapshot` or `session_artifact`;\n - `current`: whether the workspace still holds these bytes, or `null`\n when that is not a question for this reference.\n - `revision` selects an intermediate revision one of the turn's items\n recorded. The default is the reference's own revision.\n - A `file` is served from the workspace when it still holds the recorded\n revision (`current: true`). Otherwise it comes from the turn's post-turn\n snapshot (`current: false`).\n - A `tool_output` or `media` reference is read under the session artifact\n root the writer used. The same confinement, image-manifest and integrity\n checks apply as for the session route.\n- `GET /v1/threads/{id}/turns/{turn_id}/calls/{tool_call_id}/changes?limit=`\n returns what **one** tool call changed, read from the same two restore\n points the engine recorded around it (the call's `tool:` receipt and its\n `post-tool:` partner on this turn):\n\n ```json\n {\n \"thread_id\": \"thr_1a2b3c4d\", \"turn_id\": \"turn_…\", \"tool_call_id\": \"call_…\",\n \"tool_name\": \"exec_shell\",\n \"state\": \"captured\", \"reason\": null, \"truncated\": false,\n \"files\": [\n {\n \"path\": \"out/result.json\", \"change\": \"created\",\n \"added\": 12, \"removed\": 0, \"size\": 210, \"revision\": \"<sha256 hex>\",\n \"restore_snapshot_id\": \"<pre tree id>\",\n \"diff\": \"@@ -0,0 +1,12 @@\\n+{}\\n\", \"diff_truncated\": false\n }\n ]\n }\n ```\n\n - This is the per-call counterpart of the turn aggregate, and the only\n surface on which a **shell command's own writes** are attributable to that\n command: a command produces no `metadata.mutation`, so nothing else names\n what it wrote. The files a file tool changed are here too, read from the\n same span.\n - `change` is `created`, `updated` or `deleted` (git's `A`/`M`/`D`, with a\n type change read as `updated`; the diff runs with `--no-renames`, so a move\n is a delete plus a create). `added`/`removed` are `null` for a binary path.\n - `size` and `revision` are the span's **end**, never the work tree as it is\n now. `revision` is bare SHA-256 hex; pass `sha256:` followed by that hex as\n `file-revert`'s `expected_hash`, or `absent` for a path deleted by the call.\n Both are `null` when the path was deleted here or is too large to read.\n - `diff` is the patch between the two restore points, cut at 64 KiB on a char\n boundary (`diff_truncated` says so). It is `null` when there is nothing to\n render: a binary path, a change with no content delta, or no patch.\n - `state: \"pending\"` means the recorded call is queued or in progress and\n its snapshot pair is incomplete. `reason` is `null` and `files` is empty;\n read again after settlement. Once both receipts exist, the state is\n `captured`, including when the call changed nothing.\n - `state: \"unavailable\"` means the settled span cannot be resolved, and `reason`\n says why: `call_not_bounded` (the call took no receipt — the engine judged\n it read-only, or the turn predates receipts), `post_snapshot_missing` (the\n opening receipt exists and the closing one was lost), `pre_snapshot_missing`,\n or `snapshots_pruned` (the receipts are still on the turn, but the side repo\n no longer holds the trees they name — snapshots are pruned to the newest few\n while turn records are durable, so an older turn's span is regularly\n unrecoverable, and a workspace whose store was deleted reads the same way).\n This is **not** the same answer as an empty `files` list, which means the\n span changed nothing, and the receipt's own `changed_paths` remain readable\n on the turn record either way.\n - `limit` (default 200, max 1000) caps the list; `truncated` says it was cut,\n and how many paths were left off is not counted.\n - Reading the span runs `git diff` inside the side repo. Neither the work\n tree nor the index is touched.\n\n| Status | When |\n| --- | --- |\n| 404 | Unknown thread or turn, a turn of another thread, or a `tool_call_id` this turn has no item or receipt for. |\n| 400 | `limit` outside `1..=1000`. |\n| 500 | A runtime item record could not be read or parsed, or an operational failure occurred reading the snapshot repository; this is not evidence that the call was unbounded or snapshots were pruned. |\n\nThe artifact routes answer:\n\n| Status | When |\n| --- | --- |\n| 404 | Unknown thread, turn or artifact id, or a `revision` this turn never recorded. |\n| 409 | A file's recorded revision is in neither the workspace nor the snapshot store (snapshots are pruned after 50 per workspace or 7 days), or a session artifact's bytes no longer hash to the recorded revision. The message names the current revision. |\n| 410 | The turn deleted the file (restore it with `file-revert` and `restore_snapshot_id`), or a session artifact's bytes were pruned. |\n| 413 | Content over 16 MiB. |\n| 403 | A symlink, or a reference that leaves its root. |\n\nFleet receipt artifacts keep their own route\n(`GET /v1/fleet/runs/{run_id}/receipts/{task_id}/evidence`).\n\n### Runtime and account identity\n\n`GET /v1/runtime/info` reports `codewhale_version` plus the full 40-character\n`codewhale_commit` embedded by the shared CLI/TUI build. A source archive that\ncannot provide an exact commit reports `unknown`, allowing compatibility\nclients to fail closed rather than accepting an ambiguous binary pair.\n\nThe same response advertises `capabilities.account_session: true` and\n`capabilities.turn_operation_idempotency: true`. A client must require the\nlatter before relying on `operation_key`; do not infer support from a 2xx turn\nresponse because an older tolerant reader may ignore an unknown request field.\n`capabilities.turn_operation_lookup: true` separately advertises the read-only\noperation lookup below; clients must require it before relying on GET-based\nrecovery of a lost turn response.\n\n`capabilities.thread_create_idempotency: true` separately advertises\n`POST /v1/threads` with `operation_key` (or `operationKey`). Keys use the same\nvalidation as turn operation keys and are scoped to the Runtime store owner.\nExact retries return the original thread, including after response loss or\nrestart; a different effective request with the same key returns HTTP 409.\nThe fingerprint includes the resolved model/provider, workspace, reasoning,\npermission and shell policy, tool allowlist, archival state, prompt/task fields,\ndynamic tools and environments. Creation defaults are resolved before comparison.\nOperation records retain only fingerprints and reserved identity, never the raw\nkey. Missing committed thread data is an error, never permission to create a\nreplacement. Clients must require this capability before relying on creation\nreplay, because an older reader may silently ignore an unknown request field.\nThe response also includes a token-free account receipt:\n\n```json\n{\n \"account\": {\n \"schema_version\": 1,\n \"state\": \"authenticated\",\n \"api_base\": \"https://api.codewhale.net\",\n \"account_id\": \"acct_...\",\n \"session_id\": \"session_...\",\n \"scopes\": [],\n \"expires_at\": \"2026-08-01T20:00:00Z\"\n }\n}\n```\n\nThe Runtime reads this receipt from the exact profile- and API-origin-scoped\nsecure record written by `codewhale account login`; it does not run a second\nlogin flow. States are `signed_out`, `authenticated`, `offline_cached`,\n`expired`, or `revoked`. Scopes are copied only from explicit stored session\ngrants and are never inferred from account identity. Access/refresh tokens,\nemail, provider profile, and provider credentials are never returned.\n`account_id` and `session_id` are included only for a request authorized with\nthe Runtime token (or an explicitly insecure loopback server); the public\nbootstrap response remains usable but reports `signed_out`. Signed-out local\nWork remains supported and never allocates cloud compute implicitly.\n\nThe native host account routes below require the configured master token in\nthe Authorization header. Browser/mobile device sessions and insecure mode do\nnot authorize them. Responses use `Cache-Control: private, no-store`. These\nroutes are excluded from the desktop renderer's Engine request allowlist.\n\n| Route | Native host operation |\n| --- | --- |\n| `GET /v1/account/status` | Token-free account state, exact API origin/profile, opaque revision and whether legacy import is permitted. |\n| `POST /v1/account/claim` | Renew a login claim only for the supplied `expectedRevision`, preserving existing account identity. |\n| `DELETE /v1/account/claim` | Cancel exactly the supplied `expectedRevision`; a newer claim is preserved. |\n| `POST /v1/account/import` | Adopt the host's completed browser authorization with `expectedRevision` and `auth`; legacy import requires pristine storage, while explicit sign-in supplies `replaceExisting`. |\n| `POST /v1/account/lease` | Return a short-lived access credential to native main memory; Rust owns refresh and durable custody. Optional `rejectedAccessToken` requests renewal only if that credential is still current. No refresh token is returned. |\n| `DELETE /v1/account/session` | Sign out exactly the supplied `expectedRevision`, preserving provider credentials and local work. A changed revision returns conflict rather than signing out another account. |\n\nThe lease is sensitive native-host transport, separate from the token-free\nruntime receipt above. Native clients retain it only in main memory and fence\nresponses by Engine connection and account/session identity. Shared account\nrecords use revision nonces and signed-out tombstones so cancelled or stale\nlogin completion cannot restore an account after logout. Older authenticated\nrecords remain readable; older binaries fail closed on signed-out tombstones.\n\nThe `--stdio` control transport uses newline-delimited RPC. Request IDs are\nstrings or signed 64-bit integers. Output omits `jsonrpc`; input also accepts\nunknown input fields, as the pinned Codex decoder does. Complete `initialize`\nonce and receive its result. Clients then send the ID-absent `initialized`\nnotification before ordinary requests. It has no reply or authority effect;\nas in the pinned implementation, server initialization is committed by the\nsuccessful initialize request. Repeated initialization and business requests\nbefore initialize refuse. Malformed envelopes are discarded\nwithout a fabricated null-ID response; invalid-ID method objects follow the\npinned notification fallback and cannot become business calls. Probe it\nwithout spending model tokens:\n\n```bash\nprintf '%s\\n' \\\n '{\"id\":\"init\",\"method\":\"initialize\",\"params\":{\"clientInfo\":{\"name\":\"probe\",\"version\":\"1\"}}}' \\\n '{\"method\":\"initialized\"}' \\\n '{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"healthz\"}' \\\n '{\"jsonrpc\":\"2.0\",\"id\":2,\"method\":\"capabilities\"}' \\\n '{\"jsonrpc\":\"2.0\",\"id\":3,\"method\":\"shutdown\"}' \\\n | codewhale app-server --stdio\n```\n\nLocal control and ACP frames have an 8 MiB encoded body limit, excluding the\nnewline separator. Serialization stops at that limit before writing an\noversized frame. Body, newline and flush share a 30-second backpressure\ndeadline; owner attachment, request forwarding and frontend readiness keep\ntheir tighter 5-second envelopes. A partial write, flush failure or deadline\nends the affected stream; it cannot be followed by another response or an\nautomatic replay. This write deadline also applies to compatibility stdio\nreplies/events and relay stdout. Compatibility execution, history, provider\ncatalogs, output-limit preflight and saved-config reload call the same held\nnative frontend directly; they do not make loopback HTTP requests.\n\n`capabilities` returns the advertised method families (`thread/*`, `app/*`,\n`prompt/*`) and the full method list; `thread/capabilities`,\n`app/capabilities`, and `prompt/capabilities` scope it per family. The method\nset is pinned by a drift test in `crates/app-server/src/tests.rs`, so SDK and\nlocal integration clients can rely on it not changing silently. The permanently\nrefused compatibility goal setter is omitted from discovery while\n`goal_execution` is false; goal reads and clears remain advertised.\n\n### Daemon socket: `codewhale app-server --socket`\n\nThe socket transport exposes compatibility controls on a long-lived daemon.\nThe same dispatcher consumes RPC objects over private-channel WebSocket text\nmessages. Process stdio uses JSONL; the private channel upgrades before its\nattachment handshake.\n\n**Endpoint.** `--socket-path` if given; else `$CODEWHALE_HOME/run/daemon.sock`\nwhen `CODEWHALE_HOME` is set (an explicit home is an isolation boundary); else\n`$XDG_RUNTIME_DIR/codewhale/daemon.sock`; else\n`~/Library/Application Support/codewhale/daemon.sock` on macOS or\n`~/.codewhale/run/daemon.sock` elsewhere. On Unix, an implicit endpoint that\nleaves insufficient room for the store-specific socket automatically uses a\nprivate deterministic short temporary directory; the data home stays in place.\nExplicit socket selections remain exact, and an overlong explicit path is\nrefused. The directory is created `0700`, the\nsocket is `0600`, and every accepted peer must present the daemon's own uid.\nOn start, a socket file nobody answers on is removed; a live one makes the new\ndaemon exit with `a live listener already answers on <path>; refusing to\nreplace it`; a non-socket file at the path is never touched. On Windows the owner-private named-pipe transport uses the same dispatcher;\nits native application qualification remains open. There is no TCP fallback. The daemon prints\n`codewhale daemon: listening on <path>` to stderr once it is accepting.\n\n**Handshake.** After kernel peer admission, upgrade the private connection to\nWebSocket. Upgrade headers are limited to 16 KiB and five seconds; any Origin\nheader refuses. Each subsequent text message contains one complete JSON object,\nwith an 8 MiB frame and reassembled-message limit. Binary messages are unsupported\nand discarded without negotiation of compression. Control, scoped Control, ACP\nand listener attachments all use this framing. Process stdio remains JSONL.\n\nThe first RPC request must be `daemon/attach` (`healthz` is also allowed beforehand). Every\nother method is refused with `-32010 attach_required` until then. After the\nprivate attachment handshake, complete `initialize` followed by `initialized`\non the Control connection before business requests. ACP retains its JSON-RPC 2.0\nenvelope, permission response IDs and EOF cancellation policy inside WebSocket\ntext messages. Named-pipe ACLs and peer-process checks remain mandatory.\n\n```json\n{\"id\":1,\"method\":\"daemon/attach\",\"params\":{\n \"client\":{\"name\":\"codewhale-desktop\",\"version\":\"1.2.3\",\"pid\":4242},\n \"mode\":\"claim\",\n \"expect_daemon_version\":\"0.9.11\"}}\n```\n\n`mode` is `\"claim\"` (this client spawned the daemon and manages its lifetime)\nor `\"attach\"` (default: a guest that found a healthy daemon). A claim while\nanother connection owns the daemon fails with `-32011 daemon_already_claimed`\n(`data.owner` names the holder) and the client should retry with `attach`.\n`expect_daemon_version`, when present, must equal the daemon's crate version\nor the attach fails with `-32013 daemon_version_skew` (the bundle-skew guard).\nThe reply reports the granted `role` (`owner` / `attached`), the daemon's\n`pid`, `version`, `socket_path`, and `uptime_ms`, the current `owner`, and the\nlive `connections` count. A second `daemon/attach` on an attached connection\nis `-32014 already_attached`.\n\n**Capabilities.** On this transport `capabilities.methods` is the pinned stdio\nset plus `daemon/attach` (second entry, after `healthz`); `transport` reads\n`unix-socket` (or `named-pipe` on Windows), and `framing` is `websocket-text`.\nStdio advertises `framing: \"jsonl\"`. `shutdown` is advertised to every connection because the method\nexists, but only the owner may call it (below).\n\n**Ownership.** Only the owner may `shutdown`; a guest's `shutdown` is refused\nwith `-32012 not_daemon_owner` and does not interrupt anyone's turn. When the\nowner disconnects the slot frees, so a relaunched shell re-claims the daemon it\nleft running. The owner's `shutdown` stops the listener, closes every\nconnection, and removes the socket file. Journal replay from a client's\nlast-seen `seq` is not part of this transport yet.\n\n### Compatibility execution limits\n\nThe compatibility HTTP, stdio and socket interfaces advertise\n`interactive_execution: false`, `goal_execution: false`, `tool_free_turns: true` and\n`full_api_entrypoint: \"codewhale app-server --http\"`. Clients must require\n`tool_free_turns: true` before relying on an empty allowlist: older compatibility\nservers may ignore the field. Every `thread/message`,\n`thread/request` message, `prompt/run`, `prompt/request` and HTTP `/prompt` or\n`/thread` message requires an explicit `allowed_tools: []`. Omitted, null or\nnonempty lists fail with `interactive_execution_unsupported` before creating a\nRuntime thread or starting a turn. Malformed lists fail parameter decoding.\nJSON-RPC reports `-32602` with `data.error: \"interactive_execution_unsupported\"`;\nHTTP reports 400 with `error.code: \"invalid_request\"`, `error.jsonrpc_code: -32602`\nand the same detail in `error.data`. Compatibility `thread/goal/set` and\n`ThreadRequest::GoalSet` also refuse before changing the goal: setting an active\ngoal starts background Engine work. Reading and clearing existing goals remain\navailable; starting goals uses the full Runtime HTTP API. The known setter\nremains recognized for older clients to get this actionable refusal, but it is\nnot advertised as an available method. For example:\n\n```json\n{\"jsonrpc\":\"2.0\",\"id\":3,\"method\":\"thread/message\",\"params\":{\"thread_id\":\"thr_...\",\"input\":\"Summarize this conversation\",\"allowed_tools\":[]}}\n```\n\nThe bridge sends the same empty allowlist to the canonical Runtime for each\nadmitted turn, so a tool-enabled thread cannot widen it. It forwards assistant\ntext and terminal status. Unexpected approval or clarification requests fail\nand interrupt the exact owned turn; they cannot wait silently. This deliberately\nnarrows the old compatibility behavior. Tool-enabled clients must migrate to\nthe authenticated `/v1/*` API and SSE rather than enabling Full Access.\n\n### Interrupting a turn\n\n`thread/message` streams until the turn reaches a terminal state, which can\ntake minutes. The read loop keeps polling stdin while a turn streams, so a\nclient can send:\n\n```json\n{\"jsonrpc\":\"2.0\",\"id\":9,\"method\":\"thread/interrupt\",\"params\":{\"thread_id\":\"thr_...\"}}\n```\n\nand the runtime is asked to interrupt that turn\n(`POST /v1/threads/{id}/turns/{turn_id}/interrupt`). The reply carries\n`interrupted: false` when no turn is streaming for that thread — this is not\nan error, just nothing to stop. The interrupted `thread/message` then fails\nwith a `turn interrupted` error, and its reply is written before the\ninterrupt's own reply, since the turn owns the writer until it unwinds.\n\n`shutdown` sent during a live turn also interrupts first: it needs the same\nbridge that the turn holds, so without that it would wait for the very turn\nit was meant to stop. Other requests that arrive mid-turn are queued and run\nin order once the turn finishes.\n\n### Running a prompt\n\n`prompt/request` and `prompt/run` (byte-identical aliases) and the legacy\nHTTP `POST /prompt` all execute a **real turn** on the runtime, through the\nsame bridge `thread/message` uses. There is no local fallback: nothing else\nin the app-server can produce model output, so a prompt either runs or fails.\n\n- `params.prompt` is required and must be non-empty (`-32602` otherwise).\n- `params.allowed_tools` must be `[]`; interactive turns use `app-server --http`.\n- `params.thread_id` is optional. With one, the prompt runs on that thread and\n its history. Without one, the runtime gets a fresh thread for that single\n turn; the mapping is dropped when the turn ends, so a one-shot prompt is not\n addressable by `thread/interrupt`. Use `thread/message` when you need to be\n able to interrupt.\n- `params.model` selects the model only when the call is the one that creates\n the runtime thread; an existing thread keeps the model it was created with.\n- The response carries what the model actually said: `output` is the\n concatenated `agent_message` text, `model` is the model the runtime reports\n for the thread that ran it, and `events` are the real\n `response_start`/`response_delta`/`response_end` frames. Over stdio the same\n frames are also streamed to stdout while the turn runs, exactly as for\n `thread/message`.\n- If the runtime cannot be reached, the call fails with `-32005`\n (`runtime_unavailable`) on stdio, or HTTP `503` with\n `{\"error\":{\"code\":\"runtime_unavailable\", ...}}` on `POST /prompt`. Failures\n are never shaped like a successful `PromptResponse`.\n\n`POST /thread` with a `Message` body behaves the same way — it runs the turn\nand replies `status: \"completed\"` with the streamed frames in `events` — where\nit previously replied `accepted` without doing anything.\n\n### Compatibility list and read parameters\n\n`thread/list` accepts only `include_archived` and `limit`; `thread/read`\naccepts only `thread_id` and returns the complete legacy history projection.\nBoth also apply inside `thread/request`. Unknown fields fail with `-32602`\nbefore contacting the canonical owner. Workspace, cursor, `archived`, search\nand `includeTurns` parameters from the pinned v2 contract are not implemented\nby these compatibility methods; they are rejected instead of silently ignored.\nThese method names do not establish Codex wire parity.\n\n### Thread ids and restarts\n\n`thread/message`, `thread/request` messages, and HTTP `POST /thread` messages\ntake a thread id from `thread/create` (or `thread/fork`). An id that was never\ncreated fails with `-32004` (`thread_not_found`) on stdio, or HTTP `404` on\n`/thread`, before any runtime thread is started. `/prompt`, `prompt/request`,\nand `prompt/run` are different: their optional `thread_id` is any key the\ncaller chooses, and a new key starts a new conversation.\n\nThe authenticated canonical owner keeps the full saved conversation graph,\nits selected branch and the thread/session binding. Compatibility controls use\nthat owner; the old SQLite history remains a protected, read-only import source.\nImport compares the complete source graph and selected leaf before publishing\nthe canonical alias. A failed alias publication retains the source and the\nactual canonical result so recovery can report what completed.\n\nExisting validated legacy goals are imported into the owner goal store. Active\ngoals are paused during import; historical data never starts a provider call.\nSource goal fields participate in the same protected source comparison.\n\nSaved-session forks preserve the complete journal, including inactive branches,\nand copy a validated local session-goal sidecar into the new saved session.\nAn active local goal is copied as paused; the source remains unchanged.\nThat sidecar is separate from the public Runtime thread goal. A native Runtime\nthread fork does not automatically inherit the public thread goal.\n\n`thread/create`, `thread/start`, `thread/resume` and `thread/fork` carry a\nclient-generated `operation_key`. Capture one key for each user intent before\nsending it, retain it after an uncertain response, and reuse it when recovering\nthat same intent. Create carries the key in `metadata.operation_key`; Start,\nResume and Fork carry it in `operation_key`. Two intentional forks use different\nkeys. Recovery checks\nthe original operation in the existing owner store; it never creates another\nthread merely because the response was lost or the source later grew.\n\n`POST /v1/thread-history/operations/lookup` is read-only. Its closed request\ncontains `version: 1`, `operation_key`, `expected_data_dir`,\n`expected_execution_scope` and `workspace`; the response is `absent`, `pending`\nor `committed`. A pending or committed response includes the retained receipt\nand its exact action/source `association`.\n\n`POST /v1/thread-history/operations/recover` accepts that lookup request in\n`operation` and the expected `association`. It explicitly finishes an already\nprepared target under the same owner, after checking the saved document, full\ngraph, workspace, checkpoint and action/source identity. It does not rebuild\nthe original intent from a source that may already have changed. An unprepared\ntarget stays pending; a changed or unverifiable target refuses completion.\nRepeat recovery with the same key to observe the same committed result.\n\nNew history reservations retain their original document/checkpoint observation.\nAn uncommitted recovery must match the complete prepared document and the\noperation's own publication generation, with its original checkpoint unchanged.\nAn older reservation without that original observation stays pending or refuses\nrecovery; reading current records cannot retrospectively supply it.\n\nThe selected workspace comes from the acknowledged owner or the explicitly\nadmitted request. History and historical receipts cannot supply permissions,\ncredentials, endpoints or a different owner. A missing, changed, busy or\nincompatible bound store is an explicit failure. A missing canonical target\ndoes not start an empty replacement conversation.\n\n`codewhale thread resume` and `codewhale thread fork` perform the durable\nowner control and print its committed thread, session and operation receipt.\nThey do not launch an interactive frontend.\n\nGlobal `--workspace` (also `--cd`), `--profile` and `--config` select an explicit\ncontrol scope. Relative paths are captured before attachment; the client\nauthenticates the owner, then admits that scope against the same owner receipt\nand its captured worker setting. An incompatible profile or config, missing\nscope facts, or a changed owner fails explicitly. Without these options, the\nacknowledged owner supplies the workspace. Thread listing remains store-wide.\n\nFor a fresh `thread resume` or `thread fork`, global `--provider`, `--model`,\n`--approval-policy` and `--sandbox-mode` supply proposals to the existing owner\ndecoder and permission checks. The corresponding `--set` keys are `provider`,\n`model`, `default_text_model`, `approval_policy` and `sandbox_mode`. Credentials\nand endpoints stay with the owner: `--api-key`, `--base-url` and other per-run\nsettings are refused for these controls. Configure and authenticate the owning\nRuntime before using them. With a retained `--operation-key`, newly supplied\nmodel, provider, policy or sandbox proposals are refused; recovery observes\nthe original admitted intent.\n\nInteractive `codewhale resume` and `codewhale fork` use the same canonical\nhistory operation. An inactive local owner is shut down and joined before the\nexisting TUI acquires the saved-session lease and store. A live owner or an\nuncertain handoff refuses attachment. `--operation-key <KEY>` recovers the\noriginal outcome, including completing a verified prepared target; an\nunprepared or unverifiable result preserves uncertainty.\n\n### Changing config\n\n`app/config/set` and `app/config/unset` write the change to the config file\nbefore replying: the `--config` path, or the default `config.toml` used by the\ncanonical owner when no `--config` is given. They change user settings that\noutlive the app-server, not just this session. The change is applied to the\nfile as it is on disk, so edits saved by other processes are kept. If the file\ncannot be read, parsed, or written, the reply is `ok: false` and nothing\nchanges; a file that does not parse is not rewritten, so fix it by hand (or\n`app/config/reload` after fixing it). Over HTTP `/app`, a rejected key or value\nis `400` and a read or write failure is `500`.\n\n### Answering a clarification question\n\nWhen a headless turn calls `request_user_input`, the runtime emits a\n`user_input.required` event carrying a `request_id`. Reply on the runtime API:\n\n```\nPOST /v1/user-input/{thread_id}/{request_id}\n```\n\nThe compatibility control transport cannot accept that reply.\n`app/request` with `SubmitUserInput` returns `ok: false` and\n`error: \"user_input_reply_unsupported\"`. Compatibility turns require an empty\ntool catalog and refuse interactive execution at admission, so this limitation\ncannot leave a supported turn waiting for an answer. The fixed catalog and\nresponse limitation are Codewhale implementation choices.\n\n## SDK contract\n\nThe `@codewhale/runtime-sdk` package currently supplies Fleet helpers and\nread-only thread event streaming. It does not wrap thread creation, turn\nstart/steer/interrupt, approval decisions, clarification answers or model\ndiscovery. Use authenticated raw HTTP for these operations; the package guide\nprovides the [request recipe](../npm/runtime-sdk/README.md#full-engine-api).\nThe hosted Responses SDK is a separate contract.\n\nThe full `app-server --http` API exists so an external SDK can answer — without scraping TUI\noutput — *what route ran, which provider/model/reasoning/permission profile was\neffective, what events happened, how many tokens were used, and how the run\nfinished.* The durable Thread/Turn/Item data model already carries most of\nthis; the table maps each integration need to where a local client reads it.\n\n| Integration need | Where it comes from | Status |\n|---|---|---|\n| Route / effective model / billing surface | `TurnRecord` + thread `model`; per-run `--provider`/`--model` overrides | available |\n| Permission / sandbox / approval profile | thread `auto_approve`, sandbox + approval policy; `TurnRecord.permission_posture` + `TurnRecord.mode` for how *that* run was governed (the thread's own `mode` may have been switched since) | available |\n| Run / thread / turn IDs | `thread_id`, `turn_id`, SSE event envelope | available |\n| Event stream | `GET /v1/threads/{id}/events` (replay + live SSE) | available |\n| Turn status / terminal classification | `TurnRecord.status` + error summary | available |\n| Token usage | `TurnRecord.usage`; aggregate via `GET /v1/usage` | available |\n| Action receipt (files, commands, web/MCP calls, agents, approvals and who decided, failures) | `GET /v1/threads/{id}/receipt`, `GET /v1/threads/{id}/turns/{turn_id}/receipt` | available ([RECEIPTS.md](RECEIPTS.md)) |\n\nFor one-shot/headless automation, prefer `codewhale exec` with explicit\n`--provider <id> --model <id>` so a failure identifies the exact provider/model\npair. Use `app-server --http` when a local integration needs to start, resume, steer,\nor interrupt turns, list models/capabilities, follow the event stream, or read\nusage. Both paths share the same runtime, so route-effective model resolution\nand the event vocabulary match.\n\n### Release smoke\n\n`scripts/release/app-server-smoke.sh` is the committed pre-release check:\n\n```bash\nscripts/release/app-server-smoke.sh # stdio health/capabilities probe (no tokens)\nscripts/release/app-server-smoke.sh --matrix # + print the configured provider/model matrix\nscripts/release/app-server-smoke.sh --matrix --real # + exec a cheap sentinel per provider\n```\n\nThe stdio probe runs against a throwaway config, so it never reads real keys.\nThe matrix discovers configured providers from `codewhale auth list`, skips\nunconfigured providers, and maps a provider to a cheap sentinel model only when\nit has a built-in cheap default. That built-in set is deliberately conservative\n(currently `deepseek`, `zai`, `moonshot`, and `openai`); every other provider —\nincluding `arcee`, `openrouter`, `xiaomi-mimo`, and `openai-codex` — is left\nunmapped on purpose and must be given a model per run via `SMOKE_MODEL_<SLUG>`\nrather than a guessed default (#3205). Any configured-but-unmapped provider\nfails loudly in `--real` mode. `auth list` reports presence flags only and exec\noutput is passed through a redactor, so secrets are never printed. The parser is\ncovered by `scripts/release/app-server-smoke.test.sh` against a fake `codewhale`\nbinary.\n\n## ACP stdio adapter: `codewhale serve --acp`\n\nACP JSON-RPC over newline-delimited stdio is a transport over the existing\nRuntimeThreadManager and Engine. It has no provider/tool round loop, executable\nregistry, prompt composer, or conversation writer of its own. The server loads\nthe selected config/profile and plugin discovery once; each prompt uses a\ncanonical thread, Core turn, event timeline, approval waiter, and full Engine\nsession snapshot.\n\nThe editor surface supports `initialize`, `session/new`, `session/list`,\n`session/load` (including durable ID prefixes), `session/prompt`, `session/cancel`,\nmodel discovery/selection, and the advertised mode/model config options.\n`session/new` creates a durable bare UUID and empty checkpoint without calling a\nprovider. `session/load` replays the saved conversation as `session/update`\nnotifications before it responds: user and agent text, then each tool call with\nits final status. Replayed tool results are masked as on the live stream: a\n`request_user_input` answer becomes `User input submitted` and configured\ncredentials are redacted. Reasoning and images are not replayed. At most 64 idle transport bindings are retained; eviction keeps the\ndurable conversation. Resume deduplicates against the existing thread binding.\nFull histories, tool pairs, signatures, media and partial-effect receipts are\nsaved under the same checkpoint guard and session write lease used by HTTP.\nACP display text may be shortened; the saved history is the full Core snapshot.\n\nCodewhale 0.10.2 implements a partial ACP v1 adapter. Both `session/new`\nand `session/load` reject a nonempty client-supplied `mcpServers` list with\n`-32602` before creating, resuming or returning a cached session. The field may\nbe omitted or `[]`; null and other non-array values are invalid. No supplied\ncommand is executed during this validation. [ACP v1 requires stdio MCP support](https://agentclientprotocol.com/protocol/v1/session-setup#mcp-servers);\n`mcpCapabilities.http: false` and `sse: false` only describe optional transports\nand do not establish full conformance. Use the full Runtime API and its existing\nMCP management for those integrations. The restricted tool catalog below is a\nCodewhale host profile, not an ACP protocol restriction.\n\nThe trusted local ACP profile narrows Core to file/search/git/patch and admitted\nforeground shell tools. Shell requires both editor terminal support and operator\n`allow_shell`; a requested external sandbox that is unavailable removes shell.\nMCP, dynamic tools, task/PTY/background shell, interpreter, subagent and RLM\nlifecycles are unavailable on this transport. Built-in overrides remove the\nwhole compatibility alias family. Final dispatch rechecks the profile, so a\nfabricated alias, hook rewrite or omitted catalog entry cannot bypass it. Plan\nremains read-only under Full Access. Full Access and the ordinary approval\nposture are server-owned read-only options; the editor cannot widen them.\n`approval_policy = \"never\"` is not an ACP posture, so the server refuses to\nstart with it instead of serving it as Ask.\n\nFull Access is `yolo`. Start it with `codewhale serve --acp --full-access`\n(the flag may also come before `serve`; `--yolo` is an alias) or set\n`yolo = true` in config. A configured `sandbox_mode = \"danger-full-access\"` does\nnot turn it on: that value can only narrow the sandbox the permission picks, so\nunder Ask or Auto-Review it changes nothing, and `ask` is not a config key. At\nstartup the server writes one line to stderr naming the permission and sandbox it\nwill run with, so an editor's agent log shows which posture a launch command\nproduced.\n\nTool updates begin `pending`. `in_progress` means Core reached final dispatch;\n`completed`/`failed` and typed image blocks come from the actual Core result.\n`session/request_permission` correlates a private JSON-RPC ID to the same\nRuntime-minted pending approval and Core execution ID. Only an exact live\n`allow-once` response can release that waiter; wrong IDs are ignored, invalid\noptions deny, and cancellation withdraws it. ACP grants neither remembered\npermissions nor Native capabilities. Core typed rules, strict hooks, repo law,\nHeadless Auto-Review and hard floors remain in force, including the absence of a\nworkspace-write carve-out; a guardian-only decision refuses on this host.\n\nReplay uses the existing bounded event reader and sequence deduplication. A\nreplay gap, closed owner or terminal store fault is an explicit error, never a\nre-execution. Input is serviced between replayed events; each transport write has a 30-second\ndeadline. Cancel, EOF and writer\nfailure interrupt only this transport's claimed Core turn; settlement uses its\nactual terminal receipt and preserves any completed effects. An unconfirmed\ncancellation does not fabricate success. `stopReason` is `end_turn`, `cancelled`,\nor typed `max_turn_requests`; Core failure remains an error. Prompts use at most\n50 model steps (a lower configured limit still wins) and Core's bounded final\nreport response; this profile does not dispatch autonomous goal continuations.\n\nAn oversized or non-UTF8 ACP input frame produces one bounded `-32700` error.\nAfter an oversized frame, input is discarded through its actual newline before\nanother request can be admitted; cancellation preserves that discard state.\nThis applies to standalone ACP and an authenticated guest of a normal Runtime\nowner. Other input I/O failures, owner-reply validation failures and compatibility\ncontrol framing failures end the stream. An oversized ACP response becomes a\nbounded `-32603` error, with a null ID if echoing the original ID would also\nexceed the limit. Oversized notifications refuse before writing and use the\nexisting operation cleanup; they do not report truncated success.\n\nWhen no process owns the Runtime store, ACP claims its own exclusive canonical\nRuntime owner. When a non-ACP Runtime host already owns it (the desktop app,\n`codewhale app-server --http` or `codewhale serve --http`), ACP attaches to that\nowner as an authenticated guest over its local control socket and serves the\neditor from the owner's store. If that owner exits, the ACP process ends with\nan error naming the exit; pending operation outcomes may be uncertain and are\nnot replayed. A second `codewhale serve --acp` in a store another ACP server\nalready owns refuses at startup and leaves the first serving. A saved\nconversation bound to another Runtime store also refuses; ACP does not copy a\nlive conversation into a random store. This does not expose every `/v1/*`\nsteering, job or control method to an ACP editor. Use\n`codewhale app-server --http` for that full runtime API.\n\n\n## Capability endpoint: `codewhale doctor --json`\n\nReturns a JSON object describing the current installation's readiness state.\nSuitable for health-check polling from a macOS workbench. This command is\nstrictly structural and offline: it does not load workspace credential\n`.env` files, inspect credential environment values, open secret/OAuth files,\nprobe an OS keyring, contact providers, or start MCP processes.\n\n```bash\ncodewhale doctor --json\n```\n\n### Response schema (key fields)\n\n| Field | Type | Description |\n|---|---|---|\n| `version` | string | Installed version (e.g. `\"0.8.9\"`) |\n| `config_path` | string | Resolved config file path |\n| `config_present` | bool | Whether the config file exists |\n| `paths` | object | Canonical config, settings, state, sessions, logs, automations, and secrets paths |\n| `secret_backend` | object | Metadata-only file-store shape, or literal `unknown` / `not_probed` for system and unsupported backends |\n| `workspace` | string | Default workspace directory |\n| `legacy_state.primary_root` | string | Primary Codewhale state root inspected for known state paths |\n| `legacy_state.legacy_root` | string | Legacy `.deepseek` state root inspected for known state paths |\n| `legacy_state.needs_attention` | bool | Whether known `~/.deepseek` state paths need review or the read-only session recovery diagnostic found missing destination filenames / could not complete |\n| `legacy_state.legacy_only_count` | number | Count of known state paths present only under the legacy root |\n| `legacy_state.dual_present_count` | number | Count of known state paths present under both primary and legacy roots |\n| `legacy_state.entries` | array | Per-path migration status: `{name, primary_present, legacy_present, status}` |\n| `legacy_state.session_recovery.status` | string | `isolated`, `no_legacy_sessions`, `migration_pending`, `migration_incomplete`, `migration_complete`, or `scan_failed` |\n| `legacy_state.session_recovery.read_only` | bool | Always true; doctor never invokes session migration or modifies either session directory |\n| `legacy_state.session_recovery.chat_contents_read` | bool | Always false; comparison is based only on top-level `.json` filenames and filesystem metadata |\n| `legacy_state.session_recovery.checkpoint_internals_scanned` | bool | Always false; `sessions/checkpoints/` and all other directories are skipped |\n| `legacy_state.session_recovery.recoverable_files` | array | Bounded sample of up to 100 missing destination filenames with source and destination paths; no chat payloads |\n| `legacy_state.session_recovery.recoverable_file_count` | number | Total missing destination filename count, including entries beyond the bounded sample |\n| `legacy_state.session_recovery.recoverable_files_truncated` | bool | Whether more than 100 recoverable filenames were found |\n| `legacy_state.session_recovery.recovery_command` | string or null | `codewhale sessions` when additive automatic recovery is available; null for isolated, complete, empty, or failed scans |\n| `api_key.source` | string | Structural source state: `config_declared`, `env_declared`, `external_auth_declared`, `secret_store_unprobed`, `secret_store_unavailable`, `oauth_unprobed`, `external_consent`, `none`, `local_runtime`, or `unknown`; declarations are not availability proof |\n| `api_key.availability` | string | Literal `present`, `not_required`, `not_probed`, `unavailable`, or `unknown`; only `present` and `not_required` certify structural Setup/Fleet credential readiness |\n| `base_url` | string | Provider URL authority only (`scheme://host[:explicit-port]`); userinfo, path, query, and fragment are omitted |\n| `default_text_model` | string | Default model |\n| `memory.enabled` | bool | Whether the memory feature is on |\n| `memory.path` | string | Path to memory file |\n| `memory.file_present` | bool | Whether memory file exists |\n| `mcp.config_path` | string | MCP config file path |\n| `mcp.present` | bool | Whether MCP config exists |\n| `mcp.probe_scope` | string | `configuration`; doctor does not start MCP servers |\n| `mcp.live_health_checked` | bool | Always false for doctor JSON |\n| `mcp.servers` | array | Per-server structural result and counts plus separate `checks`; URL userinfo/path/query/fragment and command argv, environment, header, and token values are never emitted, and all live stages are `not_checked` |\n| `skills.selected` | string | Resolved skills directory |\n| `skills.global.path` / `.present` / `.count` | — | Codewhale global skills dir (`~/.codewhale/skills`, with legacy `~/.deepseek/skills` support) |\n| `skills.agents.path` / `.present` / `.count` | — | Workspace `.agents/skills/` dir |\n| `skills.agents_global.path` / `.present` / `.count` | — | agentskills.io global skills dir (`~/.agents/skills`) |\n| `skills.local.path` / `.present` / `.count` | — | `skills/` dir |\n| `skills.opencode.path` / `.present` / `.count` | — | `.opencode/skills/` dir |\n| `skills.claude.path` / `.present` / `.count` | — | `.claude/skills/` dir |\n| `tools.path` / `.present` / `.count` | — | Global tools directory |\n| `plugins.path` / `.present` / `.count` | — | Global plugins directory |\n| `sandbox.available` | bool | Whether sandbox is supported on this OS |\n| `sandbox.kind` | string or null | Sandbox kind (e.g. `\"macos_seatbelt\"`) |\n| `storage.spillover.path` / `.present` / `.count` | — | Tool output spillover dir |\n| `storage.stash.path` / `.present` / `.count` | — | Composer stash |\n\n### Example\n\n```json\n{\n \"version\": \"0.8.9\",\n \"config_path\": \"/Users/you/.codewhale/config.toml\",\n \"config_present\": true,\n \"workspace\": \"/Users/you/projects/codewhale-tui\",\n \"api_key\": {\n \"source\": \"secret_store_unprobed\",\n \"availability\": \"not_probed\"\n },\n \"base_url\": \"https://api.deepseek.com\",\n \"default_text_model\": \"deepseek-v4-pro\",\n \"memory\": {\n \"enabled\": false,\n \"path\": \"/Users/you/.codewhale/memory.md\",\n \"file_present\": true\n },\n \"mcp\": {\n \"config_path\": \"/Users/you/.codewhale/mcp.json\",\n \"present\": true,\n \"servers\": [\n {\"name\": \"filesystem\", \"enabled\": true, \"transport\": \"stdio\", \"args_count\": 2, \"env_count\": 0, \"status\": \"ok\"}\n ]\n },\n \"sandbox\": {\n \"available\": true,\n \"kind\": \"macos_seatbelt\"\n }\n}\n```\n\n## HTTP/SSE runtime API: `codewhale app-server --http`\n\n```bash\ncodewhale app-server --http [--host 127.0.0.1] [--port 7878] [--workers 2] [--auth-token TOKEN] [--insecure-no-auth]\ncodewhale app-server --mobile [--host 127.0.0.1] [--port 7878] [--auth-token TOKEN]\ncodewhale app-server --mobile --host ::1 [--port 7878] [--insecure-no-auth]\ncodewhale web [--port 7878]\n\n# Compatibility aliases — identical server, serve flag names:\ncodewhale serve --http [...] [--insecure]\ncodewhale serve --mobile [...] [--insecure]\n```\n\nDefaults: host `127.0.0.1`, port `7878`, 2 workers (clamped 1–8).\n\nA Unix supervisor may pass a listening TCP socket as descriptor 3 and set\n`CODEWHALE_RUNTIME_LISTEN_FD=3` for `serve --http` or `app-server --http`.\nThe Engine captures ownership before config discovery or child execution,\nsets close-on-exec, and validates TCP/listening state, disabled port sharing,\nand the exact numeric loopback host and nonzero port requested on the CLI.\nInsecure unauthenticated mode is refused. Malformed, closed, non-listening,\nwrong-address or already-consumed descriptors fail startup; only an absent\nmarker permits an ordinary bind. An inherited listener cannot attach to a live\nRuntime owner, while the existing authenticated dead-owner recovery remains.\n\nThe supervisor must retain its socket across normal Engine restarts and pass\nonly the intended descriptor to the Engine. The Engine drops its descriptor on\nexit and never shuts down the supervisor's shared socket. Permanent shutdown\nmust stop both the service and any socket-activation unit; stopping just the\nservice can allow incoming traffic to reactivate it. This descriptor contract\ndoes not itself validate a supervisor's systemd PID or descriptor-count metadata.\n\nThe server binds to `localhost` by default. Configuration is via CLI flags —\nthere is no `[app_server]` config section.\n\n`/v1/*` routes require a bearer token unless `codewhale app-server` is started\nwith `--insecure-no-auth` on a loopback bind such as `127.0.0.1`. Mobile mode\nis loopback-only: non-loopback hosts are rejected until Runtime has a TLS or\nverified-overlay transport boundary. The `codewhale serve` compatibility aliases\nuse `--insecure` for the same loopback escape hatch.\nPass `--auth-token TOKEN` or set `CODEWHALE_RUNTIME_TOKEN=TOKEN` before starting\nthe server; `DEEPSEEK_RUNTIME_TOKEN` remains a compatibility alias. If neither\nis set, the process generates a Runtime token for that process and does **not**\nprint it. `/health`, `/v1/runtime/info`, and an enabled static client shell\nremain public; Runtime mutations and thread data stay behind `/v1/*`\nauthentication. `/mobile` returns 404 when mobile mode is disabled and serves\nthe unchanged static shell when it is enabled.\n\nAuthenticated clients can provide the token as `Authorization: Bearer TOKEN`,\n`X-Codewhale-Runtime-Token: TOKEN`, the legacy\n`X-DeepSeek-Runtime-Token: TOKEN`. Query-string and raw Runtime-token cookie\nauthentication are not supported.\n\n### Local browser client\n\n`codewhale web` starts the canonical Runtime API on `127.0.0.1`, serves\ndependency-free assets embedded in the binary, prints a single-use launch URL,\nand asks the operating system to open that URL in the default browser. If the\nbrowser does not open, the printed URL remains usable for ten minutes. The\ncommand cannot bind to a non-loopback host and cannot run with Runtime auth\ndisabled.\n\nThe browser-launch URL contains a random, short-lived, one-time bootstrap\ncapability, never the Runtime token. A loopback request exchanges that\ncapability for a\n`codewhale_web_session=…; HttpOnly; SameSite=Strict; Path=/` cookie backed by a\nsingle process-local server session that expires 12 hours after the server\nprocess starts, consumes the capability immediately, and redirects to `/`.\nReused, expired, malformed, or\nnon-loopback bootstrap attempts fail closed. The Runtime bearer token is not\nplaced in rendered HTML, browser storage, logs, URL queries/fragments, or\nbrowser-launch arguments. The one-time bootstrap capability is printed in the\nlocal terminal and transits the OS browser launcher's argument list. A same-user\nprocess could race the browser to the exchange, which is why the capability is\nsingle-use, loopback-only, and expires after ten minutes — and why a same-user\nattacker has strictly easier local avenues than this race.\nWeb fetches require the session cookie plus an origin-scoped request proof;\nstreams use a fresh single-use ticket. The initial redirect carries the proof\nin a fragment, which the client removes and saves in origin-scoped\n`sessionStorage`, so a reload of that tab keeps working when the browser\nallows session storage. The server never puts\nthe proof in a page: the session cookie is host-scoped, not port-scoped, so a\nlistener on another loopback port can receive it, and the cookie alone must\nnot be enough to recover the proof. A new tab opened by typing the address\ntherefore has no proof; use the original tab, duplicate it, or restart\n`codewhale web` for a fresh one-time session. The page uses `no-store`,\ndisallows framing, and grants no cross-origin read access. Nothing extends the\nserver session or replaces an expired cookie.\nCross-origin Fetch Metadata or a mismatched Origin is rejected on web API\nrequests. Explicit bearer and Runtime-token header clients keep their existing\nbehavior. Transient stream-ticket failures retry with capped backoff; HTTP\n401/403 stops ticket retries until a fresh session is opened.\n\nThe embedded client provides a responsive thread/search rail, Runtime-owned\nsession facts, transcript and tool receipts, and a bottom composer. It can\ncreate, select, rename, and archive threads; choose a provider and model for a\nnew thread without changing Runtime defaults; start or steer turns; interrupt\nwork; resolve approvals; and answer Runtime user-input requests. Selection\nloads `GET /v1/threads/{id}` first, then opens the replayable event stream with\n`since_seq=latest_seq`; reconnection advances from the newest accepted sequence\nand drops duplicates or events from a stale selection. The thread detail\nsnapshot includes `pending_approvals`, `pending_user_inputs`, and\n`pending_dynamic_tool_calls`; clients must hydrate those fields before\nsubscribing so a reload cannot strand work whose request event is at or before\n`latest_seq`. Resolution is also published as `approval.decided`,\n`user_input.answered`, `user_input.canceled`, `tool_call.resolved`,\n`tool_call.canceled`, or `tool_call.timeout` for already-connected clients.\n\nAn existing thread's model, mode, permission posture, workspace, and branch are\ndisplay-only in this client. Files/Changes, PTY/terminal, preview, artifacts,\nprovider login or global-default switching, Fleet creation, and\nundo/retry/restore controls are not built into this page. The native desktop\nclient covers them through the workspace-file, turn-artifact, terminal and\nworkspace-restore routes documented here.\n\n### Mobile control page\n\n`codewhale serve --mobile` starts the same HTTP/SSE runtime API and serves a\nphone-friendly control page at `/mobile`. It binds only to loopback\n(`127.0.0.1` or `::1`); a non-loopback host is rejected because this Runtime\nsurface does not yet provide TLS or a verified overlay transport. The static\nHTML page contains no Runtime bearer and is not itself token-gated. When\nRuntime auth is enabled, the CLI prints a short-lived, single-use loopback\nbootstrap URL. That capability creates a 30-minute, process-local\n`Max-Age=1800; HttpOnly; SameSite=Strict` mobile session cookie plus origin-scoped browser\nproofs. A sibling port that receives the host-scoped cookie cannot use it by\nitself. The page can also exchange an explicitly entered bearer once, then\nclears it rather than storing it in browser storage or a cookie. EventSource\nconnections use separate short-lived, single-use stream tickets.\n\nThe mobile page can list/create threads, send prompts, follow live SSE events,\nsteer or interrupt an active turn, and resolve normal tool approvals through\n`POST /v1/approvals/{approval_id}`. It is a local-only convenience surface;\ndo not expose it directly to another device or public network until Runtime has\na TLS or verified transport boundary.\n\n### Endpoints\n\n**Health**\n- `GET /health`\n\n**Sessions** (durable session manager)\n- `GET /v1/sessions?limit=50&search=<fuzzy>&include_archived=false&archived_only=false&workspace=<path>&sort=recent|name|size`\n- `GET /v1/sessions/summary?…` (same query params; projected row shape)\n- `GET /v1/sessions/{id}` (add `?peek=true&entries=12` for a bounded, redacted\n read-only peek instead of the full transcript). The full response carries\n `turn_outcomes` when a turn ended `Failed`: `{ status, error, ended_at,\n after_message_count }` per failure, oldest first, bounded to 64, with the\n error text the transcript showed and secrets redacted\n- `PATCH /v1/sessions/{id}` (`{ \"title\"?: string, \"archived\"?: bool }`)\n- `DELETE /v1/sessions/{id}`\n- `POST /v1/sessions/{id}/resume-thread` returns the open thread that already\n holds the whole saved session (`200`), or seeds a new thread from it (`201`)\n when none does, including when the session grew after that thread opened it.\n Without a `mode` in the body, a new thread takes the saved mode; a saved mode\n that is not a Runtime mode, such as `exec`, resumes as `agent`. The new\n thread's tool results are stored masked as live receipts are: a\n `request_user_input` answer becomes `User input submitted` and configured\n credentials are redacted. `GET /v1/sessions/{id}` still returns them unmasked.\n- `GET /v1/sessions/{id}/artifacts` and `GET /v1/sessions/{id}/artifacts/{artifact_id}?offset=&limit=`\n (see workspace files and session artifacts above; runtime-turn spills are\n read through `GET /v1/threads/{id}/turns/{turn_id}/artifacts/{artifact_id}`)\n- `POST /v1/sessions` (`{ \"thread_id\": string, \"title\"?: string }`) exports a\n thread as a saved session. It is idempotent: it writes only the document\n whose id is derived from the thread, creating it the first time (`201`) and\n updating it after (`200`), so a retry never makes a duplicate. A session the\n thread was resumed from is left unchanged.\n- `PUT /v1/sessions` (`{ \"thread_id\"?: string, \"session_id\"?: string }`) saves\n a thread's live conversation. Naming a `session_id` that another thread is\n bound to returns `409 Conflict`.\n- `GET /v1/sessions/repair` returns the last session-store repair summary, or\n `null` if none has run\n\nSessions and threads answer the same `include_archived` / `archived_only` pair\nwith the same meaning, and `search` is the same fuzzy match (title, id,\nworkspace — substring, then subsequence) the TUI session picker and the workbar\nSessions list use. All three surfaces run one projection\n(`crates/engine/src/session_projection.rs`), so a listing cannot differ between\nthe terminal and the dashboard.\n\n`GET /v1/sessions/summary` returns rows that are field-compatible with\n`GET /v1/threads/summary` — `id`, `title`, `preview`, `model`, `mode`,\n`workspace`, `archived`, `updated_at` — plus `message_count`, `total_tokens`,\n`created_at`, `parent_session_id`, and `is_current`. One caveat stated plainly:\n`preview` is the session's recorded **title**, not its last message. Session\nmetadata does not store a last message, and reading every transcript to\nsynthesise one would make a list view an unbounded read. Full transcript\npreview lives in the TUI session picker, which reads one selected session.\n\nMetadata inspection reads at most 16 MiB from each regular session file. A\nresource refusal returns `413` with code `session_metadata_too_large` from\ncatalog and lifecycle routes; the source remains unchanged. A catalog with\nunreadable records returns an error rather than an incomplete successful list.\nThe terminal picker retains readable rows and reports its inspection warning.\nThese limits cover metadata inspection, not full transcript loading.\n\nSaved-document and checkpoint publication uses a native source captured before\nsnapshot preparation or queueing. Both physical records must still match under\nthe existing session write admission before archival, pruning, replacement or\ncheckpoint consumption. A mismatch refuses and retains recovery. Optional\nserialized `publication_id` metadata identifies a generation; it does not grant\nwrite authority. An unknown checkpoint needs explicit held resume/attach/promotion\nbefore automatic completion can consume it. Runtime prefix-restore validation\nremains distinct from the full-document predicate required for a later write.\nOptional usage-store damage remains visible as incomplete coverage while valid\nrecovery data can be read without a publication receipt. Known retirement and\nchanged raw records still refuse; this read result cannot authorize a save.\n\n`PATCH /v1/sessions/{id}` renames and/or archives a saved session and returns a\nlifecycle receipt shaped like the thread patch receipt:\n\n```json\n{\n \"session\": { \"id\": \"…\", \"title\": \"Renamed\", \"archived\": true, \"…\": \"…\" },\n \"changes\": { \"title\": \"Renamed\", \"archived\": true }\n}\n```\n\n`changes` lists only what actually moved, so a no-op patch is distinguishable\nfrom an applied one. Archiving is durable and reversible: an archived session\nstays on disk and stays loadable, disappears from default listings, and is\nnever chosen by `--continue` or by auto-resume. The route is the same writer\nthe TUI picker (`e`) and `/sessions archive <id>` use — there is no second\narchive notion.\n\nWhile a session is open in an interactive Codewhale process, that process holds\nthe authoritative copy in memory and rewrites the whole document on its next\nautosave. `PATCH`, `PUT` and `DELETE` therefore fail closed on it with\n`409 Conflict` rather than writing something that would be silently reverted.\nChange it in the terminal instead. The open process holds a lock on the\nsession (`sessions/.late-usage/<id>.live`), so this holds whether the request\nreaches the API inside that process or a separate `codewhale serve`.\n\nThe session store is repaired in the background at each launch and each\n`codewhale serve` start. The repair gives a \"Recovered:\" session to each thread\nin a Runtime store that no session is bound to. It unbinds threads whose session\ndocument is gone; they then load from their own turns. It moves unreadable\ndocuments, empty unbound stores, and old artifact directories that no session\nnames to `sessions/.set-aside/<run>/`, and writes a `MANIFEST.jsonl` there.\nNothing is deleted. `GET /v1/sessions/repair` and `codewhale doctor` report the\nlast run; `codewhale doctor --repair-sessions [--dry-run]` runs one on demand.\n\n`GET /v1/sessions/{id}?peek=true` returns a bounded, redacted, read-only view\ninstead of the transcript: at most 12 entries of at most 400 characters each\n(`&entries=N` lowers the budget, never raises it past the cap), tool calls and\nresults summarised to a name and a size rather than inlined, and\ncredential-shaped substrings masked. `omitted_before` reports how many earlier\nmessages were dropped. The payload carries `\"live\": false` and deliberately has\nno turn status, `running`, or `active` field — a saved session is a recording,\nand live state comes only from a resumed thread's SSE stream.\n\n**Threads** (durable runtime data model)\n- `GET /v1/threads?limit=50&include_archived=false&archived_only=false`\n- `GET /v1/threads/summary?limit=50&search=<optional>&include_archived=false&archived_only=false&thread_ids=<id>,<id>`\n- `GET /v1/threads/running`\n- `GET /v1/goals?limit=50&cursor=<opaque>&status=<status>` — read-only index of\n the per-thread goal records across threads, newest update first. Returns\n `{\"goals\": [ThreadGoal...], \"next_cursor\": \"<opaque>\"}`; `next_cursor` is\n absent on the last page. `limit` is 1-200 (default 50); `status` is one of\n `active`, `paused`, `blocked`, `usage_limited`, `budget_limited`, `complete`;\n goals whose thread no longer exists are omitted; a bad `status` or `cursor`, or a cursor issued under a different `status`, is `400`. Same auth as every other `/v1` read.\n Goal creation, completion and removal stay on `/v1/threads/{id}/goal`.\n- `GET /v1/threads/{id}/notices`\n- `DELETE /v1/threads/{id}/notices/{notice_id}`\n- `POST /v1/threads`\n- `GET /v1/threads/{id}`\n- `PATCH /v1/threads/{id}` (see body shape below)\n- `POST /v1/threads/{id}/resume`\n- `DELETE /v1/threads/{id}` — removes the thread with its turns, items,\n events, goal and approval grants, and stops its cached engine. A thread\n with a running turn, or one that an active automation or a pending\n trigger delivers into, answers `409` naming them. The saved session bound to it is\n deleted too unless another thread is bound to the same session or an open\n terminal holds it; the answer\n `{deleted, session_id?, session_deleted, session_kept?, session_error?}`\n says which. Tasks, automations and child threads that name it keep the\n stale id.\n- `POST /v1/threads/{id}/fork` (`POST /v1/threads` also accepts\n `parent_thread_id` naming an existing thread)\n- `POST /v1/threads/{id}/read` — record the newest item a client has shown\n (body `{\"item_id\": \"<item id>\"}`; see \"Read markers\" below)\n- `GET /v1/threads/{id}/receipt` — what the thread did, one entry per action\n (read-only; shape in [RECEIPTS.md](RECEIPTS.md))\n- `GET /v1/threads/{id}/turns/{turn_id}/receipt` — the same, for one turn;\n `404` for an unknown thread or a turn that is not this thread's\n\n`thread_ids` is a comma-separated list of up to 200 IDs (each at most 128 bytes).\nThe route filters exact IDs before applying `limit`, preserving newest-first\norder. This is a selection filter, not an ownership or authorization check;\nthe Runtime bearer token remains the access boundary.\n\n`POST /v1/threads` accepts optional execution defaults in addition to the\nprovider, model, workspace, and permission fields:\n\n```json\n{\n \"model_provider\": \"openai-codex\",\n \"model\": \"gpt-5.6\",\n \"reasoning_effort\": \"high\",\n \"allowed_tools\": [\"read_file\", \"search\"]\n}\n```\n\n`reasoning_effort` uses the canonical Runtime vocabulary (`auto`, `off`,\n`low`, `medium`, `high`, `xhigh`, `ultra`, or `max`; documented compatibility\naliases are accepted and persisted canonically). `allowed_tools` is a\nmodel-visible allowlist. Omitting it keeps the normal configured catalog; an\nexplicit empty array (`\"allowed_tools\": []`) exposes no tools to the model.\nBoth fields are additive: older thread records and clients that omit them\nretain their previous behavior.\n\n`GET /v1/threads/summary` is the read-only summary surface used by the VS Code\nAgent View. `search` matches thread `id`, `title`, and `model` (and, when the\ntitle is unset, the latest turn's input summary — the displayed title). It\ndoes not scan turn or item bodies: `preview` is filled only after a match, so\na dashboard keystroke is not a whole-store read per thread. Each item includes\n`id`, `title`, `preview`, `model`, `mode`, `archived`, `updated_at`,\n`latest_turn_id`, `latest_turn_status`, `parent_thread_id` when the thread\nwas forked from another (absent on threads forked before the field existed),\nplus workspace metadata:\n\n```json\n{\n \"id\": \"thread_...\",\n \"title\": \"Implement MCP status count\",\n \"preview\": \"The TUI footer should count project MCP servers...\",\n \"model\": \"deepseek-v4-pro\",\n \"mode\": \"agent\",\n \"branch\": \"feature/runtime-api\",\n \"head\": \"abc1234\",\n \"dirty\": false,\n \"workspace\": \"/Users/you/projects/codewhale\",\n \"archived\": false,\n \"updated_at\": \"2026-06-06T05:43:00Z\",\n \"latest_turn_id\": \"turn_...\",\n \"latest_turn_status\": \"completed\"\n}\n```\n\nRows also carry `last_read_item_id` and `unread` from the thread's read marker\n(see \"Read markers\" below). Both are `null` for a thread no client has marked\nread.\n\n`branch` is resolved from the thread workspace at request time and may be\n`null` when the workspace is not a Git repository or the branch cannot be read.\n`head` is the current short Git commit for that workspace when available.\n`dirty` is true when the workspace has staged, unstaged, or untracked changes.\n`workspace` is included so editor clients can show when an agent lane is working\noutside the current VS Code folder.\n\nThread forks are sibling runtime threads, not an in-place tree projection.\n`thread.forked` events include `source_thread_id`; internal backtrack-aware\nforks may also include `backtrack_depth_from_tail` and `dropped_turn_id`, and\na fork anchored to a named turn (`/fork-at-turn`) reports them with the depth\nresolved from that turn, and names the first user turn it dropped (not the\nanchor, which a named-turn fork keeps, and not a prompt-less turn such as a\nmanual compaction that sits between them).\nThread list and summary responses remain flat in v0.8.40, so clients that need\na graph should reconstruct it from events instead of assuming list order is a\ncomplete tree.\n\n### Read markers\n\n`POST /v1/threads/{id}/read` with `{\"item_id\": \"<item id>\"}` records that a\nclient has shown the conversation through that item, in the order\n`GET /v1/threads/{id}` returns items (turn order, then item start time). A\nclient sends the id of the newest item it rendered when the person opens or\nreads the conversation. The item must belong to the thread: an unknown thread\nis `404`, and an unknown item or one from another thread is `400`. The\nresponse is the thread record. Its `last_read` holds `item_id`, `last_in_turn`\n(that item was the last item of its turn when it was marked) and\n`watermark_item_id` (the last item that turn had recorded at that moment).\n\nMarking read changes nothing else: `updated_at`, list order and the thread's\nevent stream are untouched, so opening a conversation never moves it to the\ntop of a list or makes it unread again. The marker only moves forward:\nmarking an item that comes before the current one leaves the marker as it is,\nso a window showing an older snapshot cannot make a read conversation unread.\nForks start without a marker.\n\n`GET /v1/threads/summary` derives `unread` from the marker: `false` while the\nmarked item is still the newest, `true` once any later item exists, and `null`\nwhen the thread was never marked (older Runtimes had no marker, so existing\nconversations are not all reported unread after an upgrade; clients keep their\nown fallback for those). `last_read_item_id` is the item to place a \"new\"\ndivider after. Known limits: `unread` follows the items each turn records\nwhile it runs, so a turn that has started but not yet recorded an item does not\ncount until it does, and a notice added to a turn after it finished (such as a\npermissions change) does not count.\n\n`GET /v1/threads/running` is the running-work accounting surface\n(#6180): threads with at least one queued or in-progress turn, each with\n`thread_id`, `model`, `title`, and `active_turns` (`turn_id` + `status`).\nBackground-capable clients use it for quit/background decisions — one call,\nno inference from latest-turn status. Archive state is ignored (archiving\nhas no quiescence gate); an empty array means no owned work is live.\n\n`GET /v1/threads/{id}/notices` is the per-thread active-notice surface\n(#6180): the TUI-visible conditions a watch-only client must surface —\n`subagent-terminal` (a child settled), `elevation-needed` (a tool call is\nblocked on elevation), `model-notify` (the model asked the user to come\nback) — each with `turn_id` and a `subject` id for targeting. Notices are\nin-memory session state, bounded to 32 per thread (oldest evicted), and\nnever persisted. Clearing: elevation auto-clears when its tool call\ncompletes; terminal/notify clear on `DELETE .../notices/{notice_id}`\n(204, unknown ids 404). Unknown threads 404 on both endpoints.\n\n`archived_only=true` returns archived threads only (mutually overrides\n`include_archived`). Default behavior is unchanged: `include_archived=false`\nand `archived_only=false` returns active threads. Added in v0.8.10 (#563).\n\n`PATCH /v1/threads/{id}` body — every field is optional, missing means\n\"no change\". At least one field must be present. `title` and `system_prompt`\naccept an empty string to clear a previously-set value. Added in v0.8.10 (#562):\n\n```json\n{\n \"archived\": true,\n \"allow_shell\": false,\n \"trust_mode\": false,\n \"auto_approve\": false,\n \"model\": \"deepseek-v4-pro\",\n \"mode\": \"agent\",\n \"title\": \"User-set thread title\",\n \"system_prompt\": \"You are a useful assistant.\",\n \"model_provider\": \"custom\",\n \"model_provider_id\": \"lm-studio\"\n}\n```\n\nA `system_prompt` change applies from the next turn: a loaded engine is\nunloaded and rebuilt from the thread's history with the new prompt. Like a\n`workspace` change, it is refused while the thread has a running turn.\n\n`model_provider` switches the provider the thread's future turns use. It\ntakes a built-in kind (`deepseek`, `xai`, ...) or a configured route name, as\n`/provider` does. `model_provider_id` names one exact `[providers.<id>]` table\nand wins over a route name. The target route is resolved and its client\npreflighted before anything is saved, so an unknown or credential-less\nprovider is refused and nothing changes. Without `model`, the thread takes the\nnew provider's default model; an `auto` thread stays `auto`. The loaded\nengine and conversation history are kept, and the next turn installs the new\nroute.\n\nA change of effective permission adds a completed `status` item to the thread's\nlatest turn and publishes a `thread.permissions_changed` event (`from`, `to`,\n`source`).\n\n**Turns** (within a thread)\n- `POST /v1/threads/{id}/turns`\n- `POST /v1/threads/{id}/turns/{turn_id}/steer` - inject guidance into the running turn. The response is a receipt for what actually happened, not for what was attempted; see [Steer delivery](#steer-delivery).\n- `POST /v1/threads/{id}/turns/{turn_id}/interrupt`\n- `GET /v1/threads/{id}/turns/{turn_id}/artifacts` - what the turn produced: typed references plus the workspace-delta state. See [Turn artifacts](#turn-artifacts).\n- `GET /v1/threads/{id}/turns/{turn_id}/artifacts/{artifact_id}?offset=&limit=&revision=` - read one reference from the workspace, the post-turn snapshot, or the session artifact directory.\n- `POST /v1/threads/{id}/compact` (manual compaction)\n- `POST /v1/threads/{id}/undo` - fork the thread with the last N turns removed (`{\"depth\": N}`, default 0 = last turn only); returns the forked thread plus `original_user_text` so a GUI can pre-populate the input box. `{\"turn_id\": \"turn_…\"}` instead names the user turn to remove, together with every turn after it, and wins over `depth`; the Runtime resolves it under the thread's restore guard, so a turn another client appends meanwhile cannot move the cut. `400` when the turn is not a user turn of that thread\n- `POST /v1/threads/{id}/fork-at-turn` - fork at one named user turn (`{\"turn_id\": \"turn_…\"}`, as `GET /v1/threads/{id}` reports it). The fork *keeps* that turn and every turn before it, and drops the turns after it; naming the last turn therefore keeps the whole conversation. The receipt is `/undo`'s (`thread`, `original_user_text`, `original_user_images`), carrying the *first dropped* user turn's prompt — what was asked next, even when a prompt-less turn such as a manual `/compact` sits between — so a client can put it back in the composer for editing. The source thread, its session document and the workspace are untouched, and there is no file rollback: a fork is a sibling conversation, and rewinding the workspace would rewind the branch left behind with it. Clients should name the turn instead of computing a `depth` — the transcript they render and the turn list this cuts are not the same list (steers, image-only prompts and injected handoffs each sit on one side only), and a client-side count that is off by one forks the wrong prefix while answering `201`. `400` when the turn is not a user turn of that thread.\n- `GET /v1/threads/{id}/patch-undo?turn_id=turn_…` - validates the selected restore without changing files or publishing a fork; returns `turn_ids`, the exact dropped range, `paths` needing restoration and an optional no-change `summary`. Requires the `thread_patch_undo_turn_id` capability.\n- `POST /v1/threads/{id}/patch-undo` - rolls back the files the dropped turns changed, then the same fork (`{\"depth\": N}` or `{\"turn_id\": \"turn_…\"}`, with `turn_id` taking precedence); returns `patch_result` (`files_restored`, `summary`, `snapshot_label`) alongside the forked thread. See [Workspace restore endpoints](#workspace-restore-endpoints) for ownership, the trust, admission and abort rules, and the `error.code` values of a refusal.\n- `POST /v1/threads/{id}/file-revert` - restore exactly one file from one restore point the thread owns (`{\"path\", \"snapshot_id\", \"expected_hash\"}`); never forks the conversation. See [Workspace restore endpoints](#workspace-restore-endpoints).\n- `POST /v1/threads/{id}/retry` - fork with the last N turns removed and immediately start a new turn (`{\"depth\": N, \"prompt\": \"...\"}`; `prompt` overrides the original user text, which is re-used when omitted)\n\n`POST /v1/threads/{id}/turns` accepts the same optional\n`reasoning_effort` and `allowed_tools` fields as per-turn overrides:\n\n```json\n{\n \"prompt\": \"Review this change without running tools.\",\n \"operation_key\": \"cwc-request-01J7Y6Q9W4\",\n \"reasoning_effort\": \"max\",\n \"allowed_tools\": []\n}\n```\n\nThe same `model_provider` / `model_provider_id` fields on a turn route that\none turn through another provider. The saved thread keeps its provider.\nWithout `model`, the turn uses that provider's default model (an `auto` thread\nstays `auto`). The override is always preflighted and is part of the\n`operation_key` fingerprint.\n\nAccount-owned model turns require `capabilities.account_model_owner: true` before\nsubmission. Set `account_model_owner` to the signed-in account ID, both\n`model_provider` and `model_provider_id` to `codewhale`, and `model` to the exact\n`provider/model` account route. The Runtime requires current model access for\nthat same secure account session and uses only that account credential; missing,\nexpired or revoked access is refused. Local keys and other configured routes are\nnot substitutes. A supplied profile constitution must belong to the same account.\nThe owner participates in the durable operation fingerprint; replay of an\nalready completed exact operation returns its receipt before fresh credential\nadmission. The account gateway resolves the selected provider's current saved\nAPI key at use. Ordinary turns and saved Chat defaults retain their existing\nbehavior. The Runtime Chat relay carries the same assertion as `accountModelOwner`.\n\nResolution is deterministic: a turn override wins over the thread default,\nwhich wins over the Runtime's normal configuration. For tools, reaching normal\nconfiguration means the ordinary configured catalog; `[]` is never treated as\nmissing. Reasoning is normalized only after the exact provider/model route is\nresolved, and `auto` remains a per-prompt reasoning decision even when the\nthread uses a fixed model. The request still enters the existing\n`Op::SendMessage` path and the single `Engine::run_turn` loop.\n\nImage input uses the same turn path: `\"images\": [{\"mime\": \"image/png\",\n\"dataBase64\": \"...\"}]`. Clients must first observe\n`capabilities.turn_image_inputs: true` in `/v1/runtime/info` (or the isolated\nRuntime Chat relay catalog). Older HTTP runtimes ignore unknown fields, so a\nsuccessful text response is not evidence that an attachment was accepted.\nThe field is omitted when empty. It is also accepted by app-server\n`thread/message`, `thread/request` messages, and prompt requests; that bridge\nchecks the underlying Runtime capability before forwarding image bytes.\nLegacy remote Work commands do not support images and explicitly refuse them.\n\nNew inline images require a named model whose exact resolved route reports\n`image_input: \"supported\"`; Auto and unknown/unsupported image routes are\nrefused before classifier or provider dispatch. This does not change the\nexisting trusted-local attachment behavior for routes with unknown capability.\nA nonempty prompt is required. Inputs are limited to 10 images, 4 MiB decoded\nbytes per image, 5 MiB total, and an 8 MiB JSON body. PNG, JPEG, GIF and WebP\nmust have matching MIME, canonical padded base64 and valid bounded image\ncontent: at most 8192 pixels per dimension, 33,554,432 pixels total and 64 MiB\ndecoder allocation. The Runtime does not fetch paths or URLs from this field.\nMalformed images refuse the whole turn; callers can retain the draft for\ncorrection. Relay command polling uses an 8 MiB response budget; the sender\nmust paginate by serialized bytes without advancing past unserved commands.\n\nAccepted image bytes and order are retained in the existing turn records and\nreconstructed after restart, import and fork. Retry retains those images even\nwhen its optional `prompt` changes the text; undo responses include\n`original_user_images` when present. Image-bearing records require schema v3,\nwhich older readers refuse. Text-only records and operation fingerprints retain\ntheir prior representation. Validated stored local images retain the existing\n5 MiB per-image ceiling and prior aggregate/count semantics on import/retry;\nthis internal storage authority does not\nrelax exact model or permission checks. Image bytes, MIME and order participate in request\nidentity, so changing an image under the same operation key conflicts.\nCompaction can summarize older context; retaining the original attachment does\nnot promise that every later model request includes it. Image pixels are not\nsubject to text-secret redaction.\n\n`operation_key` is an optional idempotency key for clients that may lose an\nHTTP response after the Runtime accepted a turn. It is scoped to the current\nRuntime store and thread, may contain at most 128 UTF-8 bytes, and may not be\nempty or contain surrounding whitespace or control characters. Omitting it\npreserves the legacy create-a-new-turn behavior.\n\nThe first accepted request durably binds a SHA-256 fingerprint of the key to\nthe Runtime turn id and a canonical request fingerprint before sending the\nexisting `Op::SendMessage`. An exact retry returns that original turn in the\nnormal `{ \"thread\": ..., \"turn\": ... }` response and emits no second engine\noperation, item, or lifecycle sequence. Reusing the key on the same thread\nwith a different provider/model, prompt, reasoning policy, tool allowlist or\ndynamic-tool schema, environment, or permission policy fails closed with\n`409 Conflict`. The same caller key may be used independently on another\nthread.\n\nOnly the scoped key fingerprint, request fingerprint, thread id, and turn id\nare stored in the Runtime's private turn-operation index. The raw key is never\npersisted or logged, and request bodies, credentials, and attachments are not\ncopied into that index. Existing thread/turn persistence remains the source of\nthe returned turn after a process restart.\n\n**Exact accepted-turn lookup**\n\n`GET /v1/threads/{id}/turn-operations/{operation_key}` uses the same Runtime\nauthentication as turn submission. URL-encode each path segment. It returns\n`200 OK` with the existing bare `TurnRecord` (the `turn` object in the POST\nresponse), identified by that exact thread and operation key. It does not use\nthe thread's latest turn or require the original request body or current route\nsettings to match.\n\n- `404 Not Found`: no binding exists for that thread/key, or persisted identities\n do not match. These cases share a generic response.\n- `409 Conflict`: admission holds the operation claim, or its durable binding\n is incomplete. Retry the lookup; this response does not authorize another turn.\n- `400 Bad Request`: the thread ID or operation key is malformed. The key uses\n the same 128-byte and whitespace/control-character rules as POST.\n- `500 Internal Server Error`: storage or the existing claim lock cannot be\n checked safely. This is not evidence that the operation is absent.\n\nThe lookup holds a shared read lock on the existing operation claim while\nreading the binding and turn. It creates no files, starts no engine, emits no\nevents, and performs no replay or recovery. Normal Runtime startup may recover\nan incomplete admission before a later lookup, but GET itself never does so.\n\n**Approvals**\n- `POST /v1/approvals/{approval_id}` with body\n `{ \"decision\": \"allow\" | \"deny\", \"remember\": false }`\n- `GET /v1/approvals?limit=` (1-500, default 100) is the read-only history:\n decided and still-pending approvals across all sessions, newest ask first.\n\nEach history row carries `approval_id`, `tool_name`, `outcome`, `asked_at` and\n`decided_at` (null while pending), plus optional `decided_by` (`user`,\n`session_rule`, `posture`, `host`), `target` and `summary`. `outcome` is one of\n`allowed_once`, `denied`, `timeout`, `cancelled`, `unavailable`,\n`retry_with_policy`, or `pending`. `standing_rule` (`allow` or `deny`) is present\nonly when a remembered session rule or grant answered rather than a person\n(`decided_by: session_rule`); `outcome` is unchanged. `target` is the host of a\nURL or a workspace-relative path (absolute paths outside the workspace are\nreduced to `.../<basename>`); `summary` is a one-line description of what was\nasked. Both are redacted conservatively when the ask is recorded: a shell\ncommand keeps only the program name and plain arguments, and any quoted,\n`KEY=VALUE`, credential-like or unrecognised argument replaces all arguments\nwith `[args redacted]`. Whitespace is collapsed and text is capped at 120\ncharacters. Rows recorded before these fields existed\nomit `target` and `summary`.\n\n`approval_id` is minted by the Runtime, not by the model or the provider. It is\nan opaque `approval_<32 hex>` capability, unique per prompt, bound to the thread\nthat raised it, and single-use: the Runtime removes it when the decision is\ndelivered, when the prompt times out, or when the turn abandons it. Clients echo\nthe value they were given and must not construct, derive, or guess one.\n\nIt is deliberately **not** the provider's tool-call ID. Providers restart their\ncall-ID counters per response, so two threads can gate calls whose raw IDs are\nbyte-equal; keying approvals by that value let one thread's decision settle\nanother thread's call. The endpoint therefore performs one exact match on the\nminted ID and has no fallback: a raw tool-call ID, an expired ID, or a replayed\nID that has already been settled all return `404` and reach no engine. A `404`\nmeans the capability is not pending — it is not evidence about how the approval\nwas resolved; read `approval.decided` for that.\n\nThe Engine tool-execution ID travels separately as `tool_call_id` on\n`pending_approvals[]` and on the approval events. It is a correlator for\nattaching a prompt to the tool row it gates, and never accepted as a decision.\nEach thread-detail `pending_approvals[]` entry is\n`{ \"id\", \"turn_id\", \"tool_name\", \"input\"?, \"description\", \"intent_summary\"?, \"tool_call_id\"?, \"summary\"? }`,\nwhere `id` is the capability above. `input` holds the captured tool input. An\nomitted `input` means no captured value is available; an explicit JSON `null`\nremains a captured value. On the authenticated canonical thread surface,\n`approval.required` carries the same captured input.\n`summary` (also on `approval.required`) is\na one-line description of the gated call built from the tool name and its\narguments only, never from model text (\"Search the web for 'espresso'\",\n\"Write notes/espresso.md\"); paths inside the workspace are workspace-relative.\nClients show it first and keep the raw arguments behind it. For task and\nautomation create/update, `summary` also names the requested trust mode,\nshell, auto-approve, mode and workspace. For a shell command, task gate,\nshell input or verifier whose `curl`/`wget` request body carries a credential\nfile or a secret-named variable, or reads stdin fed from one of those or from\n`env`/`printenv`, `summary` reads ``Upload credentials (`…`) with `…` ``; the\nbuilt-in safety gate holds that call, so a session grant or saved allow rule\ncannot answer it. A shell command, shell input or verifier over 64 KiB or\n4,096 words whose text contains `curl` or `wget` anywhere, or one nested too\ndeeply to read, is held the same way under its ordinary `summary`. A shell\ncommand containing a hidden Unicode character is refused before any\napproval is created.\n\n`\"remember\": true` on an `allow` records a **session grant** for that tool and\nargument class (the approval grouping key: a shell command family for a\nsimple, known command such as `git status` whose only options are value-free\nones like `-s` or `--porcelain` — a compound, wrapper, interpreter or\nunrecognised command, one with any other option, or one whose arguments are\nwhat runs or is installed (`go run`, `make`, `git bisect`, package installs)\nis granted as its full normalized command, and a\nshell interact or wait call as the exact call — a patch's file set, a `fetch_url` host, an MCP tool, a `web.run` action kind — for\n`open`, the hosts it opened). Computer Use consent and `app_script` calls, and\nany tool without a class, are granted for the exact call only. A grant never\nchanges the thread's permission posture. Later matching calls on the thread are\napproved without a prompt: they still emit `approval.required`, then\n`approval.decided` with `\"auto\": true` and the `grant_id`. Creating a grant\nemits `approval.grant_added` with `{ \"grant\": { \"grant_id\", \"tool_name\",\n\"scope\", \"summary\", \"granted_at\" } }`; thread detail lists live grants in\n`approval_grants[]`. `DELETE /v1/threads/{id}/approval-grants/{grant_id}`\nrevokes one (emitting `approval.grant_revoked`); the next matching call\nprompts again. Archiving or deleting the thread ends all of its grants\n(archiving emits `approval.grant_revoked` for each; unarchiving does not\nrestore them). Grants are stored in the Runtime data directory\n(`approval_grants.json`). After a Runtime restart a thread's grants stay\nsuspended, and matching calls prompt again, until an authenticated client\nresumes the thread. A grant store that cannot be read holds no grants, so every\ncall prompts. A forced (non-bypassable) prompt is never answered by a grant.\n\n**User input**\n- `POST /v1/user-input/{thread_id}/{input_id}` with body\n `{ \"answers\": [{ \"id\": \"question-id\", \"label\": \"Choice\", \"value\": \"Choice\" }] }`\n\nSubmitted values are delivered to the active model turn but are deliberately\nexcluded from durable Runtime items and events. The settled tool item contains\nonly a neutral receipt and a machine-readable `response_redacted` marker. The\nRuntime accepts only an exact pending `(thread_id, input_id)` request; an\nunknown, concurrently settling, or already settled id returns 404 and is never\nplaced in the engine mailbox. It commits the secret-free\n`user_input.answered` receipt before removing the snapshot-authoritative prompt\nor delivering the answer to the engine. That settlement runs independently of\nthe HTTP connection, so disconnecting after submission cannot leave a prompt\nhalf accepted. Terminal-turn cancellation follows the same receipt-before-\nremoval ordering through `user_input.canceled`.\n\n**Client-executed dynamic tools**\n- `POST /v1/threads/{thread_id}/turns/{turn_id}/tool-calls/{call_id}/result`\n\nThe thread and turn in the result route must match the pending call. A call is\nsettled at most once; wrong-route and duplicate results return 404. Terminal\nlifecycle events carry identifiers and status only, never tool result content.\nThe Runtime commits the terminal lifecycle event before making a submitted\nresult available to the model. Result delivery, timeout, and terminal-turn\ncancellation race through one settlement owner, so exactly one of these events\nis durable for a call:\n\n- `tool_call.requested` — the typed client-executed call became pending;\n- `tool_call.resolved` — a result was durably accepted by the Runtime\n (`result_accepted: true`; `success` is result metadata, but result content is\n excluded);\n- `tool_call.timeout` — no result won before the bounded wait expired;\n- `tool_call.canceled` — the turn terminated before a submitted result won.\n\nHTTP `202 Accepted` and `tool_call.resolved` share that durable-acceptance\nmeaning. Neither claims that the model consumed the result: a concurrent turn\nshutdown may close the model receiver after acceptance. Once the Runtime has\naccepted the result, that call is terminal and a duplicate result returns 404.\n\n**Events** (SSE replay + live stream)\n- `GET /v1/threads/{id}/events?since_seq=<u64>&replay_limit=<n>&progress=true`\n\nCursors:\n\n- `since_seq` is the per-thread cursor: events with `seq > since_seq` are sent.\n Omitted (and no `Last-Event-ID`), the stream starts from the beginning of the\n thread's history.\n- Every journal frame carries `id: <seq>`, so a browser `EventSource` can\n resume through the `Last-Event-ID` header it sends on reconnect. An explicit\n `since_seq` wins over the header, so a deliberate replay from `0` is never\n overridden by a stale id. A header value that is not a decimal integer is\n ignored.\n- `replay_limit` (at most 4096) returns only the newest tail of the requested\n history; `previous_seq` on the first returned event advances past exactly the\n omitted history.\n\nDurable history parsing runs off the async server workers and reaches SSE in\nbounded batches of at most 256 events through a backpressured channel. Broadcast\ndelivery is only a wake-up optimization: a lagged receiver opens the same\nbounded durable replay from its last accepted cursor.\n\n`progress=true` adds `stream.progress` transport frames\n(`{schema_version, event, kind, thread_id, seq, state}`, `state` is\n`replaying` or `live`) at the current cursor and advertises them with\n`x-codewhale-event-progress: 1`. The stream reports `live` only after durable\nhistory and the already-queued live tail are both drained; broadcast-lag\nrecovery returns it to `replaying`. Progress frames never carry a new sequence\nnumber.\n\nFailures before the stream opens are ordinary HTTP errors with the JSON error\nbody, never SSE:\n\n| Status | When |\n| --- | --- |\n| `401` / `403` | Missing or wrong Runtime credential |\n| `404` | Unknown thread |\n| `400` | `replay_limit` above 4096 |\n| `500` | The durable history could not be opened (including a replay worker crash before the first cursor) |\n\nOnce the response is `200`, every end the server chooses is a final\n`stream.end` frame; see [Ending and resuming a thread\nstream](#ending-and-resuming-a-thread-stream).\n\n**Snapshots** (side-git restore point listing + restore)\n- `GET /v1/snapshots?limit=20&thread_id=`\n- `POST /v1/snapshots/{id}/restore?thread_id=`\n\n`/v1/snapshots` lists recent side-git restore points for the runtime workspace.\n`limit` defaults to `20` and must be between `1` and `100`. `POST\n/v1/snapshots/{id}/restore` restores workspace files from the snapshot and\nreturns `{\"restored\": \"<snapshot-id>\"}`. It is the direct operator surface for\nthe server's own workspace (the same action as the TUI's `/restore <N>`): it is\ngated by the Runtime API bearer token, not by any thread's trust flag, and it\nis refused with `409` while a turn is active in an overlapping workspace (see\nbelow). A `pre-restore:` safety snapshot is taken first. With `thread_id`\n(capability `thread_scoped_snapshots`) both routes act on that thread's\nrecorded workspace instead, under the same admission rule; an unknown thread\nanswers `404` and a missing or changed workspace `409`\n`thread_workspace_unavailable`.\n\n```json\n[\n {\n \"id\": \"snap_...\",\n \"label\": \"post-turn:1\",\n \"timestamp\": 1780730580\n }\n]\n```\n\n### Workspace restore endpoints\n\nThree routes mutate workspace files from side-git snapshots. They share one\nadmission rule and one safety net, and they differ in scope and trust.\n\n| Route | Scope | Trust | Forks the thread |\n| --- | --- | --- | --- |\n| `POST /v1/snapshots/{id}/restore` | whole server workspace, or the `thread_id` workspace | bearer token only (operator action) | no |\n| `POST /v1/threads/{id}/patch-undo` | the files the dropped turns changed | thread `trust_mode` or `auto_approve` when files would change | yes |\n| `POST /v1/threads/{id}/file-revert` | exactly one regular file | thread `trust_mode` or `auto_approve`, always | no |\n\n**Admission.** A restore reserves the same admission the Runtime uses for\nconfig reloads and session checkpoints, so no new turn starts and no saved\nhistory changes while files are being rewritten. If any thread already has an\nactive turn in the same workspace, a nested checkout of it, or a parent of it,\nthe request is refused with `409` and the message `already has an active turn`.\nThe reservation is owned by the worker performing the Git mutation, so a client\nthat disconnects mid-request cannot release it early; the operation completes\nor fails as a whole. Concurrent restores serialize. The reservation is\nruntime-wide: while a restore's safety snapshot and checkout run, new turns,\nsteering, compaction and user-input delivery on every thread wait for it to\nfinish, so a large workspace can add seconds of latency elsewhere during a\nrestore. A thread whose workspace directory is not available (unmounted\nvolume, disconnected share, missing directory) is refused with `409` rather\nthan treated as having nothing to restore.\n\n**Ownership.** A thread owns exactly the workspace restore points recorded on\nits own turns. While a turn runs, the engine reports each snapshot it takes —\n`pre_turn` before the turn, `tool` before and `post_tool` after each tool call\nthat may write (every call that is not read-only: file tools, shell commands,\nprograms, write-capable MCP tools), `post_turn` when it ends (always before\nthe turn settles) — and the Runtime appends it to the turn record's\n`workspace_snapshots`, in order:\n\n```json\n\"workspace_snapshots\": [\n { \"kind\": \"pre_turn\", \"snapshot_id\": \"<commit>\", \"tree_id\": \"<tree>\", \"session_id\": \"thr_1a2b3c4d\" },\n { \"kind\": \"tool\", \"snapshot_id\": \"<commit>\", \"tree_id\": \"<tree>\", \"session_id\": \"thr_1a2b3c4d\", \"tool_call_id\": \"call_…\", \"write_paths\": [\"src/lib.rs\"], \"changed_paths\": [] },\n { \"kind\": \"post_tool\", \"snapshot_id\": \"<commit>\", \"tree_id\": \"<tree>\", \"session_id\": \"thr_1a2b3c4d\", \"tool_call_id\": \"call_…\", \"changed_paths\": [\"src/lib.rs\"] },\n { \"kind\": \"post_turn\", \"snapshot_id\": \"<commit>\", \"tree_id\": \"<tree>\", \"session_id\": \"thr_1a2b3c4d\", \"changed_paths\": [] }\n]\n```\n\n`changed_paths` lists the workspace-relative paths whose content changed since\nthe turn's previous receipt — what happened in the span the receipt closes; it\nis absent on `pre_turn` and when it could not be computed (a snapshot in\nbetween failed). `write_paths` is set on the `tool` receipt of a file tool\n(`write_file`, `edit_file`, `apply_patch`) to the paths the call declared, as\nit named them; a tool without it (a shell command) may write any path. On a\nuser shell turn the `pre_turn` receipt carries the command's `tool_call_id`,\nsince the command runs from it to `post_turn`.\n\nEach receipt is also published as a `turn.workspace_snapshot` event (payload:\nthe receipt). The engine runs every Runtime thread under the thread's own id,\nacross restarts and engine eviction, so `session_id` is the thread id for turns\nthe thread ran itself; it does not follow the thread's saved-session binding\n(`PUT`/`POST /v1/sessions`, resume), which only names a document. A fork clones\nits source's turn records and so owns the restore points of the turns it\ninherited. Another thread's, or a TUI session's, snapshots in the same\nworkspace are never candidates. `tree_id` is the durable identity: a prune\nrebuilds the side repo and rewrites every commit id but keeps each tree, and a\nrestore point resolves only to a stored snapshot with the same tree, session\ntag and kind. The count prune after each snapshot keeps the newest 50\nsnapshots plus the newest 50 turn boundaries (`pre-turn:`/`post-turn:`), so a\nturn with more tool calls than that, or a burst from another thread, never\npushes out a recent turn's own restore points. Turns recorded before receipts existed, turns imported by\n`resume-thread`, and turns run with snapshots off or unavailable have none.\n\n**Safety net.** Every restore first records a `pre-restore:<target>` snapshot\nof the current workspace. That label is never a `/undo`, `patch-undo` or\n`file-revert` candidate, so the net does not change what later undos select.\nFor `file-revert` the backup is mandatory: if it cannot be written, or the\nrequested file is excluded from it (for example by `.gitignore`), the request\nfails and nothing is changed.\n\n**`patch-undo`.** Undoes whole turns. A named `turn_id` removes that user turn and every turn after it, under the same restore admission as `/undo`. Clients must require `thread_patch_undo_turn_id` before sending selected-turn requests: older Engines may ignore that field. Preview with `GET`, then send its exact `turn_ids` as `expected_turn_ids` and `paths` as `expected_paths` on `POST`; `409 undo_selection_changed` refuses an altered range or file plan before file changes. A preview does not reserve future execution or promise idempotency. The mutation is not automatically retryable after a lost response.\n\n For each dropped turn's `pre_turn` →\n`post_turn` window, the paths that differ between the two snapshots must all be\nthe turn's own: changed only in the span of one of the turn's tool calls (a\n`tool` → `post_tool` span, or a shell turn's whole window), and, inside a file\ntool's span, a path that call declared. A path that changed while none of the\nturn's tools could have written it — another thread, an editor, a background\nprocess — is someone else's change, and the undo is refused rather than revert\nit. Each of the turn's paths goes back to its content before the first dropped\nturn that changed it, and nothing else is touched, so later work by the user or\nanother thread in the same workspace survives. Then the conversation forks\nexactly as `/undo` does.\n\nSnapshots never hold paths excluded by the workspace's `.gitignore` files or\nthe built-in snapshot exclusions (`node_modules/`, `target/`, `dist/`, build\ncaches, binary artifacts), nor paths outside the workspace. A dropped file-tool\ncall that declared such a path is refused with `path_not_snapshotted`, because\nno snapshot can put it back. A shell command declares no paths: what it writes\nunder an excluded path (build output, dependency installs) is outside what\n`patch-undo` restores and is not reported. `201` means either files were restored (`files_restored: true`, one\n`<action> <path>` line per file in `summary`, `snapshot_label` naming the\npre-turn snapshot) or there was provably nothing to restore\n(`files_restored: false`): every dropped turn ran here without a tool call,\nchanged no file, or its files are already back at their pre-turn content.\nAnything that cannot be restored aborts the whole undo with `409`, nothing is\nchanged and no fork is published; `error.code` says why:\n\n| `error.code` | Meaning |\n| --- | --- |\n| `restore_point_unavailable` | a dropped turn that may have changed files has no complete recorded restore point (older record, imported by `resume-thread`, snapshots off, or the snapshot failed) |\n| `restore_point_pruned` | the restore point is no longer in the snapshot store |\n| `path_not_snapshotted` | a dropped file-tool call wrote a path snapshots do not hold (ignored, built-in exclusion, or outside the workspace) |\n| `workspace_changed_since_turn` | a path the turns changed was changed afterwards (or between two dropped turns), a path changed while a dropped turn ran but outside its own tool calls, or a path that is not a regular file |\n| `restore_requires_trust` | there is something to restore and the thread is not in trusted mode or Full Access |\n| `workspace_unavailable` | the workspace directory is not available |\n\nFor the first four a client can offer the conversation-only\n`POST /v1/threads/{id}/undo` instead, and `file-revert` for individual files.\nSnapshot repository, listing or comparison failures abort with `500` and also\npreserve the conversation, so a turn is never dropped while its file changes\nstay on disk. Depth and history are validated before any file changes. If the\nfork cannot be persisted after files were restored, the response is a `500`\nthat names the restored snapshot; the original thread still holds the turn and\nthe `pre-restore:` snapshot holds the previous files.\n\n**`file-revert`.** Request body:\n\n```json\n{\n \"path\": \"src/lib.rs\",\n \"snapshot_id\": \"3f2a…40-or-64 hex…\",\n \"expected_hash\": \"sha256:<64 lowercase hex digits>\"\n}\n```\n\n- `path`: workspace-relative, or absolute inside the thread workspace. The\n name is literal (brackets, spaces and glob characters are filename bytes;\n Git runs with `--literal-pathspecs`). It must name a regular file: directories,\n symlinks anywhere in the path, and `.git` components are `400`.\n- `snapshot_id`: the exact `tool` or `pre_turn` restore point of the change\n the user selected, from the thread's own turn records: a receipt's\n `snapshot_id` or `tree_id` (for a tool call, the receipt whose\n `tool_call_id` matches), or the current commit id `GET /v1/snapshots` lists\n for it. It must be a restore point recorded on one of this thread's turns;\n the server never picks \"the newest snapshot that differs\", because an\n unrelated newer snapshot can erase later user edits while leaving the tool's\n change.\n- `expected_hash`: `sha256:` of the current file bytes the client displayed,\n or `absent` when the client saw the file as deleted. It is checked before the\n safety backup and again immediately before the mutation.\n\nResponses:\n\n- `200 {\"path\", \"action\", \"snapshot_id\", \"snapshot_label\"}` — `action` is\n `modified`, `recreated` (file was missing) or `removed` (the snapshot does\n not contain the file, so the file the tool created is deleted; its parent\n directories are left in place).\n- `400`: malformed `snapshot_id`/`expected_hash`, path outside the workspace,\n or a path that is not a regular file on either side.\n- `404`: unknown thread.\n- `409`: thread not in trusted mode or Full Access; active turn in an\n overlapping workspace; workspace directory not available; snapshot unknown,\n pruned, not recorded on this thread's turns (another thread's or a TUI\n session's), or not a restore point (refresh the change record); file already\n matches the\n snapshot (nothing to revert); or the file changed after the reviewed\n `expected_hash` (refresh and review again). Nothing is changed in any of\n these cases.\n- `422`: missing or mistyped body fields.\n- `500`: Git or filesystem failure; a failure after the safety snapshot names\n that snapshot so the previous bytes can be recovered with\n `POST /v1/snapshots/{id}/restore` or `/restore`.\n\nCapability probe: `GET` on the route returns `405` where the endpoint exists\nand `404` on an older engine; clients treat any non-`404` as available and\ndegrade with an explanation otherwise.\n\n**Compatibility stream** (one-shot, backwards-compatible)\n- `POST /v1/stream`\n\n**Tasks** (durable background work)\n- `GET /v1/tasks`\n- `POST /v1/tasks`\n- `GET /v1/tasks/{id}`\n- `POST /v1/tasks/{id}/cancel`\n\n**Automations** (scheduled recurring work)\n- `GET /v1/automations`\n- `POST /v1/automations`\n- `GET /v1/automations/{id}`\n- `PATCH /v1/automations/{id}`\n- `DELETE /v1/automations/{id}`\n- `POST /v1/automations/{id}/run`\n- `POST /v1/automations/{id}/pause`\n- `POST /v1/automations/{id}/resume`\n- `GET /v1/automations/{id}/runs?limit=20`\n\nCreate and update requests accept an optional `model`. When present, each\nscheduled or manually triggered run uses that model; omitting it keeps the\nruntime's default task model.\n\nCreate and update requests also accept an optional `destination_thread_id`.\nWhen set, each run is delivered into that existing thread as a new turn\ninstead of starting a new task thread; the turn item carries\n`{\"input_provenance\":\"scheduled\",\"task_id\":...}` metadata, and the turn uses\nthe thread's own model and permission posture. If the thread is busy the run\nwaits for its current turn to finish (bounded by the task wall-time budget).\nA destination that no longer exists or is archived fails the run with a\nreceipt naming the thread; it is never rerouted to a new thread. An empty\nstring on `PATCH` clears the destination. Create and update return `404` for\nan unknown thread and `409` for an archived one.\n\n**Delayed triggers** (one-shot messages; the HTTP face of `send_later`)\n- `GET /v1/triggers?thread_id=&status=&limit=50` — newest first; `thread_id`\n filters by destination thread, `status` is one of `pending`,\n `dispatching`, `fired`, `canceled`, `failed`\n- `POST /v1/triggers` — `{\"message\", \"fire_at\" | \"delay_minutes\",\n \"thread_id\"?, \"workspace\"?, \"owner_session_id\"?}`; exactly one of `fire_at`\n (RFC 3339, in the future) or `delay_minutes` (>= 1). `owner_session_id`\n defaults to `thread_id` and is required without one. Returns `201` with the\n `DelayedTriggerRecord`; honours `Idempotency-Key` like `POST\n /v1/automations`\n- `GET /v1/triggers/{id}`\n- `DELETE /v1/triggers/{id}` — cancels a pending trigger; `409` once it has\n been admitted\n\nA trigger with `destination_thread_id` fires into that thread exactly as an\nautomation run does above. `send_later` called from inside a Runtime thread\ndefaults its destination to that thread, so the reminder comes back to the\nsame conversation; `thread_id: \"\"` keeps the old new-task-thread behaviour.\nThe `automation` tool accepts the same `thread_id`. From a tool, `thread_id`\ncan name only the calling thread (`\"current\"` or its id); targeting another\nthread is a Runtime API operation.\n\nWhen an automation or trigger names a destination thread, the Engine\npublishes typed events on that thread's stream: `automation.created`,\n`automation.updated` (`automation_id`, `name`, `rrule`, `status`,\n`next_run_at`), `trigger.scheduled`, `trigger.canceled` and `trigger.fired`\n(`trigger_id`, `message`, `fire_at`, `status`, `task_id`). They are\nnotifications for captions, not model input.\n\n**Automation mutation preconditions.** `GET /v1/runtime/info` advertises\n`capabilities.automation_mutation_preconditions: true`; require it before\nrelying on the behavior below (older Engines ignore both inputs).\n\n- Every `AutomationRecord` carries an integer `revision`: `1` on create, `+1`\n on each `PATCH`, `pause` and `resume`; legacy records read as `0`. Scheduler\n bookkeeping (`next_run_at`, `last_run_at`) does not change it.\n- `expected_revision` is an optional precondition on `PATCH`, `run`, `pause`\n and `resume`: a JSON body field on `PATCH`, or the `?expected_revision=N`\n query parameter on any of the four. On mismatch nothing is written and the\n response is `409` with\n `{\"error\":{\"code\":\"revision_conflict\",\"current_revision\":N,...}}`. Omitting\n it behaves as before.\n- `Idempotency-Key` (1-255 visible ASCII characters) is optional on `POST\n /v1/automations`, `PATCH`, `run`, `pause` and `resume`. The first successful\n response (2xx) is persisted in `idempotency.json` beside the automation\n store, bounded to 1024 entries and a 24 hour TTL. A retry with the same key\n and identical request returns the stored status and body with\n `Idempotency-Replayed: true` and performs no second create or run. The same\n key with a different request is `422` `idempotency_key_reuse`. Failed\n attempts are not stored, so they can be retried. Keys are scoped to the\n route and automation id. If the record cannot be persisted after the\n mutation committed, the success response is still returned (the failure is\n logged), so a retry of that request is not deduplicated. An unparseable\n `idempotency.json` is renamed to `idempotency.corrupt-<timestamp>.json` and\n that request fails with `500`; later requests start from an empty store.\n `DELETE` is not covered.\n\n**Operate** (always-on named operation; same `OperateRecord` as CWC\n`20de981` / PR #284)\n\n- `GET /v1/operate` — current operation + plan board\n- `POST /v1/operate` — create (`direction`, optional `burnRate`)\n- `PATCH /v1/operate` — steer direction, `burnRate`, or `leadPlan`\n- `PUT /v1/operate/plan` — set `leadPlan` (`{ slices: [...] }`)\n- `POST /v1/operate/keepalive` — observe spend / burn; never stops\n- `POST /v1/operate/cancel` — explicit cancel (`/v1/operate/stop` aliases);\n also pauses the `cw-operate` keepalive so nothing keeps spending after cancel\n- `POST /v1/operate/auto-merge/check` — call landed\n `scripts/check-auto-merge.py --repo --pr --agent` (does not merge)\n\nThe operation record (`current.json`) is persisted under a cross-process\nfile lock with atomic temp+rename writes; every PATCH / keepalive / plan\nsave reloads the latest state inside the lock, so concurrent saves merge\ninstead of losing writes. A `PATCH` that changes `direction` invalidates\nthe recorded `leadPlan` (workers stop executing superseded slices) and\npulls the keepalive lead run forward to re-plan. `POST /v1/operate`\ninstalls the hourly `cw-operate` keepalive and kicks its first lead-plan\nrun immediately instead of waiting out the first recurrence; credentials\nresolve through the normal Z.ai provider resolution (config, `api_key_env`,\nsecret store, or provider env vars — blank values count as missing).\n\n`burnRate` is `{ \"kind\": \"usd_per_hour\", \"amountUsdPerHour\": number }`,\na positive number, or `null` (unbounded). Status is\n`planning | running | idle_blocked | cancelled`. Pace\n(`unbounded | hold | throttle | widen`) is not a status: over-target\nthrottles, under-target widens, no wallet-cap stop. Idle-blocked only\nfor empty direction, awaiting lead plan, missing credentials, or a\nhuman gate. Auto-merge is `scripts/check-auto-merge.py --repo … --pr …\n--agent …` from codewhale-ops `origin/main` (exit 0), then\n`scripts/auto-merge-pr.py`. Do not invent a second checker.\n\n**Workspace trust** (the persisted project decision)\n- `GET /v1/workspace/status` reports `workspace_trusted`: whether the server’s workspace is trusted in the discovered user config. Project skills, commands and MCP configuration consult this decision. Project hooks require both workspace trust and separate approval of the exact hooks file.\n- `POST /v1/workspace/trust` with `{\"trusted\": true|false}` records that decision and answers `{workspace, workspace_trusted, config_path, message}`.\n\nThe handler reuses `config::set_workspace_trust`, the writer behind `/trust on|off --save`, and reads the decision back through `is_workspace_trusted`. The request accepts no path; it applies only to the server’s admitted workspace. `config_path` identifies the document written. Both read and write use default config discovery, including configured environment overrides; a runtime’s explicit `--config` argument does not select this trust document.\n\nProject skills and commands consult trust at their existing load or turn boundaries. Project hooks also need their separate digest approval and recheck authority before execution. Restart the engine to rebuild project MCP connections; this request does not disconnect an existing pool or interrupt a running turn. Project instruction files such as `AGENTS.md` and project rules are loaded by the existing context loader independently of this flag, so revoking workspace trust does not remove those instructions or their persisted history.\n\nThis persisted project decision is separate from a thread’s `trust_mode` / Full Access and from `/trust add <path>` external-path grants. `/trust on|off --save` changes session file-tool trust and the persisted project record. This HTTP route changes only the persisted project record. Its POST requires the same Runtime write authority as other state-changing routes; watch device tokens can read status but cannot write trust.\n\n**Introspection**\n- `GET /v1/workspace/status`\n- `GET /v1/workspace/files/search?query=<partial>&limit=<1-100>` (see workspace file suggestions above)\n- `GET /v1/workspace/files?path=<dir>&limit=<1-2000>`, `GET /v1/workspace/files/read?path=<file>&offset=&limit=`\n and `PUT /v1/workspace/files` (see workspace files and session artifacts above)\n- `GET /v1/skills`\n- `GET /v1/skills/{name}` — one skill's routing metadata (`source`,\n `invocation`, `aliases`, `bundled_tier`, `enabled`) plus its full\n `SKILL.md` body, so a client can compose an activation instruction for its\n own next turn the way TUI's `/skill <name>` does. `404` for a name no\n discovery root holds; `403` for a plugin snapshot whose authority is no\n longer current; native rows whose file has since been deleted also `404`\n rather than serving the stale body. Advertised as\n `capabilities.skill_detail` on `GET /v1/runtime/info`, which is the source a\n client should use rather than probing this path: a `404` here means \"no such\n skill\" and is indistinguishable from \"no such route\".\n- `GET /v1/apps/mcp/servers`\n- `GET /v1/apps/mcp/tools?server=<optional>`\n\nSkill activation toggles are persisted under a cross-process transaction lock.\nEach mutation reloads and merges the latest exact-name state before an atomic\nwrite, and `GET /v1/skills` refreshes that shared state so another Codewhale\nprocess's successful toggle is visible without restarting the Runtime API.\n\nSkill rows on `GET /v1/skills` carry `invocation`, `aliases`, and\n`bundled_tier` alongside the fields they always carried, so a client can build\na picker, autocomplete, or activation gate without a second request per row.\n\n**Usage** (token/cost aggregation across threads)\n- `GET /v1/usage?since=<rfc3339>&until=<rfc3339>&group_by=<day|model|provider|thread>`\n\n`since` / `until` are inclusive RFC 3339 timestamps and may be omitted (no\nbound). `group_by` defaults to `day`. Buckets are sorted by ascending key.\nEmpty time ranges produce empty `buckets` (never a 404). Cost is computed via\nthe model→pricing map; turns whose model has no pricing entry contribute\ntokens but `0.0` cost. Added in v0.8.10 (#564).\n\n```json\n{\n \"since\": \"2026-04-01T00:00:00Z\",\n \"until\": \"2026-04-30T23:59:59Z\",\n \"group_by\": \"day\",\n \"totals\": {\n \"input_tokens\": 12345,\n \"output_tokens\": 6789,\n \"cached_tokens\": 0,\n \"reasoning_tokens\": 0,\n \"cost_usd\": 0.012,\n \"turns\": 42\n },\n \"buckets\": [\n {\n \"key\": \"2026-04-30\",\n \"input_tokens\": 1234,\n \"output_tokens\": 678,\n \"cached_tokens\": 0,\n \"reasoning_tokens\": 0,\n \"cost_usd\": 0.001,\n \"turns\": 3\n }\n ]\n}\n```\n\n### Native client routes\n\nThese families serve native clients over the same bearer-token\ntransport. They reuse the runtime's existing authorities — the engine's shell\nmanager, the durable thread store, the workspace confinement layer, the\nconfig's credential plumbing — and add no second runtime, session store,\nscheduler, or credential store.\n\n**Terminal sessions** (the persistent Engine-owned shell)\n\nThe jobs family above runs one command per job. A terminal pane needs the\n*other* authority: the stateful PTY-backed shell the agent's own terminal\ntools drive, which keeps cwd and environment across inputs. These routes\nattach to that session and never create one — a name with no live session is\n`404`, because conjuring a shell from an HTTP request would give the client a\nterminal the Engine does not know about. Input is attributable by route:\n`input` is the client's writer, `terminal_send` is the agent's.\n\n- `GET /v1/terminal/{name}/output?cursor=<bytes>&max_bytes=<1-64KiB>&format=\n <base64|text>` — the resumable byte stream. `{name, offset, next_cursor,\n total, dropped, encoding, data, running, exit_code}`: pass `next_cursor`\n back to continue; reads never consume, so several clients may hold\n independent cursors; `dropped` reports bytes the 512 KiB ring discarded,\n and a cursor past `total` is answered from `total` rather than echoed back\n- `POST /v1/terminal/{name}/input` — `{ \"data\", \"encoding\"? }`, `base64` by\n default (exact bytes) or `text` for UTF-8 → `{ \"name\", \"written\" }`\n- `POST /v1/terminal/{name}/resize` — `{ \"rows\", \"cols\" }` → the kernel\n window the child draws for\n- `POST /v1/terminal/{name}/kill` — end the shell; observe the exit through\n `output` (`running` / `exit_code`) rather than the acknowledgement\n\n`GET /v1/runtime/info` advertises `terminal_stream`, `terminal_input`,\n`terminal_resize` and `terminal_kill`. All four are `false` on Windows and OpenHarmony builds\ntoday: the owner is Unix-only, those routes answer `501`, and a client\nshould gate its terminal controls on these flags rather than discovering it\nfrom a failed request. Known limitations, stated because a reader would\notherwise assume them: there is no `wait_ms` long poll (poll the cursor),\nscrollback dropped by the ring is gone with the process, a restarted Engine\nreports no session rather than pretending to reattach, and the\n`@codewhale/runtime-sdk` package has no terminal client wrapper yet — the raw\nroutes are the contract for now.\n\n**Jobs** (operator-scoped shell jobs; the terminal surface)\n- `GET /v1/jobs` — every live and known-stale job across all threads\n- `GET /v1/threads/{id}/jobs` — jobs owned by one thread's manager:\n model-launched, subagent-launched, and client-launched together\n- `POST /v1/threads/{id}/jobs` — `{ \"command\", \"cwd\"?, \"timeout_ms\"?,\n \"tty\"?, \"env\"? }` → `201 { \"job\" }`; runs as a background shell under the\n thread's projected sandbox policy. `tty: true` merges stderr into stdout\n and gives the command a terminal (required for interactive programs);\n background jobs are never killed at `timeout_ms`. A relative `cwd`\n resolves against the thread workspace. Outside trust mode `cwd` must stay\n inside it after symlinks resolve (`403` otherwise): unlike the shell tool,\n this route does not follow `workspace_follow_symlinks` or `/trust add`\n roots, so a symlink leading out of the workspace is refused. The job runs\n in the resolved directory that was checked (a later symlink retarget does\n not move it); a `cwd` that resolves to a non-UTF-8 path is a `400`\n- `GET /v1/threads/{id}/jobs/{job_id}` — one job's status + metadata\n- `GET /v1/threads/{id}/jobs/{job_id}/output?stream=<stdout|stderr>&cursor=\n <bytes>&max_bytes=<1-512KiB>&wait_ms=<0-30s>&format=<base64|text>` — the\n resumable byte stream. `{job_id, stream, offset, next_cursor, total,\n dropped, encoding, data, status, exit_code, done}`: pass `next_cursor`\n back to continue; `wait_ms` long-polls for new bytes on a running job;\n `done` means a terminal status and nothing left past the cursor\n- `POST /v1/threads/{id}/jobs/{job_id}/stdin` — `{ \"data\", \"encoding\"?,\n \"close\"? }`: `data` is UTF-8 text by default or `base64`, `close: true`\n sends EOF; works for PTY and piped jobs → `204`\n- `POST /v1/threads/{id}/jobs/{job_id}/kill` — bounded SIGTERM → SIGKILL\n escalation on the process group → `{ \"job\", \"result\" }` with the final\n snapshot\n\nReads are non-consuming: several clients may hold independent cursors, and\npolling never steals output from the engine's own delta consumer. The\nbuffer is bounded with exact drop accounting — a reader whose `cursor`\nfalls behind the retained window gets `offset` past it and `dropped > 0`,\nand must re-anchor. Evicted jobs keep a tail snapshot which the output\nroute serves as the final retained window. Jobs are scoped to the thread\nthat created them and are killed when that thread is removed; the engine's\nbackground commands use the same per-thread manager, so `GET /v1/jobs` is\nalso how a client sees model-spawned work.\n\n**Durable client job operations** (capability `job_operations`)\n\nAuthorized `GET /v1/runtime/info` supplies `job_operation_store_id`, the\npersisted runtime store identity. It survives Engine updates and restarts;\nit is a namespace, not a credential. Unauthorized discovery returns null.\nKeyed job creation adds all four fields to the existing POST body:\n`operation_key` (a fresh UUID v4 retained before submission), `purpose`\n(`setup`, `run`, `archive` or `command`), `expected_store_id`, and\n`expected_workspace`. The store must match the current store, and the workspace\nassertion must match the admitted thread workspace; otherwise dispatch refuses\n`409 job_operation_store_changed` or `409 job_operation_workspace_changed`.\nThese assertions never move a thread. Omitting all four retains the legacy\n`201 {job}` contract.\n\nA new keyed request returns `201 {job, operation, request_fingerprint}`. The same key and exact\nnormalized request return `200` with the existing operation and never spawn\nagain, including after restart or a lost response. Changing the bound request\nreturns `409 job_operation_mismatch`. `job` may be null when the durable\nreceipt survives without its live handle or retained output. The operation is:\n\n```json\n{\n \"id\": \"opaque-receipt-id\",\n \"store_id\": \"runtime-store-id\",\n \"thread_id\": \"thread-id\",\n \"workspace\": \"/canonical/workspace\",\n \"cwd\": \"/canonical/launch-directory\",\n \"purpose\": \"run\",\n \"status\": \"running\",\n \"job_id\": \"shell-job-id\",\n \"job_status\": null,\n \"exit_code\": null,\n \"created_at_ms\": 1794000000000,\n \"updated_at_ms\": 1794000000000\n}\n```\n\n`status` is `running`, `completed`, `rejected` or `unknown`. A completed receipt has a\nterminal `job_status` (`Completed`, `Failed`, `Killed` or `TimedOut`) and a\nnullable exit code; success requires `Completed` and exit code zero. Timestamps\nare Unix milliseconds; `updated_at_ms` is the last persisted transition, not\nproof of when an unknown outcome occurred. The opaque receipt ID does not\nexpose the original operation key.\n\n`request_fingerprint` binds the original request, including its requested\nworkspace spelling; the operation's workspace and cwd remain canonical. It is\nlowercase HMAC-SHA256, keyed by the operation UUID, over UTF-8 JSON for\n`[2, thread_id, expected_workspace, trimmed_command, cwd_or_null, timeout_ms,\ntty, sorted_environment_pairs, purpose]`. Command trimming uses Unicode\nWhite_Space; environment pairs sort by UTF-8 key bytes. Omitted timeout is\n120000, omitted cwd is null and omitted tty is false. Persist the expected\nfingerprint before POST, and compare it on response and exact-key GET recovery\nbefore accepting a receipt or clearing a pending intent. An alias is not a\nreason to compare the canonical receipt path literally with the requested\nspelling. A missing or mismatched binding stays unconfirmed; do not re-POST\nautomatically. The UUID and fingerprint do not provide encryption or protect\nlow-entropy request values from someone who obtains both.\n\nA `rejected` receipt proves the command never started: its job fields are all\nnull. The shell executor supplies this typed witness only for audited failures\nbefore launch, such as PTY allocation or a working-directory check. It is a\nterminal tombstone, so the same key returns it without retrying; a new key can\ntry again after the problem is fixed. The POST refusal uses `409\njob_operation_rejected`. Spawn-wrapper and post-launch failures remain unknown\nunless a trustworthy witness proves otherwise.\n\n- `GET /v1/job-operations` and `GET /v1/threads/{id}/job-operations` return\n `{operations: [...]}`, including records whose workspace folder is gone.\n- `GET /v1/threads/{id}/job-operations/{operation_key}` returns the same\n `{job, operation, request_fingerprint}` envelope. A storage or admission error is not an empty\n list or proof of absence.\n- Contended admission returns `409 job_operation_pending`. A new keyed\n setup/run/archive overlapping an active keyed workspace script returns\n `409 job_operation_busy`. An overlapping unconfirmed operation refuses\n every new job, including legacy requests, with `409\n job_operation_unconfirmed`; worktree removal is fenced as well.\n\nThe existing runtime thread store atomically records intent before spawning;\nthe existing shell manager remains the execution authority. A completion\nobserver and reads persist verified terminal parent status without consuming\noutput. Durable records contain a keyed request fingerprint rather than raw\ncommands, environment values or operation keys. A missing live handle,\ndifferent owner generation or crash around dispatch remains `unknown`, never\npermission to repeat the command. New job IDs use full UUIDs; clients must\ntreat them as opaque. This is not an exactly-once OS transaction, durable\noutput capture, automatic unknown-outcome recovery or a phased archive\ntransaction. Detached descendants, agent/tool launches and other stores remain\noutside this API operation fence.\n\nAdmission resolves one canonical workspace before project shell policy checks\nand uses it for policy and cwd resolution. It rechecks the requested root and\nlaunch directory before recording intent. These pathname checks do not provide\na directory-handle guarantee against replacement after the final check.\n\nThread deletion and empty-thread discard share job admission and refuse a\nthread whose durable client job operation is running or unconfirmed. Stop the\nknown job and wait for its terminal receipt before deleting the conversation;\nan unknown outcome requires recovery rather than silently dropping the fence.\nCompleted and rejected receipts remain in the global operation list after\nthread deletion.\n\n**Commands** (typed command catalog, APPS-28)\n- `GET /v1/commands` — `{commands: [...]}`: every registered slash command,\n builtin and user, as the TUI's own registry holds it. Per entry: `name`,\n `aliases`, `summary` and `usage` (English source text — localizing is the\n client's surface), `subcommands` (the literal verbs the usage line\n declares), `takes_arguments`, `kind` (`builtin` registered code, or `user`\n expanding a stored template), `binding` (`host` runs locally and never\n reaches the model; `prompt` expands into the request the model sees),\n `discovery` (`primary` / `advanced` / `compatibility`, builtins only),\n `hidden` for rows the product does not advertise, and `shadowed_by` /\n `shadowed_aliases` where a user command has taken a builtin's spelling.\n\n Each entry also carries the composer argument shape, computed the way the\n TUI composer computes it, so a client does not re-derive it from `usage`\n (#6230):\n - `requires_argument` — the usage line mentions any argument, required or\n optional.\n - `requires_required_argument` — the usage line has a `<required>` argument\n outside every `[optional]` group.\n - `composer_wants_trailing_space` — accepting the command leaves a trailing\n space for its arguments.\n - `palette_runs_directly` — the palette runs the command on selection\n instead of pasting it into the composer.\n - `show_in_empty_discovery` — the command is listed when the slash menu\n opens with no filter text.\n\n User commands derive these from `takes_arguments`: their arguments are\n never required, a template that takes arguments waits in the composer and\n one that does not runs directly, and a `hidden` template stays out of empty\n discovery.\n\n The same registry the TUI palette reads, so a desktop palette can be\n checked against it instead of drifting from it. Two rules a client must\n respect: a `binding: \"host\"` row is never submitted as a model prompt, and\n a user command shadowing a builtin name wins that spelling.\n\n**Hooks**\n- `GET /v1/hooks[?thread_id=...]` — `{workspace, enabled, hooks: [...],\n problems: [...]}`: the hook set Runtime API threads run for that workspace\n (the server workspace, or the named thread's). Per entry: `name`, `event`,\n `command` (credential-shaped values masked; every URL keeps only its scheme and host), `background`, `timeout_secs`,\n and `source` (`global` user config, `plugin` reviewed plugin, `project`\n trusted and approved `.codewhale/hooks.toml`). `problems` lists hooks\n rejected or warned about at load, one line each.\n\n Every Runtime thread builds its engine with this set: `tool_call_before`\n can deny a call, `shell_env` applies to shell tools, and `tool_call_after`\n and `on_error` (for a failed tool) fire as observers, as they do in the TUI.\n Clients read this route instead of keeping a hook table of their own.\n\n**Context** (per-thread context pressure, APPS-90)\n- `GET /v1/threads/{id}/context` — `input_tokens` (the conservative live\n estimate the visible meter uses), `billed_input_tokens` (last\n provider-counted prompt size when one exists), `window_tokens`,\n `output_cap_tokens`, `input_budget_ceiling`, `available_input_tokens`,\n `compaction_trigger_tokens`, `usage_percent` and `pressure`. Served by the\n live engine via `Op::GetContextBudget`. Every numeric field is nullable —\n a route that cannot express a bounded window reports `null` rather than an\n invented number — and `live: false` marks responses where the engine\n could not be loaded and only the store-recorded route's static window\n resolved.\n\n**Thread-scoped reads.** `GET /v1/git`, `/v1/changes`, `/v1/diff`,\n`/v1/workspace/diff`, `/v1/git/graph`, `/v1/workspace/files` and\n`/v1/workspace/files/read` accept an optional `thread_id`. With it, the route\nreads the workspace recorded on that thread (the runtime store's thread record\nis the only source) instead of the server's workspace, through the same\nconfined paths. Without it the route behaves exactly as before. An unknown\nthread is 404; a recorded workspace that is not an absolute path, or that is\nmissing, replaced by a link or changed during resolution is 409 with\n`error.code` `thread_workspace_unavailable`. Mutating git routes, file\nwrites, `/v1/workspace/status` and file search stay on the server workspace.\nClients must require the `thread_scoped_reads` capability on\n`GET /v1/runtime/info` before sending `thread_id`: older Engines reject the\nunknown query parameter.\n\n**Status unavailable.** `GET /v1/workspace/status` and `GET /v1/git` report\n`status_unavailable: true` when `git status` could not be read (git failed to\nstart or exited non-zero, or the repository's filter configuration could not be\ninspected). `dirty` and the change counts are then unknown, not zero, and clients\nmust not present them as clean. Status reads neutralize clean/process filter\ndrivers defined in the repository's own configuration (local or worktree scope);\ndrivers defined only in the user's global or system configuration, such as Git\nLFS, keep working.\n\n**Git** (workspace repository operations, APPS-106)\n- `GET /v1/git` — status detail: `git_repo`, `branch`, `head`\n (abbreviated, for display), `head_oid`, `index_token`, `revision`,\n `ahead`/`behind`, counts, per-file porcelain `files[]`\n (`{path, index, worktree, staged, status, old_path?, rev}`), `branches`,\n `remotes`. `files[].path` and `old_path` are workspace-relative, the same\n frame the write routes take; in a workspace that is a subdirectory of its\n repository, rows outside the workspace are not listed (the counts stay\n repository-wide). A rename out of the workspace appears as its source\n deletion. Normal Git filters and untracked settings apply. Untracked\n directories stay collapsed; only a row naming the workspace itself is\n expanded to individually addressable files. The precondition tokens are\n opaque:\n - `head_oid` — the full HEAD commit id; `null` on an unborn branch or when\n HEAD cannot be read\n - `index_token` — the whole index (mode, blob, stage and path of every\n entry, repository-wide). A stat-only refresh by `git status` does not\n change it. Status remains best-effort on repository read failures;\n unavailable tokens are `null`, and broken HEADs cannot satisfy guards\n - `files[].rev` — one row: its index entries plus the working-tree state\n of every file it covers (including a rename's in-workspace source).\n Content tokens (`c-…`) include file bytes, the executable bit on Unix,\n symlink targets, and submodule HEAD/status. Submodule dirty contents\n are summarized by porcelain status, not recursively hashed; ordinary\n stage/discard do not write those contents. A read hashes at most\n 64 MiB / 4,096 files; rows past that budget or containing a file over\n 16 MiB carry a display-only size-and-mtime token (`s-…`). Those tokens\n cannot guard writes. Unreadable paths, paths beneath symlinked\n directories, special files and broken nested repositories have `rev: null`;\n other rows retain their tokens. If a broken tracked submodule aborts\n porcelain, ordinary rows are recovered without submodule recursion and\n the unreadable submodule is shown with `status: \"unknown\"`, `worktree: \"?\"`\n - `revision` — the whole tree: `head_oid`, `index_token`, every row's\n `rev`, including the working-tree state of rows outside a subdirectory\n workspace. `null` when any row is stat-only or unreadable, a repository\n read is incomplete, or untracked paths are hidden; whole-tree guarded\n writes are unavailable in that case. Clients must not silently omit a guard\n- `GET /v1/changes` — the same porcelain `files[]` projection plus\n `head_oid`, `index_token` and `revision`, minus repo chrome\n (branches/remotes): one authority, so the change list can never disagree\n with the status read\n- `GET /v1/diff?path=` — one file's unified `diff` against `base` (`HEAD`,\n or the empty tree on an unborn branch — which reads staged adds as new\n files). Covers staged+unstaged in one patch; `truncated` reports the\n 512 KiB cap. An untracked file answers `untracked: true` with an empty\n diff — the client reads the file itself rather than mistaking it for\n unchanged\n- `GET /v1/workspace/diff?limit=` — whole-tree patch (default 256 KiB,\n max 4 MiB) plus a complete `--numstat` `files[]` inventory\n (`{path, added, deleted}`) so every changed row renders even when the\n patch is truncated\n- `GET /v1/git/graph?limit=` — bounded commit rows (`id`, `short`,\n `parents`, `author`, `timestamp`, `refs`, `subject`); an unborn branch is\n an empty graph, not an error\n- `POST /v1/git/stage` `{ \"paths\": [...] }` or `{ \"all\": true }`;\n `POST /v1/git/unstage` same; `POST /v1/git/discard` `{ \"paths\": [...] }`\n (tracked paths only — no `all`, an untracked path fails closed);\n `POST /v1/git/commit` `{ \"message\", \"all\"? }`; stage, unstage, discard\n and commit also take an optional `expect` (below). Stage, unstage and\n discard also take `patch`: hunks of the one path in `paths`, with file\n headers `diff --git a/<path> b/<path>`, `--- a/<path>`, `+++ b/<path>`\n naming that workspace-relative path. Stage applies them to the index,\n unstage reverses them in the index, discard reverses them in the working\n tree (`git apply [--cached] [--reverse]`). Renames, copies, mode changes,\n creations, deletions and binary patches are refused, and a patch that no\n longer applies fails with git's message and writes nothing. `POST /v1/git/push`\n `{ \"remote\"?, \"set_upstream\"? }` (`remote`, or `origin` when only\n `set_upstream` is given, must name a configured remote);\n `POST /v1/git/branch`\n `{ \"name\", \"create\"? }`\n- Every write above also takes `?thread_id=` (capability\n `thread_scoped_git_writes`) and then runs in that thread's recorded\n workspace instead of the server's, the same scope the read routes use. A\n thread whose workspace is missing or changed answers `409`\n `thread_workspace_unavailable`.\n- `GET /v1/worktrees?thread_id=` — `{repository, worktrees[]}` with\n `{path, branch, head, main, managed, locked, prunable}` from\n `git worktree list --porcelain`; `managed` marks the ones under\n `<repository>/.codewhale/worktrees/`. Capability `worktrees`.\n- `POST /v1/worktrees?thread_id=` `{ \"branch\", \"base\"? }` — validates the\n branch with `check-ref-format`, refuses a branch already checked out\n elsewhere (`409 branch_checked_out`) or an existing folder\n (`409 worktree_exists`), adds `/.codewhale/worktrees/` to the repository's\n `info/exclude` so the main checkout stays clean, and runs\n `git worktree add -b <branch> -- <repository>/.codewhale/worktrees/<slug> <base>`\n (`base` defaults to the main checkout's `HEAD`). Answers `201`\n `{path, branch, base, head, repository}`. A client then creates a thread\n with `workspace: path`.\n- `POST /v1/worktrees/remove?thread_id=` `{ \"path\" }` — archives a managed\n worktree: refuses unmanaged or main worktrees (`403`), a locked one\n (`409 worktree_locked`), active turns in it (`409 worktree_busy`) and\n uncommitted or untracked changes (`409 worktree_dirty` with `files[]`),\n then runs `git worktree remove` without `--force`. The branch and its\n commits stay. A worktree whose folder is already gone is removed with `--force` for that exact path; other prunable worktrees are untouched.\n\nWorktree removal also refuses `409 worktree_busy` while this runtime tracks a\nrunning shell job whose launch directory overlaps the worktree. Removal and\nAPI job starts share admission with new turns, and retain their guards until\nthe mutation finishes even if the HTTP request disconnects. This covers the\nsame runtime manager's tracked jobs. Durable client job operations additionally\nfence unconfirmed overlapping dispatches in their shared store, including\nafter Engine restart. Launch directories do not prove where a\ncommand later changes directory; escaped descendants, late independent\nsubagent launches and other runtime managers or processes remain outside this\nguard. It is not an OS-wide process reservation.\n\nDiffs and precondition token reads run through the hardened review command\n(filters, fsmonitor, hooks, lazy fetches and replace-objects neutralized).\nPorcelain status uses normal Git, matching the workspace counts and filters;\nwrites run through the non-interactive command path\n(`GIT_TERMINAL_PROMPT=0`, BatchMode ssh) so a credential or host-key prompt\ncan never hang a request. Path lists are workspace-relative under the same\nconfinement as the file routes (traversal → 400, `.git` → 403), passed after\n`--` under `--literal-pathspecs`, so `src/*` names a file called `*` and is\nnever a glob. (Unstage of the whole tree uses the `:/` root pathspec.)\nMutations answer `{ok, output, status, current}`: the refreshed status and\nthe full `GET /v1/git` detail, so a client re-reads nothing after an\noperation and can chain the next write from fresh tokens. A workspace that\nis not a repository answers `404`.\n\n*Preconditions.* Stage, unstage, discard and commit accept\n`expect: { head?, index?, revision?, files? }`, built from the last\n`GET /v1/git`. Each present field is checked and an absent one is not;\n`head: null` means \"HEAD must still be unborn\". Without `expect` (or with\n`expect: {}`) a write behaves exactly as before. Recommended use:\n\n| Operation | `expect` |\n| --- | --- |\n| stage / unstage `paths` | `{head, files: {path: rev}}` |\n| stage / unstage `all` | `{head, revision}` |\n| discard (always — it destroys edits) | `{head, files: {path: rev}}` |\n| commit | `{head, index}` |\n| commit `all` | `{head, revision}` |\n\nMalformed preconditions answer `400` before anything runs: `head` must be a\n40- or 64-hex id or `null`; `index` and `revision` 64 hex (an explicit\n`revision: null` is refused); `files` values a content-safe `c-` rev (`s-`\ntokens answer `400`). `files` keys\n(workspace-relative; `dir/` and `dir` are the same key)\nmust name exactly the requested paths, so no path is left unguarded by\naccident. `files` is refused with `all: true` (use `revision`) and on\ncommit. An unknown key inside `expect` is rejected like any other unknown\nfield. When the repository no longer matches, the route writes nothing and\nanswers `409`:\n\n```json\n{ \"error\": { \"message\": \"The repository changed since it was read (HEAD moved; src/a.rs changed). Nothing was written; refresh and review again.\",\n \"status\": 409, \"code\": \"git_state_changed\" },\n \"stale\": [\"head\", \"files\"], \"stale_paths\": [\"src/a.rs\"],\n \"current\": { \"...\": \"the GET /v1/git detail\" } }\n```\n\n`stale` lists the components that moved (`head`, `index`, `files`,\n`revision`); `stale_paths` the `files` keys whose `rev` changed. A client\nre-renders from `current`, keeps the user's selection, and asks again.\n\nStage, unstage, discard, commit and branch from one runtime are serialized,\nso a check and its write are atomic with respect to that runtime's other\nwindows; a second concurrent write answers `409` with\n`error.code: \"git_busy\"` rather than queueing behind a long commit hook.\nPush is not serialized: it only moves a remote ref and can wait on the\nnetwork for up to 120 s. The lock does not cover processes outside this\nruntime — a terminal, an editor, or Codewhale's own agent tools — which can\nstill change the repository in the moment between the check and git taking\n`index.lock`; a truly concurrent git write then fails on git's own\n`index.lock` (a `400` carrying git's message). A compare-and-swap commit via\n`commit-tree` and `update-ref` would close that window but skip the\nrepository's hooks, which a Review-sheet commit must run, so it is not used.\n\n**Diagnostics** (read-only logs, crashes, process — APPS-103)\n- `GET /v1/logs` → `{sources: [{dir, files: [{name, size, modified}]}]}` —\n the runtime's log directory plus `audit.log[.1]` from the codewhale home,\n newest first, capped\n- `GET /v1/logs/{name}?offset=<bytes>&limit=<bytes>&tail=<bytes>` →\n `{name, size, modified, offset, bytes, truncated, encoding, content}` —\n one bounded window; `tail` reads from the end and is mutually exclusive\n with `offset`; `truncated` means bytes remain after the returned window\n (a tail read at EOF is `false`), `encoding` is `utf-8` or `base64`\n- `GET /v1/crashes`, `GET /v1/crashes/{name}` — the same list/read contract\n over the crash-dump directories (`~/.codewhale/crashes`, legacy\n `~/.deepseek/crashes` merged)\n- `GET /v1/process` → `{pid, version, commit, started_at, uptime_seconds,\n executable, rss_bytes}` — `rss_bytes` only where the platform reports it\n (Linux `/proc`); absent rather than fabricated elsewhere\n\nThese routes package what already exists on disk for a client-side export;\nthere is no telemetry upload route and no second log store. Names are\nbasename-validated (no separators, no `..`), listings are capped, reads are\nbounded windows, and symlinks are never followed — a client bundles the\nfiles itself.\n\n**Targets and remote posture** (APPS-50)\n- `GET /v1/targets` → `{targets: [self], remote: {supported: true,\n attach: \"client\", probe: \"POST /v1/remote/connect\"}, ssh: {…},\n cloud: {…}}` — this runtime's own record as the attachable target plus\n per-surface ownership; the runtime keeps no persistent target registry,\n so `POST /v1/targets` and `POST /v1/targets/switch` answer\n `501 Not Implemented` — target selection is client-owned and a switch\n must never move a running task server-side\n- `GET /v1/remote` → `{bind_host, port, loopback_only, reachable_from_lan,\n auth_required, mobile, tls}` — this listener's reachability posture.\n `tls` is always `false`: the API has no TLS terminator, so non-loopback\n reachability assumes a verified overlay (VPN/mesh), never plain LAN trust\n- `POST /v1/remote/connect` `{ \"endpoint\": \"http://host:port\" }` — probes a\n candidate remote's unauthenticated `GET /v1/runtime/info` (origin only;\n any pasted path is discarded). Answers `{ok, remote: {endpoint,\n runtime_api_version, codewhale_version, auth_required, …}, attach:\n \"client\"}` on success, and `{ok: false, reason: \"unreachable\" |\n \"not a Codewhale runtime\" | …}` as data on failure. URLs carrying\n credentials are refused with 400 — the remote's token is configured\n client-side, and a connect route that forwarded one would be an\n exfiltration primitive\n- `GET /v1/ssh`, `GET /v1/cloud` → `{supported: false, owner:\n \"codewhale-control-plane\", reason}`; `POST /v1/ssh/connect` and\n `POST /v1/cloud/attach` → `501`: SSH workspace provisioning and hosted\n cloud computers belong to the Apps control plane (ASCII Box for Managed\n Computer), not to a second authority inside Core\n\nA remote Codewhale is a `serve --http` runtime with a token — that is the\nwhole attach model. These routes describe and probe it; they never execute\na remote request on the local machine.\n\n**LSP** (workspace language intelligence, APPS-93)\n- `GET /v1/lsp` — capability: `enabled`, supported `languages` with their\n server commands, `custom_languages`, operations, poll and diagnostic caps\n- `GET /v1/diagnostics?path=` — file diagnostics\n- `GET /v1/definition?path=&line=&character=` (1-based)\n- `GET /v1/references?path=&line=&character=` (1-based)\n- `GET /v1/symbols?path=&query=` — empty query returns document symbols\n\nOne lazily-built workspace-level `LspManager` serves these; engine threads\nkeep their own per-thread managers for the post-edit hook, and a server that\nnever serves an LSP route never spawns a language server. `path` is\nworkspace-relative under the same confinement as the file routes. Normal\nabsence is data: no language server, a disabled `[lsp]` config, or a timeout\nanswers `200` with `ok: false` and a machine-readable `reason`\n(`no_server`, `lsp_disabled`, `lsp_error`); malformed input is a 400 and a\nmissing file a 404.\n\n**Voice** (host dictation, APPS-98)\n- `GET /v1/voice` — capability: `available`, detected `recorder` command,\n resolved `asr` `{kind, model}`, `modes`, `send_phrases`,\n `max_record_seconds`\n- `POST /v1/voice/dictate` — record then transcribe → `{ ok, text }`\n- `POST /v1/voice/send` — same capture with the \"send it\" / 发送/發送\n suffix contract: `send: true` tells the client to submit (empty `text`\n with `send: true` means submit the client's current draft)\n- `POST /v1/voice/control` `{ \"composer\": \"draft text\" }` — assisted\n dictation that shows the model the composer text; `assisted: false` in the\n response means a free ASR backend (local whisper/Groq) handled the audio\n and the composer context was never seen\n\nThe runtime owns the host microphone and the ASR dispatch — the same\nimplementation the TUI's `/voice` commands run, headless. Recording is one\nblocking capture per host (requests serialize; the loser gets\n`ok:false`/`no_speech`, not a fought-over device). Provider ASR resolves its\nkey lazily so local-whisper and Groq paths work without provider auth.\nInterim and final transcription use the selected ASR backend. If local whisper\nor Groq fails, the error stays on that backend: the runtime never retries the\nrecording or composer text with the active model provider. Select provider ASR\nexplicitly to use that route.\nFailure is data: `no_recorder`, `no_speech`, `no_provider_auth`,\n`transcription_failed`. `CODEWHALE_DISABLE_VOICE=1` is an operator\nkill-switch — a headless `serve --http` host reports `available: false` and\nevery dictate call fails closed.\n\n## Provider and model selection\n\nThese three routes are how a GUI renders a model picker whose contents are true\nfor *this* runtime instead of guessed from a version snapshot. They were\nundocumented until 2026-08-04, which cost a desktop integration a day: the\nclient probed `/v1/models`, `/v1/runtime/models`, and `/v1/runtime/providers`\n(all correctly 404) and concluded the capability did not exist.\n\n### `GET /v1/providers`\n\n```json\n{\n \"current\": \"modelstudio-token-plan\",\n \"providers\": [\n {\n \"id\": \"modelstudio-token-plan\",\n \"model_provider_id\": \"modelstudio-token-plan\",\n \"display_name\": \"Alibaba Cloud Model Studio\",\n \"default_model\": \"qwen3.8-max\",\n \"has_model_catalog\": true,\n \"credentialState\": \"configured\"\n }\n ]\n}\n```\n\n`current` is the active generic provider id. Only the active entry carries an\nexact identity: an active built-in normally repeats its canonical id in\n`model_provider_id`, while an active named custom route has `current` set to\n`custom` and the exact configured key there (for example `lm-studio`). Other\nentries have a null exact id; a null id on the active `custom` entry identifies\nthe released legacy root-level custom route. Preserve both fields from the\nselected entry and send a non-null exact id back as `POST /v1/threads`'s\n`model_provider_id`; dropping a named custom id would collapse the selection to\nthe legacy root custom route. `credentialState` is a stable, non-secret\nprojection of the Runtime's existing structural credential classification:\n\n- `configured`: credential material is structurally available;\n- `login_required`: the route needs login or a usable login capability;\n- `missing`: an API-style credential is unavailable;\n- `no_auth`: the route explicitly disables credential use;\n- `local`: the exact route is local and keyless;\n- `legacy`: the compatibility route cannot be classified more precisely.\n\nFor an active named custom provider, this state is calculated from the exact\nroute named by `model_provider_id`, not from a generic custom-provider default.\nIt deliberately collapses saved-key and imported-token details into\n`configured`, and login/consent-source details into `login_required`.\n\nThe response never includes endpoint URLs, credential environment-variable\nnames, filesystem paths, credential values, consent-source details, or token\nmetadata. `credentialState` is not a provider canary: `configured` does not\nprove endpoint reachability, credential validity, model entitlement, or a\nsuccessful request. The models route below is also only a selection catalog; a\nnon-empty list does not prove that the route can currently serve a request.\n\n### `GET /v1/providers/{id}/models`\n\n```json\n{\n \"provider\": \"deepseek\",\n \"models\": [\n {\n \"id\": \"deepseek-v4-flash-vision-exp\",\n \"image_input\": \"supported\",\n \"reasoning_effort\": \"unknown\",\n \"reasoning_effort_levels\": [],\n \"reasoning_effort_source\": null\n }\n ]\n}\n```\n\nFor an exact configured route, supply `?model_provider_id=vision-work` and\nrequire the response to echo that same `model_provider_id`. The Runtime resolves\nthat identity under the requested provider kind before reading model support.\nUnknown or mismatched identities return `400`. Named pagination cursors bind the\nconfiguration identity, endpoint and catalog snapshot; changing any of those\nrequires restarting pagination. Omitting the query preserves the legacy catalog\nprojection and omits the identity echo.\n\nThe catalog for one provider. Returns `400` for an unknown id, and for the\nlegacy `deepseek-cn` alias, which has no provider metadata — use `deepseek`.\nAn empty `models` array means the Runtime has no discoverable or configured\nmodel ids for that provider; it does not report credential presence.\n\nThe ids returned here are exactly the values accepted by `POST /v1/threads`'s\n`model` field and by the switch route below. `image_input` is the exact resolved\nprovider/model route's capability state: `supported`, `unsupported`, or\n`unknown`. Keep `unknown` unknown rather than inferring from the model name or\nwire protocol. `supported` describes the model route; it does not mean a given\nclient implements an image-upload control.\n\n`reasoning_effort` uses the same three capability states and describes whether\nthe exact model's metadata publishes a selectable effort ladder.\n`reasoning_effort_levels` contains only canonical, recognized active effort levels\nfrom that metadata. Off and provider synonyms such as none are excluded: the\nApps/Chat protocol treats off as omission, which does not prove support for an\nexplicit provider disable command. A model capable of reasoning may still have\nan unknown active effort ladder.\nCodex levels are also excluded when native compatibility would change their\nwire value (currently minimal and auto). This projection does not change native\ncompatibility behavior or advertise a tier the Runtime cannot send unchanged.\nNo levels are inferred from a provider-wide default or a familiar model name\non a custom endpoint. `reasoning_effort_source` identifies `catalog`,\n`codex_cli_cache`, or `codex_app_server`; missing, stale, and unrecognized model\nmetadata stays unknown. Codex roster metadata describes the external CLI's\nroster, not proof that a separately configured Runtime credential belongs to\nthe same account or that an authentication boundary is approved.\n\nPass `?model_provider_id=<exact configured id>` when selecting a named route.\nThe Runtime validates the provider kind and exact identity together, returns\n`model_provider_id` alongside that route's model list, and leaves the active\nconfiguration unchanged. An empty or unknown requested identity, or a mismatched kind, returns\n`400`; it never falls back to another named route.\n\nThe Runtime Chat relay publishes the same effort fields in camelCase\n(`reasoningEffort`, `reasoningEffortLevels`, `reasoningEffortSource`). These\nmodel facts do not enable tool execution or establish account entitlement.\n\nFor a thread-scoped choice, send the provider fields from the selected entry\nalongside the selected model. Omit `model_provider_id` when it is null:\n\n```json\n{\n \"model_provider\": \"custom\",\n \"model_provider_id\": \"lm-studio\",\n \"model\": \"local-vision-model\"\n}\n```\n\nThis creates one thread on the exact named custom route without changing the\nRuntime's provider or model defaults.\n\n### `PUT /v1/providers/{id}/key` — write-only credential\n\n```json\n// request\n{ \"key\": \"sk-…\" }\n\n// response\n{ \"provider\": \"openai\", \"stored\": true, \"backend\": \"keychain\",\n \"credentialState\": \"configured\", \"configPath\": \"/…/config.toml\" }\n```\n\nThe optional `scope` field accepts `\"durable\"` (the default) or `\"session\"`.\nDurable scope stores a provider API key through the same transactional write as\n`codewhale auth set --provider <id> --api-key-stdin`: the secret store under\nthe provider write lock, plus the `[providers.<id>] auth_mode` metadata\nmarker persisted to the config document and mirrored into the live runtime\nconfig so `GET /v1/providers` reports the new state immediately. `backend`\nnames which secret backend holds the key and `configPath` which config\ndocument carries the marker (the user-global file when the ambient config\nis workspace-scoped).\n\nFor a key held only in the running Engine's memory, send:\n\n```json\n// request\n{ \"key\": \"sk-…\", \"scope\": \"session\" }\n\n// response\n{ \"provider\": \"openai\", \"stored\": false, \"scope\": \"session\",\n \"backend\": \"memory\", \"credentialState\": \"configured\" }\n```\n\nA session key matches only the exact provider identity; it never falls back to\na sibling sharing a durable slot, such as `deepseek` and `deepseek-anthropic`.\nIt is bound to the full resolved endpoint it was set for. An accepted config\nreload removes incompatible bindings; rejected reloads leave the live overlay\nunchanged. Lookups through older config snapshots cannot remove newer keys. It never applies to `auth_mode = \"none\"` or remote\ncustom endpoints, and session writes to those routes are refused. Within those\nlimits it overrides other credential sources, except a route bound to a\nrequired Codewhale account owner retains that requirement without fallback.\nIt is never serialized or written to the secret store or config document, and\ndoes not change the live provider settings. It survives config reloads within\nthe Engine process while the endpoint remains the same, but does not persist\nacross an Engine restart. Running turns retain the key they started with;\nclearing the overlay takes effect from the next turn. The response omits\n`configPath` because no config document is saved.\nRequest bodies reject unknown fields, so an older Engine that does not support\n`scope` rejects a session request instead of silently storing its key.\n\nSet `CODEWHALE_RUNTIME_KEY_SCOPE=session` before HTTP startup to require\nsession-only provider keys. A durable request, including one that omits\n`scope`, returns `409`. The policy is fixed for the process and preserved\nthrough reloads. An unset or empty variable preserves normal behavior; any\nother value refuses server startup. Strict mode does not use stored, environment,\nsubscription OAuth or configured alternate-auth headers as a fallback. Explicit\nkeyless routes remain keyless. Required Codewhale account ownership remains a\nseparate restriction and cannot be overridden with a session key.\n\n`GET /v1/runtime/info` advertises `capabilities.session_provider_keys: true`\nonly in this strict mode, where both set and clear are memory-only. Browser\nsign-in is unavailable in this mode. Ordinary subagents share the in-process\ncredential context, but external Fleet workers resolve their own credentials\nand have no session-key handoff. Strict mode therefore refuses Fleet creation,\nactivation, restart and launch, including restored leases, and advertises its\nFleet create/start/local-target capabilities as false. It never forwards a\nprovider key through a worker command line, environment or config file.\nOther configuration and MCP routes may still write their non-key settings to\ndisk; this is a provider credential policy, not a read-only filesystem.\n\nMemory-only custody is not a vault: anyone with root access on the machine can\nread a running Engine's memory. The key is not zeroized or locked against swap,\nand this route does not protect it from a snapshot that captures process\nmemory. Existing process hardening disables core dumps on Linux when its\nhardening calls succeed; failures log a warning, and that hardening is a no-op\non other platforms.\n\nThe key is never returned — there is no read route for credential material,\nand neither the key nor its length appears in the response, errors, or\nlogs; the response carries only the readiness projection\n(`credentialState`). An unknown provider id, the `deepseek-cn` legacy\nalias, a key that is empty after normalization, or a normalized key over 4 KiB\nis `400`. Both scopes remove whitespace, control and invisible characters with\nthe shared key normalizer before validation; `openai-codex` refuses API-key writes.\n`credentialState: \"local\"` after a successful durable write is honest output for\na keyless local route: the key is\naccepted, but the route classifies as not needing one.\n\n### `DELETE /v1/providers/{id}/key` — clear a Codewhale-owned credential\n\n```json\n// response\n{ \"provider\": \"openai\", \"cleared\": true, \"credentialState\": \"missing\" }\n```\n\nRemoves any session key for the exact provider identity before attempting a\ndurable clear, even when the durable operation subsequently fails. Unless\n`CODEWHALE_RUNTIME_KEY_SCOPE=session`, it also clears the durable credential\nthrough the same shared owner as\n`codewhale auth clear`: the config document is snapshotted and restored if\nits save fails, the secret store is only touched once that save has landed,\nand the cleared markers are mirrored into the live runtime config so\n`GET /v1/providers` reports `missing` on the next read rather than after a\nrestart.\n\nUnder `CODEWHALE_RUNTIME_KEY_SCOPE=session`, DELETE removes only the memory\noverlay and does not touch the durable secret store or config document:\n\n```json\n// response under session-only policy\n{ \"provider\": \"openai\", \"cleared\": true, \"scope\": \"session\", \"backend\": \"memory\" }\n```\n\nThis response omits `credentialState` and does not probe or clear a durable\nstore. After removal, subsequent turns cannot fall back to stored or environment\nkeys. A running parent turn and children using its already-materialized client\nretain their credential until they finish; clearing memory is not provider-side\nrevocation. This mode validates canonical provider identity but does not apply\ndurable ownership checks to removing the Engine's own session key.\n\nClearing an already-clear route returns `cleared: true` — a client retrying\na revoke must not be told something went wrong. If the config entry is\ncleared but the secret backend refuses the delete, the route answers `500`\nand names the slot: reporting success while the key is still in the keyring\nwould be a lie about a security action.\n\n### Credential ownership: `credentialSource` and `credentialWritable`\n\nDurable PUT and the durable portion of DELETE refuse a route whose credential\nCodewhale does not own, independently of any session key. `GET /v1/providers`\nclassifies that underlying durable owner so a client can disable its durable\ncredential control *before* submitting instead of failing late:\n\n| `credentialSource` | `credentialWritable` | Meaning |\n| --- | --- | --- |\n| `secret_store` | `true` | A Codewhale-owned durable credential slot. |\n| `account_session` | `false` | Codewhale account model access owns the credential; disconnect through account sign-out. |\n| `config` | `false` | A literal key in a config file, which wins at request time when no session key is present. |\n| `external_auth` | `false` | An active external consent (OAuth) owns the credential. |\n| `none` | `false` | The route sends no credential, or has no credential slot. |\n\nWhen `credentialWritable` is `false`, `credentialWritableReason` carries\nuser-facing copy naming the owner. Durable PUT and the durable portion of\nDELETE answer `409` with that same reason. DELETE still removes the memory\noverlay regardless of the durable credential owner. An existing session key\ncan be updated with session scope without changing that owner, subject to the\nendpoint and auth-mode restrictions above. The ownership classification is\nbased on declared auth mode, consent state and the *kind* of any configured\n`api_key` value. When account model access is bound, it also resolves the\ncredential source with the session overlay excluded to determine whether the\naccount owns the route. The result is a class and never a value, a path, or an\nenvironment variable name.\n\n### `POST /v1/providers/orcarouter/connect` — browser sign-in without a terminal\n\n```json\n// response\n{ \"attemptId\": \"0f3c…\", \"authorizeUrl\": \"https://www.orcarouter.ai/auth?…\",\n \"expiresIn\": 900 }\n```\n\nRuns the same OrcaRouter OAuth 2.0 + PKCE sign-in as `codewhale auth orcarouter`\nfor a client that has no terminal. The Engine binds its loopback callback\n(`http://127.0.0.1:<port>/callback`), returns the consent URL, and waits on a\nworker for up to `expiresIn` seconds. The client opens `authorizeUrl` in the\nsystem browser — never an embedded view — and polls the attempt. When the\nbrowser comes back, the Engine exchanges the code, refuses a key whose granted\nscope is not `api`, and stores the key through the same seam as the CLI, then\nmirrors the credential markers into the live config the way\n`PUT /v1/providers/{id}/key` does. It does not switch the active provider; use\n`POST /v1/providers/{id}/switch` for that.\n\nThe callback listens on the Engine's own computer, so a client offers this\nroute only for an Engine on the same computer as the browser and outside\nsession-only key policy. Strict mode refuses before binding a callback or\nstarting authorization. A new `POST`\ncancels the previous attempt, and only the current attempt can be read.\nRefusals: `400` for an id other than `orcarouter`, `409` when the route's\ncredential is not writable (the same reason `credentialWritableReason`\ncarries), `500` when the callback cannot be bound. `GET /v1/providers` marks a\nroute that supports this with `\"browserConnect\": true`; the field is absent\notherwise and on Engines without the route.\n\n### `GET /v1/providers/orcarouter/connect/{attemptId}` and `DELETE …` — status and cancel\n\n```json\n{ \"attemptId\": \"0f3c…\", \"status\": \"failed\", \"reason\": \"key_not_saved\" }\n```\n\n`status` is `waiting`, `connected`, `declined` (the user refused in the\nbrowser), `expired` (no callback in time), `cancelled` or `failed`. `failed`\ncarries a `reason`: `authorization_error` (the browser came back with another\nerror), `callback_failed`, `not_writable` (the route's endpoint or credential authority changed before\nlocal activation), `exchange_failed`,\n`scope_refused` or `key_not_saved`. After `scope_refused` or `key_not_saved`,\nOrcaRouter created a key that Codewhale did not keep; the user should revoke\nit under Authorized Apps in OrcaRouter. `exchange_failed` can follow a key\nOrcaRouter created before the response was lost.\n\n`DELETE` cancels a pending attempt, including an exchange in progress, and\nprevents its later local activation. It does not revoke a key already created\nby OrcaRouter; revoke that key under Authorized Apps. If local activation\ncompleted first, cancellation returns the terminal status instead. Clearing\nor replacing the provider key also cancels its pending connect attempt. These\nmutations retain their serialization if the HTTP client disconnects. An unknown\nor superseded `attemptId` is `404`. Only the `POST` response carries the consent\nURL; no status, error or log line carries it, the key, the code or the verifier.\n\n### `POST /v1/providers/{id}/switch`\n\n```json\n// request (model is optional; omit to take the provider default)\n{ \"model\": \"qwen3.8-max\" }\n\n// response\n{ \"provider\": \"modelstudio-token-plan\", \"model\": \"qwen3.8-max\",\n \"message\": \"…\", \"persisted\": true }\n```\n\n**Use this rather than simulating a switch with repeated `POST /v1/config`\nwrites plus a reload.** Provider and model move together here, the change is\nvalidated against the provider's catalog before it is applied, and `persisted`\nreports whether it was written to config or applied to the live session only.\nRejects an unknown provider id and the `deepseek-cn` alias with `400`.\n\n## Runtime data model\n\nThe runtime uses a durable Thread/Turn/Item lifecycle.\n\n- **ThreadRecord** — `id`, `created_at`, `updated_at`, `model`,\n `model_provider` (generic kind), `model_provider_id` (optional exact configured\n route), `workspace`, `mode`, `task_id`, `system_prompt`, `latest_turn_id`,\n `latest_response_bookmark`, `archived`\n- **TurnRecord** — `id`, `thread_id`, `status` (`queued|in_progress|completed|\n failed|interrupted|canceled`), `effective_provider`, `effective_model`,\n `effective_billing_surface`, timestamps, duration, usage, error summary,\n `artifacts` and `workspace` (see [Turn artifacts](#turn-artifacts))\n- **TurnItemRecord** — `id`, `turn_id`, `kind` (`user_message|agent_message|\n tool_call|file_change|command_execution|context_compaction|status|error`),\n lifecycle `status`, `metadata`, `artifacts` and the legacy\n `artifact_refs` projection\n\nEvents are append-only with a global monotonic `seq` for replay/resume.\n\n`effective_billing_surface` is a non-secret classification derived from the\nendpoint that served the turn. Recognized StepFun routes use `stepfun-payg` or\n`stepfun-plan`; unknown and custom endpoints leave it unset. The raw base URL is\nnot persisted in `TurnRecord`.\n\n### Restart semantics\n\n- If the process restarts while a turn or item is `queued` or `in_progress`,\n the recovered record is marked `interrupted` with an `\"Interrupted by\n process restart\"` error.\n- The trailing newline is an event append's commit marker. On startup, a final\n JSONL fragment without that delimiter is truncated and fsynced even when its\n bytes form valid JSON; it is an uncommitted append, and its already-reserved\n sequence number is not reused. Newline-terminated malformed records are not\n identifiable crash debris and continue to fail closed during replay.\n- If a terminal turn record reached disk but its terminal event sequence did\n not, the first async read reconciles any unresolved dynamic calls as\n `tool_call.canceled` and then emits one `turn.completed`. Existing terminal\n call and turn receipts are detected and never duplicated.\n- Turn-operation bindings survive restart. Retrying the same `operation_key`\n and request returns the original recovered turn (including an `interrupted`\n turn recovered from an in-progress process exit); mismatched reuse remains a\n conflict. A crash-created binding that never acquired a turn is discarded at\n startup because engine submission happens only after both records are\n durable.\n- Task execution performs its own recovery on top of the same persisted\n thread/turn store.\n\n### Approval model\n\n- The `auto_approve` flag applies to the runtime approval bridge and engine\n tool context. When enabled for a thread/turn/task, approval-required tools\n are auto-approved in the non-interactive runtime path, shell safety checks\n run in auto-approved mode, and spawned sub-agents inherit that setting.\n- When omitted, `auto_approve` defaults to `false`.\n- [Authorization order](AUTHORIZATION_ORDER.md) describes where typed rules,\n registered tool requirements, safety floors, repository law, approval\n transport, and sandbox enforcement sit relative to one another.\n\n### SSE event stream\n\nThe SSE event payload shape for `/v1/threads/{id}/events`:\n\n```json\n{\n \"schema_version\": 1,\n \"seq\": 42,\n \"previous_seq\": 38,\n \"event\": \"item.delta\",\n \"kind\": \"item.delta\",\n \"thread_id\": \"thr_1234abcd\",\n \"turn_id\": \"turn_5678efgh\",\n \"item_id\": \"item_90ab12cd\",\n \"timestamp\": \"2026-02-11T20:18:49.123Z\",\n \"created_at\": \"2026-02-11T20:18:49.123Z\",\n \"payload\": {\n \"delta\": \"partial output\",\n \"kind\": \"agent_message\"\n }\n}\n```\n\nCompatibility notes:\n\n- `schema_version` is the HTTP/SSE envelope schema version. It is independent of\n the runtime store schema used for persisted thread/turn/event records.\n- `event` remains the SSE event name in existing clients; it is preserved as-is.\n- `kind` mirrors `event` in the stable envelope for typed clients.\n- `seq` is allocated globally across all Runtime threads. Consequently, gaps\n between a thread's events are normal when other threads interleave. On this\n per-thread SSE stream, `previous_seq` is the sequence of the last event\n delivered for this thread (or the requested replay cursor for the first\n event); clients detect loss by comparing it with their accepted per-thread\n cursor, not by requiring `seq == previous_seq + 1`. Sequence allocation is\n also not rewound after an append is transactionally rolled back, so a retry\n can intentionally skip an unused value without implying a missing event.\n- `thread.started`, `turn.started`, and `turn.completed` are emitted as SSE event\n names exactly as before.\n- `timestamp` remains the canonical event time for schema version 1. `created_at`\n is an equivalent alias for clients that use `created_at` naming elsewhere; do\n not require both fields to be present.\n\n### Ending and resuming a thread stream\n\nWhenever the server ends a `/v1/threads/{id}/events` stream that already\nreturned `200`, the last frame is `stream.end`, sent exactly once and always\n(it does not need `progress=true`):\n\n```\nevent: stream.end\ndata: {\"schema_version\":1,\"event\":\"stream.end\",\"kind\":\"stream.end\",\"thread_id\":\"thr_1234abcd\",\"reason\":\"replay_failed\",\"last_seq\":42,\"retryable\":true}\n```\n\n- It is a transport frame, not a journal event: it has **no `seq`** and **no\n SSE `id:`**. Clients that acknowledge on `seq` skip it, and a browser's\n `Last-Event-ID` stays on the last real event.\n- `last_seq` is the stream's cursor when it ended: the last journal `seq`\n delivered on this connection, or the effective start cursor if none was\n (after a `replay_limit` tail, that is already past the omitted history). It\n is exactly the `since_seq` that resumes with no loss and no repeats.\n- `retryable` says whether resuming from `last_seq` can succeed. Key the client\n on it, not on the reason list. Treat an unknown `reason` by its `retryable`.\n- There is no free-text message. The underlying error is in the Runtime log,\n and it can contain store paths.\n\n| `reason` | Meaning | `retryable` |\n| --- | --- | --- |\n| `replay_failed` | The durable history read that feeds the opening replay failed, including a replay worker crash after the first cursor | `true` |\n| `catch_up_failed` | After broadcast lag, the durable re-read from the stream's cursor could not be opened or failed | `true` |\n| `runtime_shutdown` | The Runtime API server is stopping (SIGINT, SIGTERM, or SIGHUP; Ctrl+C or Ctrl+Break on Windows). Open streams get this frame within a bounded drain window before the process exits | `true` |\n\nEvery `200` response carries `x-codewhale-stream-end: 1`. With that header, an\nEOF **without** `stream.end` means the connection or the Runtime process died\nwithout the server choosing to end the stream: a network or proxy drop, a\ncrash, or a kill that allows no drain (for example `SIGKILL`). A Runtime that\npredates this frame sends no header; there EOF stays ambiguous, so treat it as\nconnection loss.\n\nClient resume rule:\n\n1. Keep `cursor` = the `seq` of the last journal frame you accepted. Ignore\n frames with `seq <= cursor`. If a frame's `previous_seq` is not your\n `cursor`, you missed events: reload the thread snapshot rather than trust\n local state.\n2. On `stream.end` with `retryable: true`, reconnect with\n `since_seq = last_seq` after a bounded backoff, and tell the user what the\n Runtime said (for example, \"Runtime is shutting down — reconnecting\").\n With `retryable: false`, stop, show the reason, and fall back to the\n snapshot.\n3. On EOF or a transport error without `stream.end`, reconnect with\n `since_seq = cursor` after a bounded backoff and show a connection problem,\n not a Runtime error.\n4. A `401`/`403` or `404` before the stream opens is terminal (a credential or\n thread problem). Retry `5xx` with backoff.\n5. Never reconnect from `0` to \"start over\": replay is idempotent only by\n cursor.\n\nFleet streams (`/v1/fleet/runs/{run_id}/events`) keep their own end frames,\n`fleet.stream.error {retryable}` and `fleet.replay.cursor_unavailable`. They\ndiffer from `stream.end`: they carry no cursor, because a Fleet client resumes\nfrom the opaque `cursor` of the last Fleet event it accepted.\n\n### Steer delivery\n\nPutting a steer into the engine's mailbox is not the same as the model reading\nit. The engine discards a steer whose turn has already moved on, and an\ninterrupted or failed turn drops whatever it had queued. The API reports the\nengine's real verdict rather than the attempt:\n\n- The item is persisted `queued` when the steer is accepted into the mailbox.\n- **Delivered.** The engine committed the text into the turn's record: the item\n becomes `completed`, `steer_count` rises, and `turn.steered` + `item.completed`\n are emitted. `POST .../steer` returns `200` with that turn.\n- **Not delivered.** The turn moved on, was interrupted, or failed first: the\n item becomes `canceled`, `steer_count` does not rise, and `turn.steer_dropped`\n is emitted carrying `input`, `reason`, and the settled `item`. `POST .../steer`\n returns `409`, so a client can keep the user's text and resend it rather than\n clearing a composer over guidance that was never seen.\n- **Still pending.** A steer sent while the engine is inside a long tool call\n cannot settle until that call returns, and the request does not hang for it.\n After a short wait `POST .../steer` returns `200` with the item still `queued`;\n the eventual `turn.steered` or `turn.steer_dropped` event carries the verdict.\n\nA client that treats `200` as \"the model saw it\" is therefore wrong in the third\ncase: read the item's status, or wait for the event.\n\nCommon event names: `thread.started`, `thread.forked`, `turn.started`,\n`turn.lifecycle`, `turn.usage`, `turn.steered`, `turn.steer_dropped`,\n`turn.interrupt_requested`,\n`turn.completed`, `turn.artifacts`, `item.started`, `item.delta`, `item.completed`,\n`item.failed`, `item.interrupted`, `approval.required`, `approval.decided`,\n`approval.timeout`, `user_input.required`, `user_input.answered`,\n`user_input.canceled`, `tool_call.requested`, `tool_call.resolved`,\n`tool_call.timeout`, `tool_call.canceled`, `sandbox.denied`,\n`turn.workspace_snapshot`, `turn.provider_request`, `runtime.store_failure`.\n\n`turn.usage` is one model call's receipt, emitted when a parent model call\nreports usage — not once per turn. Its `payload` carries the call's `usage`,\nits `duration_ms` / `first_token_ms` / `request_ms` timings, the\n`maxOutputTokens` allowance, `billed_prompt_tokens` (the prompt size the\nprovider billed: the last itemised request's input when retries were reported,\nelse the call's own input count), and `cost` — that call priced on the route it\nwas dispatched on, as `{usd, cny, usd_priced, cny_priced}`.\n\n`cost` is `null` when the route cannot price every token class the call used; a\nclient must not read that as a free call. A currency whose `*_priced` flag is\nfalse is unpriced. These receipts cover the parent model calls; child and\ncompaction usage arrive through other paths. Summed per-call prices are\ntherefore partial estimates, not the complete turn bill.\n\n`turn.provider_request` retains the captured route, logical request identity,\nbounded `physical_requests` identities, typed `transmission`, `coverage`, reported\nusage and outcome. An observed send or absent response headers do not prove\nprovider acceptance or free work. Unknown primary outcomes remain unpriced in\nthe turn diagnostics and usage totals; known primary usage stays in parent\naccounting. These receipts do not yet reconcile late cumulative primary usage.\nInference automatically retries only an ordinary rate limit or a proven\ntransient connection failure before dispatch. A later explicit user turn is a\nnew request, not an automatic replay of an uncertain request.\n\n`turn.completed` carries the turn's authoritative cumulative token usage in\n`payload.turn.usage`, including routed work. It replaces the running token\ncounts; it does not supply an authoritative last-turn cost. The thread totals\nat `GET /v1/threads/{id}/usage` remain the accounting record. The mobile page\nkeeps the observed priced-call estimate after completion, marks it as a\npartial estimate, and resets its figure when a new turn starts. Replayed\njournal sequences are counted once. `billed_prompt_tokens` is available to\nAPI clients; the mobile header shows token usage and partial cost.\n\n`turn.provider_request` carries diagnostic request observations, not additional\nbilling usage. See [per-request cache receipts](CACHE.md#per-request-receipts).\n\n`runtime.store_failure` is the runtime reporting a fault in the operator's own\non-disk state: a thread, turn, or item record under the session's runtime\nstore could not be read, parsed, or written. The payload carries `operation`\n(`read` | `parse` | `write`), `record_kind` (`thread` | `turn` | `item`),\n`record_id`, `path`, the full `error` chain, the root-cause `reason`, a\n`next_action` (which file to move aside, or where to check free space and\npermissions), and a one-line `message`. When `terminal` is `true`, the turn's\nown record is unreadable or unwritable and no `turn.completed` will follow;\nclients waiting on that turn should treat it as failed.\n\nAgent-message and reasoning deltas are materialized into the item projection\nbefore their corresponding `item.delta` event is sequenced. To avoid an fsync\nfor every provider fragment, adjacent deltas are coalesced to configured bounds\nof at most 32 ms or approximately 16 KiB before publication (an indivisible\nupstream chunk can itself exceed the byte target). A process crash inside that\nunpublished window can lose the recent suffix; no durable event claims that\nsuffix existed. Once an `item.delta` is durable, snapshots at or beyond its\ncursor include the same materialized prefix.\n\n`approval.required` events may include a `matched_rule` string when an\nexecution-policy rule caused the prompt. This field is explanatory metadata for\nclients and does not grant or persist permissions.\n\n`approval.required`, `approval.decided`, and `approval.timeout` carry two\ndistinct identifiers. `approval_id` is the Runtime-minted, single-use capability\ndescribed under **Approvals** — the only value `POST /v1/approvals/{id}` accepts\n— and `approval.required` also repeats it in the legacy `id` field for older\nclients. `tool_call_id` is the Engine tool-execution ID, present for\ncorrelation only. Automatically resolved prompts (thread `auto_approve`, and the\nAuto-Review posture, which never opens a modal) mint an `approval_id` as well, so\nthe field has one meaning on every path; those IDs register no waiter and are\ninert against the endpoint. Clients must never treat `tool_call_id` as an\napproval capability or assume it is unique across threads.\n\nThe thread event stream forwards these payloads intact. The compatibility turn\nstream carries `approval_id`, its `id` alias and `tool_call_id`; the pending\nsnapshot carries the same capability and correlator so reconnecting clients can\nattach an approval prompt to its tool row. Resolutions carry their resolution\nflags through that projection too: `timeout: true` marks the deny produced when\nthe configured decision budget expires, and `cancelled: true` marks a deny\nforced by a turn interrupt or turn teardown where no user selection\nwas made — clients should clear the pending prompt rather than report a\nrefusal.\n\n## Security boundary\n\n- **Localhost by default**. The server binds to `127.0.0.1` by default.\n `--mobile` is also loopback-only and rejects a non-loopback host until a TLS\n or verified-overlay transport boundary exists. The runtime does not provide\n user isolation or TLS.\n- **Optional token guard**. `--auth-token` or `DEEPSEEK_RUNTIME_TOKEN`\n requires a matching bearer token for `/v1/*` routes. This is a local\n convenience guard, not a replacement for TLS, VPN, or a trusted reverse\n proxy on public networks.\n- **No provider-token custody**. The server never returns the API key. The\n `api_key.source` capability field reports `env`, `config`, or `missing` —\n never the key itself.\n- **No hosted relay**. The app-server is a local process under the user's\n control. There is no cloud component.\n- **Capability responses** never leak secrets, file contents, or session\n message bodies. They report *metadata*: presence, counts, status flags.\n\n### CORS allow-list\n\nThe runtime API ships with a built-in dev-origin allow-list:\n`http://localhost:3000`, `http://127.0.0.1:3000`, `http://localhost:1420`,\n`http://127.0.0.1:1420`, `tauri://localhost`. To add additional origins (e.g.\nwhen developing a UI on Vite's default `:5173`), use any of:\n\n- CLI flag (repeatable): `codewhale serve --http --cors-origin http://localhost:5173`\n- Env var (comma-separated): `DEEPSEEK_CORS_ORIGINS=\"http://localhost:5173,http://localhost:8080\"`\n- Config (`~/.codewhale/config.toml`):\n ```toml\n [runtime_api]\n cors_origins = [\"http://localhost:5173\"]\n ```\n\nUser-supplied origins **stack on top of** the built-in defaults; they do not\nreplace them. Wildcard origins are not supported — the explicit allow-list\nmodel is preserved. Cross-origin preflights advertise only `Authorization`,\n`Content-Type`, `Accept`, `X-Codewhale-Runtime-Token`, and the compatibility\n`X-DeepSeek-Runtime-Token` request header; custom request headers are not\nallowed. Added in v0.8.10 (#561), tightened in v0.9.1 (#4454).\n\n## Managed Fleet Runtime and SDK helpers\n\n`GET /v1/fleet/profiles` reads the captured workspace's effective roster,\nincluding its selected Fleet and admitted plugins, without opening a run ledger.\n`GET /v1/fleet/profiles/{id}/document` addresses an exact effective member ID.\nIts shared `FleetProfileDocument` contract returns `id`, `kind` (`profile` or\n`fleet`), `origin`, nullable `path`, `text`, opaque source-bound `revision`,\n`writable` and nullable `refusal_reason`. Personal/project profile documents and\nselected Fleet files return exact UTF-8 bytes through a bounded, pinned reader\n(one MiB maximum). Changed identity, missing source or linked paths are refused.\nBuilt-in, config-backed and foreign-managed members instead return an explicitly\nread-only effective profile projection with no file path; sentinel paths and\nnative preset entry identifiers are never opened as documents.\n\nAll documents currently advertise `writable: false`. No document PUT is exposed;\nuse the terminal or the defining source to edit. A revision is not write\npermission or a live-roster adoption receipt. Watch clients may read these\nresources under the existing workspace and authentication boundaries.\n\nThe Runtime SDK lives in `npm/runtime-sdk` and is exposed as\nthe `@codewhale/runtime-sdk` workspace package. It is deliberately thin: every\nhelper calls the local Rust Runtime API and therefore cannot bypass Codewhale's\nsandbox, approval prompts, provider configuration, or fleet ledger authority.\n\n```js\nimport { createRuntimeClient } from \"@codewhale/runtime-sdk\";\n\nconst client = createRuntimeClient({\n baseUrl: \"http://127.0.0.1:7878\",\n token: process.env.CODEWHALE_RUNTIME_TOKEN,\n});\n\nconst created = await client.createFleetRun({\n target: \"this_computer\",\n roles: [{ name: \"reviewer\" }, { name: \"verifier\" }],\n workflow: {\n id: \"release-check\",\n kind: \"parallel\",\n tasks: [\n { id: \"review\", name: \"Review\", instructions: \"Review locally.\", worker: { role: \"reviewer\" } },\n { id: \"verify\", name: \"Verify\", instructions: \"Verify locally.\", worker: { role: \"verifier\" } },\n ],\n },\n});\n\n// POST /runs only prepares durable work. This call crosses the launch gate.\nawait client.startFleetRun(created.run.id);\n\nlet cursor;\nfor await (const event of client.fleetEvents(created.run.id, { after: cursor })) {\n if (event.cursor) cursor = event.cursor;\n if (event.event === \"fleet.replay.cursor_unavailable\") {\n // Reload getFleetRun(created.run.id), then reconnect without the old cursor.\n }\n}\n```\n\nThe managed path is deliberately two-step. `POST /v1/fleet/runs` validates and\npersists the run and queue without starting a worker. A separate authenticated\n`POST /start` activates it and schedules the executor driver; its `202` response\nreports `leased: 0` because the driver performs all leasing after it owns the\nrun. Creation requires named roles, one task owner per role, a `parallel`\nWorkflow, and an explicit Runtime target. v0.9.4 executes\nonly `this_computer`; `another_computer` and `cloud` return `501` rather than\nsilently executing locally. Worker IDs are generated per run; caller-assigned\n`worker_specs` return `501` until custom workers can be given collision-free\nmanaged identities. Parallel tasks with overlapping effective write roots are\nrejected before the run is journaled. Managed `security_policy` overrides also\nfail closed until that document can be enforced end to end; executable\nauthority comes from each named role's tool posture and bounded task workspace\nscope.\n\nFleet helpers cover this HTTP surface:\n\n| Helper | Runtime API route |\n|---|---|\n| `createFleetRun(spec)` | `POST /v1/fleet/runs` |\n| `startFleetRun(runId)` | `POST /v1/fleet/runs/{run_id}/start` |\n| `listFleetRuns()` | `GET /v1/fleet/runs` |\n| `getFleetRun(runId)` | `GET /v1/fleet/runs/{run_id}` |\n| `listFleetWorkers(runId)` | `GET /v1/fleet/runs/{run_id}/workers` |\n| `getFleetWorker(workerId)` | `GET /v1/fleet/workers/{worker_id}` |\n| `interruptWorker(workerId)` | `POST /v1/fleet/workers/{worker_id}/interrupt` |\n| `stopWorker(workerId)` | `POST /v1/fleet/workers/{worker_id}/stop` |\n| `restartWorker(workerId)` | `POST /v1/fleet/workers/{worker_id}/restart` |\n| `stopFleetRun(runId)` | `POST /v1/fleet/runs/{run_id}/stop` |\n| `replayFleetEvents(runId, options)` | `GET /v1/fleet/runs/{run_id}/events/replay` |\n| `fleetEvents(runId, options)` | `GET /v1/fleet/runs/{run_id}/events` (SSE) |\n\n`stopWorker` durably cancels that worker's active task and leaves the rest of\nthe Fleet running. `interruptWorker` is the compatibility name for the same\nattempt-fenced cancellation transition. `stopFleetRun` cancels every queued or\nactive task and marks the whole run cancelled.\n\nReplay covers aggregate run/task transitions and privacy-bounded individual\nworker transitions. Event bodies omit prompts, tool call IDs, completion text,\nartifact paths/checksums, and cancellation identities; bounded failure reasons\npass through secret redaction. `cursor` is opaque and stable across ordinary\nappends and Runtime restarts. Clients reconnect with `after=<cursor>`. A fresh\nrequest returns a bounded newest tail and marks `history_truncated` when older\nhistory exists. Ledger compaction can remove an old cursor; the JSON endpoint\nthen returns `409`, while the SSE endpoint emits\n`fleet.replay.cursor_unavailable`, so the client reloads the current run\nprojection instead of accepting a silent gap.\n\n`GET /v1/runtime/info` advertises `fleet_run_create`, `fleet_run_start`,\n`fleet_event_replay`, `fleet_event_stream`, and `fleet_local_target`. Older\nruntimes without a requested route still produce a typed SDK\n`RuntimeCapabilityError`.\n\nVerification:\n\n```bash\nnpm test --workspace @codewhale/runtime-sdk\n```\n\n## Thread Fleet members\n\n`thread_fleet_members` in runtime capabilities enables\n`POST /v1/threads/{id}/members`. The body is\n`{operation_key, member_id, display_name, prompt, fleet}`, where `fleet` is a\ncomplete Fleet v2 document (the same shape as a saved `fleets/*.toml`, as JSON)\nand `member_id` names one of its members with an exact `provider` and `model`.\nThe thread's Engine starts that member as an ordinary child of the session,\nthrough the same spawn path as the `agent` tool: the member's provider and\nmodel are bound exactly and never fall back to the session route, its\ninstructions join the child's task prompt as its Fleet profile, and the child\nkeeps the session's permissions and spawn limits. The document is used for this\nstart only; it is not saved and does not change the selected Fleet.\n\nThe response is `200 {thread_id, member_id, agent_id, state: \"started\"}`, or\n`202 {state: \"queued\"}` when the thread is mid-turn: host requests are handled\nbetween turns. Repeating the same `operation_key` with the same body returns\nthe original `agent_id` once started; a different body under the same key is\nrefused. The child appears in `/v1/agent-runs` with `owner_session_id` set to the\nthread and its `member_id` as `session_name` (a display name may hold spaces or\nnon-ASCII text, which session names refuse); `agent.spawned`,\n`agent.progress` and `agent.completed` arrive on the thread's event stream.\nStop it with `POST /v1/agent-runs/{id}/cancel`, which also stops its\ndescendants. A second start for a member whose child is still running is\nrefused by the existing session-name rule; follow up with that child instead.\n\n## Agent Run Receipts\n\nSub-agent lanes persist compact run receipts in\n`.codewhale/state/subagents.v1.json`. The Runtime API exposes those receipts as\na read-only inspection surface:\n\n| Operation | Endpoint |\n|---|---|\n| List persisted agent runs | `GET /v1/agent-runs` |\n| Inspect one run | `GET /v1/agent-runs/{run_id}` |\n| Stop one run | `POST /v1/agent-runs/{run_id}/cancel` |\n\nThe response is the same worker-record shape surfaced by `agent` receipts:\n`spec.run_id`, `actor_kind`, lifecycle `status`, bounded `events`,\n`follow_up`, `takeover`, `artifacts`, `usage`, and `verification`. `run_id`\nfalls back to the worker id for older records, and `{run_id}` may be either the\nrun id or the worker id.\n\nThese endpoints do not start or steer sub-agents. The API surface exists so\napp/editor/headless clients can inspect the same handoff receipts that the TUI\nand parent model see, and stop a run they are showing.\n\n`POST /v1/agent-runs/{run_id}/cancel` takes no body. It stops the run through\nthe same session-scoped path as the TUI's stop and the `agent/cancel` tool:\ndescendants stop with it, and a write-scoped child's changed files are named in\nits result rather than dropped. It answers with the worker record:\n\n- `200` when the record is terminal (stopping an already-finished run is a\n no-op that returns its receipt);\n- `202` when the owning engine accepted the stop but has not recorded the\n terminal receipt within a few seconds; poll `GET /v1/agent-runs/{run_id}`;\n- `404` for an unknown run;\n- `409` when the run belongs to a session this runtime is not hosting (for\n example a separate terminal session); stop it from that session.\n\n## Session lifecycle (native UI supervision)\n\n| Operation | Endpoint |\n|---|---|\n| List sessions | `GET /v1/sessions` |\n| List session summaries | `GET /v1/sessions/summary` |\n| Get session | `GET /v1/sessions/{id}` |\n| Rename / archive session | `PATCH /v1/sessions/{id}` |\n| Delete session | `DELETE /v1/sessions/{id}` |\n| Session store repair summary | `GET /v1/sessions/repair` |\n| Resume into thread | `POST /v1/sessions/{id}/resume-thread` |\n| Create thread | `POST /v1/threads` |\n| List threads | `GET /v1/threads` |\n| Attach to events | `GET /v1/threads/{id}/events?since_seq=0` |\n| Send message | `POST /v1/threads/{id}/turns` |\n| Steer | `POST /v1/threads/{id}/turns/{turn_id}/steer` |\n| Interrupt | `POST /v1/threads/{id}/turns/{turn_id}/interrupt` |\n| Compact | `POST /v1/threads/{id}/compact` |\n\n## Compatibility tests\n\nContract snapshots live in `crates/protocol/tests/`. Run:\n\n```bash\ncargo test -p codewhale-protocol --test parity_protocol --locked\n```\n\nThis validates that the app-server's event schema hasn't drifted from the\ndocumented contract. CI runs this on every push to `main` and on release tags.\n\nThe app-server stdio control surface has its own drift guard — the advertised\n`capabilities` method set is pinned in `crates/app-server/src/tests.rs`:\n\n```bash\ncargo test -p codewhale-app-server capabilities\n```\n\nBefore a release, run the headless smoke (stdio probe + optional provider\nmatrix, no secrets leaked):\n\n```bash\nscripts/release/app-server-smoke.sh --matrix # dry-run plan\nbash scripts/release/app-server-smoke.test.sh # parser self-test (fake binary)\n```\n\n## Profile constitution\n\n`profile_constitution` in runtime capabilities enables profile snapshots on\n`POST /v1/threads/{id}/turns`. The optional `profile_constitution` field contains\n`{accountId, revision, constitution}`. `constitution` is exactly\n`{schemaVersion: 1, detail, initiative, collaboration, notes}`; choices are\n`brief|balanced|detailed`, `check|judgment|moving`, and `direct|critical|coach`.\nNotes are limited to 4,000 Unicode characters. Invalid data is refused.\n\nThe authenticated account transport supplies the snapshot, which participates in\nthe turn's replay identity. The Engine renders it through its existing personal\nconstitution renderer and records it in native session history. The snapshot is\nunchanged across provider retries and compaction. A new snapshot fully replaces\nearlier personal preferences. Permissions and approval policy are unaffected.\nInternal follow-ups and RLM child calls inherit the admitted preferences;\nthey do not re-read the host operator's account in the middle of that work.\n\nWithout a supplied snapshot, the Engine reads the signed-in profile from the\nconfigured account service at turn admission. When that Engine-loaded profile is\nunavailable or invalid, the turn uses the signed-out local constitution and shows\na notice once per session; an invalid host-supplied `profile_constitution` snapshot still fails\nthe turn, and `GET /v1/constitution` still reports the error. An account without saved\npreferences uses an explicit default snapshot; a signed-out account uses the\nexisting local constitution. Hosted transports always supply the owning\naccount snapshot, including defaults, so local preferences cannot leak between\naccounts.\n\n`GET /v1/constitution` reads the next-turn profile and its model guidance. It is\nnot a receipt that an active turn adopted the edit. `POST /v1/constitution/preview`\naccepts a constitution document and returns `{modelGuidance, saved:false}` without\nsaving anything. `modelGuidance` is the Engine-rendered guidance body: GET and\npreview return identical text for the same document. It excludes the session\nrecord's attribution line or envelope; `source` and `revision` carry provenance.\nBoth routes use normal Runtime authorization. Older runtimes\nmust be upgraded before account transports submit profile-bearing turns.\n\n\n### Attested watch-event turns (preview)\n\n`watch_event_turns` is true only when the dedicated route and configured Runtime bearer exist. `POST /v1/threads/{id}/watch-turns` accepts only `operation_key`, `prompt`, and `watch_event`. It requires the actual master `Authorization: Bearer ...` header. Device Watch/Drive tokens, cookies, token-header aliases and insecure no-auth do not authorize this route. The existing thread's provider, workspace, tools and permissions remain authoritative; the body cannot grant or replace them.\n\nThe closed version-1 receipt is at most 4 KiB when normalized. Required fields are `version`, `platformAccountId`, `projectId`, `watchId`, `watchRevision`, `definitionSha256`, `sourceKind`, `occurrenceSha256`, `eventSha256`, `runId`, `roomThreadRootId`, and `source`. References are bounded ASCII (200 bytes), except `repo` has two `[A-Za-z0-9_.-]` components of 1..100 bytes each (201 including `/`). Revisions are positive safe integers and hashes are lowercase SHA-256. `roomThreadRootId` is empty or a `pm_` id. `sourceKind: \"schedule\"` carries only `source.slot`, the exact canonical 24-byte UTC ISO schedule occurrence `YYYY-MM-DDTHH:mm:ss.sssZ` with a valid calendar/time (no offsets, date-only or leap seconds); `\"github.pull_request\"` carries only installationId, bindingId, repo, number, deliveryId and eventName. Unknown fields, mismatched kind/source or grant fields refuse. Runtime-owner attestation does not independently verify Platform's webhook signature or current watch policy.\n\nThis manager-owned source uses Runtime provenance through the same turn admission and execution path. It cannot count as a person's answer or resume a goal requiring that answer. The operation fingerprint includes normalized intent, prompt and Runtime-derived owner/thread/workspace context. Fresh admission returns 201, exact replay 200, conflicting intent 409. UserMessage metadata durably retains `input_provenance: \"watch_event\"`, `watch_event`, SHA-256 of the exact UTF-8 admitted prompt (preserved without trimming for typed watch input) as `input_prompt_sha256`, and the derived `runtime_binding`. Metadata alone never selects this authority. Ordinary `/turns` and the local `/rc` consumer refuse watch authority.\n\nThe existing authenticated operation lookup returns the same flat TurnRecord for ordinary turns and additive watch fields from that durable input for watch turns. It holds the existing shared operation claim while reading, creates no record or lock, and never starts a turn. Missing, incomplete or mismatched lookup is not proof of nonadmission. A Platform accepted command ACK precedes native admission; recovery may acknowledge an exact already-admitted receipt but must not send a replacement POST on absence, revocation or uncertainty. Disable/re-enable fencing belongs to Platform's existing queue and revalidation owner. This route does not make revocation atomic with later dispatch or confirm cancellation of earlier accepted work, and it does not implement whole continuous operation.\n\n\n### App Server wire migration status\n\nThe selected reference is Codex `rust-v0.166.0`, commit\n`6ff1e1ca3b30fc8b914720681fcf5fe62e95f856`. The shared RPC envelope and\ninitialization lifecycle serve JSONL stdio and private-channel WebSocket text\nmessages. The decoder includes request, notification, response and error\nvariants with signed-i64 or string IDs. Legacy thread/message and response\nevents remain compatibility methods; they do not establish Codex thread/turn/item\nparity. TCP WebSocket listener, full-tool turn/start, independent Codex subscriptions, bidirectional interactions, durable idempotent\nsettlement and the complete SDK remain required. The native Engine journal\nand turn loop remain authoritative. No default approval deadline is introduced.\nOwner restart interrupts active work rather than re-offering pending interactions.\n\n`/v1/chat/completions` admits one nonempty user text message through the existing\nmanager and journal. Unsupported histories, system/tool messages, tools, images,\nstreaming and other options refuse before admission. Standard usage and\nfinish_reason remain null until their native completeness mapping is implemented;\nno zero or final-reason guess is made. This bounded compatibility input does not\nestablish complete chat or Codex parity.\n\n## Local whale voice\n\nA local whale's `SOUL.md` describes its personality and voice. Read the exact local file with `GET /v1/whale/soul` and save it with `PUT /v1/whale/soul`. Both operations require the host Runtime token. The response carries the path, text, revision and writable state. Send the returned revision quoted in `If-Match` with a JSON object containing `text`. A stale edit returns HTTP 412 so the editor can keep the draft. Files are private to the current OS user (mode 0600 on Unix), UTF-8, regular single-link files with a 16 KiB limit. An existing `SOUL.md` requires a matching `whale.toml` identity (`id = \"default\"`); an absent local whale is created only by an explicit save. A concurrent edit returns HTTP 409 instead of waiting for the other writer. An explicitly imported account whale has a read-only local copy.\n\nThe root Engine captures the local whale's voice at turn admission and charges it to the global instruction budget. The instruction receipt includes `whale_soul_source_path` and `whale_soul_revision`; internal follow-ups reuse the captured voice. Its personality applies within the existing permissions.\n"
|
||
}
|
||
]
|
||
},
|
||
{
|
||
"id": "web",
|
||
"slug": "web",
|
||
"sources": [
|
||
{
|
||
"path": "docs/WEB.md",
|
||
"sha256": "e38dbc33c3f768fa9ea208a2c6231445406fe8f6421c56c961c21afa7382901e",
|
||
"content": "# Local browser client\n\n> 阅读简体中文版:[zh_hans/WEB.md](zh_hans/WEB.md)。\n\n`codewhale web` opens Codewhale's embedded browser client over the canonical\nRuntime API. It is a local surface: the server always binds to\n`127.0.0.1`, cannot be rebound to a LAN address, and cannot run with Runtime\nauthentication disabled.\n\n## Start it\n\nFrom the workspace Codewhale should operate in, run:\n\n```bash\ncodewhale web\n```\n\nThe default address is `http://127.0.0.1:7878`. To avoid a local port\ncollision, choose another loopback port:\n\n```bash\ncodewhale web --port 8788\n```\n\nCodewhale starts the Runtime API, serves the dependency-free client embedded\nin the installed binary, prints a single-use launch URL, and asks the operating\nsystem to open that URL in the default browser. If the browser does not open,\nuse the printed URL within ten minutes. Stop the process with `Ctrl+C`; the\nbrowser session ends with it.\n\n## What the browser can do\n\nThe embedded client provides a responsive thread and search rail, Runtime-owned\nsession facts, transcript and tool receipts, and a composer. It can create,\nselect, rename, and archive threads; start or steer turns; interrupt work;\nresolve approvals; and answer Runtime user-input requests.\n\nThe browser is another view of the same local Runtime. It does not create a\nsecond cloud account, copy provider credentials into browser storage, or\nweaken the configured approval and sandbox policies.\n\n## Authentication boundary\n\nThe browser-launch URL contains a random, short-lived, one-time bootstrap\ncapability. It never contains the Runtime bearer token. A loopback request\nexchanges the capability for an `HttpOnly`, `SameSite=Strict`, process-local\nsession cookie and immediately invalidates the capability.\n\nReused, expired, malformed, and non-loopback bootstrap attempts fail closed.\nThe Runtime token is not placed in rendered HTML, browser storage, URL\nqueries or fragments, or browser-launch arguments. The one-time bootstrap\nvalue is printed in the local terminal and briefly passes through the operating\nsystem's browser launcher. It is single-use and expires after ten minutes, but\na hostile process already running as the same OS user remains inside the local\ntrust boundary.\n\nCookie-authenticated state-changing requests must also present the exact\nlocal web origin. Cross-origin browser requests are rejected. Existing\nexplicit bearer and Runtime-token-header clients retain their normal Runtime\nAPI behavior.\n\n## Local means local\n\n`codewhale web` accepts only `--port`; there is no `--host` or insecure-auth\noption on this command. Do not treat it as a public website or expose its port\ndirectly through router forwarding, a public reverse proxy, or a tunnel.\n\nThe separate `codewhale app-server --mobile` and `--http` modes have different\ndeployment and authentication contracts. Read [RUNTIME_API.md](RUNTIME_API.md)\nbefore operating either one, especially before selecting a non-loopback bind.\n\n## Troubleshooting\n\n- If port `7878` is occupied, pass an unused `--port` value.\n- If the browser does not open, copy the printed single-use bootstrap URL into\n a browser on the same machine within ten minutes. Start `codewhale web` again\n if that URL has already been used or expired.\n- If the page loads but a provider is unavailable, inspect `codewhale doctor`\n and `/provider`; the web command does not configure or move provider\n credentials.\n- If a session expired, stop and restart `codewhale web` to mint a new\n process-local session. Reusing an old bootstrap URL is expected to fail.\n- A new tab opened by typing the address is not signed in: the session's\n request proof stays in the tab that opened it. Use that tab, duplicate it,\n or restart `codewhale web`.\n\nFor integration endpoints, headers, events, and the complete web-session\ncontract, see [RUNTIME_API.md](RUNTIME_API.md).\n"
|
||
}
|
||
]
|
||
},
|
||
{
|
||
"id": "troubleshooting",
|
||
"slug": "troubleshooting",
|
||
"sources": [
|
||
{
|
||
"path": "docs/OPERATIONS_RUNBOOK.md",
|
||
"sha256": "e466c8ffd55a0754167e29c64530943577ab8758645061248d671b26f2062d34",
|
||
"content": "# codewhale Operations Runbook\n\n> 阅读简体中文版:[zh_hans/OPERATIONS_RUNBOOK.md](zh_hans/OPERATIONS_RUNBOOK.md)。\n\nThis runbook covers practical debugging and incident response for the local CLI/TUI runtime.\n\n## Quick Triage\n\n1. Confirm binary + config:\n - `cargo run -- --version`\n - `cat ~/.codewhale/config.toml` (or inspect configured profile)\n2. Enable verbose logs:\n - `RUST_LOG=codewhale_tui=debug cargo run`\n - For HTTP retries/reconnects: `RUST_LOG=codewhale_tui::client=debug cargo run`\n3. Capture current state:\n - `ls ~/.codewhale/sessions`\n - `ls ~/.codewhale/sessions/checkpoints`\n - `ls ~/.codewhale/tasks`\n\n## Incident: Turn Hangs or Stream Stops\n\nSymptoms:\n- TUI remains in loading state\n- partial assistant output with no completion\n\nChecks:\n1. Inspect retry/health logs (`codewhale_tui::client`)\n2. Verify endpoint connectivity:\n - `curl -sS https://api.deepseek.com/beta/models -H \"Authorization: Bearer $DEEPSEEK_API_KEY\"`\n3. Confirm no local sandbox/permission deadlock in tool output\n\nActions:\n1. If a foreground shell command is running, press `Ctrl+B` to move it to the background (the turn keeps running and the command becomes a background job under `/jobs`); use `Ctrl+C` instead if you want to cancel the turn.\n2. If the command was started in the background, ask the assistant to use `Bash` with `action: \"cancel\"` and the returned process id.\n3. Use `Esc` or `Ctrl+C` to interrupt the current turn when you want to stop the request itself.\n4. Retry prompt; if still failing, restart TUI.\n5. On restart, verify the previous queued/in-flight runtime turn is shown as interrupted rather than left in a running state.\n\n## Incident: Network Outage / Offline Behavior\n\nExpected behavior:\n- New prompts are queued while offline mode is active\n- Queue state persists per session to\n `~/.codewhale/sessions/checkpoints/<session-id>.offline_queue.json`; a legacy\n global `offline_queue.json` is adopted once on upgrade\n\nChecks:\n1. Open queue in TUI: `/queue list`\n2. Confirm persisted queue file exists and updates timestamp\n\nActions:\n1. Restore connectivity\n2. Re-send queued entries (from `/queue edit <n>` + Enter, or normal input flow)\n3. Ensure queue file clears when queue is empty\n\n## Incident: Crash Recovery Needed\n\nExpected behavior:\n- Each session checkpoints to `~/.codewhale/sessions/checkpoints/<session-id>.json`;\n a legacy `latest.json` is still read for recovery but is no longer written\n- Startup begins a fresh session unless `--resume`/`--continue` is supplied\n\nActions:\n1. Resume prior work explicitly via `codewhale --resume <id>` (alias\n `codewhale resume <id>`; `codewhale --continue` recovers the newest\n interrupted checkpoint for the workspace) or `Ctrl+R` in TUI\n2. If checkpoint inspection is needed, inspect `checkpoints/<session-id>.json` (or a leftover legacy\n `latest.json`) for schema mismatch/details\n3. If schema is newer than binary supports, upgrade binary or remove stale checkpoint\n\n## Incident: Persistent State Schema Errors\n\nSymptoms:\n- Errors like `schema vX is newer than supported vY`\n\nAffected stores:\n- sessions (`~/.codewhale/sessions/*.json`)\n- runtime thread/turn/item records\n- tasks (`~/.codewhale/tasks/tasks/*.json`)\n\nActions:\n1. Confirm binary version and migration expectations\n2. Back up the state directory before editing\n3. Either:\n - run with a newer compatible binary, or\n - archive incompatible records and regenerate state\n\n## Incident: MCP/Tool Execution Failures\n\nChecks:\n1. Validate `~/.codewhale/mcp.json` schema and server command paths\n2. Confirm server process can start manually\n3. Check sandbox denials in TUI history / logs\n\nActions:\n1. Retry with required approvals (or YOLO only when appropriate)\n2. Temporarily disable failing MCP server and isolate issue\n3. Re-enable after verification with `/mcp` diagnostics\n\n## Post-Incident Checklist\n\n1. Preserve logs and relevant state files\n2. Record trigger, impact, and mitigation\n3. Add or update regression tests (retry/recovery/schema)\n4. Update this runbook and architecture docs if behavior changed\n"
|
||
},
|
||
{
|
||
"path": "docs/DOCKER.md",
|
||
"sha256": "3845d9e1bc72942ab1f6d714e6d6ed4bd204ccc2f40b29a87fa333893767ea79",
|
||
"content": "# Docker\n\n> 阅读简体中文版:[zh_hans/DOCKER.md](zh_hans/DOCKER.md)。\n\nCodewhale publishes a multi-arch Linux image to GitHub Container Registry\nfor each release.\n\nThe latest published image is 0.10.0 at `ghcr.io/hmbown/codewhale`.\nTo test the current `main` source, use [Building locally](#building-locally)\nin a checkout of [`codewhale-hq/Codewhale`](https://github.com/codewhale-hq/Codewhale).\n\n```bash\ndocker pull ghcr.io/hmbown/codewhale:latest\n```\n\n## Quick start\n\nRun the published image with a Docker-managed data volume:\n\n```bash\ndocker volume create codewhale-home\n\ndocker run --rm -it \\\n -e DEEPSEEK_API_KEY=\"$DEEPSEEK_API_KEY\" \\\n -v codewhale-home:/home/codewhale/.codewhale \\\n -v \"$PWD:/workspace\" \\\n -w /workspace \\\n ghcr.io/hmbown/codewhale:latest\n```\n\nUse a pinned release tag for reproducible installs:\n\n```bash\ndocker run --rm -it \\\n -e DEEPSEEK_API_KEY=\"$DEEPSEEK_API_KEY\" \\\n -v codewhale-home:/home/codewhale/.codewhale \\\n -v \"$PWD:/workspace\" \\\n -w /workspace \\\n ghcr.io/hmbown/codewhale:v0.10.0\n```\n\nThe pinned example uses the [0.10.0 release](https://github.com/codewhale-hq/Codewhale/releases/tag/v0.10.0).\n\n## Default image contract\n\n`ghcr.io/hmbown/codewhale:latest` and the semver tags are conservative runtime\nimages:\n\n- the container runs as the non-root `codewhale` user with UID/GID `1000:1000`\n- the image does not grant passwordless `sudo`\n- the image is meant to run Codewhale against mounted workspaces, not to mutate\n the base operating system at runtime\n- user state belongs in a volume mounted at `/home/codewhale/.codewhale`\n\nThat default is intentional. Keep using it for the smallest trust boundary. If a\nproject needs `apt-get`, compiler toolchains, Node/Python package managers,\ncustom CA certificates, or other host-like setup inside Docker, build an\nexplicit toolbox image instead of changing the default image contract.\n\n## Opt-in toolbox/custom image\n\nThe repository includes an example\n[`docs/examples/Dockerfile.toolbox`](examples/Dockerfile.toolbox) that extends\nthe official image with passwordless `sudo` and common development packages.\nBuild it with a pinned Codewhale tag when you want repeatable project\nenvironments:\n\n```bash\ndocker build -f docs/examples/Dockerfile.toolbox \\\n --build-arg CODEWHALE_IMAGE=ghcr.io/hmbown/codewhale:v0.10.0 \\\n --build-arg TOOLBOX_PACKAGES=\"git openssh-client curl build-essential pkg-config python3 python3-pip nodejs npm\" \\\n -t codewhale-toolbox:my-project .\n```\n\nUse `latest` only for throwaway testing. For shared projects, keep the\n`CODEWHALE_IMAGE` value pinned and review package additions like any other\ndevelopment-environment change.\n\nRun the toolbox image with the same workspace and state mounts:\n\n```bash\ndocker volume create codewhale-my-project-home\n\ndocker run --rm -it \\\n -e DEEPSEEK_API_KEY=\"$DEEPSEEK_API_KEY\" \\\n -v codewhale-my-project-home:/home/codewhale/.codewhale \\\n -v \"$PWD:/workspace\" \\\n -w /workspace \\\n codewhale-toolbox:my-project\n```\n\nInside this opt-in image, Codewhale can use commands such as\n`sudo apt-get update` and `sudo apt-get install -y <package>`. For repeatable\ncontainers, prefer baking those packages into the toolbox Dockerfile instead of\nletting a long-lived container drift.\n\nDo not bake API keys, SSH private keys, or other secrets into custom images.\nPass API keys at runtime and mount any SSH material deliberately, preferably\nread-only and only for projects that need it.\n\n### Compose toolbox template\n\nIf you prefer a repeatable `docker compose` entry point, use\n[`docs/examples/compose.toolbox.yml`](examples/compose.toolbox.yml). It builds\nthe toolbox image from [`docs/examples/Dockerfile.toolbox`](examples/Dockerfile.toolbox)\nand keeps the project state volume explicit:\n\n```bash\nCODEWHALE_IMAGE=ghcr.io/hmbown/codewhale:v0.10.0 \\\nCODEWHALE_TOOLBOX_IMAGE=codewhale-toolbox:my-project \\\nCODEWHALE_HOME_VOLUME=codewhale-my-project-home \\\nCODEWHALE_WORKSPACE=\"$PWD\" \\\ndocker compose -f docs/examples/compose.toolbox.yml run --rm codewhale\n```\n\nUse a different `CODEWHALE_TOOLBOX_IMAGE` and `CODEWHALE_HOME_VOLUME` for each\nproject that needs an independent toolchain or independent `.codewhale` state.\nThe Compose file also shows opt-in, read-only mounts for SSH material and local\nCA certificates; keep those commented out unless the project needs them.\n\n## Multiple independent projects\n\nUse one named state volume per project so sessions, config, skills, memory, and\nthe offline queue do not bleed across workspaces:\n\n```bash\nproject=\"$(basename \"$PWD\")\"\nimage=\"codewhale-toolbox:${project}\"\ndocker volume create \"codewhale-${project}-home\"\n\ndocker run --rm -it \\\n --name \"codewhale-${project}\" \\\n -e DEEPSEEK_API_KEY=\"$DEEPSEEK_API_KEY\" \\\n -v \"codewhale-${project}-home:/home/codewhale/.codewhale\" \\\n -v \"$PWD:/workspace\" \\\n -w /workspace \\\n \"$image\"\n```\n\nFor projects with different toolchains, build different toolbox tags, for\nexample `codewhale-toolbox:frontend` and `codewhale-toolbox:backend`. The\nseparate launcher idea discussed in issue #2217 can build on this contract, but\nit is intentionally outside the core Docker image.\n\n## Project bootstrap scripts\n\nCodewhale does not automatically execute `.codewhale/setup.sh` or legacy\n`.deepseek/setup.sh`. If you keep one of those files as a local project recipe,\nrun it explicitly. For shared team setup, prefer a committed project script or\nthe toolbox Dockerfile so the environment can be reviewed and rebuilt.\n\nFor example, to run a committed bootstrap script before starting Codewhale:\n\n```bash\ndocker run --rm -it \\\n -e DEEPSEEK_API_KEY=\"$DEEPSEEK_API_KEY\" \\\n -v codewhale-my-project-home:/home/codewhale/.codewhale \\\n -v \"$PWD:/workspace\" \\\n -w /workspace \\\n --entrypoint bash \\\n codewhale-toolbox:my-project \\\n -lc './scripts/bootstrap-dev.sh && exec codewhale'\n```\n\nUse the toolbox image for bootstrap scripts that need `sudo`. The default image\nwill not elevate privileges.\n\n## Custom CA certificates and proxies\n\nFor corporate proxies, dev-sidecar, or self-signed internal services, prefer\nbaking trusted CA certificates into a custom toolbox image:\n\n```dockerfile\nUSER root\nCOPY docker/certs/*.crt /usr/local/share/ca-certificates/\nRUN update-ca-certificates\nUSER codewhale\n```\n\nAll files copied into `/usr/local/share/ca-certificates/` must use the `.crt`\nextension. Keep private CA material out of public images.\n\nFor a local-only run, mount certificates read-only and update the trust store at\ncontainer start:\n\n```bash\ndocker run --rm -it \\\n -e DEEPSEEK_API_KEY=\"$DEEPSEEK_API_KEY\" \\\n -v codewhale-my-project-home:/home/codewhale/.codewhale \\\n -v \"$PWD:/workspace\" \\\n -v \"$PWD/docker/certs:/usr/local/share/ca-certificates/local:ro\" \\\n -w /workspace \\\n --entrypoint bash \\\n codewhale-toolbox:my-project \\\n -lc 'sudo update-ca-certificates && exec codewhale'\n```\n\nThis CA workflow requires the opt-in toolbox image because the default image\ndoes not include passwordless `sudo`.\n\n## Local build\n\nBuild the image locally from a checkout:\n\n```bash\ndocker build -t codewhale .\n```\n\nThen run it with the same Docker-managed data volume:\n\n```bash\ndocker run --rm -it \\\n -e DEEPSEEK_API_KEY=\"$DEEPSEEK_API_KEY\" \\\n -v codewhale-home:/home/codewhale/.codewhale \\\n -v \"$PWD:/workspace\" \\\n -w /workspace \\\n codewhale\n```\n\nDocker Hub publishing is not configured; GHCR is the supported prebuilt image\nregistry.\n\n## Environment variables\n\n| Variable | Required | Description |\n|-----------------------|----------|--------------------------------------------------|\n| `DEEPSEEK_API_KEY` | yes | DeepSeek API key |\n| `DEEPSEEK_BASE_URL` | no | Custom API base URL (e.g. `https://api.deepseek.com`) |\n| `DEEPSEEK_NO_COLOR` | no | Set to `1` to disable terminal colour output |\n\n## Volumes\n\nMount `/home/codewhale/.codewhale` to persist sessions, config, skills, memory,\nand the offline queue across container restarts. The image also keeps\n`/home/codewhale/.deepseek` available for legacy compatibility. A\nDocker-managed named volume is the safest default because Docker creates it with\nownership the container can write:\n\n```bash\n-v codewhale-home:/home/codewhale/.codewhale\n```\n\nWithout this mount the container starts fresh each time.\n\nIf you bind-mount an existing host directory instead, the image runs as the\nnon-root `codewhale` user with UID/GID `1000:1000`. The mounted directory must be\nwritable by that user, or startup can fail while creating runtime directories\nunder `.codewhale/tasks`. On Linux hosts, either use the named volume above or\nprepare the bind mount explicitly:\n\n```bash\nmkdir -p ~/.codewhale\nsudo chown -R 1000:1000 ~/.codewhale\n\ndocker run --rm -it \\\n -e DEEPSEEK_API_KEY=\"$DEEPSEEK_API_KEY\" \\\n -v ~/.codewhale:/home/codewhale/.codewhale \\\n ghcr.io/hmbown/codewhale:latest\n```\n\nThat `chown` changes ownership of the host `~/.codewhale` directory. Skip it if\nyou do not want the container UID to own your local config, and use a named\nvolume instead.\n\n## Non-interactive / pipeline usage\n\nWhen stdin is not a TTY, `codewhale` drops to the dispatcher's one-shot mode\n(`codewhale -c \"…\"`). Pipe a prompt on stdin:\n\n```bash\necho \"Explain the Cargo.toml in structured English.\" | \\\n docker run --rm -i -e DEEPSEEK_API_KEY ghcr.io/hmbown/codewhale:latest\n```\n\n## Building locally\n\n```bash\n# Single platform (your host architecture)\ndocker build -t codewhale .\n\n# Multi-platform (requires a builder with emulation)\ndocker buildx create --use\ndocker buildx build --platform linux/amd64,linux/arm64 -t codewhale .\n```\n\n## Devcontainer\n\nThe repository includes a [`.devcontainer/devcontainer.json`](../.devcontainer/devcontainer.json)\nconfiguration for VS Code / GitHub Codespaces. It builds a dedicated development\nimage with the Rust toolchain, Git, `pkg-config`, and the DBus development headers\nrequired by the workspace. The first open runs `cargo build --locked` and installs\nrust-analyzer and the other editor extensions.\n\nThe source checkout remains mounted from the host. Codewhale state and Cargo build\nartifacts use Docker named volumes instead, so the configuration works when VS Code\ncannot provide a POSIX-style `HOME` variable (notably on Windows), and builds do not\nwrite thousands of small files through a Windows bind mount. Rebuild the container\nafter changing the Dev Container configuration.\n\n## Release status\n\nDocker image publishing is part of the release gate. The image is published to\nGHCR for `linux/amd64` and `linux/arm64` with semver tags plus `latest`.\n"
|
||
}
|
||
]
|
||
},
|
||
{
|
||
"id": "contribution",
|
||
"slug": "contribution",
|
||
"sources": [
|
||
{
|
||
"path": "CONTRIBUTING.md",
|
||
"sha256": "c2747cbfb73a335fb77ef217497a5e8f07c70eab0922fcd23dfb1a46e28ced4b",
|
||
"content": "# Contributing to codewhale\n\nThank you for your interest in contributing to codewhale! This document provides guidelines and instructions for contributing.\n\n## Getting Started\n\n### Prerequisites\n\n- Rust 1.89 or later (edition 2024)\n- Cargo package manager\n- Git\n\n### Setting Up Development Environment\n\n1. Fork and clone the repository:\n ```bash\n git clone https://github.com/YOUR_USERNAME/CodeWhale.git\n cd CodeWhale\n ```\n\n2. Build the current engine and terminal:\n ```bash\n CODEWHALE_BUILD_SHA=\"$(git rev-parse HEAD)\" cargo build --locked -p codewhale-cli\n ```\n\n3. Run the tests near your change (see [Fast local loop](#fast-local-loop)):\n ```bash\n scripts/dev-test.sh tui your_test_filter\n ```\n\n4. Run with development settings:\n ```bash\n ./target/debug/codewhale --version\n ./target/debug/codewhale\n ```\n\n### Testing the latest source\n\nThe canonical source is [`codewhale-hq/Codewhale`'s `main` branch](https://github.com/codewhale-hq/Codewhale/tree/main).\nRelease candidates land there after their CI gates pass, so contributors can\ntest and build on the same source. Tagged downloads remain the latest published\nrelease; their version can lag the development version on `main`.\n\nFor a fresh checkout of the current source:\n\n```bash\ngit clone --branch main https://github.com/codewhale-hq/Codewhale.git\ncd Codewhale\nCODEWHALE_BUILD_SHA=\"$(git rev-parse HEAD)\" cargo build --release --locked -p codewhale-cli\n./target/release/codewhale --version\n./target/release/codewhale\n```\n\nInclude the commit shown by `--version` when reporting a problem. If you work\nfrom a fork, add the canonical repository as `upstream` and fetch `upstream/main`\nbefore starting a change; preserve any local work when updating your branch.\n\n## Development Workflow\n\n### Code Style\n\n- Run `cargo fmt` before committing to ensure consistent formatting\n- Run `cargo clippy` and address all warnings\n- Follow Rust naming conventions (snake_case for functions/variables, CamelCase for types)\n- Add documentation comments for public APIs\n\n### Testing\n\n- Write tests for new functionality\n- Run the tests near your change (see [Fast local loop](#fast-local-loop));\n your pull request runs the Linux workspace suite and a Windows build for\n you (see [What happens after you open a pull request](#what-happens-after-you-open-a-pull-request))\n- Colocate unit tests beside the code they cover (standard Rust `#[cfg(test)]`\n modules), and add integration tests under the owning crate's `tests/`\n directory (for example `crates/tui/tests/` or `crates/state/tests/`). The\n repository root `tests/` directory is not used\n\n### Pre-push verification\n\nThese are the Rust commands your pull request runs on Linux (the `Lint` and\n`Test (ubuntu-latest)` checks). You do not need all of them before every\npush: run `cargo fmt`, then check and test the crates you touched (see\n[Fast local loop](#fast-local-loop)). Run the full set locally when a change\nspans many crates, or let the pull request run it for you:\n\n```bash\ncargo fmt --all -- --check\ncargo clippy --workspace --all-targets --all-features --locked -- \\\n -D warnings \\\n -A clippy::uninlined_format_args \\\n -A clippy::too_many_arguments \\\n -A clippy::unnecessary_map_or\ncargo test --workspace --all-features --locked\n```\n\nClippy lints test, bench and example targets too (`--all-targets`), and the\nthree allowances above are the only ones CI and the release lane accept.\n\n#### Fast local loop\n\nThe full gate above is what CI enforces, but you do not need it for every\nedit. `crates/tui` is a ~750k-line crate, so the loop that stays fast is\nthe one that avoids rebuilding it more than necessary (numbers and the\nreasoning are in [`docs/BUILD_PERFORMANCE.md`](docs/BUILD_PERFORMANCE.md)):\n\n```bash\n# 1. Type-check first (seconds after the first build; no codegen, no link).\nscripts/dev-cargo.sh check -p codewhale-tui\n\n# 2. Run only the tests near your change (one crate, one filter).\nscripts/dev-test.sh tui fleet_setup\n# or: scripts/dev-test.sh crates/command-contract/src/elapsed.rs\n\n# 3. Run a whole crate's unit suite. scripts/dev-test.sh uses nextest when\n# it is installed (one process per test, all cores busy, slow tests\n# named; ~100 s here vs ~270 s with libtest).\ncargo install cargo-nextest --locked # once\nscripts/dev-test.sh tui\nscripts/dev-cargo.sh nextest run --workspace --all-features --locked\n\n# 4. The Linux suite your PR runs:\ncargo test --workspace --all-features --locked\n```\n\n`.config/nextest.toml` already serializes the PTY suite and bounds the\nintegration tests that spawn the real binary, so `cargo nextest run` is\nsafe to use on the whole workspace (nextest does not run doctests; the\nauthoritative `cargo test` gate does). Tests must not depend on running in\nthe same process as another test (nextest gives every test its own\nprocess); if a test needs the rustls crypto provider, install it in that\ntest as production does at startup.\n\nOn a machine with less than 16 GB of RAM (or when cross-compiling, e.g.\nfor OHOS), build one rustc at a time: `CARGO_BUILD_JOBS=1` (or `-j1`), one\ncrate at a time, `--lib` for tests, never `--workspace`/`--all-targets`.\nThe tui library needs ~6 GB for its own rustc and its unit-test build ~8 GB;\n`cargo test --workspace` runs both at once. Numbers and the full recipe:\n[`docs/BUILD_PERFORMANCE.md`](docs/BUILD_PERFORMANCE.md#low-memory-build-recipe-machines-with--16-gb-cross-builds).\n\nIf you work in several worktrees, do **not** share one `CARGO_TARGET_DIR`\nby default: two cargos on the same target flock and serialize. Use\n`scripts/dev-cargo.sh` / `scripts/dev-test.sh`, which give each workspace\nits own Cargo `build-dir` (`{workspace-path-hash}` under\n`${CODEWHALE_CACHE_ROOT:-${XDG_CACHE_HOME:-$HOME/.cache}/codewhale}`).\n`CODEWHALE_DEV_CACHE=local` keeps `./target` if you want that.\n`sccache` wraps rustc only when incremental compilation is already off\n(`CARGO_INCREMENTAL=0` or `CODEWHALE_SCCACHE=1`) and `sccache` is on\n`PATH`; a missing binary is a printed fallback, not an error. Override\nthe cache root with `CODEWHALE_CACHE_ROOT` — there is no machine-specific\ndefault. A single shared `CARGO_TARGET_DIR` remains valid only for\nserialized trunk work. See\n[`docs/BUILD_PERFORMANCE.md`](docs/BUILD_PERFORMANCE.md).\n\nSome checks are platform-bound or intentionally excluded from an ordinary\nchange. Choose them for the risk they answer rather than treating every\navailable suite as ritual. Visible TUI behavior is accepted in the actual\nterminal at the sizes and interaction path affected by the change; the former\nfull-screen PTY assertion suite was removed because it froze layout and copy\nwhile missing product quality.\n\n- **Long-running process acceptance** should use a sealed local home, local\n fixtures, and the real binary. Record the terminal size, inputs, visible\n result, and any filesystem side effect instead of adding a full-screen\n golden.\n- **OCR** (`image_ocr`) uses the macOS Vision framework or a locally\n installed `tesseract`; its platform-specific paths are\n `cfg(target_os = \"macos\")`-gated and depend on host tooling.\n- **Seatbelt sandbox** tests are macOS-only (`cfg(target_os =\n \"macos\")` at the module level) and do not run elsewhere.\n\n#### Local git hooks are optional\n\nThis repository does not install git hooks, and no hook installer\nexists; CI is the enforced gate. If you want a local `pre-push` hook\nthat runs the commands above, add it yourself (`.git/hooks/pre-push` or\n`git config core.hooksPath`). Constraints for any local hook:\n\n- A hook must never push, tag, publish, deploy, mutate credentials, or\n rewrite the working tree (no auto-fix commits or silent file\n modification). It may only verify and report.\n- To bypass your own hook for a knowingly documented reason (for\n example, pushing work-in-progress to your own fork branch), use\n `git push --no-verify` and say so in the PR description. Bypassing a\n local hook does not make the gates pass — CI still runs them, and a\n bypassed gate must never be reported as a passing one.\n- Release publication (tags, GitHub Releases, crates/npm artifacts) is a\n separate, owner-approved gate. Neither local hooks nor a green local\n run authorize any publication step.\n\n### Commit Messages\n\nUse clear, descriptive commit messages following conventional commits:\n\n- `feat:` New feature\n- `fix:` Bug fix\n- `docs:` Documentation changes\n- `refactor:` Code refactoring\n- `test:` Adding or updating tests\n- `chore:` Maintenance tasks\n\nExample: `feat: add doctor subcommand for system diagnostics`\n\n**Changelog entries are written on `main` at merge time, not in PRs.** Do not\nedit `CHANGELOG.md` or `crates/tui/CHANGELOG.md` on a branch: every PR\ntouching them re-conflicts with every other PR touching them. The release\nmanager writes one batched \"receipts\" commit per merge session, and\n`./scripts/sync-changelog.sh` keeps the packaged slice in sync. A PR that\ncarries changelog hunks will be asked to strip them\n(`git checkout origin/main -- CHANGELOG.md crates/tui/CHANGELOG.md`).\n\nOne exception is enforced by CI: a `feat:` commit whose message mentions an\nissue (`#N`) must add `#N` to `CHANGELOG.md` in the same PR\n(`scripts/release/check-feature-release-notes.sh`). To avoid touching the\nchangelog, put issue numbers in the PR description instead of in `feat:`\ncommit messages; the maintainer writes the entry at merge time.\n\n**AI-assistant co-author trailers are fine.** Using an assistant is welcome and\nneeds no disclosure, and CI no longer rejects an auto-appended\n`Co-authored-by: <some tool>` line. What we do care about is that the humans who\ndid the work are named — `Co-authored-by` feeds the GitHub contribution graph.\nRemove an auto-appended line only if you want to:\n\n```bash\ngit rebase -i origin/main # reword each commit, delete the Co-authored-by line\n```\n\nCo-author a *person* freely; the address must be their GitHub-linked one\n(`[email protected]`) or the credit does not register.\n\nWhen a commit harvests code from a community PR (see \"How Your Contribution\nLands\" below), include a `Harvested from PR #N by @author` line in the commit\nbody. An auto-close workflow watches for this pattern and closes the\nreferenced PR with credit so the contributor gets a clear signal that\ntheir work shipped.\n\n## What happens after you open a pull request\n\nOpening a pull request against `main` starts the checks below. They run on\nGitHub's hosted runners with no repository secrets. GitHub may ask a\nmaintainer to approve the first run for a brand-new GitHub account.\n\n| Check | What it runs | When it is red |\n| --- | --- | --- |\n| `Lint` (required) | `cargo fmt --check`, clippy on every target, and the fast drift checks (locales, READMEs, provider registry, dead-code and blocking-call budgets) | Run `cargo fmt --all` and the clippy command in [Pre-push verification](#pre-push-verification); the log names the file and the check |\n| `Test (ubuntu-latest)` | The whole workspace suite on Linux with `cargo nextest`, plus doctests | Re-run the named test locally, for example `scripts/dev-test.sh tui <test name>` |\n| `Test (windows-latest)` (required) | Compiles every workspace target for Windows with warnings denied, and the installer PATH helper tests | Usually a `cfg(windows)` path or an import only Windows sees; the log has the compiler error |\n| `npm wrapper smoke (ubuntu-latest)` (required) | Builds `codewhale` and installs it through the npm wrapper | The log shows the failing install or entrypoint step |\n| `Safety gate` | The sandbox, authorization and auto-review tests | Treat it as a real regression in a trust boundary |\n| `Version drift` | Version files, the `feat:` release-note rule, release helper tests and contributor credit | The credit step only reports: a maintainer adds your credit when the PR merges |\n| `PR closes an issue` | Your description has `Closes #N`, `Refs #N` or `No-Issue: <reason>` | Edit the description, then re-run the check |\n| `DCO` | Lists commits without `Signed-off-by` | Advisory; it never blocks |\n| `Contribution gate` | Labels PRs from authors not yet in `.github/APPROVED_CONTRIBUTORS` | A label, not a judgement; see [Contribution Gate](#contribution-gate) |\n\nSmaller checks cover MSRV, the VS Code extension, integrations, the extension\nhost and plugin conversion, plus workflow lint when you change\n`.github/workflows`. A documentation-only change skips the Rust builds but\nstill reports the required checks, so it can merge. The Linux test suite took\n45 minutes on a recent pull request; the other checks run alongside it.\n\nYour pull request does not run the macOS and Windows test suites, the slow\nruntime-contract and persistence-backlog measurements, the dependency audits\nor the OpenHarmony build. Maintainers run those once per release, on the exact\ncommit being released. If that run finds a problem your change introduced, we\nfix it on `main`; we will not ask you to chase it.\n\n## How Your Contribution Lands\n\n**Your pull request merges as itself.** That is the default for every\ncontribution: a maintainer, or an agent working for one, reviews it and merges\nit, so your commits reach `main` with you as their author.\n\nBefore merging, a maintainer may:\n\n- ask questions or request changes in review;\n- push small fixes, or a merge of current `main`, to your branch when GitHub\n allows edits by maintainers, instead of asking you to rebase around our churn;\n- write the `CHANGELOG.md` entry and your credit in `docs/CONTRIBUTORS.md` on\n `main` when the PR merges. You do not edit those files in your PR.\n\n### Fallback — harvest\n\nRarely, a branch cannot merge in reasonable time, for example when it is in a\nfork we cannot push to and the conflicts cannot be resolved there. Then a\nmaintainer may **harvest** the useful commits into `main` instead. This is\n**not a rejection**: your code landed, and your credit is kept mechanically.\n\n- You stay the author of the harvested commit, with your GitHub-linked address\n from `.github/AUTHOR_MAP` or\n `gh api users/<login> --jq '\"\\(.id)+\\(.login)@users.noreply.github.com\"'`,\n never a raw, `.local` or old-style noreply email from your machine.\n- The commit body includes `Harvested from PR #N by @your-handle`. That line is\n the contract: it is your credit and the signal that your work shipped.\n- The `CHANGELOG.md` entry for the next release credits you by handle.\n- `auto-close-harvested.yml` closes your PR with a thank-you and a link to the\n commit on `main`.\n\nWhen a maintainer closes a harvested PR by hand, the closing comment\nfollows this template (the pattern set on PR #2634):\n\n```text\nClosing with harvest credit, @handle — <what landed> landed via\n<commit sha(s) or PR #N>. <If work remains:> The remainder is tracked\nin #NNN — follow-ups welcome there.\nThank you for <one specific thing the contribution got right>.\n```\n\nThree required elements: the contributor's handle, the exact commits or\nPRs where their work landed, and — when the PR contained more than what\nlanded — a tracking issue for the remainder. A harvested PR is never\nclosed with a bare \"superseded\".\n\nTo help your PR merge quickly as itself:\n\n1. **Keep PRs single-purpose.** One bug fix per PR; one feature per PR.\n Don't mix a refactor with a feature.\n2. **Start from current `main`.** If `main` moves while your PR is in review,\n you do not need to rebase; we resolve conflicts our changes caused.\n3. **Include tests** with new behavior. A PR with tests needs fewer review\n round trips.\n4. **Avoid the trust-boundary surface** without prior maintainer\n sign-off. That includes auth/credential flows, sandbox policy,\n publishing/release plumbing, and `prompts/` content. PRs that touch\n these without prior discussion are unlikely to merge directly even\n when the change is well-implemented.\n\n## Layered and EPIC-Sized Work\n\nSome architecture work is too large for one PR but still needs to be built in\ndependent layers. For those changes, use this workflow:\n\n1. Start with a tracking issue or EPIC when the work spans multiple PRs. Name\n the intended slices and state what each slice is not trying to close yet.\n2. Keep each implementation PR focused on one behavior boundary.\n3. Later layers may stay in your fork or open as draft PRs while the lower\n layer is still moving. Draft stacked PR titles or descriptions should say\n `Draft / depends on #NNNN`.\n4. A dependent PR is not ready for merge review until the lower layer has\n landed, the branch has been rebased onto current `main`, and the PR targets\n `main`.\n5. The PR body should identify which earlier PR it builds on, what is in scope,\n what is explicitly out of scope, which issues it references, and which local\n commands were run.\n6. Use `Closes #...` only when the slice fully satisfies an issue. Use\n `Refs #...` with a short `(partial)` note when the PR advances a broad issue\n but leaves follow-up work.\n7. Structured commits are fine during review. Maintainers merge the PR itself\n (merge commit, squash or rebase) with you as the author. If a layer has to\n be harvested instead, the [fallback](#fallback--harvest) rules apply.\n\nBefore asking for merge review on a layered PR, check that it is:\n\n- rebased onto current `main`\n- marked ready for review, not draft\n- focused to one behavior boundary\n- backed by local command evidence in the PR body\n- green in its pull-request checks, or has any remaining red check clearly\n explained\n- covered by round-trip or migration-preservation tests when it changes config\n or schema behavior\n- referencing broad issues as partial unless it really closes them\n\nFor layered work, a useful PR description shape is:\n\n```text\nSummary:\nScope:\nNot in this slice:\nBuilds on:\nIssues:\nValidation:\n```\n\n## Which branch to target\n\n**`main`, for everything.** There is no separate staging branch. An earlier\nversion of this guide pointed layered refactors at `codex/v0.9.0-stewardship`;\nthat branch no longer exists, so please ignore any instruction you find\nelsewhere to base work on it.\n\n**We do not expect you to rebase around our churn.** If your PR conflicts only\nbecause `main` moved while it was in review, say so and a maintainer resolves\nit on your branch. If your branch is in a fork we cannot push to, we ask on the\nPR first; harvesting is the last resort.\n\n## Labels\n\nIssues and pull requests are labelled by `.github/workflows/triage.yml`, which\nonly ever adds labels that already exist. Pull requests get their labels from\nthe paths they change and their conventional-commit title prefix; issues get\nthem from the title and form answers.\n\n| Family | Meaning |\n| --- | --- |\n| `area:*` | The subsystem or client: `tui`, `core`, `tools`, `mcp`, `state`, `sandbox`, `plugins`, `providers`, `runtime-api`, `install`, `localization`, `ci`, `desktop`, `web`, `mobile`, `remote-control`, `subagents` |\n| `os:*` | Platform-specific behaviour: `windows`, `macos`, `linux`, `freebsd`, `ohos` |\n| `priority:p0` / `p1` / `p2` | Set by a maintainer; `release-blocker` still marks work that must land before a release |\n| `bug`, `enhancement`, `documentation`, `question`, `cleanup`, `performance`, `security` | What kind of change it is |\n| `accessibility` | Screen readers, keyboard use, contrast, motion |\n| `contributor:external` | Opened by someone outside the maintainer team |\n| `lang:zh` | Written in Chinese |\n| `needs-triage`, `needs-info`, `agent-ready`, `agent-in-progress`, `needs-human`, `keep-open`, `pinned`, `stale` | Workflow state; other workflows read these names |\n\nRelease targeting uses milestones, not version labels.\n\n## Contribution Gate\n\nCodewhale uses a maintainer-managed contribution gate for the community front\ndoor. Maintainers and collaborators bypass this gate automatically. The gate\nworkflows run in dry-run mode and only add labels, so maintainers can observe\nthe signal before changing contributor flow.\n\nThe maintainer posture is documented in\n[docs/AGENT_ETHOS.md](docs/AGENT_ETHOS.md): automation should reduce load while\nkeeping good-faith contributors seen, credited, and able to keep helping.\n\nIssues are never auto-closed by the contribution gate. Unapproved external\nissues receive a triage label and remain open for maintainers. Codewhale\ndepends on real edge cases from real users, so issue intake should stay warm\nand open.\n\nPull requests are different because they can touch code, CI, release plumbing,\nauth, sandboxing, provider policy, and other trust-boundary surfaces. The PR\ngate can be switched from dry-run to enforcement when maintainers decide they\nneed that safety control, but it should be treated as a review-load control,\nnot a judgment on contributor quality. Before enabling PR enforcement, seed the\nallowlist broadly enough for active external contributors who should not be\ninterrupted by the rollout.\n\nThe allowlist is scoped:\n\n- `pr:username` allows pull requests.\n- `issue:username` allows issues.\n- `all:username` allows both.\n\nA maintainer can approve someone by commenting `/lgtm` on a pull request for PR\naccess, or `/lgtmi` on an issue for issue access. The exact bare commands\n`lgtm` and `lgtmi` are also accepted for compatibility, but the prefixed forms\nare preferred because they are harder to trigger accidentally in ordinary review\ndiscussion.\n\nApprovals do not edit `main` directly. The approval workflow opens a small\nallowlist update PR so the new entry is reviewable before it takes effect.\n\nIf the PR gate fires on a good contributor incorrectly, use the same approval\nflow to restore them: comment `/lgtm`, merge the generated allowlist PR, then\nreopen the affected pull request. If GitHub will not allow the closed PR to be\nreopened, ask the contributor to resubmit after the allowlist PR is merged.\n\n## Agent-Assisted Improvements\n\nCodewhale is allowed to help improve Codewhale, but the contribution still has\nto be shaped for human review. The recommended workflow is the recursive self-improvement prompt\nin the private `codewhale-ops` repo: run it\nfrom a fresh fork or branch, let the agent find exactly one small friction point,\nand stop after one patch. DeepSeek V4 Pro is the reference path for this loop\ntoday, but any configured provider works — the review shape matters more than\nthe provider.\n\nAgents and maintainers should follow the stewardship posture in\n[docs/AGENT_ETHOS.md](docs/AGENT_ETHOS.md): use automation for evidence,\nverification, and narrow patches while keeping the final community decision\nhuman-reviewed.\n\nThe useful output is not \"ideas for improvement.\" The useful output is a\nspecific reproduction, a minimal diff, focused checks, and a PR description that\nexplains the trade-off. Do not use an agent to touch auth, credentials, sandbox\npolicy, publishing/release plumbing, provider policy, telemetry, sponsorship,\nbranding, or global prompts without prior maintainer sign-off.\n\n## How maintainers and agents ship\n\nMaintainers, and the agents working for them, do not open pull requests. They\nland verified work directly on `main` with the local checks they ran and their\ncounts in the commit message. A push to `main` starts no GitHub build or test\nworkflow: GitHub only mirrors `main` to CNB and GitCode and closes harvested\nPRs. CNB then runs its own Linux gates on the mirror and reports them on CNB,\nnot on GitHub. Do not write `[skip ci]`; nothing needs skipping, and it would\nalso skip the mirrors and the harvest auto-close.\n\nThe exhaustive gate runs once, at the final release step, from one dispatch on\nthe frozen candidate commit:\n\n```bash\ngit fetch origin main\ncandidate_sha=\"$(git rev-parse origin/main)\"\ngh workflow run ci.yml --repo codewhale-hq/CodeWhale --ref main \\\n -f expected_sha=\"${candidate_sha}\"\n```\n\nIt stops if `main` no longer resolves to that commit. Otherwise it runs every\nCI gate on Linux, macOS and Windows, the budget measurements, the\ndocumentation build, `cargo-deny`, the cargo and npm audits and the\nOpenHarmony build. Publishing also needs a green `release-candidate.yml` run\non the same commit, which `release.yml` checks; the full sequence is in\n[docs/RELEASE_RUNBOOK.md](docs/RELEASE_RUNBOOK.md).\n\nContributor pull requests still merge as pull requests, as described in\n[How Your Contribution Lands](#how-your-contribution-lands).\n\n## Project Structure\n\nCodewhale is a Cargo workspace with one Engine implementation in\n`crates/engine/src/core/engine/`. The public `codewhale` executable links the\nTUI/runtime library; interactive sessions, noninteractive runs and the Runtime\nAPI share that Engine.\n\n| Path | Purpose |\n| --- | --- |\n| `crates/cli/` | Public command entrypoint, configuration commands and runtime dispatch |\n| `crates/tui/` | Interactive terminal, Engine, tools, Runtime API and embedded local web client |\n| `crates/core/`, `crates/protocol/`, `crates/state/` | Request construction, session/turn types, protocol framing and persistence |\n| Other `crates/` | Shared configuration, credentials, telemetry, hooks, workflow and packaging support; see each Cargo manifest |\n| `web/` | Public Next.js website and documentation; separate from the embedded Runtime web client |\n| `telemetry-ingest/` | Telemetry service, schemas and service tests |\n| `extensions/`, `integrations/` | Editor integration and external-service bridges |\n| `npm/`, `packaging/`, `nix/` | npm wrappers/SDK and platform installation definitions |\n| `computer/snapshots/` | Cloud Computer image definitions, pinned independently of the source checkout |\n| `deploy/` | Deployment templates consumed by setup scripts, including Tencent Lighthouse services |\n| `fleets/`, `workflows/` | Distributed Fleet definitions and workflow examples |\n| `brand/` | Source artwork and generated brand variants used by the README, website and terminal |\n| `docs/` | User/developer documentation, schemas, fixtures and referenced release material |\n| `scripts/`, `.github/`, `.cnb.yml` | Development, validation, CI and release tooling |\n| `patches/` | Vendored dependency fixes, including their licensing files |\n\nGenerated files that the product embeds or validates, such as model catalogs,\nwebsite facts and schemas, remain tracked with their generators. Platform\nmirrors such as `.winget/` are retained when their packaging tools require them.\nKeep local critique output, temporary verification reports and personal\noperator instructions outside the tracked product tree; describe the change\nand its validation in the pull request. Do not copy workspace-level operator\n`AGENTS.md` or `CLAUDE.md` files into this repository.\n\nSee [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) for the runtime data flow and\n[the build guide](docs/BUILD_PERFORMANCE.md) for crate dependencies and local\nverification.\n\n## Submitting Changes\n\n1. Create a feature branch from `main`:\n ```bash\n git checkout -b feat/your-feature\n ```\n\n2. Make your changes and commit them\n\n3. Run the pre-push verification commands (see\n [Pre-push verification](#pre-push-verification) above for the exact\n gate and the stricter release clippy form)\n\n4. Push your branch and create a Pull Request\n\n5. Describe your changes clearly in the PR description\n\n## Pull Request Guidelines\n\n- Use the [pull request template](.github/PULL_REQUEST_TEMPLATE.md) when opening\n a PR — what and why, the issue line, and how you tested it\n- The PR description needs one issue line, checked by CI\n (`.github/workflows/pr-issue-link.yml`): `Closes #N` (or `Fixes` /\n `Resolves`) when the PR finishes the issue, `Refs #N` for related or partial\n work, or `No-Issue: <one-line reason>`. Never write a negated closing\n keyword such as \"does not close #N\": GitHub closes the issue anyway, so CI\n rejects it\n- If you add a new layer, module, or abstraction, say which one it replaces\n or deletes\n- Keep PRs focused on a single change\n- Update documentation if needed\n- Add tests for new functionality\n- Get the pull-request checks green before requesting review, or say in the\n PR why one is red\n\n## Shape of a Typical PR\n\nA well-structured PR follows a consistent pattern. Recent exemplars include:\n\n- **#386** — `/init` command: new `crates/tui/src/commands/groups/project/init.rs` module, project-type detection,\n AGENTS.md generation, command registration in `commands/mod.rs`, localization strings.\n- **#389** — Inline LSP diagnostics: LSP subsystem in `crates/lsp/src/`, engine hooks in\n `crates/engine/src/core/engine/lsp_hooks.rs`, config toggle, test coverage.\n- **#387** — Self-update: new `crates/cli/src/update.rs` module, CLI subcommand registration,\n HTTP download + SHA256 verification + atomic binary replacement.\n- **#393** — `/share` session URL: new `crates/tui/src/commands/groups/project/share.rs`, HTML rendering,\n `gh gist create` integration, command registration.\n- **#343/#346** — (v0.8.5) Runtime thread/turn timeline and durable task manager refactors.\n\nTypically each PR touches 1–3 new files, modifies 2–5 existing files for wiring\n(registries, dispatch matches, localization), and adds or updates tests. Changes\nare scoped to a single feature or fix — if you discover related work that needs\ndoing, open a separate issue rather than expanding the PR scope.\n\nBefore submitting, run the commands in\n[Pre-push verification](#pre-push-verification).\n\n## Reporting Issues\n\nWhen reporting issues, please use one of the issue templates:\n\n- [Bug report](.github/ISSUE_TEMPLATE/bug_report.yml) — for reproducible problems\n or regressions\n- [Feature request](.github/ISSUE_TEMPLATE/feature_request.yml) — for ideas and\n improvements\n\nThe forms ask for what a report needs (`codewhale --version`, OS, how you got\nCodewhale, and steps to reproduce). Questions go to\n[Discussions](https://github.com/codewhale-hq/CodeWhale/discussions) or\n[Discord](https://discord.gg/37gfS3ksug).\n\n## Security\n\nIf you discover a security vulnerability, please do **not** open a public issue.\nSee [SECURITY.md](.github/SECURITY.md) for the responsible disclosure process and\ncontact information.\n\n## Code of Conduct\n\nBe respectful and inclusive. We welcome contributors of all backgrounds and\nexperience levels. See [CODE_OF_CONDUCT.md](.github/CODE_OF_CONDUCT.md) for the full\ncode of conduct.\n\n## License\n\nBy contributing to codewhale, you agree that your contributions will be licensed under the MIT License.\n\n## Questions?\n\nFeel free to open an issue for any questions about contributing.\n"
|
||
},
|
||
{
|
||
"path": "docs/AGENT_ETHOS.md",
|
||
"sha256": "d9cd36fc7b6d9d236c68e62ffc92f41d3f3fab6e2e5eb9bf2bb2ae1d53f82331",
|
||
"content": "# Agent Ethos\n\n> 阅读简体中文版:[zh_hans/AGENT_ETHOS.md](zh_hans/AGENT_ETHOS.md)。\n\nCodewhale is maintained with agents, but it is not maintained by automation\nalone. Treat community reports and patches as real collaboration: people are\nbringing us machines, providers, regions, shells, packages, and edge cases we\ncould not cover by ourselves.\n\n## Stewardship\n\n- Verify live truth before acting. Check the current branch, release state,\n registry state, CI, and linked issues instead of trusting a handoff.\n- Issues are intake, not a privilege boundary. Do not auto-close good-faith\n issues because the reporter is not allowlisted. Ask for missing reproduction\n detail and leave room for maintainer triage.\n- PR gates exist for code review, CI load, and trust-boundary safety. They are\n not a quality judgment on the contributor. Keep dry-run mode unless a\n maintainer deliberately enables enforcement, and use warm copy when the gate\n comments.\n- Be generous with recurring contributors. When someone repeatedly brings\n useful reports or patches, use `/lgtmi` for issue access or `/lgtm` for PR\n access so the automation gets out of their way.\n- Preserve contributor credit. When harvesting work, inspect the PR and linked\n issues, keep author/co-author attribution where possible, add\n `Harvested from PR #N by @handle`, and credit the contributor in the\n changelog or release notes.\n- Make credit machine-readable. If a harvested commit cannot preserve the\n contributor as the author, add a `Co-authored-by` trailer with the GitHub\n numeric noreply address from `.github/AUTHOR_MAP` or\n `gh api users/<login> --jq '\"\\(.id)+\\(.login)@users.noreply.github.com\"'`.\n Do not use `.local`, placeholder, bot/tool, or raw third-party emails for\n human contributor credit.\n- Deferral is a maintainer action, not a dismissal. If a PR or issue is not\n ready, say what is blocked, what evidence would change the decision, and\n which part of the work remains valuable.\n\n## Agent Workflow\n\n- Use sub-agents for exploration, review, and verification, but keep a human\n maintainer posture in the parent session. Sub-agent output is evidence; the\n parent is responsible for the final decision.\n- Personally review community PRs before merging, harvesting, closing, or\n deferring them. Do not close work based only on title, labels, or an agent's\n summary.\n- Prefer narrow, reversible changes that match the existing codebase. Avoid\n drive-by refactors while harvesting community work.\n- Run the smallest meaningful validation first, then broaden tests when a\n change touches shared behavior, release plumbing, auth, sandboxing,\n providers, or UI workflows.\n- Do not tag, publish, push release artifacts, or create GitHub releases\n without explicit maintainer approval.\n\n## Product Tone\n\nCodewhale should feel like a capable coding harness with a public community,\nnot a closed queue. Automation should reduce maintainer load while making\ncontributors feel seen, credited, and able to keep helping.\n"
|
||
},
|
||
{
|
||
"path": "docs/CONTRIBUTORS.md",
|
||
"sha256": "496f63dcef2dcf8ff98b663fed50737411368cb065154d046f957d25f9a78bb5",
|
||
"content": "# Contributors\n\nCodewhale is built in the open with a growing community of contributors. Every\nissue report and pull request is real project work — welcome at any experience\nlevel. This is the full per-PR contributor record, grouped into release/time\nbands with the newest band first so it stays scannable. Expand any band to see\neveryone.\n\nFor the live list, see the\n[GitHub contributors page](https://github.com/codewhale-hq/CodeWhale/graphs/contributors),\n[`AUTHOR_MAP`](https://github.com/codewhale-hq/CodeWhale/blob/main/.github/AUTHOR_MAP),\nand [CHANGELOG.md](../CHANGELOG.md).\n\n<!-- contributor-wall:start -->\n## Everyone\n\n[](#everyone)\n\n421 people have code, co-authored commits, carried-forward pull requests or credited reports in Codewhale. GitHub's contributor list shows only commit authors whose email is linked to their account, so it counts 203 of them. If your commits are missing there, add your commit email to your GitHub account and they will link.\n\n[0gl20shk0sbt36](https://github.com/0gl20shk0sbt36) · [1Git2Clone](https://github.com/1Git2Clone) · [20bytes](https://github.com/20bytes) · [47Cid](https://github.com/47Cid) · [7jrxt42BxFZo4iAnN4CX](https://github.com/7jrxt42BxFZo4iAnN4CX) · [8sero](https://github.com/8sero) · [A-Corner](https://github.com/A-Corner) · [aboimpinto](https://github.com/aboimpinto) · [adity982](https://github.com/adity982) · [AdityaVG13](https://github.com/AdityaVG13) · [AGSaturn](https://github.com/AGSaturn) · [ai-dengwsh](https://github.com/ai-dengwsh) · [aiapienthusiast](https://github.com/aiapienthusiast) · [Aitensa](https://github.com/Aitensa) · [AiurArtanis](https://github.com/AiurArtanis) · [alitvak69](https://github.com/alitvak69) · [all-lopezg](https://github.com/all-lopezg) · [alvin1](https://github.com/alvin1) · [amuthantamil](https://github.com/amuthantamil) · [AnaheimEX](https://github.com/AnaheimEX) · [Andrea-Bruno](https://github.com/Andrea-Bruno) · [Angel-Hair](https://github.com/Angel-Hair) · [angus-guo](https://github.com/angus-guo) · [angziii](https://github.com/angziii) · [AnonymousUser443](https://github.com/AnonymousUser443) · [antarikshraya](https://github.com/antarikshraya) · [Anyexyz](https://github.com/Anyexyz) · [Apeiron0w0](https://github.com/Apeiron0w0) · [aqilaziz](https://github.com/aqilaziz) · [AresNing](https://github.com/AresNing) · [ArronAI007](https://github.com/ArronAI007) · [Artenx](https://github.com/Artenx) · [asdfg314284230](https://github.com/asdfg314284230) · [Asteria1028](https://github.com/Asteria1028) · [asto18089](https://github.com/asto18089) · [atmosuwiryo](https://github.com/atmosuwiryo) · [axobase001](https://github.com/axobase001) · [aznikline](https://github.com/aznikline) · [baendlorel](https://github.com/baendlorel) · [banqii](https://github.com/banqii) · [Beltran12138](https://github.com/Beltran12138) · [bevis-wong](https://github.com/bevis-wong) · [BH8GCJ](https://github.com/BH8GCJ) · [BigBenLabs](https://github.com/BigBenLabs) · [Biilow](https://github.com/Biilow) · [bistack](https://github.com/bistack) · [BrathonBai](https://github.com/BrathonBai) · [bruce6135](https://github.com/bruce6135) · [BryonGo](https://github.com/BryonGo) · [buiducnhat](https://github.com/buiducnhat) · [buko](https://github.com/buko) · [BX166](https://github.com/BX166) · [c020627](https://github.com/c020627) · [cacdcaecawae](https://github.com/cacdcaecawae) · [caeserchen](https://github.com/caeserchen) · [caiyilian](https://github.com/caiyilian) · [CCChisato](https://github.com/CCChisato) · [ccomma](https://github.com/ccomma) · [cenab](https://github.com/cenab) · [ChaceLyee2101](https://github.com/ChaceLyee2101) · [chatman-media](https://github.com/chatman-media) · [chennest](https://github.com/chennest) · [chinaqy110](https://github.com/chinaqy110) · [chnjames](https://github.com/chnjames) · [chuntseevolving](https://github.com/chuntseevolving) · [ci4ic4](https://github.com/ci4ic4) · [cian-lien](https://github.com/cian-lien) · [cmdcorp6534](https://github.com/cmdcorp6534) · [cmyyy](https://github.com/cmyyy) · [codepgq](https://github.com/codepgq) · [Colorful-glassblock](https://github.com/Colorful-glassblock) · [ComeFromTheMars](https://github.com/ComeFromTheMars) · [CrepuscularIRIS](https://github.com/CrepuscularIRIS) · [ct-jaryn](https://github.com/ct-jaryn) · [ctxyao](https://github.com/ctxyao) · [cy2311](https://github.com/cy2311) · [cyq1017](https://github.com/cyq1017) · [czf0718](https://github.com/czf0718) · [dajiaohuang](https://github.com/dajiaohuang) · [DarrellThomas](https://github.com/DarrellThomas) · [demian-welt](https://github.com/demian-welt) · [Dev-next-gen](https://github.com/Dev-next-gen) · [dfwqdyl-ui](https://github.com/dfwqdyl-ui) · [DI-HUO-MING-YI](https://github.com/DI-HUO-MING-YI) · [DieMoe233](https://github.com/DieMoe233) · [DingYong4223](https://github.com/DingYong4223) · [djairjr](https://github.com/djairjr) · [dmitri-0](https://github.com/dmitri-0) · [dmt4](https://github.com/dmt4) · [donglovejava](https://github.com/donglovejava) · [douglarek](https://github.com/douglarek) · [Dr3259](https://github.com/Dr3259) · [DracheTek](https://github.com/DracheTek) · [drpars](https://github.com/drpars) · [dst1213](https://github.com/dst1213) · [duanchao-lab](https://github.com/duanchao-lab) · [Duducoco](https://github.com/Duducoco) · [dumbjack](https://github.com/dumbjack) · [dxfq](https://github.com/dxfq) · [dzyuan](https://github.com/dzyuan) · [e792a8](https://github.com/e792a8) · [Edouard-Legoupil](https://github.com/Edouard-Legoupil) · [elowen53](https://github.com/elowen53) · [eltociear](https://github.com/eltociear) · [EmiyaKiritsugu3](https://github.com/EmiyaKiritsugu3) · [encyc](https://github.com/encyc) · [eng2007](https://github.com/eng2007) · [eugenicum](https://github.com/eugenicum) · [EvanProgramming](https://github.com/EvanProgramming) · [F1LT3R](https://github.com/F1LT3R) · [Final527](https://github.com/Final527) · [findshan](https://github.com/findshan) · [Fire-dtx](https://github.com/Fire-dtx) · [fleitz](https://github.com/fleitz) · [formp3](https://github.com/formp3) · [freedomlovesfrank](https://github.com/freedomlovesfrank) · [fuleinist](https://github.com/fuleinist) · [Gabriel-Degret](https://github.com/Gabriel-Degret) · [gaord](https://github.com/gaord) · [gaslebinh-glitch](https://github.com/gaslebinh-glitch) · [Geallier](https://github.com/Geallier) · [gerryqi](https://github.com/gerryqi) · [giancarlocp](https://github.com/giancarlocp) · [Giggitycountless](https://github.com/Giggitycountless) · [giovanni-paolilla](https://github.com/giovanni-paolilla) · [GK012](https://github.com/GK012) · [goodvecn-dev](https://github.com/goodvecn-dev) · [googio](https://github.com/googio) · [goransh-walia](https://github.com/goransh-walia) · [gordonlu](https://github.com/gordonlu) · [greyfreedom](https://github.com/greyfreedom) · [GTC2080](https://github.com/GTC2080) · [Guan0923](https://github.com/Guan0923) · [h3c-hexin](https://github.com/h3c-hexin) · [hardy922](https://github.com/hardy922) · [harryvgiunta](https://github.com/harryvgiunta) · [harryvizcaino-oss](https://github.com/harryvizcaino-oss) · [harvey2011888](https://github.com/harvey2011888) · [hatakes](https://github.com/hatakes) · [hejia-v](https://github.com/hejia-v) · [heloanc](https://github.com/heloanc) · [henrikdukefoss](https://github.com/henrikdukefoss) · [hhhaiai](https://github.com/hhhaiai) · [hmr-BH](https://github.com/hmr-BH) · [hodeswildsmith455-boop](https://github.com/hodeswildsmith455-boop) · [hongchen1993](https://github.com/hongchen1993) · [hongqitai](https://github.com/hongqitai) · [hsdbeebou](https://github.com/hsdbeebou) · [huangxianzhan](https://github.com/huangxianzhan) · [hufanexplore](https://github.com/hufanexplore) · [HUQIANTAO](https://github.com/HUQIANTAO) · [hxfhd](https://github.com/hxfhd) · [hxy91819](https://github.com/hxy91819) · [idling11](https://github.com/idling11) · [IIzzaya](https://github.com/IIzzaya) · [imkingjh999](https://github.com/imkingjh999) · [Implementist](https://github.com/Implementist) · [Inference1](https://github.com/Inference1) · [ivysrono](https://github.com/ivysrono) · [J3y0r](https://github.com/J3y0r) · [JafarAkhondali](https://github.com/JafarAkhondali) · [JasonOA888](https://github.com/JasonOA888) · [jayanthvee](https://github.com/jayanthvee) · [JayBeest](https://github.com/JayBeest) · [jazzi](https://github.com/jazzi) · [Jefsky](https://github.com/Jefsky) · [jeoor](https://github.com/jeoor) · [jghwwnq](https://github.com/jghwwnq) · [Jianfengwu2024](https://github.com/Jianfengwu2024) · [JiarenWang](https://github.com/JiarenWang) · [jieshu666](https://github.com/jieshu666) · [jimmyzhuu](https://github.com/jimmyzhuu) · [jinpengxuan](https://github.com/jinpengxuan) · [JoeKerF](https://github.com/JoeKerF) · [jrcjrcc](https://github.com/jrcjrcc) · [jretz](https://github.com/jretz) · [junskyeed](https://github.com/junskyeed) · [k0tran](https://github.com/k0tran) · [khalid-hungerstation](https://github.com/khalid-hungerstation) · [KhalidAlnujaidi](https://github.com/KhalidAlnujaidi) · [knqiufan](https://github.com/knqiufan) · [kobihikri](https://github.com/kobihikri) · [krisclarkdev](https://github.com/krisclarkdev) · [KUK4](https://github.com/KUK4) · [kunpeng-ai-lab](https://github.com/kunpeng-ai-lab) · [lalala-233](https://github.com/lalala-233) · [laoye2020](https://github.com/laoye2020) · [lawrencewzen](https://github.com/lawrencewzen) · [lbcheng888](https://github.com/lbcheng888) · [Lee-take](https://github.com/Lee-take) · [Lellansin](https://github.com/Lellansin) · [leo119](https://github.com/leo119) · [LeoAlex0](https://github.com/LeoAlex0) · [LeoCa](https://github.com/LeoCa) · [LeoLin990405](https://github.com/LeoLin990405) · [lerugray](https://github.com/lerugray) · [LHqweasd](https://github.com/LHqweasd) · [LI-Jialu](https://github.com/LI-Jialu) · [LIghtJUNction](https://github.com/LIghtJUNction) · [lihuan215](https://github.com/lihuan215) · [LING71671](https://github.com/LING71671) · [linletian](https://github.com/linletian) · [linzhiqin2003](https://github.com/linzhiqin2003) · [lioryx](https://github.com/lioryx) · [LittleBlacky](https://github.com/LittleBlacky) · [Liu-Vince](https://github.com/Liu-Vince) · [liuhq](https://github.com/liuhq) · [lixiasky-back](https://github.com/lixiasky-back) · [lloydzhou](https://github.com/lloydzhou) · [LmeSzinc](https://github.com/LmeSzinc) · [loongmiaow-pixel](https://github.com/loongmiaow-pixel) · [lordwedggie](https://github.com/lordwedggie) · [lpeng1711694086-lang](https://github.com/lpeng1711694086-lang) · [Lstarsky0](https://github.com/Lstarsky0) · [luestr](https://github.com/luestr) · [luismateusvargas](https://github.com/luismateusvargas) · [Lujc0523](https://github.com/Lujc0523) · [M-Maciej](https://github.com/M-Maciej) · [mac119](https://github.com/mac119) · [maker316](https://github.com/maker316) · [malsony](https://github.com/malsony) · [manaskarra](https://github.com/manaskarra) · [mangdehuang](https://github.com/mangdehuang) · [mars-base](https://github.com/mars-base) · [mdrkrg](https://github.com/mdrkrg) · [MeAiRobot](https://github.com/MeAiRobot) · [melody0709](https://github.com/melody0709) · [membphis](https://github.com/membphis) · [MengZ-super](https://github.com/MengZ-super) · [merchloubna70-dot](https://github.com/merchloubna70-dot) · [mky](https://github.com/mky) · [MMMarcinho](https://github.com/MMMarcinho) · [mo-vic](https://github.com/mo-vic) · [moduvoice](https://github.com/moduvoice) · [MoriTang](https://github.com/MoriTang) · [Mr-Moon121](https://github.com/Mr-Moon121) · [mrluanma](https://github.com/mrluanma) · [MuRongMoQing](https://github.com/MuRongMoQing) · [musichen](https://github.com/musichen) · [muyuliyan](https://github.com/muyuliyan) · [mvanhorn](https://github.com/mvanhorn) · [MXAntian](https://github.com/MXAntian) · [NASLXTO](https://github.com/NASLXTO) · [nasus9527](https://github.com/nasus9527) · [nbiish](https://github.com/nbiish) · [Nefelibata1024](https://github.com/Nefelibata1024) · [Neo-millunnium](https://github.com/Neo-millunnium) · [New2Niu](https://github.com/New2Niu) · [nightfallsad](https://github.com/nightfallsad) · [nightt5879](https://github.com/nightt5879) · [noaft](https://github.com/noaft) · [NorethSea](https://github.com/NorethSea) · [nsfoxer](https://github.com/nsfoxer) · [octo-patch](https://github.com/octo-patch) · [OctoBored](https://github.com/OctoBored) · [Oliver-ZPLiu](https://github.com/Oliver-ZPLiu) · [oooyuy92](https://github.com/oooyuy92) · [ousamabenyounes](https://github.com/ousamabenyounes) · [pengyou200902](https://github.com/pengyou200902) · [pingg02](https://github.com/pingg02) · [pizofreude](https://github.com/pizofreude) · [pkeging](https://github.com/pkeging) · [printf033](https://github.com/printf033) · [puneetdixit200](https://github.com/puneetdixit200) · [punkcanyang](https://github.com/punkcanyang) · [PurplePulse](https://github.com/PurplePulse) · [qamranmushtaq-collab](https://github.com/qamranmushtaq-collab) · [qinlinwang](https://github.com/qinlinwang) · [qinxianyuzou](https://github.com/qinxianyuzou) · [qiuYliangM](https://github.com/qiuYliangM) · [qiyan233](https://github.com/qiyan233) · [qiyuanlicn](https://github.com/qiyuanlicn) · [rafacouto](https://github.com/rafacouto) · [rafaelcavalheri](https://github.com/rafaelcavalheri) · [Ram9199](https://github.com/Ram9199) · [raphamorim](https://github.com/raphamorim) · [redjade75723](https://github.com/redjade75723) · [redstar](https://github.com/redstar) · [RefuseOdd](https://github.com/RefuseOdd) · [reidliu41](https://github.com/reidliu41) · [Rene-Kuhm](https://github.com/Rene-Kuhm) · [RepentStar](https://github.com/RepentStar) · [RinZ27](https://github.com/RinZ27) · [RobertEmprechtinger](https://github.com/RobertEmprechtinger) · [rockeverm3m](https://github.com/rockeverm3m) · [rockyzhang](https://github.com/rockyzhang) · [roian6](https://github.com/roian6) · [ronohara](https://github.com/ronohara) · [saieswar237](https://github.com/saieswar237) · [SakuraNeko](https://github.com/SakuraNeko) · [SamhandsomeLee](https://github.com/SamhandsomeLee) · [sandofree](https://github.com/sandofree) · [seanthefuturegorilla](https://github.com/seanthefuturegorilla) · [senka9h](https://github.com/senka9h) · [sequico](https://github.com/sequico) · [Serendo](https://github.com/Serendo) · [serephus](https://github.com/serephus) · [SergiuSSoluS](https://github.com/SergiuSSoluS) · [sfdzhmr](https://github.com/sfdzhmr) · [shenjackyuanjie](https://github.com/shenjackyuanjie) · [shentoumengxin](https://github.com/shentoumengxin) · [shenyongqing](https://github.com/shenyongqing) · [shi7ku9](https://github.com/shi7ku9) · [ShikiSuen](https://github.com/ShikiSuen) · [shuxiangxuebiancheng](https://github.com/shuxiangxuebiancheng) · [simuusang](https://github.com/simuusang) · [sky-sun-moon](https://github.com/sky-sun-moon) · [skyzhao1223](https://github.com/skyzhao1223) · [slowly247](https://github.com/slowly247) · [snail-vs](https://github.com/snail-vs) · [sockerch](https://github.com/sockerch) · [SparkofSpike](https://github.com/SparkofSpike) · [springeye](https://github.com/springeye) · [Sskift](https://github.com/Sskift) · [staryxchen](https://github.com/staryxchen) · [Statter](https://github.com/Statter) · [sternelee](https://github.com/sternelee) · [stream2stream](https://github.com/stream2stream) · [sximelon](https://github.com/sximelon) · [T-Phuong-Nguyen](https://github.com/T-Phuong-Nguyen) · [taiwan988](https://github.com/taiwan988) · [taixinguo](https://github.com/taixinguo) · [tdccccc](https://github.com/tdccccc) · [THatch26](https://github.com/THatch26) · [thejayjetson](https://github.com/thejayjetson) · [THINKER-ONLY](https://github.com/THINKER-ONLY) · [tiangangQiu](https://github.com/tiangangQiu) · [tiger-dog](https://github.com/tiger-dog) · [TIMER-succ](https://github.com/TIMER-succ) · [toi500](https://github.com/toi500) · [tuohai666](https://github.com/tuohai666) · [tyouter](https://github.com/tyouter) · [USTHzhanglu](https://github.com/USTHzhanglu) · [VerrPower](https://github.com/VerrPower) · [vFONGv](https://github.com/vFONGv) · [vibecoding-skills](https://github.com/vibecoding-skills) · [victorcheng2333](https://github.com/victorcheng2333) · [VincentCorleone](https://github.com/VincentCorleone) · [Vishnu1837](https://github.com/Vishnu1837) · [w1w218](https://github.com/w1w218) · [Watcher24](https://github.com/Watcher24) · [Water-Run](https://github.com/Water-Run) · [WavesMan](https://github.com/WavesMan) · [wavezhang](https://github.com/wavezhang) · [Wenjunyun123](https://github.com/Wenjunyun123) · [wgeeker](https://github.com/wgeeker) · [whp233](https://github.com/whp233) · [whtis](https://github.com/whtis) · [wjq2026](https://github.com/wjq2026) · [wlon](https://github.com/wlon) · [woyxiang](https://github.com/woyxiang) · [wplll](https://github.com/wplll) · [wucm667](https://github.com/wucm667) · [wuisabel-gif](https://github.com/wuisabel-gif) · [wuwuzhijing](https://github.com/wuwuzhijing) · [wuxixing](https://github.com/wuxixing) · [wywsoor](https://github.com/wywsoor) · [WyxBUPT-22](https://github.com/WyxBUPT-22) · [xavierpestel-ai](https://github.com/xavierpestel-ai) · [xfy6238](https://github.com/xfy6238) · [XhesicaFrost](https://github.com/XhesicaFrost) · [XiaoHuo888-hue](https://github.com/XiaoHuo888-hue) · [xiaoray-blip](https://github.com/xiaoray-blip) · [xiechimon](https://github.com/xiechimon) · [xieshutao](https://github.com/xieshutao) · [xin1104](https://github.com/xin1104) · [xsstomy](https://github.com/xsstomy) · [xuezhaoyu](https://github.com/xuezhaoyu) · [xulongzhe](https://github.com/xulongzhe) · [xyuai](https://github.com/xyuai) · [xyzs996](https://github.com/xyzs996) · [y0sif](https://github.com/y0sif) · [yan-zay](https://github.com/yan-zay) · [yangdaowan](https://github.com/yangdaowan) · [YaYII](https://github.com/YaYII) · [yekern](https://github.com/yekern) · [yetuge](https://github.com/yetuge) · [yiheng-kkk](https://github.com/yiheng-kkk) · [yrk111222](https://github.com/yrk111222) · [yuanchenglu](https://github.com/yuanchenglu) · [YuniqueUnic](https://github.com/YuniqueUnic) · [yusufgurdogan](https://github.com/yusufgurdogan) · [yyyCode](https://github.com/yyyCode) · [zerx-lab](https://github.com/zerx-lab) · [zhangweiii](https://github.com/zhangweiii) · [zhuangbiaowei](https://github.com/zhuangbiaowei) · [ZhulongNT](https://github.com/ZhulongNT) · [zhuowp](https://github.com/zhuowp) · [zhyuzhyu](https://github.com/zhyuzhyu) · [zichen0116](https://github.com/zichen0116) · [zlh124](https://github.com/zlh124) · [zxyasfas](https://github.com/zxyasfas) · [zyxrealhero](https://github.com/zyxrealhero)\n\nAlso with us, from commits whose email is not linked to an account: AccMoment · AmosLeeeeeeee · artanis · Assassin-D007 · bluth · codgo · kurise · LeHaiDang · lei · liushiao · liuyang · Lê Hải Đăng · markchang · ningjingkun · ornn.ban · pinvou3-dev · sanbo · ts25504 · v_wanghuanping01 · wangfeng · xiedongsheng · z27014 · zero · Zhiping · Zhuoran Deng · ZZHAsus · 浩淼 · 郭闯.\n<!-- contributor-wall:end -->\n\n## Organizational thanks\n\n- **[DeepSeek](https://github.com/deepseek-ai)** — the models and support that got this project started. 感谢 DeepSeek 提供模型与支持。\n- **[DataWhale](https://github.com/datawhalechina)** 🐋 — for the support and for welcoming us into the Whale Brother family. 感谢 DataWhale 的支持。\n- **[OpenWarp](https://github.com/zerx-lab/warp)** — for prioritizing Codewhale support and collaborating on a better terminal-agent experience.\n- **[Open Design](https://github.com/nexu-io/open-design)** — for support and collaboration around design-forward agent workflows.\n\nThe maintainer rule: reports and PRs are real project work, even when the final\npatch has to be narrowed, delayed, or harvested into a maintainer branch.\nAdapted contributions keep visible credit in commits and PR bodies, the\nchangelog or release notes, and this contributor record.\n\n---\n\n## Contributors by time\n\n<details open>\n<summary><strong>v0.10.3 — unreleased candidate</strong></summary>\n\n**Contributions**\n\n- **[zhuowp](https://github.com/zhuowp)** (git author `z27014`) — added bounded HTTP rate-limit recovery and corrected retry deadlines, quota guidance and recovery receipts ([#7220](https://github.com/codewhale-hq/Codewhale/pull/7220)).\n- **[printf033](https://github.com/printf033)** — enabled exclusive rename on Android so Fleet publication does not fall back to SELinux-denied hard links (`fbe9f24ee`).\n- **[liuyy2](https://github.com/liuyy2)** — diagnosed case-sensitive model selection collisions in the provider picker ([#7214](https://github.com/codewhale-hq/Codewhale/issues/7214)); the compatibility-preserving selection order was supplied by [SparkofSpike](https://github.com/SparkofSpike).\n- **[M-Maciej](https://github.com/M-Maciej)** — reported answerless output-limit stops in long Fleet proofs ([#6990](https://github.com/codewhale-hq/Codewhale/issues/6990)).\n- **[Statter](https://github.com/Statter)** — reported black-on-black text on terminals that do not report their background ([#7043](https://github.com/codewhale-hq/Codewhale/issues/7043)).\n- **[eugenicum](https://github.com/eugenicum)** — asked for a visible Full Access flag for `serve --acp` ([#6992](https://github.com/codewhale-hq/Codewhale/issues/6992)).\n- **[luestr](https://github.com/luestr)** — reported file and conversation rollback diverging ([#6970](https://github.com/codewhale-hq/Codewhale/issues/6970)), the Linux multiline send key ([#7093](https://github.com/codewhale-hq/Codewhale/issues/7093)) and session export guidance ([#6996](https://github.com/codewhale-hq/Codewhale/issues/6996)).\n- **[isaacs](https://github.com/isaacs)** — upstream minimatch preprocessing and glob cancellation fixes reused in compatible web build dependency backports.\n- **[Gabriel-Degret](https://github.com/Gabriel-Degret)** — reported that a hosted output limit truncated reasoning on a custom endpoint ([#7179](https://github.com/codewhale-hq/Codewhale/issues/7179)).\n- **[soarkit](https://github.com/soarkit)** — reported that `/patch-undo` ignored `turn_id` and undid the latest turn ([#7216](https://github.com/codewhale-hq/Codewhale/issues/7216)).\n- **[Felixkw12](https://github.com/Felixkw12)** — Opper as a bundled provider ([#7131](https://github.com/codewhale-hq/Codewhale/pull/7131)).\n- **[ddlw](https://github.com/ddlw)** — repaired clippy, rustfmt, extension-host imports and Windows DSH receipt bytes on main for the 0.10.3 batch ([#7108](https://github.com/codewhale-hq/Codewhale/pull/7108), [#7109](https://github.com/codewhale-hq/Codewhale/pull/7109)).\n- **[laozidabo](https://github.com/laozidabo)** — localized `/diff` feedback ([#7101](https://github.com/codewhale-hq/Codewhale/pull/7101)), flushed queued saves before signal exit ([#7122](https://github.com/codewhale-hq/Codewhale/pull/7122)) and kept `/rc` start failures visible ([#7127](https://github.com/codewhale-hq/Codewhale/pull/7127)).\n- **[Boyce / Aboysky](https://github.com/Aboysky)** — refreshed the README translation source stamps ([#7104](https://github.com/codewhale-hq/Codewhale/pull/7104)) and localized `/undo` feedback ([#7121](https://github.com/codewhale-hq/Codewhale/pull/7121)).\n- **[Ricardo-M-L](https://github.com/Ricardo-M-L)** — opened `.tsx` documents with the `typescriptreact` language ID ([#7140](https://github.com/codewhale-hq/Codewhale/pull/7140)).\n- **[7jrxt42BxFZo4iAnN4CX](https://github.com/7jrxt42BxFZo4iAnN4CX)** — measured repeated goal text and misleading context counts ([#7172](https://github.com/codewhale-hq/Codewhale/issues/7172)), identified crash recovery gaps in bridge message handling ([#6555](https://github.com/codewhale-hq/Codewhale/issues/6555)) and contributed per-scope instruction budgets with visible truncation ([#7086](https://github.com/codewhale-hq/Codewhale/pull/7086)).\n- **[Lstarsky0](https://github.com/Lstarsky0)** — fixed markup and link targets across wrapped table cells ([#7181](https://github.com/codewhale-hq/Codewhale/pull/7181)) and `/diff` pager resize, goal-clear wording and unpriced `/status` cost ([#7087](https://github.com/codewhale-hq/Codewhale/pull/7087), [#7089](https://github.com/codewhale-hq/Codewhale/pull/7089), [#7090](https://github.com/codewhale-hq/Codewhale/pull/7090)), and vim Normal-mode keys on terminals without bracketed paste ([#7126](https://github.com/codewhale-hq/Codewhale/pull/7126), [#7130](https://github.com/codewhale-hq/Codewhale/pull/7130)), the quote rail on wrapped blockquote rows ([#7144](https://github.com/codewhale-hq/Codewhale/pull/7144)), nested list paragraph indent, `~~~` fences, literal asterisks and bare URLs at full-width punctuation ([#7146](https://github.com/codewhale-hq/Codewhale/pull/7146), [#7156](https://github.com/codewhale-hq/Codewhale/pull/7156), [#7157](https://github.com/codewhale-hq/Codewhale/pull/7157), [#7158](https://github.com/codewhale-hq/Codewhale/pull/7158)), link targets with their own parentheses and code and links inside emphasis ([#7170](https://github.com/codewhale-hq/Codewhale/pull/7170), [#7171](https://github.com/codewhale-hq/Codewhale/pull/7171)), and the fleet-roster and stream-timeout footer statuses ([#7152](https://github.com/codewhale-hq/Codewhale/pull/7152), [#7169](https://github.com/codewhale-hq/Codewhale/pull/7169)). Preserved indentation and space-only inline code in table cells ([#7184](https://github.com/codewhale-hq/Codewhale/pull/7184)). Reported `file_search` matches relative to the workspace ([#7213](https://github.com/codewhale-hq/Codewhale/pull/7213)). Matched brace and nested-directory globs in `grep_files` ([#7215](https://github.com/codewhale-hq/Codewhale/pull/7215)). Kept the draft when the command palette inserts a command ([#7205](https://github.com/codewhale-hq/Codewhale/pull/7205)).\n- **[gaord](https://github.com/gaord)** — a running turn's cost and context ([#7074](https://github.com/codewhale-hq/Codewhale/pull/7074)) and forks that measure the history they carry ([#7147](https://github.com/codewhale-hq/Codewhale/pull/7147)), and private state on a HarmonyOS mount that cannot express ownership ([#7168](https://github.com/codewhale-hq/Codewhale/pull/7168)). Capped WeCom reply chunks by UTF-8 bytes ([#7209](https://github.com/codewhale-hq/Codewhale/pull/7209)). Accepted media and voice messages on the WeCom bridge ([#7212](https://github.com/codewhale-hq/Codewhale/pull/7212)). Steered a running turn from the WeCom bridge ([#7211](https://github.com/codewhale-hq/Codewhale/pull/7211)). Surfaced WeCom API error details in bridge replies ([#7208](https://github.com/codewhale-hq/Codewhale/pull/7208)) and rotated WeCom stream replies before the ten-minute expiry ([#7210](https://github.com/codewhale-hq/Codewhale/pull/7210)); both were harvested with the original commits kept.\n- **[luochen211](https://github.com/luochen211)** — one form per term in the zh-Hans pack ([#7150](https://github.com/codewhale-hq/Codewhale/pull/7150)) and blanket dead-code allows dropped in fleet alerts and the pet worker ([#7149](https://github.com/codewhale-hq/Codewhale/pull/7149)).\n- **[gannp](https://github.com/gannp)** — vLLM effort values that follow the local model template ([#7124](https://github.com/codewhale-hq/Codewhale/issues/7124), [#7125](https://github.com/codewhale-hq/Codewhale/pull/7125)).\n- **[Sh1Zuku / SparkofSpike](https://github.com/SparkofSpike)** — relocated state root before session-side state ([#7097](https://github.com/codewhale-hq/Codewhale/pull/7097)) goal continuation that pauses to ask ([#7175](https://github.com/codewhale-hq/Codewhale/pull/7175)) and recall of the transcript compaction replaced ([#7173](https://github.com/codewhale-hq/Codewhale/pull/7173)). Also clarified goal-tool guidance while awaiting an answer ([#7177](https://github.com/codewhale-hq/Codewhale/pull/7177)) and kept star keycap graphemes intact through Markdown parsing ([#7176](https://github.com/codewhale-hq/Codewhale/pull/7176)). Their journal fix keeps a provisional input on the same session branch when the Engine stamps it ([#7180](https://github.com/codewhale-hq/Codewhale/pull/7180)). Added the bare DeepSeek V4.1 Flash catalog entry ([#7183](https://github.com/codewhale-hq/Codewhale/pull/7183)). Stopped a home-directory workspace from refusing its own state ([#7207](https://github.com/codewhale-hq/Codewhale/pull/7207)).\n- **[asto18089](https://github.com/asto18089)** — diagnosed the NoNewPrivs child-matrix failure on hardened containers ([#7021](https://github.com/codewhale-hq/Codewhale/issues/7021)), dropped the false sandbox claim from the `fetch_url` description ([#7222](https://github.com/codewhale-hq/Codewhale/pull/7222)) and limited nested-repository discovery to the sub-agent worktree anchor ([#7224](https://github.com/codewhale-hq/Codewhale/pull/7224)). Accepted absolute in-workspace paths in sub-agent write claims ([#7227](https://github.com/codewhale-hq/Codewhale/pull/7227)). Repaired pre-0.10.0 topology checkpoint placement on session restore ([#7229](https://github.com/codewhale-hq/Codewhale/pull/7229)). Pointed the `verify` description at the Run tool instead of a retired name ([#7223](https://github.com/codewhale-hq/Codewhale/pull/7223)). Swept stale mode-jump and rail copy from the non-English packs ([#7225](https://github.com/codewhale-hq/Codewhale/pull/7225)), accepted absolute image paths that resolve inside the workspace ([#7226](https://github.com/codewhale-hq/Codewhale/pull/7226)) and reported follow-up delivery truthfully ([#7228](https://github.com/codewhale-hq/Codewhale/pull/7228)); all three were harvested with the original commits kept.\n\n</details>\n\n<details>\n<summary><strong>v0.10.2 — terminal, recovery and reliability</strong></summary>\n\n**Contributions**\n\n- **[Dhruv Singal / dsingal0](https://github.com/dsingal0)** — added Herdr agent-state reporting, session resume commands and pane release ([#7064](https://github.com/codewhale-hq/Codewhale/pull/7064)).\n- **[shenjackyuanjie](https://github.com/shenjackyuanjie)** — diagnosed and fixed the Windows OpenHarmony CMake generator and CLI hashing-module build failures ([#6988](https://github.com/codewhale-hq/Codewhale/issues/6988), [#6989](https://github.com/codewhale-hq/Codewhale/pull/6989)).\n- **[printf033](https://github.com/printf033)** — confined-file I/O on Android/Termux: SELinux-denied `open(\"/\", O_RDONLY|O_DIRECTORY)` anchor replaced with an `O_PATH` walk (Linux-cfg-gated) plus a re-materialized final parent, restoring journal, compaction and artifact writes in search-only SELinux domains ([#6983](https://github.com/codewhale-hq/Codewhale/pull/6983)), with the diagnosis and on-device reproducer in [#6980](https://github.com/codewhale-hq/Codewhale/issues/6980). Source change; compiled Android and device acceptance remain unqualified.\n- **[Paulo Aboim Pinto / aboimpinto](https://github.com/aboimpinto)** — moved config-command parsing and replies onto portable contracts while the host retains effects ([#6973](https://github.com/codewhale-hq/Codewhale/pull/6973)).\n- **[Henrik Bakke Dukefoss / henrikdukefoss](https://github.com/henrikdukefoss)** — added the Standard Compute compatible-host descriptor, with live model discovery and its billing basis kept Unknown ([#6985](https://github.com/codewhale-hq/Codewhale/pull/6985)).\n- **[Sh1Zuku / SparkofSpike](https://github.com/SparkofSpike)** — live network-policy updates and goal milestone hand-back ([#6928](https://github.com/codewhale-hq/Codewhale/pull/6928), [#6930](https://github.com/codewhale-hq/Codewhale/pull/6930)), and a Codewhale home reached through a symlink or junction ([#6947](https://github.com/codewhale-hq/Codewhale/issues/6947)). Also removed repeated catalog construction and credential-readiness work from the model picker ([#7026](https://github.com/codewhale-hq/Codewhale/pull/7026)).\n- **[dajiaohuang](https://github.com/dajiaohuang)** — cache-write token accounting, with missing and explicit-zero usage preserved ([#6913](https://github.com/codewhale-hq/Codewhale/pull/6913)).\n- **[gaord](https://github.com/gaord)** — embedder-declared telemetry surface support without changing collection policy ([#6916](https://github.com/codewhale-hq/Codewhale/pull/6916)), and the DeepSeek saved-key marker diagnosis and fix ([#6972](https://github.com/codewhale-hq/Codewhale/issues/6972), [#6974](https://github.com/codewhale-hq/Codewhale/pull/6974)).\n- **[Lstarsky0](https://github.com/Lstarsky0)** — localized `/profile` replies across all 15 complete locale packs ([#6919](https://github.com/codewhale-hq/Codewhale/pull/6919)), and workflow gates that block their own role rejected at plan admission ([#6952](https://github.com/codewhale-hq/Codewhale/pull/6952)). Also made feedback truncation readable in ASCII-safe mode, added a statusline scroll rail, and removed duplicate clipped descriptions from split-view hotbar setup ([#7027](https://github.com/codewhale-hq/Codewhale/pull/7027), [#7028](https://github.com/codewhale-hq/Codewhale/pull/7028), [#7029](https://github.com/codewhale-hq/Codewhale/pull/7029)).\n- **[LIghtJUNction](https://github.com/LIghtJUNction)** — plugin shell install and in-TUI plugin OAuth sign-in with `/connect` and `/disconnect` ([#6950](https://github.com/codewhale-hq/Codewhale/pull/6950)).\n- **[jayanthvee](https://github.com/jayanthvee)** — Windows npm launcher guidance and targeted process shutdown ([#6906](https://github.com/codewhale-hq/Codewhale/pull/6906)).\n- **[leoca](https://github.com/leoca)** — fixed the command splitter behind the destructive-command checks so a backslash-escaped quote outside quotes is an ordinary character, as the shell treats it (commit 82a14ee2d1).\n- **[asto18089](https://github.com/asto18089)** — reference fix for longer browser sign-in callback windows ([#6865](https://github.com/codewhale-hq/Codewhale/issues/6865)), and owner-only CNB mirroring so forks skip it while missing upstream credentials fail explicitly ([#7016](https://github.com/codewhale-hq/Codewhale/pull/7016)), with initial absent-secret handling co-authored by [zhuowp](https://github.com/zhuowp). Also fixed Claude sampling parameters ([#7014](https://github.com/codewhale-hq/Codewhale/pull/7014)), Kimi empty schema items ([#7015](https://github.com/codewhale-hq/Codewhale/pull/7015)), compaction checkpoint recognition ([#7017](https://github.com/codewhale-hq/Codewhale/pull/7017)), terminal event attribution ([#7018](https://github.com/codewhale-hq/Codewhale/pull/7018)), Anthropic redacted-thinking replay ([#7019](https://github.com/codewhale-hq/Codewhale/pull/7019)), and named custom-route wire selection ([#7020](https://github.com/codewhale-hq/Codewhale/pull/7020)); several were ported from the Pinvou/CodeWhale fork with [zhuowp](https://github.com/zhuowp).\n\n**Reports and verification**\n\n- **[BX166](https://github.com/BX166)** — reported three provider failure modes measured on AICraft's own traffic: a cold model that reads as a dead connection, a listed model that fails on every call, and a response cut at the output ceiling that reports `stop` ([#6889](https://github.com/codewhale-hq/Codewhale/issues/6889)).\n- **[harryvizcaino-oss](https://github.com/harryvizcaino-oss)** — reported the invalid Full Access configuration guidance shown to the Paseo ACP client ([#6992](https://github.com/codewhale-hq/Codewhale/issues/6992)).\n- **[rafacouto](https://github.com/rafacouto)** — reported Linux sandbox mount failures for symlinked credential directories and supplied a standalone bubblewrap reproduction ([#7007](https://github.com/codewhale-hq/Codewhale/issues/7007)).\n- **[luestr](https://github.com/luestr)** — reported file and conversation rollback diverging ([#6970](https://github.com/codewhale-hq/Codewhale/issues/6970)) and refusals when starting a new session ([#6981](https://github.com/codewhale-hq/Codewhale/issues/6981)).\n- **[SergiuSSoluS](https://github.com/SergiuSSoluS)** — reported queued turns being delivered again after a session was restored ([#6978](https://github.com/codewhale-hq/Codewhale/issues/6978)).\n- **[Asteria1028](https://github.com/Asteria1028)** — asked for `/init` to write `AGENTS.md` in the interface language ([#6964](https://github.com/codewhale-hq/Codewhale/issues/6964)).\n- **[SakuraNeko](https://github.com/SakuraNeko)** — asked for HarmonyOS PC support, which shaped the HarmonyOS install guidance ([#1072](https://github.com/codewhale-hq/Codewhale/issues/1072)).\n- **[8sero](https://github.com/8sero)** — reported the TUI crash in non-Latin-script sessions, with the panic signature that located the byte-index slice ([#7048](https://github.com/codewhale-hq/Codewhale/issues/7048)).\n- **[Statter](https://github.com/Statter)** — reported rate-limit retry behavior that informed the bounded current-quota 429 retry fix ([#6923](https://github.com/codewhale-hq/Codewhale/issues/6923)).\n- **[7jrxt42BxFZo4iAnN4CX](https://github.com/7jrxt42BxFZo4iAnN4CX)** — reported oversized-session refusals that informed compaction error preservation and size-limit attribution ([#6955](https://github.com/codewhale-hq/Codewhale/issues/6955)).\n\n</details>\n\n<details>\n<summary><strong>v0.10.1 — reliability and first-run fixes</strong></summary>\n\n**Merged or adapted contributions**\n\n- **[gaord](https://github.com/gaord)** — exposed call-specific workspace changes to Runtime clients ([#6817](https://github.com/codewhale-hq/Codewhale/pull/6817)) and canonical skill-detail/review routes ([#6869](https://github.com/codewhale-hq/Codewhale/pull/6869)).\n- **[asto18089](https://github.com/asto18089)** — fixed preferred search-language selection ([#6860](https://github.com/codewhale-hq/Codewhale/pull/6860)), image metadata for model input ([#6858](https://github.com/codewhale-hq/Codewhale/pull/6858)), automation deletion wording and retained-run cleanup ([#6864](https://github.com/codewhale-hq/Codewhale/pull/6864)), and compaction-anchor regression coverage ([#6857](https://github.com/codewhale-hq/Codewhale/pull/6857)).\n- **[aboimpinto](https://github.com/aboimpinto)** — extracted portable config/status/permission command contracts while preserving host-owned mutation and queue-worker readiness ([#6832](https://github.com/codewhale-hq/Codewhale/pull/6832)).\n- **[hodeswildsmith455-boop](https://github.com/hodeswildsmith455-boop)** — added OrcaRouter account sign-in with PKCE and its live chat catalog ([#6867](https://github.com/codewhale-hq/Codewhale/pull/6867)).\n- **[LIghtJUNction](https://github.com/LIghtJUNction)** — added reviewed plugin-provided AI routes with host-owned OAuth PKCE credentials and request-time authority checks ([#6805](https://github.com/codewhale-hq/Codewhale/pull/6805)).\n- **[AdityaVG13](https://github.com/AdityaVG13)** — supplied the discovery-cache priority correction adapted from [#6393](https://github.com/codewhale-hq/Codewhale/pull/6393); highest-ranked tools survive cache overflow. The broader echo and fork-inheritance draft remains open.\n- **[Guan0923](https://github.com/Guan0923)** — accepted case-insensitive HTTP(S) schemes in `config doctor` without rewriting the configured URL ([#6819](https://github.com/codewhale-hq/Codewhale/pull/6819)).\n- **[harryvgiunta](https://github.com/harryvgiunta)** — added Yolo-Auto as a bundled OpenAI-compatible host descriptor, with its billing basis recorded as unreviewed rather than guessed ([#6408](https://github.com/codewhale-hq/Codewhale/pull/6408)).\n- **[asto18089](https://github.com/asto18089)** — contributed the integrated runtime liveness, context, search, JavaScript execution, stopship scout and pet repairs, preserving the original contributor commits ([#6799](https://github.com/codewhale-hq/Codewhale/pull/6799)).\n- **[qiuYliangM](https://github.com/qiuYliangM)** — made provider-bound project instruction and constitution labels stable across directory moves and kept their absolute paths in operator reports ([#6799](https://github.com/codewhale-hq/Codewhale/pull/6799)).\n- **[zhuowp](https://github.com/zhuowp)** — supplied the process-scoped PowerShell execution-policy repair adapted for Codewhale; machine and user Group Policy still take precedence ([#6745](https://github.com/codewhale-hq/Codewhale/issues/6745)).\n- **[Andrea-Bruno](https://github.com/Andrea-Bruno)** — designed the Superfast Decision Gate and contributed its off-by-default shadow classifier ([#6604](https://github.com/codewhale-hq/Codewhale/pull/6604), [#6603](https://github.com/codewhale-hq/Codewhale/issues/6603)).\n- **[aiapienthusiast](https://github.com/aiapienthusiast)** — added Cheaper Inference to the bundled provider catalog ([#6761](https://github.com/codewhale-hq/Codewhale/pull/6761)).\n- **[gaord](https://github.com/gaord)** — let a client fork a thread at a named turn ([#6580](https://github.com/codewhale-hq/Codewhale/pull/6580)), let undo roll back files for the turn it is undoing ([#6483](https://github.com/codewhale-hq/Codewhale/pull/6483)), stopped resume and fork from duplicating threads and sessions ([#6406](https://github.com/codewhale-hq/Codewhale/pull/6406)), exposed user-defined provider routes to native clients ([#6404](https://github.com/codewhale-hq/Codewhale/pull/6404)), and kept a fork going when a turn lost its tool call ([#6664](https://github.com/codewhale-hq/Codewhale/pull/6664)).\n- **[Lstarsky0](https://github.com/Lstarsky0)** — moved the docs/work, legal, digest and FAQ pages onto the dictionary spine ([#6405](https://github.com/codewhale-hq/Codewhale/pull/6405), [#6417](https://github.com/codewhale-hq/Codewhale/pull/6417), [#6499](https://github.com/codewhale-hq/Codewhale/pull/6499), [#6574](https://github.com/codewhale-hq/Codewhale/pull/6574)), tightened the Chinese-branching ceiling to 18 ([#6403](https://github.com/codewhale-hq/Codewhale/pull/6403)), and made Fleet publish without a two-link window ([#6431](https://github.com/codewhale-hq/Codewhale/pull/6431)). Also moved the constitution page onto the dictionary spine and kept its install link in the selected locale ([#6733](https://github.com/codewhale-hq/Codewhale/pull/6733)). Translated the session-only note after model switches across the complete TUI locale packs ([#6875](https://github.com/codewhale-hq/Codewhale/pull/6875)).\n- **[aboimpinto](https://github.com/aboimpinto)** — restored a green Linux full-workspace test gate without loosening any test, twice ([#6581](https://github.com/codewhale-hq/Codewhale/pull/6581), [#6666](https://github.com/codewhale-hq/Codewhale/pull/6666)), and completed the session command group's shared boundary ([#6793](https://github.com/codewhale-hq/Codewhale/pull/6793)).\n- **[dajiaohuang](https://github.com/dajiaohuang)** — validated `config set` values against the settings schema ([#6568](https://github.com/codewhale-hq/Codewhale/pull/6568)).\n- **[Water-Run](https://github.com/Water-Run)** — ingested namespaced model-only catalog entries so models present only in the canonical `models` map reach the offering list ([#6400](https://github.com/codewhale-hq/Codewhale/pull/6400)), and retired the blanket dead-code allowance and its unused feature stages, tightening the budget to match ([#6402](https://github.com/codewhale-hq/Codewhale/pull/6402)).\n- **[wuisabel-gif](https://github.com/wuisabel-gif)** — designed the `tool_call_after` execution-receipt contract and its tests on a reference branch, which landed re-implemented on the current hook seam ([#6689](https://github.com/codewhale-hq/Codewhale/issues/6689), [#6713](https://github.com/codewhale-hq/Codewhale/pull/6713)).\n- **[Sh1Zuku / SparkofSpike](https://github.com/SparkofSpike)** — let making room survive a provider request-body limit (HTTP 413) by shrinking, then replacing, inline images for that one summary pass ([#6642](https://github.com/codewhale-hq/Codewhale/pull/6642)). Also translated seventeen more guides into Simplified Chinese, among them plugins, memory, sandbox, workflow authoring, Docker, Termux and HarmonyOS, and brought the existing zh_hans docs onto one terminology standard ([#6662](https://github.com/codewhale-hq/Codewhale/pull/6662)), then translated thirteen developer and internal docs, among them the runtime API, architecture, tool surface, authorization order, operations runbook and Workroom security ([#6663](https://github.com/codewhale-hq/Codewhale/pull/6663)).\n\n**Reports and reproductions**\n\n- **[7jrxt42BxFZo4iAnN4CX](https://github.com/7jrxt42BxFZo4iAnN4CX)** — reported indefinite human questions being cancelled by the TUI hung-tool watchdog and supplied the timer evidence ([#6872](https://github.com/codewhale-hq/Codewhale/issues/6872)).\n- **[cenab](https://github.com/cenab)** — requested the Tsubasa provider row and supplied its endpoint, key and model values ([#6695](https://github.com/codewhale-hq/Codewhale/issues/6695)).\n- **[BX166](https://github.com/BX166)** — reported the AICraft provider row missing its key console, docs link and guidance, and supplied the values ([#6616](https://github.com/codewhale-hq/Codewhale/issues/6616)); the report also surfaced that no bundled descriptor's links reached the setup form.\n- **[jayanthvee](https://github.com/jayanthvee)** — reported and diagnosed that killing the npm launcher's `node.exe` ends Windows sessions without cleanup, with reproductions and fix directions ([#6827](https://github.com/codewhale-hq/Codewhale/issues/6827)).\n\n</details>\n\n<details>\n<summary><strong>v0.10.0 — redesigned workbench, session recovery, and runtime surfaces</strong></summary>\n\n**Merged or adapted contributions**\n\n- **[AdityaVG13](https://github.com/AdityaVG13)** — composer wrapping, pasted/editor draft history, painted-column transcript copying, tab/caret placement, explicit terminal foregrounds, and headless user-input tool availability ([#6363](https://github.com/codewhale-hq/Codewhale/pull/6363), [#6365](https://github.com/codewhale-hq/Codewhale/pull/6365)); both PRs merged with their original ancestry.\n- **[aboimpinto](https://github.com/aboimpinto)** — moved the TUI session-export slice onto shared command contracts (FEAT-025): a session-export contract facet with one shared sanitizer, `/export` routed through the facet, pinned with baseline-captured goldens and gates ([#6096](https://github.com/codewhale-hq/Codewhale/pull/6096)).\n- **[gaord](https://github.com/gaord)** — recorded the mode each turn ran in so mixed-mode sessions stay legible ([#6321](https://github.com/codewhale-hq/Codewhale/pull/6321), harvested), stated the approval posture a task thread starts on ([#6386](https://github.com/codewhale-hq/Codewhale/pull/6386)), and rebuilt the runtime-API thread summary in one store pass instead of one per row ([#6376](https://github.com/codewhale-hq/Codewhale/pull/6376)).\n- **[zhuowp](https://github.com/zhuowp)** — preserved chat roles across compaction, protected user turns on recompaction, and kept the operate contract intact through the same path ([#6286](https://github.com/codewhale-hq/Codewhale/pull/6286)).\n- **[h3c-hexin](https://github.com/h3c-hexin)** — rate-limit-adaptive subagent launch scheduling: the `DynamicGate` that replaces fixed spawn pacing when a provider throttles ([#6055](https://github.com/codewhale-hq/Codewhale/pull/6055), harvested).\n- **[asto18089](https://github.com/asto18089)** — co-authored the rate-limit-adaptive launch scheduling work ([#6055](https://github.com/codewhale-hq/Codewhale/pull/6055)).\n- **[yrk111222](https://github.com/yrk111222)** — ModelScope provider support, landed through the shared `ChildGrant` fleet authority ([#6299](https://github.com/codewhale-hq/Codewhale/pull/6299), harvested).\n- **[xiechimon](https://github.com/xiechimon)** — capped child tool results at capture time rather than at render ([#6294](https://github.com/codewhale-hq/Codewhale/pull/6294), harvested).\n- **[VincentCorleone](https://github.com/VincentCorleone)** — defined and implemented the weixin-bridge Quick Start so a first run works from the README alone ([#6170](https://github.com/codewhale-hq/Codewhale/pull/6170), harvested).\n- **[Serendo](https://github.com/Serendo)** — fixed the `/config` theme editor exiting the panel on an arrow key ([#6336](https://github.com/codewhale-hq/Codewhale/pull/6336)).\n- **[yetuge](https://github.com/yetuge)** — dropped the retired `token_budget` field from the subagent documentation ([#6335](https://github.com/codewhale-hq/Codewhale/pull/6335)).\n- **[Water-Run](https://github.com/Water-Run)** — translated the local browser client guide into Simplified Chinese ([#6401](https://github.com/codewhale-hq/Codewhale/pull/6401)). The catalog and dead-code work (#6400, #6402) landed after the v0.10.0 tag and is credited under v0.10.1.\n\n**Reports and reproductions**\n\n- **[BX166](https://github.com/BX166)** — contributed the AICraft provider template and its documentation ([#6171](https://github.com/codewhale-hq/Codewhale/pull/6171)). The PR was closed unmerged, but it is what surfaced the decision to stop special-casing named OpenAI-compatible hosts and make them ordinary providers ([#6289](https://github.com/codewhale-hq/Codewhale/issues/6289)); its base URL and key env carry into that work.\n- **[7jrxt42BxFZo4iAnN4CX](https://github.com/7jrxt42BxFZo4iAnN4CX)** — reported the session-retention defects behind archive-past-the-cap and empty-session cap occupancy ([#6136](https://github.com/codewhale-hq/Codewhale/issues/6136), [#6137](https://github.com/codewhale-hq/Codewhale/issues/6137)), the resume-failure design behind durable transcript errors ([#6138](https://github.com/codewhale-hq/Codewhale/issues/6138)), and the gaps behind the opt-in approval timeout ([#6101](https://github.com/codewhale-hq/Codewhale/issues/6101)), `codewhale exec --hooks` ([#6099](https://github.com/codewhale-hq/Codewhale/issues/6099)), Markdown drag-copy ([#6156](https://github.com/codewhale-hq/Codewhale/issues/6156)), and the browsable, current-aware session picker ([#6014](https://github.com/codewhale-hq/Codewhale/issues/6014)); the goal token-budget hard stop ([#6013](https://github.com/codewhale-hq/Codewhale/issues/6013)) and the fleet no-progress guard shared with child workers ([#6015](https://github.com/codewhale-hq/Codewhale/issues/6015)) landed as first slices of two larger proposals, and the runtime-store session refusal ([#6207](https://github.com/codewhale-hq/Codewhale/issues/6207)) stays open after the first fix was reverted on a race.\n- **[Lstarsky0](https://github.com/Lstarsky0)** — reported TUI tests reading machine state instead of hermetic fixtures; the `lock_test_env` remedy from that report shaped two more hermetic fixes for shared UI fixtures and the compaction budget test ([#5359](https://github.com/codewhale-hq/Codewhale/issues/5359)).\n- **[Lujc0523](https://github.com/Lujc0523)** — reported `/hooks edit` splitting keystrokes between the editor and the composer, fixed by pausing the TUI input pump inside the editor handoff ([#6165](https://github.com/codewhale-hq/Codewhale/issues/6165)).\n- **[Statter](https://github.com/Statter)** — reported the Gemini `/models` failure that now surfaces the provider's reason instead of an empty error ([#6173](https://github.com/codewhale-hq/Codewhale/issues/6173)).\n- **[sequico](https://github.com/sequico)** — reported the ACP `session/new` ids that `session/load` could not resolve, fixed by minting resolvable session ids ([#6174](https://github.com/codewhale-hq/Codewhale/issues/6174)).\n- **[bevis-wong](https://github.com/bevis-wong)** — reported the mid-run engine freeze behind the bounded turn-end foreground-child join, and the resume path that re-ran identical tool-call repair on every load instead of persisting it ([#6184](https://github.com/codewhale-hq/Codewhale/issues/6184), [#6185](https://github.com/codewhale-hq/Codewhale/issues/6185)).\n\n</details>\n\n<details open>\n<summary><strong>v0.9.13 — paste integrity, dispatch safety, and the Computer Use bundle</strong></summary>\n\n**Merged or adapted contributions**\n\n- **[nightt5879](https://github.com/nightt5879)** — added bounded Gemini signature recovery guidance and streaming/non-streaming gateway transport regressions ([#6081](https://github.com/codewhale-hq/Codewhale/pull/6081)).\n- **[c020627](https://github.com/c020627)** — repaired translated contributor-guide and Windows screenshot links ([#6080](https://github.com/codewhale-hq/Codewhale/pull/6080)).\n- **[h3c-hexin](https://github.com/h3c-hexin)** — contributed full-fidelity session archives ([#6056](https://github.com/codewhale-hq/Codewhale/pull/6056)), GLM-5.3 reasoning controls and the model-facing documentation/tool-gating audit ([#6051](https://github.com/codewhale-hq/Codewhale/pull/6051), [#6052](https://github.com/codewhale-hq/Codewhale/pull/6052)).\n- **[asto18089](https://github.com/asto18089)** — co-authored full-fidelity session archives ([#6056](https://github.com/codewhale-hq/Codewhale/pull/6056)), GLM-5.3 reasoning controls and tool-gating/documentation fixes ([#6051](https://github.com/codewhale-hq/Codewhale/pull/6051), [#6052](https://github.com/codewhale-hq/Codewhale/pull/6052)).\n- **[Hmbown](https://github.com/Hmbown)** — updated JavaScript dependencies across the website, telemetry package and VS Code extension ([#6057](https://github.com/codewhale-hq/Codewhale/pull/6057)).\n- **[gaord](https://github.com/gaord)** — contributed the file-scoped restore endpoint and trust-gated whole-tree rollback ([#6111](https://github.com/codewhale-hq/Codewhale/pull/6111)), Fleet schema inspection, role precedence and worker deliverable receipts, and linked the community VS Code frontend ([#5944](https://github.com/codewhale-hq/Codewhale/pull/5944), [#5945](https://github.com/codewhale-hq/Codewhale/pull/5945), [#5946](https://github.com/codewhale-hq/Codewhale/pull/5946), [#5992](https://github.com/codewhale-hq/Codewhale/pull/5992)).\n- **[goransh-walia](https://github.com/goransh-walia)** — contributed the propose-only commit-planning rework ([#5870](https://github.com/codewhale-hq/Codewhale/pull/5870)).\n- **[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/codewhale-hq/Codewhale/pull/5996), [#5952](https://github.com/codewhale-hq/Codewhale/issues/5952), [#5954](https://github.com/codewhale-hq/Codewhale/issues/5954), [#6003](https://github.com/codewhale-hq/Codewhale/issues/6003), [#6004](https://github.com/codewhale-hq/Codewhale/issues/6004), [#6006](https://github.com/codewhale-hq/Codewhale/issues/6006), [#6007](https://github.com/codewhale-hq/Codewhale/issues/6007)).\n- **[SparkofSpike](https://github.com/SparkofSpike)** — contributed two-stage consent for opting out of model-bound credential redaction and session titles derived from real user prompts instead of runtime handoffs ([#5982](https://github.com/codewhale-hq/Codewhale/pull/5982), [#6012](https://github.com/codewhale-hq/Codewhale/pull/6012)).\n- **[aboimpinto](https://github.com/aboimpinto)** — moved session lifecycle and session-control commands onto shared command contracts ([#5902](https://github.com/codewhale-hq/Codewhale/pull/5902), [#5951](https://github.com/codewhale-hq/Codewhale/pull/5951)).\n- **[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/codewhale-hq/Codewhale/issues/5908), [#5909](https://github.com/codewhale-hq/Codewhale/issues/5909), [#5910](https://github.com/codewhale-hq/Codewhale/pull/5910), [#5911](https://github.com/codewhale-hq/Codewhale/pull/5911), [#5912](https://github.com/codewhale-hq/Codewhale/pull/5912)).\n- **[wuisabel-gif](https://github.com/wuisabel-gif)** — exposed workspace file suggestions through the Runtime API ([#6120](https://github.com/codewhale-hq/Codewhale/pull/6120)) and added custom-theme discovery, preview and selection in the theme picker ([#5907](https://github.com/codewhale-hq/Codewhale/pull/5907)).\n- **[zhuowp](https://github.com/zhuowp)** — matched model-visible shell guidance to the interpreter selected for execution ([#5900](https://github.com/codewhale-hq/Codewhale/pull/5900)).\n- **[googio](https://github.com/googio)** — added the opt-in Serply web-search provider ([#6100](https://github.com/codewhale-hq/Codewhale/pull/6100)).\n\n**Reports and reproductions**\n\n- **[Hmbown](https://github.com/Hmbown)** — identified missing recovery guidance when a compatible gateway rejects Gemini tool-call replay without thought signatures ([#6048](https://github.com/codewhale-hq/Codewhale/issues/6048)).\n- **[7jrxt42BxFZo4iAnN4CX](https://github.com/7jrxt42BxFZo4iAnN4CX)** — proposed global usage and tool diagnostics and independent goal verification ([#6011](https://github.com/codewhale-hq/Codewhale/issues/6011), [#6013](https://github.com/codewhale-hq/Codewhale/issues/6013)); these broader requests remain open.\n- **[nsfoxer](https://github.com/nsfoxer)** — reported the multiline-paste regression and incomplete provider model lists ([#5981](https://github.com/codewhale-hq/Codewhale/issues/5981), [#6009](https://github.com/codewhale-hq/Codewhale/issues/6009)).\n- **[Nefelibata1024](https://github.com/Nefelibata1024)** — confirmed the multiline-paste regression's impact ([#5981](https://github.com/codewhale-hq/Codewhale/issues/5981)).\n- **[Gabriel-Degret](https://github.com/Gabriel-Degret)** — reported the loss of the allow_insecure_http provider setting and that saved agent profiles were silently ignored when spawning sub-agents ([#5991](https://github.com/codewhale-hq/Codewhale/issues/5991), [#6117](https://github.com/codewhale-hq/Codewhale/issues/6117)).\n- **[LmeSzinc](https://github.com/LmeSzinc)** — requested Runtime API access to the TUI fuzzy file search ([#6095](https://github.com/codewhale-hq/Codewhale/issues/6095)).\n- **[dmt4](https://github.com/dmt4)** — requested Linux copy-on-select and middle-click paste ([#6116](https://github.com/codewhale-hq/Codewhale/issues/6116)).\n- **[Lujc0523](https://github.com/Lujc0523)** — reported the ACP initialize schema violation affecting strict IDE clients ([#5969](https://github.com/codewhale-hq/Codewhale/issues/5969)).\n- **[mo-vic](https://github.com/mo-vic)** — proposed storing evicted context on disk so it can be retrieved later ([#6008](https://github.com/codewhale-hq/Codewhale/issues/6008)).\n- **[giancarlocp](https://github.com/giancarlocp)** — requested a plugin authoring guide and OpenCode plugin conversion ([#5827](https://github.com/codewhale-hq/Codewhale/discussions/5827)).\n- **[hxfhd](https://github.com/hxfhd)** — supplied a Windows reproduction of a turn stopping before its stated next tool action ([#6010](https://github.com/codewhale-hq/Codewhale/discussions/6010)).\n\n</details>\n\n<details open>\n<summary><strong>v0.9.12 — native search, live token truth, and runtime surfaces</strong></summary>\n\n**Merged or adapted contributions**\n\n- **[hexin / h3c-hexin](https://github.com/h3c-hexin)** — provider-native web\n search across four routes: the DeepSeek adapter (#5683), Z.AI and BigModel\n (#5685), and Xiaomi MiMo (#5687), each fail-closed for undocumented models,\n plus the constraint enforcement that runs before any fallback (#5682); also\n authoritative edit-last-turn boundaries (#5621), Kimi Code k3-256k support\n (#5622), post-compaction input-token reporting (#5623), and preserving the\n scheduled model selection in automations (#5650)\n- **[Isabel Wu / wuisabel-gif](https://github.com/wuisabel-gif)** — live\n session token totals (#5624), persisted context-pressure warnings (#5629),\n discoverable Fleet roster editing (#5604), the capability-gated cursor\n accent (#5599), and `/copy` for the latest completed response (#5692)\n- **[Paulo Aboim Pinto / aboimpinto](https://github.com/aboimpinto)** —\n preserved Windows verbatim-path operands through POSIX word splitting\n (#5610), moved the plugins group onto the command shapes (#5657), built\n FEAT-022 skills command shapes with retained-host validation (#5825), and\n re-landed FEAT-020 plugin command shapes on main (#5865)\n- **[Alex Musichen / musichen](https://github.com/musichen)** — fixed the\n configured-view DeepSeek model picker so it shows a stable DeepSeek heading\n and keeps every official catalog model for the active provider visible\n (#5689)\n- **[gaord](https://github.com/gaord)** — added the `GET /v1/fleet/profiles`\n runtime API endpoint, reusing the FleetManager validation path (#5688)\n- **[Sh1Zuku / SparkofSpike](https://github.com/SparkofSpike)** — corrected\n English documentation inaccuracies and contributed the first zh_hans\n translations for the Tier-2 docs (#5613)\n- **[M-Maciej](https://github.com/M-Maciej)** — goal continuation cadence fix\n (#5591) and the per-session control socket (#5533, #5831)\n- **[Serephus / serephus](https://github.com/serephus)** — nixpkgs update\n (#5669)\n- **[秋月凉梦 / qiuYliangM](https://github.com/qiuYliangM)** — co-authored the\n authoritative edit-last-turn boundaries (#5621), Kimi Code k3-256k support\n (#5622), and post-compaction input-token reporting (#5623)\n- **[whp233](https://github.com/whp233)** — `wire = responses|anthropic` for\n openai-compatible custom routes and opencode-zen muse-spark (#5716, landed\n as #5719)\n- **[Gabriel Degret / Gabriel-Degret](https://github.com/Gabriel-Degret)** —\n found the reasoning-only retry gap and built the first fix; landed as the\n `[reasoning_only]` retry ceiling with a request-scoped nudge (#5867)\n- **[huangxianzhan](https://github.com/huangxianzhan)** — the\n `x-opencode-session` header for OpenCode Go and Zen gateways (#5868)\n- **[zhuowp](https://github.com/zhuowp)** — task origin preserved in job\n snapshots (#5869)\n- **[AdityaG / AdityaVG13](https://github.com/AdityaVG13)** — a ten-commit\n performance pass across the TUI and config: zero-copy LaTeX streaming,\n single-pass token accounting, memoized provider resolution, once-per-process\n models.dev catalog parsing (interactive boot -70%), adaptive shell-completion\n poll cadence, and diagnostic worker caps\n- **[Nightt / nightt5879](https://github.com/nightt5879)** — isolated remote\n recovery lease generations (#5790)\n- **[yiheng-kkk](https://github.com/yiheng-kkk)** — replaced stale todo\n transcript snapshots so a rewritten todo list renders its current tasks\n (#5871, #5873)\n- **[Lfanxing](https://github.com/Lfanxing)** — Moonshot routes degrade\n incompatible tool definitions per request instead of failing the turn\n- **[WissssleyL / Lstarsky0](https://github.com/Lstarsky0)** — moved\n `docs/subagents` and `docs/mcp` onto the dictionary spine (#5337), with the\n metaTitle probe that keeps them there\n\n**Reports and reproductions**\n\n- **[slowly247](https://github.com/slowly247)** — reported the Ollama input\n budget collapsing to 1,024 tokens on 32K local models (#5820)\n- **[ronohara](https://github.com/ronohara)** — reported the engine stopping\n after recoverable network errors and reproduced the discarded approval (#5769)\n- **[Lujc0523](https://github.com/Lujc0523)** — asked for ACP session mode and\n model configuration (#5863)\n- **[senka9h](https://github.com/senka9h)** — reported the missing ACP\n `session/list` and `session/load` methods (#5864)\n</details>\n\n<details>\n<summary><strong>v0.9.11 — portable command surfaces and turn-loop structure</strong></summary>\n\n**Merged or adapted contributions**\n\n- **[xyzs996 / Mark Yan](https://github.com/xyzs996)** — DeepSeek V4 billing treats whole Beijing weekends as off-peak, and the scorecard window comment now matches ([#5545](https://github.com/codewhale-hq/Codewhale/pull/5545))\n- **[Sun Zhenyuan / bistack](https://github.com/bistack)** — extracted tool-call\n planning, execution, and result processing from the turn loop while preserving\n the existing sandbox-policy, hook, budget, and batch-execution behavior\n (#5523)\n- **[Isabel Wu / wuisabel-gif](https://github.com/wuisabel-gif)** — added\n bounded, workspace-relative multi-file diagnostics to the existing model-\n visible `lsp` tool, reusing the shared transport pool without expanding the\n 75-tool catalog (#5524)\n- **[Paulo Aboim Pinto / aboimpinto](https://github.com/aboimpinto)** — added\n portable presentation, media-attachment, and operation-digest facets to the\n command contract, then moved all seven utility command handlers onto the\n contract-backed dispatch path (#5525)\n\n**Reviewed overlap**\n\n- **[Lstarsky0](https://github.com/Lstarsky0)** — reviewed #5524 and identified\n the closed-channel, severity-filtering, and truncation-receipt gaps covered by\n the maintainer hardening pass.\n- Isabel Wu's PR #5530 independently routed the legacy plural completion\n command through the public CLI generator. Equivalent behavior, including the\n published `codew` alias registration, had already landed in the integration\n lane, so the PR is recorded here as reviewed project work rather than counted\n as a second merged contribution.\n\n**Reports and reproductions**\n\n- **[RepentStar](https://github.com/RepentStar)** — reported the stale shell-\n completion generator and missing `codew` registration that led to the\n completion fixes and upgrade guidance in #5526.\n\n</details>\n\n<details>\n<summary><strong>v0.9.10 — retention, identity, and durable approvals</strong></summary>\n\n**Merged or adapted contributions**\n\n- **[Sh1Zuku / SparkofSpike](https://github.com/SparkofSpike)** — restored\n `/title` as an independent, persistent terminal-window title in PR #5509,\n and contributed the Tier 1 Chinese and Indonesian documentation move to\n `docs/zh_hans/` and `docs/id/` with one-release redirect stubs (#5507,\n epic #5482)\n- **[Sun Zhenyuan / bistack](https://github.com/bistack)** — extracted the\n turn-loop stream processor in PR #5514 while preserving retry,\n cancellation, usage, TTFT, steering, and partial-response behavior\n- **[OctoBored](https://github.com/OctoBored)** — supplied the working\n no-token Star History mirror used by the localized README set after the\n canonical chart endpoint began returning a restricted placeholder (#5510)\n- **[cacdcaecawae](https://github.com/cacdcaecawae)** — added provider-neutral\n typed MCP image forwarding in PR #5515; the adapted version also emits a\n visible omission receipt for malformed image fields\n- **[cyq1017](https://github.com/cyq1017)** — approval outcomes are persisted\n before execution can proceed: receipts commit to a session-owned log first,\n unpersistable evidence blocks the tool, stale decisions are rejected, and\n resume reconstructs closed and interrupted approvals (#5491, closes #5360)\n- **[aboimpinto](https://github.com/aboimpinto)** — the TUI-owned\n dependency-injection and migration infrastructure behind safe slash-command\n extraction: seven capability facets, a dual-path dispatch seam, and\n source-aware CI enforcement so a command slice cannot claim migration while\n it still accepts concrete `App` (#5506, EPIC-005/FEAT-015 under the #5316\n umbrella they filed)\n- **[wuisabel-gif](https://github.com/wuisabel-gif)** — the TUI header now\n names the active repository or linked worktree before the branch and dirty\n marker, derived from Git's common directory and capped by shell density\n tier (#5511, the repo/worktree slice of #5437)\n- **[Lstarsky0](https://github.com/Lstarsky0)** — `docs/hooks` and\n `docs/troubleshooting` move onto the dictionary spine, retiring their inline\n bilingual literals in favor of locale dictionaries with token-aware code\n spans (#5504, closes #5337 they filed)\n\n**Reports, reproductions, and verification**\n\n- **[DingYong4223](https://github.com/DingYong4223)** — reported the\n narrow-terminal completion truncation closed by the bounded hover reveal\n (#998)\n- **[AiurArtanis](https://github.com/AiurArtanis)** and\n **[formp3](https://github.com/formp3)** — identified the completion surfaces\n affected by narrow-terminal truncation (#998)\n- **[sky-sun-moon](https://github.com/sky-sun-moon)** — reported the missing\n full per-turn input, reasoning, tool, and assistant pages that shaped Turn\n Inspector navigation (#1682)\n- **[cy2311](https://github.com/cy2311)** — reported the Windows launch path\n now covered by the Windows Terminal-aware batch launcher (#1854)\n- **[demian-welt](https://github.com/demian-welt)** — provided the reproducible\n pre-header SSE transport failure behind the bounded HTTP/1.1 retry (#4683)\n- **[BrathonBai](https://github.com/BrathonBai)** — reported the Tabby/CJK IME\n candidate-window jump that led to the cursor hide-diff-position-show\n transaction (#5023)\n- **[M-Maciej](https://github.com/M-Maciej)** — provided the organization-\n coordinator use case and 5-30 minute cadence requirement behind cancellable\n cross-turn goal delays (#5508)\n- **[thejayjetson](https://github.com/thejayjetson)** — the header\n status-indicator report that pinned the regression to a specific setting,\n having already ruled out every value, both themes, `fancy_animations`, the\n terminal auto-off rules, and a clean `codewhale doctor` (#5512)\n- **[hxfhd](https://github.com/hxfhd)** — reported the deterministic\n cross-session contamination class behind this release's session-ownership\n boundary, plus route budgeting evidence (#5518)\n- **[sfdzhmr](https://github.com/sfdzhmr)** — reported the route budgeting\n root causes that exact route-limit propagation now closes (#5516)\n\n</details>\n\n<details>\n<summary><strong>v0.9.9 — truth and resilience</strong></summary>\n\n**Merged or adapted contributions**\n\n- **[Lstarsky0](https://github.com/Lstarsky0)** — compact rows no longer reserve a hidden session-metrics strip ([#5486](https://github.com/codewhale-hq/Codewhale/pull/5486)); docs shell and shared component pick-text onto the typed dictionary spine ([#5488](https://github.com/codewhale-hq/Codewhale/pull/5488), [#5490](https://github.com/codewhale-hq/Codewhale/pull/5490))\n- **[Sh1Zuku / SparkofSpike](https://github.com/SparkofSpike)** — topbar session title ([#5481](https://github.com/codewhale-hq/Codewhale/pull/5481))\n- **[h3c-hexin](https://github.com/h3c-hexin)** — a concrete route/offering\n output limit now outranks the conservative 8,192-token compatibility guess\n for an uncatalogued model; routes that publish no limit stay fail-closed\n (#5461, closes #5460); every web tool surface uses the noisy-result soft\n limit (#5474); lowercase saved selectors resolve against the owning\n Z.ai/DeepSeek catalog row (#5475); configured-skill prompt paths remain\n stable across ephemeral roots and operating systems (#5492, #5473)\n- **[Gabriel-Degret](https://github.com/Gabriel-Degret)** — configurable\n auto-router classifier timeout (#5494; first contribution)\n\n**Reports, reproductions, and verification**\n\n- **[asto18089](https://github.com/asto18089)** — diagnosed the Z.ai `glm-5.2`\n casing collision and wrote the first provider-scoped fix downstream\n (Pinvou/CodeWhale#14, carried upstream in #5475)\n\n- **[hardy922](https://github.com/hardy922)** — the context-window honesty\n report that became the every-surface labeling pass (#5239)\n- **[redstar](https://github.com/redstar)** — bwrap container essentials and\n configurable extra roots (#5410)\n- **[all-lopezg](https://github.com/all-lopezg)** — SSE UTF-8 garbling on\n DeepSeek Flash with screenshots, fixed fail-closed in every dialect (#5374)\n- **[alitvak69](https://github.com/alitvak69)** — the unverified-live-pricing\n report behind the bundled fallback rates and DeepSeek V4 tiers (#5241)\n- **[wuisabel-gif](https://github.com/wuisabel-gif)** — bounded reproduction\n attempts for the macOS filtered-suite hang note on #5056\n\n</details>\n\n<details>\n<summary><strong>v0.9.8 — marketplace, Google, Ollama Cloud</strong></summary>\n\n**Merged or adapted contributions**\n\n- **[xiaoray-blip / Arlo](https://github.com/xiaoray-blip)** — MCP list responses omit `nextCursor` when there are no further pages ([#5336](https://github.com/codewhale-hq/Codewhale/pull/5336), reported in [#5335](https://github.com/codewhale-hq/Codewhale/issues/5335))\n- **[EvanProgramming](https://github.com/EvanProgramming)** — webhook hook\n sink no longer panics when its HTTP client fails to build (#5381); session-index\n JSONL writes are serialized so concurrent stores cannot drop an append\n (#5382)\n- **[Lstarsky0](https://github.com/Lstarsky0)** — session peek hides internal\n runtime events (#5376); thinking-ladder tests re-pinned (#5378);\n provider-count follow-ups (#5383/#5384); macOS agy fixture\n canonicalization (#5392); website zh-Hans 宪章 terminology (#5397);\n regenerated website facts harvested and corrected so Google and Ollama\n Cloud are listed and Antigravity is not advertised as a route (#5398)\n- **[mvanhorn](https://github.com/mvanhorn)** — read-only, repository-fact\n driven settings preview on the website models page (#5411, fixes #5370)\n- **[buiducnhat](https://github.com/buiducnhat)** — canonical `ultra`\n reasoning effort mapped to every provider's maximum tier (#5409); session\n titles truncated by character count instead of byte offset so CJK and\n emoji names cut at the intended width (#5415)\n- **[SparkofSpike](https://github.com/SparkofSpike)** — `/title` and the\n saved session name in the terminal tab/window title, plus the fix for the\n deadlock when the title changed during an active turn; landed as a\n discoverable alias of `/rename` on the one persisted session name (#5419)\n- **[redstar](https://github.com/redstar)** — Eden AI registered as a named\n OpenAI-compatible provider with `EDENAI_API_KEY`, `eden-ai`/`eden_ai`\n aliases, and the global and EU endpoints (#5422)\n- **[wuisabel-gif](https://github.com/wuisabel-gif)** — background verifier\n test isolated from the rustup `rustc` shim and `$HOME`, one narrow\n reliability slice of #5056 (#5423)\n\n</details>\n\n<details>\n<summary><strong>v0.9.7 — the catalog stays ordinary</strong></summary>\n\n**Merged or adapted contributions**\n\n- **[XhesicaFrost](https://github.com/XhesicaFrost)** — copy from the message\n context menu now takes canonical content, keeping authored Unicode,\n Markdown, and hard line breaks in the clipboard while role glyphs,\n continuation rails, and visual wrapping stay out of it (PR #5319)\n- **[h3c-hexin](https://github.com/h3c-hexin)** — split durable-session\n snapshot reads from crash recovery so an embedding host inspecting a\n session while a tool is still running no longer gets a spurious crash\n repair (PR #5320)\n- **[XiaoHuo888-hue](https://github.com/XiaoHuo888-hue)** — OrcaRouter as a\n first-class named provider with `ORCAROUTER_API_KEY`, auto-routing,\n CLI `--provider` selection, and TUI picker entries (PR #5321)\n\n**Reports and verification**\n\n- **[USTHzhanglu](https://github.com/USTHzhanglu)** — reported the Auto-Review\n shell/write regression with a Windows reproduction and audit-log evidence\n (issue #5323)\n- **[Lstarsky0](https://github.com/Lstarsky0)** — traced the Auto-Review\n regression to the exact permission-posture change and documented the safe\n recovery choices (issue #5323)\n\n</details>\n\n<details>\n<summary><strong>v0.9.6 — fewer bumpers, truthful endings</strong></summary>\n\n**Merged or adapted contributions**\n\n- **[Xavier Pestel / xavierpestel-ai](https://github.com/xavierpestel-ai)** —\n Mistral AI (la Plateforme) as a first-class provider route, including the\n adjustable-reasoning wire contract and its multi-turn thinking replay\n (PR #5295)\n- **[Ben Younes / ousamabenyounes](https://github.com/ousamabenyounes)** —\n inherited nested-agent depth cap, keeping child agents from widening the\n budget provided by their parent (PR #5317)\n- **[Rafael Cavalheri / rafaelcavalheri](https://github.com/rafaelcavalheri)** —\n multi-round ACP file, search, Git, patch, and explicitly enabled shell tool\n execution with cancellation and fail-closed shell safety (PR #5225)\n\n</details>\n\n<details>\n<summary><strong>v0.9.5 — one runtime, managed controls, and uninterrupted turns</strong></summary>\n\n**Merged or adapted contributions**\n\n- **[Sh1Zuku / SparkofSpike](https://github.com/SparkofSpike)** — stale cached\n session-title recovery so `New Session` yields to the first real prompt\n- **[Paulo Aboim Pinto / aboimpinto](https://github.com/aboimpinto)** — the\n shared alias-aware command discovery contract across the palette, slash\n completion, dispatch, and acceptance harness\n- **[Sun Zhenyuan / bistack](https://github.com/bistack)** — background\n incremental MCP Registry refreshes that return the local snapshot immediately\n- **[SKY ZHAO / skyzhao1223](https://github.com/skyzhao1223)** — prompt-based\n `model = \"auto\"` tier selection in PR #5257\n\n</details>\n\n<details>\n<summary><strong>v0.9.4 — calmer output, recovery, and release hardening</strong></summary>\n\n**Merged or adapted contributions**\n\n- **[Sh1Zuku / SparkofSpike](https://github.com/SparkofSpike)** — LaTeX\n rendering in PR #4981, circled-digit and keycap width handling in PR #5001,\n and actionable File-edit recovery in PR #5008; sub-agent checkpoint resume\n in PR #5242, real shell wait elapsed time in PR #5240, and keeping\n alternate-scroll off while mouse capture is active in PR #5234\n- **[XhesicaFrost](https://github.com/XhesicaFrost)** — long Windows user-PATH\n preservation in PR #5006\n- **[Paulo Aboim Pinto / aboimpinto](https://github.com/aboimpinto)** — the\n user-command dispatch acceptance contract in PR #4992\n- **[shenjackyuanjie](https://github.com/shenjackyuanjie)** — Windows\n OpenHarmony linker re-quoting for spaced SDK paths in PR #5095\n- **[bistack](https://github.com/bistack)** — MCP Registry discovery with\n Registry-first tool selection in PR #5238\n- **[vFONGv](https://github.com/vFONGv)** — the zh-CN Windows beginner guide\n with screenshots in PR #5229\n- **[mky](https://github.com/mky)** — FreeBSD build fix (`rquickjs` `bindgen` on FreeBSD, PR #5254)\n- **[cacdcaecawae](https://github.com/cacdcaecawae)** — embedder-owned sub-agent state roots (PR #5252)\n\n**Reports, reproductions, and verification**\n\n- **[DracheTek](https://github.com/DracheTek)** — the multilingual, CRLF-heavy\n File-edit failure report in issue #5003\n- **[An Ziwu / MuRongMoQing](https://github.com/MuRongMoQing)** — the Windows\n PATH-overwrite defect report in issue #4685\n\n</details>\n\n<details>\n<summary><strong>v0.9.3 — DeepSeek Responses, canonical tools, and runtime hardening</strong></summary>\n\n- **[greyfreedom](https://github.com/greyfreedom)** (Turisla) — the documented\n and tested authorization-order contract from PR #4980\n- **[nightt5879](https://github.com/nightt5879)** (Nightt) — foreground shell\n detachment before steering from PR #4979\n- **[Watcher24](https://github.com/Watcher24)** — the macOS File Provider\n sandbox report and reproduction in #4085\n- **[Fred Leitz / fleitz](https://github.com/fleitz)** — required\n source-candidate credit for the canonical `Bash` no-`cwd` workspace fix and\n regression in PR #4673 (issue #4674)\n\n</details>\n\n<details>\n<summary><strong>v0.9.2 — routing, workflow, localization, and inspectability</strong></summary>\n\n- **[greyfreedom](https://github.com/greyfreedom)** — exact repository-scoped\n allow grants and the cross-platform path-semantics repairs from PR #4761\n- **[nightt5879](https://github.com/nightt5879)** — off-event-loop terminal\n clipboard writes (PR #4896), complete locale exposure in settings (PR #4856),\n CRLF-safe edits (PR #4942), and automatic-routing reasoning preservation\n (PR #4961)\n- **[XhesicaFrost](https://github.com/XhesicaFrost)** — the configurable\n session-token header from PR #4610 and context-menu hover alignment in PR #4897\n- **[cyq1017](https://github.com/cyq1017)** — the hooks configuration/executor\n split from PR #4087\n- **[snail-vs](https://github.com/snail-vs)** (snailoniu) — OpenCode Zen's\n model-aware route, authentication, documentation, and test-isolation work\n from closed PR #4467, whose contributor commits are preserved in the\n candidate\n- **[Sh1Zuku / SparkofSpike](https://github.com/SparkofSpike)** — the zh-Hans\n translation quality review harvested from PR #4908, duplicate-session fix in\n PR #4963, LaTeX implementation from PR #4973 landed through #4974, and the\n context-menu reproduction in #4803\n- **[GTC2080](https://github.com/GTC2080)** (TaoMu) — the `dryrun` concept\n behind `/preview-request`: preview the next outbound request from the real\n request-building seam rather than a hand-rolled summary (PR #1099,\n harvested)\n- **[h3c-hexin](https://github.com/h3c-hexin)** — the non-UTF-8 `fetch_url`\n report and implementation direction harvested from PR #4909\n- **[Fred Leitz / fleitz](https://github.com/fleitz)** — required\n source-candidate credit for the canonical `Bash` no-`cwd` workspace fix and\n regression in PR #4673 (issue #4674)\n- **[LmeSzinc](https://github.com/LmeSzinc)** — the Windows CRLF `edit_file`\n reproduction, root-cause analysis, and affected-code anchors in issue #4764\n- **[atmosuwiryo](https://github.com/atmosuwiryo)** — ACP numeric-id\n compatibility (PR #4929) and the Indonesian documentation and website locale\n (PRs #4962 and #4972)\n- **[M-Maciej](https://github.com/M-Maciej)** — the expanded-by-default\n reasoning setting and report (PR #4928, issue #4925)\n- **[raphamorim](https://github.com/raphamorim)** — migration of the QA PTY\n frame parser to `rio-vt` (PR #4931)\n- **[LI-Jialu](https://github.com/LI-Jialu)** — truthful finalization of stale\n restored shell cells (PR #4937)\n- **[kobihikri](https://github.com/kobihikri)** — release-container SBOM and\n explicit provenance mode (PR #4958)\n- **[EvanProgramming](https://github.com/EvanProgramming)**,\n **[adity982](https://github.com/adity982)**, and\n **[vibecoding-skills](https://github.com/vibecoding-skills)** — equivalent\n fix direction for effective-home resolution (#4760), MCP call-once behavior\n (#4756), and streaming/non-streaming timeout separation (#4743)\n- **[antarikshraya](https://github.com/antarikshraya)** — the LaTeX transcript\n rendering report in #4957\n- **[eugenicum](https://github.com/eugenicum)** — the token-header request and\n output-presentation measurements in #4520 and #4468\n- **[whp233](https://github.com/whp233)** — the StepFun/OpenCode subscription\n route request in #4526\n- **[redjade75723](https://github.com/redjade75723)** — stale resumed-workspace\n diagnosis input from #4100\n- **[hardy922](https://github.com/hardy922)** — the native DeepSeek tool-token\n visibility report in #3880\n- **[JayBeest](https://github.com/JayBeest)** — contributor-onboarding product\n direction in #4227\n- **[elijahchan2019](https://github.com/elijahchan2019)** — the optional\n reasoning-highlight request in #4089\n- **[cy2311](https://github.com/cy2311)** — sessions rail, archive, and\n auto-resume direction in #2934\n- **[wywsoor](https://github.com/wywsoor)** — export/restore correlation\n direction in #2494\n\n</details>\n\n<details open>\n<summary><strong>v0.9.1 — provider and runtime hardening</strong></summary>\n\n- **[h3c-hexin](https://github.com/h3c-hexin)** — calendar-anchored hourly\n automation recurrence across DST and lifecycle transitions (PR #4381),\n explicit limits for unknown local models (PR #4656 / #4655), and\n idle-timeout progress telemetry (PR #4657)\n- **[gaord](https://github.com/gaord)** — Runtime API provider registry and\n atomic provider-switch endpoints (PR #4658)\n- **[SamhandsomeLee](https://github.com/SamhandsomeLee)** — the unified\n `/skills` root catalog, audit/provenance model, validated mutations, manager\n UI, and acceptance coverage (PR #4679), plus Enter-send lag diagnosis and\n fix direction for #4605 (PR #4654; landed through the release-lane\n async-dispatch split)\n- **[Paulo Aboim Pinto / aboimpinto](https://github.com/aboimpinto)** — the\n Layer 5.1 user-command registry boundary from PR #3278; the exact authored\n evidence commit from PR #4046, preserved intact in the integration graph;\n and the #2870 follow-up audit whose metadata and malformed-sibling gaps\n shaped the final corrections. Paulo also provided the structured, redacted\n Agent Details and `current_activity` design direction preserved from\n #2694/#2889 and the real-PTY lifecycle acceptance direction from #2886\n- **[baendlorel](https://github.com/baendlorel)** — TelecomJS TokenHub provider\n and key-scoped live-catalog direction, source-partitioned refresh behavior,\n and both refresh-order regressions from PR #4370, harvested into the current\n provider architecture with co-authorship preserved\n- **[zhangweiii](https://github.com/zhangweiii)** and\n **[Sterne Lee / sternelee](https://github.com/sternelee)** — the original\n first-class OpenCode Go implementations in PRs #773 and #1050, harvested\n into the current provider architecture with co-authorship preserved in the\n landing commit\n- **[Sean Tse / seanthefuturegorilla](https://github.com/seanthefuturegorilla)**\n — the canonical OpenCode Go/Zen provider request and acceptance direction in\n #1481\n- **[nightt5879](https://github.com/nightt5879)** — `/debt` compatibility\n aliases with dispatch-consistent user-command shadowing across help and\n slash discovery (PR #4680), plus the Solarized Light background preservation\n fix for the underwater shell (PR #4471)\n- **[AiurArtanis](https://github.com/AiurArtanis)** — the Solarized Light\n v0.9.0 regression report and reproduction (#4457)\n- **[shenjackyuanjie](https://github.com/shenjackyuanjie)** — the HarmonyOS\n workflow-js bindgen, portable-pty gating, and SDK environment work\n (PR #4470)\n- **[shenyongqing](https://github.com/shenyongqing)** — the original HarmonyOS\n workflow-js bindgen approach (PR #4384), carried into the landed\n implementation with credit\n- **[Luis Mateus Vargas / luismateusvargas](https://github.com/luismateusvargas)**\n — the Windows hook-process leak reproduction, process-tree analysis, and EOF\n fix direction (#4489)\n- **[redjade75723](https://github.com/redjade75723)** — the persistent Windows\n PTY failure report that exposed lossy high-bit exit-status handling (#4100)\n- **[w1w218](https://github.com/w1w218)** — the Windows ARM64 release request\n and cross-compilation report that led to native release, npm, updater, and\n archive support (#4267)\n- **[stream2stream](https://github.com/stream2stream)** — the legacy-session\n recovery report that led to the read-only doctor diagnostic (#4032, #4539)\n- **[Sh1Zuku / SparkofSpike](https://github.com/SparkofSpike)** — the Windows\n Ctrl+O reproduction that exposed pre-pager result truncation and conflicting\n composer shortcut routing (#4482)\n- **[Lu Shihan / Angel-Hair](https://github.com/Angel-Hair)** — session-owned\n read-before-edit tracking and the explicit, backwards-compatible\n `apply_patch` replacement contract (PRs #4475 and #4476)\n- **[dmitri-0](https://github.com/dmitri-0)** — configurable cache-hit\n visibility in the phase strip (PR #4474)\n- **[Fred Leitz / fleitz](https://github.com/fleitz)** — canonical `Bash`\n no-`cwd` workspace resolution and its regression test, keeping isolated\n sub-agent commands inside their selected worktree (PR #4673; issue #4674)\n- **[Sh1Zuku / SparkofSpike](https://github.com/SparkofSpike)** — exact\n Vim-space regression reproduction and verification that the v0.9.1 input\n path already contains the needed global binding (PR #4477)\n\n</details>\n\n<details open>\n<summary><strong>v0.9.0 — underwater shell, runtime persistence & release evidence</strong></summary>\n\nThe v0.9.0 lane grew from a maintenance sweep into the underwater shell,\nmessage-first Operate, broader Fleet and Workflow behavior, runtime-thread\npersistence, terminal selection, UTF-8 handling, internationalization, and\nrelease evidence. The reports and pull requests below materially shaped it.\n\n- **[Amuthan / amuthantamil](https://github.com/amuthantamil)** — approval-time\n transcript review report that led to live Page Up/Down, modified-arrow,\n Home/End, and mouse-wheel navigation while the decision card stays active\n (#4371)\n- **[Angel-Hair](https://github.com/Angel-Hair)** — reports that restored the\n opt-in `remember` tool to the first-turn catalog, made required user\n confirmation a real goal blocker, and added a clear recovery notice for\n cached approval denials (#4373, #4374, #4375)\n- **[Bruce / bruce6135](https://github.com/bruce6135)** — Kimi Coding Plan\n reproduction that exposed the false 1K emergency-compaction budget (#4368)\n- **[Matt Van Horn / mvanhorn](https://github.com/mvanhorn)** — first-turn\n `remember` regression coverage and the Kimi output-budget correction that\n prevents false emergency compaction (#4377, #4378)\n- **[郝某人BH / hmr-BH](https://github.com/hmr-BH)** — native Simplified\n Chinese review that replaced literal legal/doctrinal metaphors with clear\n collaboration terminology across setup and `/constitution` (#4369)\n- **[WavesMan](https://github.com/WavesMan)** — large-tree `@` completion\n reproduction that led to bounded background discovery and exact-path\n resolution on send (#4365)\n- **[SamhandsomeLee](https://github.com/SamhandsomeLee)** — input-budget-aware\n compaction work harvested from PR #4293 with co-authorship preserved\n- **[idling11](https://github.com/idling11)** — keyboard-driven keyword search\n for the docs and FAQ surfaces (PR #4364)\n- **[LeoLin990405](https://github.com/LeoLin990405)** — initial bounded\n workspace-walk approach and root-cause analysis for large-tree `@` mention\n discovery (PR #4367), harvested into the final fail-soft background\n implementation for #4365\n- **[octo-patch](https://github.com/octo-patch)** — MiniMax Messages provider\n support for MiniMax-M3 and MiniMax-M2.7 across OpenAI-compatible and Messages\n routes, regional endpoints, catalog metadata, pricing, and request coverage\n (PR #4354)\n- **[Wenshan Deng / findshan](https://github.com/findshan)** — original offline\n token/cache/cost scorecard and regression gate (#3388), extended with\n provider-aware provenance in #4335\n- **[hongqitai](https://github.com/hongqitai)** — localization extraction and\n English-locale enforcement that keep UI copy on the typed translation path\n (#4225, #4194)\n- **[nsfoxer](https://github.com/nsfoxer)** — bounded, fail-soft MCP capability\n discovery with advertised/legacy `tools/list` handling and Unicode-safe\n description formatting (#4308, harvested with co-authorship)\n- **[maple / yekern](https://github.com/yekern)** — root-cause analysis and fix\n direction for age-evicting terminal worker records from long-lived\n sub-agent state ledgers (#4217)\n- **[moduvoice](https://github.com/moduvoice)** — Korean (ko) UI locale with\n full key parity and onboarding/setup wiring (PR #4347)\n- **[qinlinwang](https://github.com/qinlinwang)** — Anthropic tool-schema\n sanitization for top-level oneOf/anyOf/allOf failures (PR #4346)\n- **[knqiufan](https://github.com/knqiufan)** — Anthropic cache-write token\n pricing at published rates (PR #4348, #4318)\n- **[Chavdar Ivanov / ci4ic4](https://github.com/ci4ic4)** — NetBSD QuickJS\n bindgen support for codewhale-workflow-js (PR #4349)\n- **[eugenicum](https://github.com/eugenicum)** — copy-paste rail-pollution\n report with code-aware fix direction (#4208)\n- **[JayBeest](https://github.com/JayBeest)** — parent\n `--disallowed-tools` inheritance through sub-agents and Fleet workers,\n harvested from PR #4096 with authorship preserved (#4042)\n- **[wuisabel-gif](https://github.com/wuisabel-gif)** — move runtime-thread\n coordination onto `parking_lot::Mutex` to remove async lock contention\n (#4243, #4149)\n- **[cyq1017](https://github.com/cyq1017)** — Windows Python stdio UTF-8\n handling and richer active-tool summaries (#4281/#4202, #3818)\n\n- **[Jeffrey Luna / Mr-Moon121](https://github.com/Mr-Moon121)** — anti-polling\n constitution for sub-agent waiting (harvested into #4097 / PR #4229 from\n PR #4098)\n\n- **[MXAntian](https://github.com/MXAntian)** — persist compaction summaries\n into thread records so `/v1` engine reloads keep compacted context (#4091)\n- **[nightt5879](https://github.com/nightt5879)** — keep native terminal\n selection usable when mouse capture is disabled, and advance fuzzy edit\n matches on UTF-8 character boundaries; preserve inline skill task text in\n PR #4372 (#4088, #4045, #3915)\n- **[gaord](https://github.com/gaord)** — add the community-maintained\n Codewhale for VS Code GUI frontend to the English and Chinese READMEs (#4035)\n- **[Darrell Thomas](https://github.com/DarrellThomas)** — remove the unused\n whale route taxonomy module and its dead tests (#4041)\n- **[Taixin Guo](https://github.com/taixinguo)** — CJK fuzzy-edit panic report\n and fix direction credited in the UTF-8 boundary patch (#3971, #4045)\n- **[CCChisato](https://github.com/CCChisato)** — preserve task text when\n sending dollar, unified-slash, and explicit skill invocations (#3915,\n co-authored)\n- **[Sun Zhenyuan / bistack](https://github.com/bistack)** — dynamic MCP server\n infrastructure and approval-gated model-started MCP servers from chat\n context (#3869, #3866, harvested with authorship preserved)\n\n</details>\n\n<details>\n<summary><strong>v0.8.66 — release readiness, provider intake & UI hardening</strong></summary>\n\nThe v0.8.66 release prepared the 0.8.66 package lane, hardened provider/model\nrouting and modal surfaces, advanced Hotbar/sub-agent UI reliability, and pulled\nin several community provider and bridge contributions with release credit.\n\n- **[lerugray](https://github.com/lerugray)** — Sakana AI Fugu provider\n support across config, CLI, TUI provider picker, docs, and model completions\n (#3748, harvested)\n- **[greyfreedom](https://github.com/greyfreedom)** — read-only `/config\n ask-rules` view for resolved ask-rule paths, status, and configured\n tool/command/path rules (#3569, merged)\n- **[noaft](https://github.com/noaft)** — `/links` provider docs fallback\n update, including the current Codewhale docs URL and a Baidu Qianfan docs\n link (#3621, harvested)\n- **[noaft](https://github.com/noaft)** — OpenModel provider support across\n config, CLI, TUI provider picker, docs, and registry checks (#3585,\n harvested)\n- **[pkeging](https://github.com/pkeging)** — original plugin manifest,\n discovery, CLI, and MCP foundation (#3708, #3709, #3710, merged), plus WeCom\n Bridge deployment and security documentation, including the approval-timeout\n configuration surface (#3640, harvested)\n- **[codepgq](https://github.com/codepgq)** — cross-client plugin-workflow\n migration request that shaped the explicit compatibility and trust boundary\n (#1172)\n- **[Wenshan Deng / findshan](https://github.com/findshan)** — original offline\n token/cache/cost scorecard and regression gate (#3388), extended with\n provider-aware provenance in #4335\n- **[buko](https://github.com/buko)** — precise Ctrl+O external-editor freeze\n reproduction that shaped the terminal input-pump fix (#3657)\n- **[cyq1017](https://github.com/cyq1017)** — sub-agent progress-event\n headroom report that shaped the fanout reliability hardening (#3783)\n\n</details>\n\n<details>\n<summary><strong>v0.8.63 — sub-agent budgets, command extraction & reliability</strong></summary>\n\n\nThe v0.8.63 release hardened sub-agent fanout with token-budget governance and\nqueue-and-drain admission, split the TUI command surface into focused modules,\nand landed reliability fixes for app-server teardown, JavaScript-execution\nproxying, and DeepSeek thinking tool calls — alongside community contributions.\n\n- **[donglovejava](https://github.com/donglovejava)** — per-worker sub-agent\n token-budget enforcement, so a `token_budget`/`max_tokens` on an individual\n `agent` call bounds that worker mid-run with a clean `budget_exhausted` stop\n (#3321, harvested)\n- **[cyq1017](https://github.com/cyq1017)** — `js_execution` proxy-environment\n handling (#3331), Hugging Face API-key env in the auth probe (#3329), and Codex\n Responses request retry (#3344) — harvested into the train\n- **[aboimpinto](https://github.com/aboimpinto)** — FEAT-005 command extraction:\n core/session command groups split into focused modules via `RegisterCommand`,\n `/swarm` migration, and Gherkin acceptance coverage (#3330, merged literally\n with authorship preserved)\n- **[wuisabel-gif](https://github.com/wuisabel-gif)** — tear down the delegated\n serve/app-server child process when the dispatcher exits (#3259 / #3317)\n- **[nightt5879](https://github.com/nightt5879)** — keep the onboarding marker in\n the codewhale home view (#3302) and branch-hygiene check hardening (#3348)\n- **[gaord](https://github.com/gaord)** — preserve thinking/tool blocks when\n seeding a thread from a saved session, plus Hugging Face provider env (#3329)\n- **[greyfreedom](https://github.com/greyfreedom)** — persist ask-permission rules\n from approvals and stabilize the CI verifier/provider-registry checks\n- Reports that shaped fixes: **[lordwedggie](https://github.com/lordwedggie)**\n (#3331 proxy env), **[Final527](https://github.com/Final527)** (#3240 legacy\n state migration), **[dxfq](https://github.com/dxfq)** (#3228 sidebar default)\n\n</details>\n\n<details>\n<summary><strong>v0.8.62 — provider/model routing, TOML comment preservation & community closeout</strong></summary>\n\n\nThe v0.8.62 release retuned provider/model routing (GLM-5.2 as the default direct\nZ.AI model, `type: \"explore\"` sub-agents defaulting to the cheaper same-family\nsibling), added TOML comment preservation and the Codewhale-only skill discovery\ngate, and shipped the static Linux x64 musl binary — alongside a broad community\ncloseout and a retroactive credit reconciliation pass.\n\n- **[zlh124](https://github.com/zlh124)** — preserve user comments and formatting\n when rewriting `config.toml`/`settings.toml`/`tui.toml` (with a malformed-file\n fallback) and Linux build deps in the cargo install guides (#3270)\n- **[idling11](https://github.com/idling11)** — Kimi `type:object` schema root for\n all parameter shapes (#3281), `approval_mode` restore on Plan→Agent with a\n wait-for-user guard (#3279), and workroom metadata draft types\n- **[LeoLin990405](https://github.com/LeoLin990405)** — Poppler `pdftotext -v`\n detection (#1667), session persistence before stall/cancel recovery (#2739),\n and debounced thinking-stream re-renders (#1620)\n- **[nightt5879](https://github.com/nightt5879)** — Codewhale-only skill discovery\n gate (`[skills].scan_codewhale_only`) ignoring cross-tool directories (#3296) and\n app-server no-auth loopback docs\n- **[reidliu41](https://github.com/reidliu41)** — slash commands exposed as hotbar\n actions (#3269)\n- **[wavezhang](https://github.com/wavezhang)** — static Linux x64 (musl) release\n binaries\n- **[wuisabel-gif](https://github.com/wuisabel-gif)** — per-tool snapshot gate\n respecting `[snapshots].enabled` (#3292) and composer history written under\n `.codewhale`\n- **[gaord](https://github.com/gaord)** — `workspace_follow_symlinks` setting for\n symlink-aware tool operations with hardened path handling\n- **[greyfreedom](https://github.com/greyfreedom)** — ask-permission rules honored\n at runtime (#3295)\n- **[aboimpinto](https://github.com/aboimpinto)** — EPIC-001 command-boundary\n replay and user-registry review feedback\n- **[h3c-hexin](https://github.com/h3c-hexin)** — volatile workspace path moved\n out of the static system prefix (prefix-cache hygiene)\n- **[hongchen1993](https://github.com/hongchen1993)** — heuristic-only auto routing\n when the flash router is unavailable\n- **[lucaszhu-hue](https://github.com/lucaszhu-hue)** — Atlas Cloud provider setup\n docs\n- Retroactive reconciliation (shipped earlier, credited now):\n **[manaskarra](https://github.com/manaskarra)** / **[xfy6238](https://github.com/xfy6238)** (#1157),\n **[djairjr](https://github.com/djairjr)** (#1309 alongside reidliu41),\n **[Geallier](https://github.com/Geallier)** (#1470),\n **[quentin-lian](https://github.com/quentin-lian)** / **[k0tran](https://github.com/k0tran)** (#1531 / #1992),\n **[F1LT3R](https://github.com/F1LT3R)** (#1656),\n **[cmyyy](https://github.com/cmyyy)** (#1842),\n **[Final527](https://github.com/Final527)** (#3058)\n\n</details>\n\n<details>\n<summary><strong>v0.8.61 — runtime control plane & community closeout</strong></summary>\n\n\nThe v0.8.61 release was a community closeout: the runtime control plane, provider\npatches, and TUI fixes landed alongside first-time and returning contributor work.\n\n- **[idling11](https://github.com/idling11)** — DeepInfra provider support with\n OpenAI-compatible routing and model registry entries (#3235, closes #3231)\n- **[greyfreedom](https://github.com/greyfreedom)** — atomic ask-only\n permission-rule persistence so an execution-policy rule survives the write\n that triggered the prompt (#3233)\n- **[VincentCorleone](https://github.com/VincentCorleone)** — WeChat bridge\n (`integrations/weixin-bridge`) leveraging Feishu + Tencent OpenClaw (#3206)\n- **[nightt5879](https://github.com/nightt5879)** — whale-accent rename (#3197)\n and `$skillname` aliases for `/skill` activation (#3241)\n- **[mvanhorn](https://github.com/mvanhorn)** — non-DeepSeek model pricing\n coverage (#3201)\n- **[cyq1017](https://github.com/cyq1017)** — Telegram polling transport\n (#3195) and VS Code read-only API documentation (#3013)\n- **[RobertEmprechtinger](https://github.com/RobertEmprechtinger)** — mobile\n event history (#3220)\n- **[gaord](https://github.com/gaord)** — runtime-API session save (#3199)\n- **[hongchen1993](https://github.com/hongchen1993)** — `DEEPSEEK_BASE_URL` /\n `MODEL` honored in `exec` (#3221)\n\n</details>\n\n<details>\n<summary><strong>Forward track — recent v0.9 work (newest)</strong></summary>\n\n\n- **[xyuai](https://github.com/xyuai)** — canonical Codewhale settings path,\n provider persistence, provider picker, logout-scope, and MiMo auth cleanup\n work (#2730, #2714, #2715, #2717, #2718)\n- **[shenjackyuanjie](https://github.com/shenjackyuanjie)** — HarmonyOS /\n OpenHarmony porting work and MatePad Edge validation trail (#2634)\n- **[ousamabenyounes](https://github.com/ousamabenyounes)** — AZERTY/AltGr\n composer shortcut fix for Windows keyboard layouts (#2863, #2867)\n- **[reidliu41](https://github.com/reidliu41)** — hotbar action-registry\n foundation and Ollama model-completion cleanup for the forward track (#2866,\n #2742)\n- **[ljm3790865](https://github.com/ljm3790865)** — multi-tab\n core/persistence foundation and broader tab collaboration direction (#2864,\n #2753)\n- **[sximelon](https://github.com/sximelon)** — saved-session resume footer\n hint work plus provider-trait metadata registry direction reviewed and\n harvested for the forward track (#2758, #2760, #2479)\n- **[aboimpinto](https://github.com/aboimpinto)** — sidebar command polish and\n pausable custom-command lifecycle direction harvested into the forward track,\n plus the directly merged command-support boundary cleanup and broader command\n layer design direction (#2788, #2732, #2871, #2851, #2791)\n- **[AdityaVG13](https://github.com/AdityaVG13)** — Workflow orchestration\n and cost-tracking drafts that shaped the maintained workflow IR and\n TraceStore foundation (#2482, #2486)\n- **[lbcheng888](https://github.com/lbcheng888)**,\n **[AiurArtanis](https://github.com/AiurArtanis)**, and\n **[nasus9527](https://github.com/nasus9527)** — VS Code extension scaffold\n direction, Agent View request, and IDE plugin request that shaped the\n official Phase 0 extension (#1022, #1584, #2580)\n- **[HUQIANTAO](https://github.com/HUQIANTAO)** — `web_run` cache-state\n lock-splitting, turn-metadata prefix-cache stability, and project-context\n cache work (#2502, #2517, #2636)\n- **[idling11](https://github.com/idling11)** — PlanArtifact continuity,\n dense tool-call transcript collapse, sidebar detail popovers, and\n HarnessPosture provider/model policy direction (#2733, #2738, #2734,\n #2741, #2692, #2694, #2693)\n- **[h3c-hexin](https://github.com/h3c-hexin)** — sub-agent model inheritance,\n configured `skills_dir` discovery, prompt-environment stability, and static\n prompt composer direction (#2736, #2737, #2786)\n- **[gaord](https://github.com/gaord)** — runtime thread workspace updates and\n completed-thread saved-session API work (#2640, #2639)\n- **[cyq1017](https://github.com/cyq1017)** — trusted workspace MCP config,\n provider auth rollback, custom search endpoint, custom completion sound,\n restore-listing, and pending-input delivery-mode label work (#2751, #2755,\n #2510, #2512, #2513, #2532, #2054)\n- **[yusufgurdogan](https://github.com/yusufgurdogan)** — Sofya search\n provider implementation harvested as a non-default search backend (#2790)\n- **[LeoAlex0](https://github.com/LeoAlex0)** — runtime prompt metadata cache\n direction harvested into the maintained prompt/cache path (#2687);\n `allow_shell` prefix-cache decoupling and `visibility=\"internal\"`\n explanation for mode-flip stability (#2949, #2951)\n- **[hongchen1993](https://github.com/hongchen1993)** — Volcengine provider\n in TUI dispatcher and dispatcher API-key preference (#2923, #2928)\n- **[NASLXTO](https://github.com/NASLXTO)** and\n **[wuxixing](https://github.com/wuxixing)** — large-workspace startup\n reports that shaped the bounded project-context fallback (#697, #1827)\n- **[shuxiangxuebiancheng](https://github.com/shuxiangxuebiancheng)**,\n **[hongqitai](https://github.com/hongqitai)**, and\n **[cyq1017](https://github.com/cyq1017)** — third-party\n OpenAI-compatible path-suffix report and follow-up review trail (#1874,\n #2508, #2506)\n\n\n</details>\n\n<details>\n<summary><strong>Recurring & historical contributors</strong></summary>\n\n\n- **[merchloubna70-dot](https://github.com/merchloubna70-dot)** — 28 PRs spanning features, fixes, and VS Code extension scaffolding (#645–#681)\n- **[WyxBUPT-22](https://github.com/WyxBUPT-22)** — Markdown rendering for tables, bold/italic, and horizontal rules (#579)\n- **[loongmiaow-pixel](https://github.com/loongmiaow-pixel)** — Windows + China install documentation (#578)\n- **[20bytes](https://github.com/20bytes)** — User memory docs and help polish (#569)\n- **[staryxchen](https://github.com/staryxchen)** — glibc compatibility preflight (#556)\n- **[Vishnu1837](https://github.com/Vishnu1837)** — glibc compatibility improvements and terminal restoration on SIGINT/SIGTERM (#565, #1586)\n- **[shentoumengxin](https://github.com/shentoumengxin)** — Shell `cwd` boundary validation (#524)\n- **[toi500](https://github.com/toi500)** — Windows paste fix report\n- **[xsstomy](https://github.com/xsstomy)** — Terminal startup repaint report\n- **[melody0709](https://github.com/melody0709)** — Slash-prefix Enter activation report\n- **[lloydzhou](https://github.com/lloydzhou)** and **[jeoor](https://github.com/jeoor)** — Compaction cost reports; lloydzhou also contributed deterministic environment context (#813, #922) and KV prefix-cache stabilisation (#1080)\n- **[Agent-Skill-007](https://github.com/Agent-Skill-007)** — README clarity pass (#685)\n- **[woyxiang](https://github.com/woyxiang)** — Windows install documentation (#696)\n- **[wangfeng](mailto:[email protected])** — Pricing/discount info update (#692)\n- **[zichen0116](https://github.com/zichen0116)** — CODE_OF_CONDUCT.md (#686)\n- **[dfwqdyl-ui](https://github.com/dfwqdyl-ui)** — model ID case-sensitivity compatibility report (#729)\n- **[Oliver-ZPLiu](https://github.com/Oliver-ZPLiu)** — stale `working...` state bug report, Windows clipboard fallback, MCP Streamable HTTP session fixes, and Homebrew tap automation (#738, #850, #1643, #1631)\n- **[reidliu41](https://github.com/reidliu41)** — resume hint, workspace trust persistence, Ollama provider support, thinking-block stream finalization, CI cache hardening, streaming wrap, and DeepSeek model completions (#863, #870, #921, #1078, #1603, #1628, #1601)\n- **[xieshutao](https://github.com/xieshutao)** — plain Markdown skill fallback (#869)\n- **[GK012](https://github.com/GK012)** — npm wrapper `--version` fallback (#885)\n- **[y0sif](https://github.com/y0sif)** — parent turn-loop wakeup after direct child sub-agent completion (#901)\n- **[mac119](https://github.com/mac119)** and **[leo119](https://github.com/leo119)** — `codewhale update` command documentation (#838, #917)\n- **[dumbjack](https://github.com/dumbjack)** / **浩淼的mac** — command-safety null-byte hardening (#706, #918)\n- **macworkers** — fork confirmation with the new session id (#600, #919)\n- **zero** and **[zerx-lab](https://github.com/zerx-lab)** — notification condition config and richer OSC 9 notification body (#820, #920)\n- **[chnjames](https://github.com/chnjames)** — cached @mention completions, config recovery polish, and Windows UTF-8 shell output (#849, #927, #982, #1018)\n- **[angziii](https://github.com/angziii)** — config safety, async cleanup, Docker hardening, and command-safety fixes (#822, #824, #827, #831, #833, #835, #837)\n- **[elowen53](https://github.com/elowen53)** — UTF-8 decoding and deterministic test coverage (#825, #840)\n- **[wdw8276](https://github.com/wdw8276)** — `/rename` command for custom session titles (#836)\n- **[banqii](https://github.com/banqii)** — `.cursor/skills` discovery path support (#817)\n- **[junskyeed](https://github.com/junskyeed)** — dynamic `max_tokens` calculation for API requests (#826)\n- **Hafeez Pizofreude** — SSRF protection in `fetch_url` and Star History chart\n- **Unic (YuniqueUnic)** — Schema-driven config UI (TUI + web)\n- **Jason** — SSRF security hardening\n- **[axobase001](https://github.com/axobase001)** — snapshot orphan cleanup, npm install guards, session telemetry fixes, model-scope cache clear, symlinked skill support, npm mirror-escape-hatch guidance, proxy preservation for child tasks, mobile runtime control, Docker toolbox docs, large-output receipts, and activity detail context (#975, #1032, #1047, #1049, #1052, #1019, #1051, #1056, #1608, #1968, #2296, #2297, #2298)\n- **[MengZ-super](https://github.com/MengZ-super)** — `/theme` command foundation and SSE gzip/brotli decompression (#1057, #1061)\n- **[DI-HUO-MING-YI](https://github.com/DI-HUO-MING-YI)** — Plan-mode read-only sandbox safety fix (#1077)\n- **[bevis-wong](https://github.com/bevis-wong)** — precise paste-Enter auto-submit reproducer (#1073)\n- **[Duducoco](https://github.com/Duducoco)** and **[AlphaGogoo](https://github.com/AlphaGogoo)** — skills slash-menu and `/skills` coverage fix (#1068, #1083)\n- **[ArronAI007](https://github.com/ArronAI007)** — window-resize artifact fix for macOS Terminal.app and ConHost (#993)\n- **[THINKER-ONLY](https://github.com/THINKER-ONLY)** — OpenRouter and custom-endpoint model-ID preservation (#1066)\n- **[Jefsky](https://github.com/Jefsky)** — DeepSeek endpoint correction report (#1079, #1084)\n- **[wlon](https://github.com/wlon)** — NVIDIA NIM provider API-key preference diagnosis (#1081)\n- **[Horace Liu](https://github.com/liuhq)** — Nix package support and install documentation (#1173)\n- **[jieshu666](https://github.com/jieshu666)** — terminal repaint flicker reduction (#1563)\n- **[gordonlu](https://github.com/gordonlu)** — Windows Enter / CSI-u input fix, status picker localization (7 MessageIds), approval dialog localization across 7 locales, and mode picker + composer Vim indicator localization across 7 locales (#1612, #2896, #2891, #2239)\n- **[mdrkrg](https://github.com/mdrkrg)** — first-run onboarding crash fix when the API key is missing (#1598)\n- **[Aitensa](https://github.com/Aitensa)** — CJK wrapping propagation for diff and pager output (#1622)\n- **[qiyan233](https://github.com/qiyan233)** — legacy DeepSeek CN provider alias compatibility (#1645)\n- **[zlh124](https://github.com/zlh124)** — WSL2/headless startup report, clipboard-init fix, Codewhale tab-title polish, localized context-menu labels, and approval-dialog fixes (#1772, #1773, #2319, #2320, #2325)\n- **[aboimpinto](https://github.com/aboimpinto)** — Windows alt-screen\n logging, Home/End composer, runtime log follow-ups, sidebar command polish,\n and pausable command lifecycle work (#1774, #1776, #1748, #1749, #1782,\n #1783, #2788, #2732)\n- **[LeoLin990405](https://github.com/LeoLin990405)** — provider model passthrough, reasoning replay, thinking-only turn, and Windows quoting fixes (#1740, #1743, #1742, #1744)\n- **[nightt5879](https://github.com/nightt5879)** — Ctrl+C prompt restore, provider registry drift docs, tool-search defaults, footer git branch display, and startup prompt interactivity (#1764, #2274, #2344, #2347, #2373)\n- **[donglovejava](https://github.com/donglovejava)** — paste @file consolidation, CJK panic fix, user feedback, RLM routing, edit_file retry, hidden-worktree discovery skip, IME composer routing, and eager shell companion tools (#2154-#2168, #2302, #2329, #2330, #2331)\n- **[encyc](https://github.com/encyc)** — session token breakdown in footer and `/status` (#2152)\n- **[saieswar237](https://github.com/saieswar237)** — review pipeline docs (#2178)\n- **[sximelon](https://github.com/sximelon)** — paste Enter suppression, key handler extraction (#2174, #2042)\n- **[nanookclaw](https://github.com/nanookclaw)** — search provider in doctor output (#2135)\n- **[Sskift](https://github.com/Sskift)** — CLI default env override prevention and statusline footer clearing (#2119, #2248)\n- **[xin1104](https://github.com/xin1104)** — Homebrew codewhale binary install (#2105)\n- **[mrluanma](https://github.com/mrluanma)** — Metaso search provider (#2059)\n- **[Lellansin](https://github.com/Lellansin)** — skip config merge at home dir (#2055)\n- **[zhuangbiaowei](https://github.com/zhuangbiaowei)** — update release channels and legacy MCP SSE fixes (#2145, #2301)\n- **[cy2311](https://github.com/cy2311)** — Windows `.bat` launcher for Codewhale (#1861)\n- **[LING71671](https://github.com/LING71671)** — effective cost currency context, custom provider docs, and core tool taxonomy prompt block (#1902, #2287, #2292)\n- **[dzyuan](https://github.com/dzyuan)** — Volcengine provider support with DeepSeek V4 Pro/Flash models (#1993)\n- **[mvanhorn](https://github.com/mvanhorn)** — live request-shape test factories and global `~/.agents/AGENTS.md` fallback (#2107, #2236)\n- **[malsony](https://github.com/malsony)** — Matrix-inspired theme and theme picker improvements (#2129)\n- **[gaord](https://github.com/gaord)** — external GUI runtime event bridge, session detail serialization, and skills API discovery alignment (#2133, #2265, #2285)\n- **[yuanchenglu](https://github.com/yuanchenglu)** — Feishu per-chat model switching (#2149)\n- **[HUQIANTAO](https://github.com/HUQIANTAO)** — Xiaomi balance/status work, stalled-turn recovery, approval intent summaries, mobile smoke/QR support, Claude theme, and broad docs/test/CI coverage (#2257, #2267, #2283, #2384, #2385, #2389, #2403, #2440-#2458, #2460)\n- **[h3c-hexin](https://github.com/h3c-hexin)** — web-search URL decoding, prompt/instructions override hooks, sub-agent guidance, SSRF fake-IP trust configuration, and prompt-cache-friendly environment placement (#2245, #2311, #2313, #2314, #2354, #2355, #2356)\n- **[tdccccc](https://github.com/tdccccc)** — approval prompt key-detail and shell-preview work harvested into the maintained approval path (#1991, #2269)\n- **[AresNing](https://github.com/AresNing)** — first-run guide, message-submit hook transform design, and turn-end observer hook work harvested into the maintained hooks path (#2278, #2318, #2434, #2578)\n- **[Implementist](https://github.com/Implementist)** — Volcengine Ark search provider and reliability hardening (#2426, #2429, #2439)\n- **[lihuan215](https://github.com/lihuan215)** — Unix socket hook sink design harvested into the opt-in hook event path (#2333, #2430)\n- **[AdityaVG13](https://github.com/AdityaVG13)** — Xiaomi MiMo provider support (#2246)\n- **[New2Niu](https://github.com/New2Niu)** — macOS display notifications (#2260)\n- **[AiurArtanis](https://github.com/AiurArtanis)** — Solarized Light theme and\n canonical-background regression report (#2270, #4457)\n- **[Lee-take](https://github.com/Lee-take)** — task migration and session environment isolation fixes (#2272)\n- **[LeoAlex0](https://github.com/LeoAlex0)** — session persistence fixes for message counts and tool-output cache preservation (#2388, #2395)\n- **[jimmyzhuu](https://github.com/jimmyzhuu)** — Baidu AI Search backend for `web_search` (#2371)\n- **[rockyzhang](https://github.com/rockyzhang)** — RISC-V prebuilt binary support (#2383)\n- **[mo-vic](https://github.com/mo-vic)** — `/purge` slash command for agent-driven context pruning (#2387)\n- **[hufanexplore](https://github.com/hufanexplore)** — Java and Vue language-server defaults (#2367)\n- **[hoclaptrinh33](https://github.com/hoclaptrinh33)** — Vietnamese localization support (#2358)\n- **[AccMoment](https://github.com/AccMoment)** — proxy option for the update command (#2281)\n- **[idling11](https://github.com/idling11)** — durable debt ledger and `/hunt` rename/trophy-card work (#2161, #2306)\n- **[cyq1017](https://github.com/cyq1017)** — runtime event envelope, render-diff debug logging, and deterministic composer history flushing (#2252, #2332, #2375)\n- **[hongqitai](https://github.com/hongqitai)** — state schema parent-entry support and clippy/fmt cleanup (#2308, #2432)\n- **[BryonGo](https://github.com/BryonGo)** — effective-model compaction budgeting fix (#2437)\n- **[xyuai](https://github.com/xyuai)** — provider persistence to config, /logout scope clarification, provider picker key replacement shortcut, MiMo auth state cleanup (#2714, #2715, #2717, #2718)\n- **[RefuseOdd](https://github.com/RefuseOdd)** — configurable `path_suffix` for OpenAI-compatible endpoints (#2558)\n\n\n</details>\n\n<details>\n<summary><strong>v0.8.48 — reports, repros & verification (earliest listed)</strong></summary>\n\nReports, repros, and verification that shaped v0.8.48 also deserve visible\ncredit: **[@buko](https://github.com/buko)**, **[@yyyCode](https://github.com/yyyCode)**,\n**[@gaslebinh-glitch](https://github.com/gaslebinh-glitch)**, **[@Dr3259](https://github.com/Dr3259)**,\n**[@lpeng1711694086-lang](https://github.com/lpeng1711694086-lang)**, **[@VerrPower](https://github.com/VerrPower)**,\n**[@yan-zay](https://github.com/yan-zay)**, **[@jretz](https://github.com/jretz)**,\n**[@Neo-millunnium](https://github.com/Neo-millunnium)**, **[@caeserchen](https://github.com/caeserchen)**,\n**[@T-Phuong-Nguyen](https://github.com/T-Phuong-Nguyen)**, **[@zhyuzhyu](https://github.com/zhyuzhyu)**,\n**[@0gl20shk0sbt36](https://github.com/0gl20shk0sbt36)**, **[@hatakes](https://github.com/hatakes)**,\n**[@goodvecn-dev](https://github.com/goodvecn-dev)**, **[@bevis-wong](https://github.com/bevis-wong)**,\n**[@PurplePulse](https://github.com/PurplePulse)**, and **[@nbiish](https://github.com/nbiish)**.\n\n---\n\n</details>\n\n<details>\n<summary><strong>Reconciled credits — earlier contributors restored to the record</strong></summary>\n\nA credit-reconciliation pass mapped every shipped commit author to a GitHub\nhandle and found these contributors whose merged work was not yet listed here or\nin the changelog. Restoring them with thanks — every one shipped real code.\n\n- **[MoriTang](https://github.com/MoriTang)** — account balance status-bar item, with a request timeout, reused HTTP client, stale-balance-on-failure handling, and DeepSeek-gated display\n- **[mars-base](https://github.com/mars-base)** — session title shown in the composer border and `gh` discovery across common install paths (#836)\n- **[Giggitycountless](https://github.com/Giggitycountless)** — auto-add `.deepseek/` to `.gitignore`, gitignore-check robustness, and `/clear` resetting the Todos panel\n- **[Inference1](https://github.com/Inference1)** — vLLM provider support, README pricing/structure clarity, and the Model Studio reasoning route guard: a fail-closed host check so Alibaba's `enable_thinking` never reaches a custom gateway, plus catching that Codewhale was sending a disable switch to the always-thinking `qwen3.8-max` (#737, #776, #5233)\n- **[membphis](https://github.com/membphis)** — bordered Markdown table rendering and Shift+Enter newline in the composer (#801)\n- **[JasonOA888](https://github.com/JasonOA888)** — `web_run` network-policy enforcement and refusing to snapshot `$HOME` (#798, #800)\n- **[tuohai666](https://github.com/tuohai666)** — recursive skills-directory reading plus hook-dispatch and approval-branch test coverage (#811)\n- **[xuezhaoyu](https://github.com/xuezhaoyu)** — DEC 2026 synchronized-update flicker fix for GPU terminals, guaranteeing `END_SYNC_UPDATE` even when a draw fails\n- **[manaskarra](https://github.com/manaskarra)** — global `~/.deepseek/AGENTS.md` fallback loading (#1157)\n- **[gerryqi](https://github.com/gerryqi)** — wrap fire-and-forget `spawn_blocking` calls with a panic dump (#810)\n- **[ccomma](https://github.com/ccomma)** — skip snapshots for dangerous workspaces (#804)\n- **[AGSaturn](https://github.com/AGSaturn)** — preserve requested model-ID casing in registry resolution (#733)\n- **[wucm667](https://github.com/wucm667)** — accept provider-prefixed DeepSeek model IDs (#794)\n- **[quentin-lian](https://github.com/quentin-lian)** — portable-pty 0.9 upgrade for LoongArch64 support (#1992)\n- **[Beltran12138](https://github.com/Beltran12138)** — treat `deepseek-chat` / `deepseek-reasoner` aliases as reasoning models\n- **[chuntseevolving](https://github.com/chuntseevolving)** — send `TurnStarted` before snapshot to prevent WSL2 timeout\n- **[lawrencewzen](https://github.com/lawrencewzen)** — preserve UTF-8 while stripping ANSI\n- **[hhhaiai](https://github.com/hhhaiai)** — keep workspace skills visible when the prompt budget truncates\n- **[khalid-hungerstation](https://github.com/khalid-hungerstation)** — bundle the delegate skill alongside skill-creator\n- **[Anyexyz](https://github.com/Anyexyz)** — GitHub Actions workflow to sync with the CNB repo\n- **[nightfallsad](https://github.com/nightfallsad)** — clearer `/continue` hint copy\n- **[zxyasfas](https://github.com/zxyasfas)** — align Rust MSRV references with the workspace (#739)\n\n_A follow-up audit of harvested commits (work reimplemented onto a maintainer\nbranch rather than merged) surfaced contributors whose machine-readable credit\nwas dropped. Restoring them here — every one shipped real code:_\n\n- **[CrepuscularIRIS](https://github.com/CrepuscularIRIS)** — OpenHarmony→Linux npm binary mapping, O(1) job-panel refresh, file-mention UTF-8 boundary safety, Kitty keyboard protocol on Windows, and auto low-motion under Termius/SSH (#1479, #1483, #1494, #1495, #1499, #1475)\n- **[MMMarcinho](https://github.com/MMMarcinho)** — `image_analyze` vision tool (#1467)\n- **[MeAiRobot](https://github.com/MeAiRobot)** — toast-stack overlay z-order fix (#1485)\n- **[NorethSea](https://github.com/NorethSea)** — `update` refreshing the sibling TUI binary (#1492)\n- **[SamhandsomeLee](https://github.com/SamhandsomeLee)** — bundled\n v4-best-practices skill (#1448) and input-budget-aware compaction trigger\n (#4293)\n- **[YaYII](https://github.com/YaYII)** — opt-in `/translate` command (#1462)\n- **[sandofree](https://github.com/sandofree)** — Tavily and Bocha `web_search` backends (#1294)\n- **[tiger-dog](https://github.com/tiger-dog)** — approval one-line banner and Markdown underscore handling (#1455)\n- **[Jianfengwu2024](https://github.com/Jianfengwu2024)** — preserving MSVC toolchain vars in the child environment (#1487)\n- **[wplll](https://github.com/wplll)** — prompt-cache warmup keys, tool-catalog handling, a dedup test, and pack ordering (#2390, #2391, #2392, #2393)\n\nAdditional harvested PRs from the same audit, credited to contributors already\nlisted above: **[axobase001](https://github.com/axobase001)** (#2400, #2405, #2406, #2407, #2408, #2415), **[cyq1017](https://github.com/cyq1017)** (#2516, #2534, #2540), **[Oliver-ZPLiu](https://github.com/Oliver-ZPLiu)** (#1451, #1456), **[reidliu41](https://github.com/reidliu41)** (#1444, #1493), **[lucaszhu-hue](https://github.com/lucaszhu-hue)** (#1436, #2343), **[h3c-hexin](https://github.com/h3c-hexin)** (#1480), **[Duducoco](https://github.com/Duducoco)** (#1345), **[zhuangbiaowei](https://github.com/zhuangbiaowei)** (#1416), **[wdw8276](https://github.com/wdw8276)** (#1498), and **[buko](https://github.com/buko)** (#2377).\n\n_A further machine-credit pass restored these contributors, missing from both the\nlist above and the contribution graph (AUTHOR_MAP entries added; logins/IDs\nverified against the GitHub user API) — every one shipped real code:_\n\n- **[1Git2Clone](https://github.com/1Git2Clone)** — `Ctrl+P`/`Ctrl+N` slash-menu navigation\n- **[rockeverm3m](https://github.com/rockeverm3m)** — community ACP adapter reference in the docs\n- **[hxy91819](https://github.com/hxy91819)** — stable MCP tool ordering for prefix-cache stability (#1319)\n- **[heloanc](https://github.com/heloanc)** — Home/End keys moving the cursor in the input box (#1246)\n\n**Earlier reports, proposals and reviewed overlap**\n\n- **[dajiaohuang](https://github.com/dajiaohuang)** — independently diagnosed the local-model output-budget problem and submitted a reviewed patch; equivalent behavior had already shipped ([#5883](https://github.com/codewhale-hq/Codewhale/pull/5883)).\n- **[Edouard-Legoupil](https://github.com/Edouard-Legoupil)** — proposed user-controlled skill evolution from repeated work and corrections ([#5860](https://github.com/codewhale-hq/Codewhale/issues/5860)).\n- **[freedomlovesfrank](https://github.com/freedomlovesfrank)** — proposed an optional Rust allocator alongside the existing default ([#5872](https://github.com/codewhale-hq/Codewhale/issues/5872)).\n- **[zyxrealhero](https://github.com/zyxrealhero)** — reported malformed tool-call history after a Runtime restart, with a reproduction and payload analysis ([#5823](https://github.com/codewhale-hq/Codewhale/issues/5823)).\n- **[cmdcorp6534](https://github.com/cmdcorp6534)** — reported Chinese IME pre-edit and composer behavior in Windows Terminal, including input shown behind modal dialogs ([#2323](https://github.com/codewhale-hq/Codewhale/issues/2323)).\n- **[ivysrono](https://github.com/ivysrono)** — proposed switching to a configured backup profile after provider rate-limit errors ([#855](https://github.com/codewhale-hq/Codewhale/issues/855)).\n- **[JoeKerF](https://github.com/JoeKerF)** — proposed desktop project and task workflows with managed runtime startup, recovery, and updates ([#4986](https://github.com/codewhale-hq/Codewhale/issues/4986)).\n- **[ShikiSuen](https://github.com/ShikiSuen)** — provided a detailed reproduction of SwiftPM test failures inside the macOS sandbox ([#2617](https://github.com/codewhale-hq/Codewhale/issues/2617)).\n\n</details>\n\n---\n\nMissed someone? Open an issue or PR — credit is kept current, and names are happily added. See [CONTRIBUTING.md](../CONTRIBUTING.md) to get started.\n"
|
||
},
|
||
{
|
||
"path": "docs/RELEASE_CHECKLIST.md",
|
||
"sha256": "07893b226a5605e524fdb5389b9e2c56746aff80562c3de2be4c092990020ed1",
|
||
"content": "# Release Checklist\n\nFollow this sequence from the canonical `/Volumes/VIXinSSD/CW/codewhale`\ncheckout on `main`. Agent-owned preparation lands as verified slices on `main`;\nit requires no release branch, worktree, nested clone or preparation PR.\nContributor PRs still land as PRs. Treat any unchecked required gate as a release\nblocker.\n\nFor deeper context on the underlying tools (preflight scripts, npm smoke,\npublish-crates), see [`RELEASE_RUNBOOK.md`](RELEASE_RUNBOOK.md).\nFor larger milestone releases, add any version-specific acceptance matrix to\nthe existing release packet before tagging; use it for provider routes, feature\ngates, GUI/runtime smoke, remote-workbench decisions, and credit hygiene that the\ngeneric checklist does not enumerate. Record its candidate SHA, owners, local\ncheck results, hosted run URLs, artifact inventory, install/provider receipts,\nknown limitations and publication approval. These proofs are separate.\n\n## 0. Reconcile the canonical checkout\n\n- [ ] Inspect the branch, status, upstream and alternate checkouts before editing:\n ```bash\n git branch --show-current\n git status --short --branch\n git log -5 --oneline\n git worktree list\n git fetch origin main\n git log --left-right --oneline main...origin/main\n ```\n- [ ] The canonical checkout is on `main`. Recover stranded work only after\n preserving dirty/untracked files and comparing commits with current\n `origin/main`. Keep a recoverable backup and unresolved inventory; do not\n reset away work, stash it indefinitely or create another development lane.\n- [ ] Uncommitted work found in the checkout is yours (founder, 2026-10-22):\n read it, prove it, and commit it by explicit path. A file changed in the\n last 15 minutes or held by a live session is mid-edit: ask that writer\n to commit first. Check status before staging; never `git add -A`.\n Historical branches/checkouts are inventoried;\n deleting them requires explicit Hunter approval even if a helper calls\n their tips safe to delete.\n\n## 1. CHANGELOG entry exists for the version\n\n- [ ] `CHANGELOG.md` has a `## [X.Y.Z] - YYYY-MM-DD` heading at the top\n- [ ] The entry credits every external contributor, harvested PR author,\n linked issue reporter, reproduction/log provider, reviewer, and\n verification helper whose work materially shaped this version. Get the\n commit list with:\n ```\n git log vPREV..HEAD --no-merges --format=\"%h %an <%ae> %s\" \\\n | grep -v '<your-email@…>'\n ```\n For each contributor, link both their display name and (when known)\n `@github-handle`. Then inspect linked issues and harvested PRs so\n reporters/helpers are not lost just because they did not author commits.\n Keep `docs/CONTRIBUTORS.md` and `web/lib/release-credits.ts` consistent.\n- [ ] The entry uses the Keep a Changelog headers — `Added`, `Changed`,\n `Fixed`, `Security`, `Removed`, `Deprecated`. Add `Known issues` only\n if there is something material the user must work around.\n- [ ] The entry mentions all referenced issue/PR numbers as `#NNNN` so the\n auto-linker on GitHub picks them up.\n- [ ] Run `scripts/sync-changelog.sh` to regenerate `crates/tui/CHANGELOG.md`\n (the recent-releases slice embedded in the binary for `/change`). Do\n not edit that file by hand, and do not copy the full root changelog\n into it — older entries live in `docs/CHANGELOG_ARCHIVE.md`.\n- [ ] Run `scripts/release/check-feature-release-notes.sh vPREV HEAD`. Every\n issue-linked `feat` commit must leave a receipt in `CHANGELOG.md` or the\n archive; the Version drift CI gate runs the same check with full history.\n\n## 2. Version pins are in sync\n\n- [ ] Use `./scripts/release/prepare-release.sh X.Y.Z` when version pins or\n generated sources need preparation. Reuse a valid preparation receipt\n when those sources are already current. The helper bumps the\n workspace version, every per-crate dependency pin, the npm wrapper\n (`version` + `codewhaleBinaryVersion`), Runtime SDK, VS Code extension\n and lock, remote-smoke default, public source-candidate facts, and README\n install-tag examples; it refreshes the Cargo/npm locks, regenerates\n `crates/tui/CHANGELOG.md` and `web/lib/facts.generated.ts`, and ends\n by running the version and OHOS gates. Write the CHANGELOG entry\n **before** running it. The helper is safe to rerun at the requested\n workspace version; it skips replacements but refreshes both generated\n files and reruns the gates.\n- [ ] `npm/deepseek-tui/package.json` remains private/compatibility-only and\n is **not** bumped or published.\n- [ ] `./scripts/release/check-versions.sh` reports\n `Version state OK: workspace=X.Y.Z, npm=X.Y.Z, npm-binary=X.Y.Z, lockfile in sync.`\n- [ ] `./scripts/release/check-ohos-deps.sh` reports that the OpenHarmony\n Windows linker keeps the target/sysroot flags, the target enables the\n `rquickjs-sys` bindgen edge, and its graph does not pull the unsupported\n `nix` 0.28/0.29, `portable-pty`, `starlark`, `arboard`, or `keyring`\n crates.\n\n## 3. Verify and land preparation slices\n\n- [ ] Run the existing checks appropriate to each changed surface once; batch\n coherent Rust edits before compiling. During implementation, use the\n smallest local check or running-product observation that proves the\n change. After implementation and product review, freeze the candidate\n commit and run final validation once, including\n `npm test && npm run check:web` from the repository root. Hosted CI owns\n exhaustive workspace and platform coverage at that final step. Rerun\n only checks invalidated by a fix. Record actual results and failures in\n commit messages.\n- [ ] Commit verified owned paths directly to `main` and push normally. Resolve\n push rejection with the other writer while preserving dirty work; never\n force-push. Reuse valid receipts instead of rebuilding merely to commit\n or push. Existing contributor PRs retain their review and merge process.\n- [ ] The live milestone and PR queue no longer contain work intended for this\n version:\n ```\n gh issue list --repo codewhale-hq/CodeWhale --milestone \"vX.Y.Z\" --state open\n gh pr list --repo codewhale-hq/CodeWhale --state open --limit 100\n ```\n- [ ] Any remaining same-theme work is explicitly retargeted to a later\n version or called out as a known issue. Do not freeze/tag while still\n planning to merge more same-version fixes.\n- [ ] The release tag does not already point at an older source SHA, or the\n maintainer has deliberately chosen to publish exactly that older SHA:\n ```\n git ls-remote origin refs/heads/main refs/tags/vX.Y.Z\n gh release view vX.Y.Z --repo codewhale-hq/CodeWhale\n ./scripts/release/check-published.sh X.Y.Z\n ```\n- [ ] If `vX.Y.Z` exists with no GitHub Release/packages and `main` has moved\n on, stop. Choose one of: publish the existing tag as-is, bump the later\n work to the next patch version, or explicitly approve deleting/recreating\n the unpublished tag. Publishing older source after canonical `main` has\n advanced needs a separately authorized recovery plan; do not silently\n move tags or switch the shared checkout. An absent release/unpublished\n packages are expected at this stage, not permission to alter a tag.\n- [ ] Freeze the final source: all intended work is committed and pushed;\n clean local `HEAD`, `main` and freshly fetched `origin/main` agree.\n Record the full 40-character SHA/version and coordinate with shared\n writers to preserve that source through packaging and publication.\n- [ ] `./scripts/release/check-versions.sh --require-dated-release` and\n `python3 scripts/check-contributor-credit.py` pass for the candidate.\n- [ ] `./scripts/release/publish-crates.sh dry-run` passes for the frozen\n candidate. Cargo 1.90+ verifies every release tarball and its real size\n without uploading. CI/RC does not replace this check. Retain the receipt;\n do not repeat it for unchanged source just to commit or push. Publish mode\n necessarily verifies the packages again before uploading.\n\n## 4. Exact-SHA full CI and release candidate\n\n- [ ] Dispatch full CI and the build-only release candidate against frozen\n `main` with the same independent SHA guard:\n ```bash\n candidate_sha=\"$(git rev-parse HEAD)\"\n gh workflow run ci.yml --ref main -f expected_sha=\"${candidate_sha}\"\n gh workflow run release-candidate.yml --ref main -f expected_sha=\"${candidate_sha}\"\n ```\n- [ ] Both runs resolve to that SHA and all required jobs execute and succeed.\n A green workflow badge alone is insufficient: ordinary main-push CI can\n delegate Linux work to CNB and omit some smoke coverage. Manual full CI\n forces the release gates on Linux, macOS and Windows.\n- [ ] The candidate's Parity job succeeds, and its artifact jobs report all eight\n targets and the complete 38-file asset inventory, including Android\n arm64, Windows arm64, native HarmonyOS arm64, `codew`, the NSIS installer,\n archives, checksum manifests, and eight compatibility-only\n `codewhale-tui-*` release filenames that are not installed commands. The\n packed npm wrapper installs against those assets and runs its delegated\n entrypoints. Keep run URLs and\n required evidence before the Actions artifacts expire; these are not a\n public release.\n- [ ] Windows static import receipts cover x64/ARM64 CLI, shim and compatibility\n assets, any optional host, and the NSIS setup plus its actual embedded PE\n plugins. Verify the selected architecture profile and its conservative\n limits in [ENVIRONMENTS.md](ENVIRONMENTS.md#windows-support-and-release-qualification).\n Preserve the generated candidate Uninstall.exe import/SHA receipt. Static\n import/profile PASS does not replace final release startup/install on the\n promised oldest x64 OS, native ARM64 package acceptance, or packaged/native\n app owner-crash recovery. Record or explicitly resolve these remaining\n runtime gates before tagging.\n- [ ] Complete the version-specific acceptance packet. Cross-building Android\n does not prove a real Termux/device session. Local install, actual provider\n calls and customer acceptance need separate receipts; provider spend still\n requires authorization. Required unproven acceptance blocks release;\n disclose an explicitly accepted limitation as such.\n- [ ] If source changes or `main` moves before tagging, deliberately choose the\n new candidate and replace affected proofs. Tag only a SHA with its exact\n CI/RC evidence. Do not duplicate hosted exhaustive gates locally.\n\n## 5. Publication approval\n\n- [ ] Hunter explicitly approves the exact version/SHA and publication targets:\n tag, GitHub Release, npm, GHCR, CNB release tag and legacy Homebrew\n automation, plus Cargo or other downstream publication where intended.\n Source readiness and build-only CI/RC do not authorize a tag, registry\n write, deployment or public launch.\n\n## 6. Tag and publish the approved source\n\n- [ ] Fetch `origin/main` again and verify that clean canonical `main`,\n `origin/main` and the approved SHA still agree.\n- [ ] The release source is reachable from `main`:\n `./scripts/release/ensure-release-on-main.sh HEAD`\n- [ ] Create `vX.Y.Z` from the final `main` SHA using the **Create release tag**\n workflow, or create and push a signed local tag:\n `git tag -s vX.Y.Z -m \"vX.Y.Z\" && git push origin vX.Y.Z`\n Tagging can start public automation immediately. If no run appears,\n first exclude a queued/active tag-triggered run, then dispatch only the\n existing tag: `gh workflow run release.yml --ref vX.Y.Z -f version=X.Y.Z`.\n Do not dispatch `release.yml` from `main` or start duplicate runs.\n- [ ] The Release workflow succeeds: it repeats parity/build gates, verifies\n the draft asset inventory before GitHub publication, then runs dependent\n npm, GHCR, CNB and Homebrew publication. It requires an exact-SHA green\n RC receipt and does not publish Cargo crates.\n- [ ] The public GitHub Release assets are proven to match the tag commit\n before publishing Cargo or npm:\n ```\n git fetch origin tag vX.Y.Z\n ./scripts/release/verify-release-assets.sh X.Y.Z\n ./scripts/release/require-release-tag-checkout.sh X.Y.Z\n ```\n This checks the local tag, remote tag, successful Release workflow SHA,\n full binary/archive/installer asset set, and both checksum manifests. If\n it fails, stop publication and follow the runbook's recovery gates.\n Publish from the same clean canonical checkout on `main` when `HEAD`\n matches the immutable remote tag. Do not create a detached worktree,\n switch release lanes or force-fetch a conflicting tag. If `main` advanced,\n coordinate with Hunter for a new candidate or separately authorized\n recovery plan.\n- [ ] Publish Cargo manually within the approved scope with the\n [runbook's step 5 block](RELEASE_RUNBOOK.md#rust-crates-release), which\n runs `./scripts/release/publish-crates.sh publish` from a new, empty\n `CARGO_HOME` and build directory with the token in `CARGO_REGISTRY_TOKEN`. It\n verifies every package, skips already-published versions and waits for\n each new dependency to appear. npm normally uses Trusted Publishing;\n manual recovery keeps the same clean-source and public-asset guards.\n\n## 7. Verify public results\n\n- [ ] The live GitHub Release body has its own `## Contributors` or\n `## Credits` section; do not rely on \"see CHANGELOG\" alone. Verify with:\n ```\n gh release view vX.Y.Z --repo codewhale-hq/CodeWhale --json body \\\n --jq '.body | test(\"## (Contributors|Credits)\")'\n ```\n- [ ] `npm view [email protected] version codewhaleBinaryVersion --json`\n reports the new version on the npm registry.\n- [ ] `npm view deepseek-tui deprecated` is non-empty. The legacy npm package\n is deprecated and must not receive an `X.Y.Z` publish.\n- [ ] Distribution channels are canonical-first: the website install page\n (codewhale.net/install) shows Codewhale-native commands first (`npm install -g\n codewhale`, `curl .../install.sh | sh`); Homebrew is labeled as legacy\n compatibility; the shell installer uses codewhale-native names as documented\n in `docs/REBRAND.md#homebrew`.\n- [ ] Every release crate has the new version on crates.io; there is no\n automated Cargo publish job. Partial publication is incomplete.\n- [ ] `ghcr.io/codewhale-hq/codewhale:vX.Y.Z` and `:latest` are updated.\n- [ ] The CNB release tag and approved downstream results are verified\n separately; a successful GitHub Release does not prove them.\n- [ ] The final registry verification passes:\n ```\n ./scripts/release/check-published.sh X.Y.Z\n ```\n- [ ] Published install/upgrade smoke has its own receipt; candidate installs\n do not prove public delivery. Deployment/customer acceptance remain\n separate gates where required.\n\n## 8. Handoff and tracking\n\n- [ ] Edit the GitHub release notes to expand any CVE-style or attack\n details within the approved publication scope.\n- [ ] Re-run the GitHub Release body check after any release-workflow rerun;\n workflows can overwrite notes and accidentally remove contributor credit.\n- [ ] Note any deferred items in the next release's tracking issue.\n- [ ] Close any issues that this release fixed.\n- [ ] Update existing tracking with evidence and follow the current contributor\n communication rules in [AGENTS.md](../AGENTS.md). Handle the existing\n `sync-release-record` workflow's\n metadata proposal after publication: it currently opens a bot PR, not a\n required preparation branch. A source record does not prove deployment.\n- [ ] Report canonical path, branch, SHA, upstream state and dirty/stranded\n work. Historical branch/checkout removal needs explicit cleanup approval\n and does not block a release whose source is already reconciled.\n\n---\n\nIf a step fails, **fix the underlying cause** rather than skipping it. Pre-commit\nhooks, signing, and CI are all here to catch real problems. Do not skip hooks or\nsigning, force-push shared refs, replace public assets or move release tags to\nmake the checklist look green.\n"
|
||
}
|
||
]
|
||
},
|
||
{
|
||
"id": "vocabulary",
|
||
"slug": "vocabulary",
|
||
"sources": [
|
||
{
|
||
"path": "docs/FLEET.md",
|
||
"sha256": "10a8c50860305920357042c95fb1c471696a68cc8fd219e571157563e8494654",
|
||
"content": "# Agent fleet\n\n> Baca terjemahan bahasa Indonesia: [id/FLEET.md](id/FLEET.md)\n\nAgent fleet is the local-first roster and member-selection layer for durable\nmulti-worker runs. It does not execute or authorize work. After fleet resolves\nwho should participate, the delegated coordinator launches a headless\n`codewhale exec` run and the Runtime tracks it durably. See\n[AGENT_RUNTIME.md](AGENT_RUNTIME.md) for how sub-agents, `exec`, and\nfleet-backed workers converge on one runtime. In product language, a user may\nstill \"open a sub-agent\"; in architecture language, durable nested work uses a\nfleet member identity with delegated runtime execution.\n\n## Naming and compatibility boundary\n\n**Fleet** is the public product noun. The durable ledger, saved rosters, config\ntables, and `--fleet` flag share that name:\n\n| Surface | Canonical |\n| --- | --- |\n| CLI | `codewhale fleet …` |\n| Slash command | `/fleet …` |\n\nThese shared names are load-bearing wherever changing them would break\nexisting workspaces, receipts, or scripts:\n\n- the durable ledger `.codewhale/fleet.jsonl` and the log directories\n `.codewhale/fleet/` and `.codewhale/fleet-host/`;\n- saved rosters `fleets/<name>.toml` and their `schema = \"fleet\"` header, under\n `$CODEWHALE_HOME/` or the workspace's `.codewhale/` (checked-in rosters at the\n workspace root's `fleets/` are still read);\n- the `[fleet]` config table (inline `[fleets.*]` tables were removed in 0.9.14; named fleets live in `fleets/<name>.toml` files);\n- the `codewhale workflow run --fleet <name>` flag;\n- wire, receipt, and control-plane operation ids such as `fleet.status`.\n\nThe rest of this document uses fleet as the public product noun and retains\nthese literal paths, keys, flags, and ids.\n\nUse a fleet roster rather than anonymous short-lived `agent` fanout whenever a\ndelegated run needs stable member identities across retries, sleep/restart,\nremote execution, receipts, or a ledgered audit trail. The initial CLI surface\nis:\n\nFor a guided start-to-monitor walkthrough that combines fleet task specs with\nWorkflow authoring, see [fleet + Workflow Tutorial](FLEET_WORKFLOW_TUTORIAL.md).\n\n```sh\ncodewhale fleet init\ncodewhale fleet run tasks.json --check # validate only; nothing is created or launched\ncodewhale fleet run tasks.json --max-workers 4\ncodewhale fleet status\ncodewhale fleet inspect <worker-id>\ncodewhale fleet logs <worker-id>\ncodewhale fleet artifacts <worker-id>\ncodewhale fleet interrupt <worker-id>\ncodewhale fleet restart <worker-id>\ncodewhale fleet resume <run-id>\ncodewhale fleet stop --all\n```\n\n`codewhale fleet resume <run-id>` is the restart-recovery verb: it replays the\nledger, reconciles any in-flight lease whose worker stopped heartbeating\n(retrying within the task's budget, else failing and escalating per the alert\npolicy), and prints the post-resume status. It launches no new work and is\nidempotent, so it is safe to run after a manager exit, laptop sleep, or runtime\nrestart.\n\nCoordinator state for fleet-backed runs is stored under the workspace in\n`.codewhale/fleet.jsonl`. Worker logs and adapter logs are stored under\n`.codewhale/fleet/` and `.codewhale/fleet-host/`.\n\n## Public contract: identity, membership, and selection\n\n**Fleet** = the user's model inventory: who is in the roster and which member is selected.\n\nA public fleet identity consists only of:\n\n- a stable member id and an optional user-facing name;\n- a semantic role, such as `explore`, `implement`, or `reviewer` (the legacy\n spellings `worker`, `scout`, `builder`, `verifier`, `consultant`, and `oracle`\n are still accepted on input and map to `general`, `explore`, `implement`,\n `test`, and `advisor`);\n- an exact provider/model identity, or an explicit inherited route;\n- visible roster state or origin.\n\nProject or workspace trust, filesystem and network reach, secret access,\napproval mode, sandboxing, tool authorization, and every other form of runtime\nauthority are separate delegated-coordination and Runtime policy inputs. They\nare never fleet identity fields, and they never select or reroute a fleet\nmember. The Runtime applies and clamps those policies only after member\nselection; if the selected member cannot run inside the effective envelope,\nlaunch fails closed instead of choosing somebody else.\n\nNatural-language member selection is deterministic. A caller may name:\n\n- an exact member id, optionally as `member:<id>` or `id:<id>`;\n- a unique user-facing member name, optionally as `name:<name>`;\n- a unique semantic role, for example `explore` or `role:explore`;\n- an exact pinned model id, for example `deepseek-v4-flash`, or its offline\n display name, for example `DeepSeek V4 Flash`; or\n- an exact `route:<provider>/<model>`.\n\nAn unqualified exact member id wins. Every other match succeeds only when it\nidentifies one distinct roster member. Multiple matches produce an ambiguity\nerror that names the candidates and asks for `member:<id>`; Codewhale never\npicks whichever match happened to be listed first. Users do not need to know\nan internal role label such as `explore`: a unique member name, display model, or\nexact model id is equally valid. Saved v2 fleets store that optional human name\nas `display_name` (the input alias `name` is also accepted); it must be one\ntrimmed printable line of at most 80 characters.\n\n### Your fleet as models\n\nThe same fleet file answers a third question: **which models has this person\nput in their fleet?** Every exact `provider` + `model` pin in the selected\nfleet — the operator route, each pinned member, and each explicitly marked\nshortlist row — is a fleet model. Executable member rows supply the roles it\nfills; a shortlist row has no role. All remain in the same fleet file. Legacy\nmembers that omit `role` still use their id as the role identity.\nShortlist rows carry only model choices; reasoning, instructions, and capability\nrequirements belong on executable role members and are refused on shortlist rows.\n\n- `/fleet models` prints the fleet: `provider/model · roles · price · context ·\n tools`, facts read from the model catalog. With no selected fleet the line\n reads \"Your fleet is the session model only\".\n- `/fleet add <provider> <model> [role…]` adds a model (one member row per\n role, or one `shortlist = true` row for a role-less add). The provider must be one you configured\n and, when the catalog knows the provider, must serve that exact id.\n A role member asked to run the fleet's own operator route inherits it\n instead of pinning — the role follows when the operator moves; a pin on\n any other route is the deliberate opt-out. Files that already pin the\n operator route are read as inheritance.\n With no fleet selected, a user-global fleet named `My fleet` is created and\n selected first. `/fleet remove <provider> <model>` drops every row that pins\n the route; the operator route is changed with `/fleet save`, not removed.\n- In `/model`, `⇧F` adds or removes an explicit shortlist row for that exact\n route. It preserves saved role pins and the operator route;\n fleet models are listed first, labelled `fleet · <roles>`, ahead of your\n own `⇧P` pins and the provider lists. `/models` prints the fleet before the\n provider's list.\n\nThe operator model reads this list when it assigns sub-agents (design\n`MODEL-ROUTING-CATALOG-20260901.md` §10, slice F2).\nThe model-facing roster resolves roles through the same route admission code\nas a start. Explicit profiles and manual role pins remain authoritative; a\nunique saved role pin also applies to a start naming only its role. Task model\nchoices are available for unpinned roles, constrained to the selected models\nplus the session route. Shortlist model rows disclose their own exact route\nindependently of any role pin.\n\n### Interactive and persistent status\n\n`/fleet status` and `codewhale fleet status` are the **same** command on two\nsurfaces. Both read the durable `.codewhale/fleet.jsonl` ledger for the\nworkspace, through one shared control-plane contract, and both report the same\nverb id (`fleet.status`), read-vs-write authority, persistence scope, and\nreceipt. When the workspace has no ledger they say so with a typed reason\n(`no_fleet_ledger`) instead of rendering an empty-looking \"all clear\" — and\nneither creates the ledger as a side effect of reading it.\n\nThe current interactive session's sub-agents are a **different set**, and now\nhave their own name:\n\n- `/fleet workers` (or `/subagents`, or Tab / `w` from the `/fleet` roster)\n shows sub-agents attached to the current TUI session. It does not read the persistent ledger.\n- `/fleet list|status|interrupt|resume` and `codewhale fleet\n list|status|interrupt|resume` act on the durable ledger.\n- `codewhale fleet restart <worker-id>` is CLI-only: it re-leases the task and\n then drives the manager loop to completion. `/fleet restart` does not\n silently do a smaller thing — it reports `surface_not_supported` and names\n the CLI command.\n\nThe contract behind this — descriptors, availability reasons, exact-identity\ntargets, receipts, typed unknowns, and bounds — is documented in\n[`docs/COMMAND_CONTROL_PLANE.md`](COMMAND_CONTROL_PLANE.md).\n\n## Authoring agent profiles (`/fleet setup`)\n\nAgents: the durable artifact is the profile TOML described below; the\nkey-by-key walkthrough is the human interactive path.\n\n`/fleet setup` (also `/fleet setup edit` / `new`) opens an in-TUI wizard for\nauthoring a reusable agent-team profile. Bare `/fleet` and the\n`roster`/`roles`/`profiles`/`party` aliases open the selected fleet's member\nroster. `/fleet saved` opens the named saved-fleet picker. `/fleet workers` opens the\ncurrent-session worker view; `/subagents` is a\ncompatibility shortcut for that view. For durable run history, use\n`/fleet status` or the shell command `codewhale fleet status` described above —\nthey are the same command.\n\nThe wizard is progressive: you make one focused choice at a time — a **role**,\nthen a **model** (`inherit`, or a concrete model from *any configured\nprovider*, not only the one the parent session is currently using), then\n**where the profile lives**, and finally a **review** of the member identity\nand route. When the review also previews thinking, tools, approvals, or another\nexecution control, those rows summarize separate Runtime policy; they do not\nbecome fleet identity or member selectors. The header shows \"Saves to: …\" on\nevery step — the choice you still have to make, or the exact resolved file\nonce you have made it. Nothing is written until you activate the save control\non the review step.\n\nThe **Destination** step is a focused two-option list:\n\n- **This project** writes `<workspace>/.codewhale/agents/<role>.toml`. It\n applies to this project only and takes precedence over a Personal profile\n with the same id. When project profiles are disabled for the session\n (`--no-project-config`) or the workspace folder is unavailable, the option is\n shown disabled with that reason; the wizard never falls back to Personal on\n its own.\n- **Personal** writes `$CODEWHALE_HOME/agents/<role>.toml` and is available in\n every project on this machine, except where a project has its own profile\n with the same id.\n\nFor the highlighted option the step shows the exact file, whether saving would\ncreate a new file or **replace an existing one**, and the precedence\nconsequence for the roster. The review step repeats those facts under\n\"Saves to\" and names the final action by its effect — **Save to this\nproject**, **Save as Personal profile**, or **Replace …**. Replacing an\nexisting file asks for a second confirmation on the save control. Reopening a\nsaved member from `/fleet` starts from what is on disk: its member identity,\nroute, and save scope. Thinking (`inherit`, `off`, `low`, `medium`, `high`,\n`max`, or `auto`) is adjusted on the review step, but remains a route\nexecution setting rather than part of the member's fleet identity.\n\nProfile scope controls where a role definition is reusable; it does not widen\nthe authority of a running operation and is not a project-trust setting. To\ncoordinate several nearby repositories, start Codewhale from their shared\nparent directory so that parent is the workspace. Project/workspace trust,\nexternal paths, filesystem and network reach, secrets, approvals, sandboxing,\nand tool authorization come from delegated-coordination and Runtime policy.\nFor nested delegation, Runtime intersects the requested child posture with the\nlive parent. For standalone `codewhale fleet` execution, Runtime instead uses\nthe bounded tool-authority envelope minted from the task's explicit write\nscope together with live config, sandbox, and platform enforcement. Neither\npath reads authority from the profile's storage scope or identity selector.\nA worker whose envelope grants read-only shell access runs the same read-only\ncommand grammar as an in-session read-only agent, including pipelines, chains\nand a leading `cd` (see \"Read-only shell commands\" in `docs/SUBAGENTS.md`);\nAdmitted `gh` reads also need the envelope's network grant. `npm view` remains\noutside the read-only grammar because npm configuration can select executable\nhelpers; a network grant does not authorize those helpers.\n\nPicking a concrete model pins its provider explicitly: the saved profile records both\n`model` and `provider` fields, so the route it names doesn't depend on\nwhichever provider happens to be active when the profile is later loaded.\nPressing **Enter** (\"start\") on the review step previews the exact starter\nprofile TOML inline on that same screen; nothing is written until you save it.\nThe `provider` field may be a built-in provider id such as `openrouter` or a\nuser-named OpenAI-compatible provider configured under `[providers.<name>]`\nsuch as `lm-studio`; the launch path preserves that id and fails closed if the\nprovider is not configured.\n\nProfiles are also how the model-facing `agent` tool selects a route: a child\neither runs as a `profile` (whose saved route and thinking tier it uses\nexactly) or inherits the operator's model. Per-task `model`, `model_strength`,\nand `thinking` remain advertised for unpinned roles; saved profile and manual\nrole pins refuse overrides. See docs/SUBAGENTS.md for the advertised field\nlist and the parse-accepted compat list.\n\nWhen a provider is configured, the review step also offers model-assisted\ndrafting behind an explicit preview-before-save gate:\n\n- Press **`m`** to have your first configured model draft the profile. The\n draft arrives sanitized and bounded. Separately, the Runtime keeps its\n conservative execution floor (no shell or trust escalation and approval\n required) regardless of what the model proposes.\n- **Drafting is not saving.** The exact rendered TOML preview renders\n inline on the review step (not in a separate scrollable viewer), so nothing\n is saved until you press **`g`** or **Enter** to save (or press `m` again\n to redraft). Saving writes the profile to the project or personal scope\n shown in the preview.\n\n## Naming: Modes, Workflow, and fleet\n\nThese names describe different layers, not competing systems. Plan and Act are\nthe everyday work modes. Operate accepts ordinary messages and keeps the\nparent's normal tool surface under the same approval, sandbox, shell, ask-rule,\nand repository protections as Act. It prefers background fleet workers for\nindependent, parallel, isolated, or long-running work, but does not require a\nworker for every executable step. Workflow is an optional orchestration overlay\nfor work that needs ordering, gates, shared budgets, replay, or deterministic\nfan-in.\n\nThe short public vocabulary is:\n\n- **Fleet** is the durable roster and deterministic member-selection surface.\n It records member ids and names, semantic roles, provider/model identities,\n and roster state. Fleet is also the name used by storage and wire formats.\n- **Workflow** = what order the work follows: phases, gates, budgets, replay,\n and fan-in.\n- **Lane** = one running Workflow instance and its live progress.\n- **Runtime** = where, how, and with what authority selected work executes.\n Runtime owns the local or remote process, provider route, project/workspace\n trust, filesystem, network, secrets, approvals, sandbox, tools, and API\n boundary.\n\n- **Workflow** is the repeatable plan and user-facing orchestration\n overlay: a script/IR that decides which phases and agents run next, keeps\n intermediate results out of the main conversation, and can be inspected or\n rerun. A Workflow run should have a visible progress view and a clear active\n header state instead of feeling like a hidden background task.\n- **Fleet** is the durable roster and deterministic member-selection surface:\n member ids and names, semantic roles, pinned or inherited provider/model\n identities, and roster state. The delegated\n coordinator and Runtime own launch concurrency, leases, heartbeats, logs,\n receipts, tools, sandboxing, approvals, and authority.\n- **High fan-out** is a behavior of a Workflow run, not a separate system:\n when a phase needs many workers at once, Workflow dispatches them as a\n fleet-backed run (durable workers, receipts, goal re-dispatch) rather than\n reviving prompt-only sub-agent fanout.\n- **Fan-in is explicit:** when the user needs one combined result, an owner\n aggregates, verifies, and synthesizes the worker receipts. Independent tasks\n may finish separately; dispatch is never presented as completion.\n\nUI guidance: keep the main transcript calm. A Workflow run should appear as a\ncompact progress card plus workbar rows (the strip under the composer, or\na side workbar) with phase names, worker counts, receipts, and nested\nindentation for child workers. Use the whale mark sparingly as an active\nheader/status signal; avoid repeating emoji-heavy rows for every worker.\n\n## Saved fleets and the Reasoning Router\n\nA selected v2 fleet freezes each selected member's id, semantic role, provider,\nand model identity into the durable run before a Workflow starts. Save the\nfleet as `fleets/<name>.toml` under the workspace's `.codewhale/` (where the\nfleet editor saves folder fleets) or under `$CODEWHALE_HOME`; a checked-in\n`fleets/<name>.toml` at the workspace root is also read.\nModels cannot replace those identity or route assignments at runtime:\n\n```toml\nschema = \"fleet\"\nschema_revision = 2\nname = \"release\"\n\n[operator]\nprovider = \"deepseek\"\nmodel = \"deepseek-v4-pro\"\n\n[[members]]\nid = \"implementer\"\ndisplay_name = \"Release Builder\"\nrole = \"implement\"\nprovider = \"zai\"\nmodel = \"glm-5.2\"\n\n[[members]]\nid = \"advice\"\nrole = \"advisor\"\nprovider = \"openai\"\nmodel = \"gpt-5.6\"\n```\n\nThe workflow crate's older `schema = \"exact\"`, revision 1 files are migration\ninput only. Do not author revision-1 files; the selected roster and setup UI\nread and write only `schema = \"fleet\"`, revision 2.\n\n`workflow(fleet: \"release\")` runs a saved Fleet without selecting it. At\nWorkflow start, a member with no pin takes the Fleet's `[operator]` route or,\nwithout one, the session route and reasoning tier; that frozen route is what\nruns and what receipts name, and editing the file mid-run changes only the next\nWorkflow. If a saved Fleet and an older exact/legacy file share a name, the\nWorkflow refuses to guess; qualify the saved one as `user/<name>` or\n`folder/<name>`. Members with `instructions` or `requires` cannot run in a\nWorkflow yet.\n\nReasoning is a separate route-execution decision, not fleet identity. The\noptional Reasoning Router is a reusable Runtime service, not a fleet member.\nSave one profile at `routers/<name>.toml` in any search root and reference it\nfrom any number of fleets:\n\n```toml\nname = \"luna-low\"\nschema = \"reasoning_router\"\nschema_revision = 1\nprovider = \"openai\"\nmodel = \"gpt-5.6-luna\"\ncall_reasoning = \"low\"\n```\n\nAt runtime it may choose only the reasoning tier for an already-frozen worker\nroute. It cannot change the member, provider, model, or semantic role. The\nRouter call itself is capped at `off` or `low`; more expensive values are\nrejected. A manually selected worker reasoning tier makes no Router call. Route\nand reasoning receipts name the worker model and, when used, the Router's exact\nprovider/model so the operator can see which model did which job. If the same\nbare Router or fleet name exists in more than one root, qualify it as\n`codewhale_home/<name>`, `workspace/<name>` (the workspace's `.codewhale/`), or\n`workspace_root/<name>` (the workspace root) instead of relying on shadowing.\n\nCompatibility schemas may serialize `reasoning`, `permissions`, tool hints, or\nother execution settings beside a member. Those values are not fleet identity,\nmember selectors, or active authority. A valid legacy `schema = \"exact\"` roster snapshot\nretains its old `permissions` bytes only while verifying and replaying that\nsnapshot's recorded content hash; a fresh capture emits the authority-free\nmember shape. New-run validation rejects legacy roster\n`security_policy` and worker `trust_level` fields; configure execution authority\nthrough Runtime policy. The delegated coordinator resolves and durably freezes\nthe member first. Runtime then applies either the delegating parent's effective\nceiling or, for standalone fleet CLI work, Runtime execution configuration plus\nlive sandbox/platform enforcement. That boundary may\nreduce or refuse the selected worker's execution surface, but it must never\nchoose a different member or route. See\n[`docs/MODES.md`](MODES.md), [`docs/SUBAGENTS.md`](SUBAGENTS.md), and\n[`docs/AGENT_RUNTIME.md`](AGENT_RUNTIME.md) for the enforcement contract.\n\nReasoning receipts record the requested tier *and* the tier the provider was\nactually asked for. Those differ whenever a route cannot express the requested\none — Codewhale's route normalizer sends `high` for a requested `low` on most\nroutes, and Z.AI's GLM routes express only thinking on/off — so the receipt\nreports the real request rather than the label that was selected. The value a\ncall actually carries is spelled by that route's own normalizer, not by the tier\nlabel: an OpenAI Codex route is asked for `xhigh`, not `max`, and cannot be\nasked for `off` at all.\n\nA durable fleet CLI receipt keeps the selected profile id in\n`effective_permissions.profile_id`, the resolved semantic role in\n`resolved_route.role`, and the effective Runtime surface in the permission,\nshell, and tool-scope fields. An exact Workflow launch receipt records\n`member_role` separately from an optional Runtime `posture_role`, plus the\nfingerprint of the effective authority envelope checked at the spawn boundary.\nA member named `auditor` can therefore retain that identity while Runtime\nreports a `custom` posture and independently proves the narrower surface it\nenforced.\n\nA Workflow start fails closed on anything decidable locally: an unresolvable\nprovider or model, a missing credential, a client that cannot be built for a\nmember's route, or an `auto` member with no usable Reasoning Router. Per-task\nvalidation that the spawn boundary would refuse anyway — notably a write-capable\nmember with no declared `write_roots`/`exact_files`/`coordination_contracts` —\nis checked before the Router is called, so an invalid task never spends a\nrouting request. If a spawn fails *after* a Router decision, the receipt is\nstill recorded: the tokens were spent, and any cross-provider disclosure already\nhappened.\n\n## Manager-owned Workflow fan-in\n\nWhen parallel work must return one combined answer, prefer a manager-owned\nWorkflow over a flat `agent` fan-out. Default shape:\n\n1. **Cast one manager** (operator or workflow orchestrator).\n2. **Fan out** child tasks through `workflow` (`task()`, `parallel()`,\n `pipeline()`, `phase()`) or a single manager session that owns the children.\n3. **Wait** for child receipts or completion events.\n4. **Aggregate and verify** load-bearing claims before treating them as facts.\n5. **Synthesize** one result the operator can depend on.\n\nRaw `agent` fan-out fits independent work with no combined result. When\nresults must be merged, compared, or verified, route through `workflow` so\nthe manager owns fan-in — that is what the shape above is for, not a ban\non simpler patterns when nothing needs combining.\n\n## Workflow on fleet\n\nThe intended high-capability path is agent-authored. When the main agent\ndecides a task needs more durable coordination than turn-by-turn sub-agent\ncalls, it drafts a Workflow script/IR, presents the run plan according to the\nactive permission mode, and the runtime compiles it into typed fleet work.\n\nfleet remains the sub-agent roster and member-selection surface. It owns member\nidentity, membership, semantic roles, saved provider/model pins or inheritance,\nand roster state. Workflow owns the orchestration plan:\nbranch, sequence, loop, expand, review, and reduce decisions. The delegated\ncoordinator and Runtime own slot admission, launch concurrency, the execution\nledger, and every authority decision. A workflow script receives no direct\nshell, filesystem, network, provider-secret, cancellation, or TUI authority;\nworkers perform real work as `codewhale exec` processes under the effective\nRuntime policy.\n\nDefault Workflow-to-fleet validation is intentionally bounded:\n\n- 1,000 total worker agents per Workflow run;\n- 16 live worker agents at once; larger populations queue (block) on the host's\n per-run concurrency gate until a live slot frees, then route through fleet;\n- Workflow IR structural nesting no deeper than 5;\n- Runtime child delegation defaults to 3 levels and has an opt-in hard ceiling\n of 8, independently of the Workflow document's structural depth;\n- bounded loops only (`max_iterations` required);\n- bounded dynamic expansion only (`max_children` plus a template required).\n\nThese are delegated-coordination population limits, not fleet identity and not\na demand to launch everything at once. A 1,000-agent Workflow should still\ndrain through the configured Runtime worker pool. They are also not model-step\nbudgets: omitted or zero `max_steps` remains unbounded. An explicit positive\n`max_steps` may cap that task, while wall-clock timeouts, cancellation,\nprovider safeguards, heartbeats, and admission controls remain independent.\n\nRecommended model layouts, such as a DeepSeek Pro orchestrator with Flash\nworkers in the first ring and cheaper workers farther out, are presets only.\nEvery slot can inherit the active model or carry an explicit model override.\nInheritance is literal: the model you select in `/model` is the **operator**\n(the pinned first row in `/fleet roster`), and any worker whose task spec and\nroster profile pin no model runs on that session model. Once a selected member\nhas an exact provider/model pin, the Runtime does not silently reroute that\nidentity because a policy input differs; it either runs that route inside the\neffective envelope or fails closed. Route receipts record the requested and\nresolved identity.\n\nThe setup UI should render this as an expanding grid: an orchestrator plus a\nsmall number of visible sub-agent slots, with Right/Enter drilling into a slot's\nnext recursive ring rather than trying to show the whole tree at once.\n\n## Task Spec\n\n`codewhale fleet run` accepts JSON or TOML. `codewhale fleet run <spec> --check`\nruns every validation a real run performs (spec shape, roster members, agent\nprofiles, model routes) and prints the same warnings, then stops: no ledger is\ncreated, no run is written, no worker starts, and nothing is spent. A minimal\nJSON spec:\n\n```json\n{\n \"name\": \"local smoke\",\n \"tasks\": [\n {\n \"id\": \"lint\",\n \"name\": \"Lint\",\n \"instructions\": \"Run the lint check and report failures.\",\n \"expected_artifacts\": [\"log\"]\n }\n ]\n}\n```\n\nWorkers are optional. If omitted, Codewhale creates local worker slots up to\n`--max-workers`.\n\nA spec file takes one of three shapes, chosen by its structure before any\nfield is read:\n\n- a **document** — an object with `tasks` (and optionally `name`, `labels`,\n `workers`, `usage_ceiling`);\n- a **task array** — a bare JSON array of task objects;\n- a **single task** — one task object with `id` / `instructions` at the top\n level (JSON or TOML; a TOML file is never a task array).\n\nArray and single-task files take their run name from the file name. Because\nthe shape is picked first, a malformed spec reports the real problem, for\nexample ``JSON spec document at tasks[1] (id \"review\"): missing field\n`instructions` at line 7 column 5``. The checked-in\n[`docs/examples/fleet-dogfood.toml`](examples/fleet-dogfood.toml) and the\ntutorial's `tasks.json` are parsed by the test suite, so they stay valid.\n\nTask specs are typed in Rust and keep verification data separate from worker\ntranscripts. Only the `worker` member/role reference participates in fleet\nidentity selection. The remaining execution fields are delegated-coordination\nor Runtime inputs applied after the member is resolved. A task can declare:\n\n- `id`, `name`, `description`, `objective`, and `instructions`\n- `worker` role, tool profile, tools, and required capabilities\n- `workspace` root, required files, writable paths, and environment allowlist\n- `input_files`, extra `context`, `budget`, `timeout_seconds`, and `retry_policy`\n- `expected_artifacts`, `scorer`, `tags`, and free-form `metadata`\n\nNone of those execution-policy fields becomes part of a fleet identity or an\nalternate member selector. Omitted or zero `max_steps` means no model-step\nceiling; Codewhale must not synthesize a default step budget. (This is the\nfleet file task-spec convention; model-facing `agent` calls differ — the tool\nparser rejects an explicit zero. See `docs/SUBAGENTS.md`.) Explicit positive\nstep limits, timeouts, cancellation, provider safeguards, heartbeats, and\nadmission control are enforced independently by the delegated coordinator and\nRuntime.\n\nWorkers write bounded artifact files under `.codewhale/fleet/` and ledger only\nthe artifact refs: kind, path, checksum, MIME type, and size. Receipts record\n`pass`, `fail`, `partial`, `skip`, or `timeout`; failed receipts may also mark\nthe source as `transport`, `task`, or `verifier`. `codewhale fleet status`\nsurfaces those failure-source counts separately.\n\nDeterministic built-in scorers are `exit_code`, `file_exists`, `regex_match`,\nand `json_path`. Specs may also declare `command`,\n`code_whale_verifier_prompt`, or `manual`; those record a partial receipt until\nan explicit verifier pass completes.\n\n### Using Role Presets\n\nTasks can reference a semantic role name to select one unique roster member.\nBuilt-in role names (`smoke-runner`, `reviewer`, `builder`, `read-only`) remain\navailable for compatibility, and custom roles may be defined in\n`[fleet.roles]`.\n\n```json\n{\n \"name\": \"smoke check\",\n \"tasks\": [\n {\n \"id\": \"lint\",\n \"name\": \"Lint check\",\n \"instructions\": \"Run lint and report failures.\",\n \"worker\": { \"role\": \"smoke-runner\" },\n \"expected_artifacts\": [\"log\"]\n }\n ]\n}\n```\n\nAfter identity resolution, compatibility role presets may provide tool,\ntimeout, or retry defaults to the delegated coordinator. Those defaults do not\ngrant authority, do not change which member was selected, and remain subject to\nRuntime clamping. A task spec may request its execution settings explicitly:\n\n```json\n{\n \"id\": \"deep-review\",\n \"name\": \"Deep review\",\n \"instructions\": \"Review the entire crate for soundness issues.\",\n \"worker\": {\n \"role\": \"reviewer\",\n \"tools\": [\"cargo\", \"rg\", \"git\"],\n \"capabilities\": [\"rust\"]\n },\n \"input_files\": [\"crates/**/*.rs\"],\n \"budget\": { \"max_tokens\": 32000 },\n \"expected_artifacts\": [\"log\", \"report\"],\n \"scorer\": { \"kind\": \"regex_match\", \"path\": \".codewhale/fleet/report.md\", \"pattern\": \"finding|all clear\" }\n}\n```\n\n### Multi-Task Run Example\n\nA single fleet run can dispatch several independent tasks in parallel:\n\n```json\n{\n \"name\": \"CI gate\",\n \"tasks\": [\n {\n \"id\": \"check\",\n \"name\": \"Compile check\",\n \"instructions\": \"Run cargo check --workspace and report errors.\",\n \"worker\": { \"role\": \"builder\" },\n \"expected_artifacts\": [\"log\"],\n \"scorer\": { \"kind\": \"exit_code\" }\n },\n {\n \"id\": \"clippy\",\n \"name\": \"Clippy lint\",\n \"instructions\": \"Run cargo clippy --workspace and report warnings.\",\n \"worker\": { \"role\": \"reviewer\", \"tools\": [\"cargo\", \"cargo-clippy\"] },\n \"expected_artifacts\": [\"log\"],\n \"scorer\": { \"kind\": \"exit_code\" }\n },\n {\n \"id\": \"security\",\n \"name\": \"Secret audit\",\n \"instructions\": \"Search for plaintext secrets and report any matches.\",\n \"worker\": { \"role\": \"read-only\", \"tools\": [\"rg\"] },\n \"input_files\": [\"crates/**/*.rs\"],\n \"expected_artifacts\": [\"log\", \"report\"],\n \"retry_policy\": { \"max_attempts\": 1 }\n }\n ]\n}\n```\n\n## Alerts\n\nfleet alerting is disabled by default. A caller must supply an enabled alert\nconfig before anything is sent. Routes match typed fleet event classes, not log\nstrings:\n\n- `stale`\n- `restart_exhausted`\n- `needs_human`\n- `budget_exceeded`\n- `verifier_failed`\n- `run_completed`\n\nAdapter config stores environment variable names, not secret values. Send-time\ncode resolves those names from the environment or a future secrets provider.\nLedger records store only audit labels such as `slack`, `webhook`, or\n`pagerduty`; task specs persisted in the ledger redact webhook URLs and routing\nkeys.\n\nExample alert config shape:\n\n```json\n{\n \"enabled\": true,\n \"dry_run\": true,\n \"routes\": [\n {\n \"events\": [\"stale\", \"restart_exhausted\", \"verifier_failed\"],\n \"adapter\": \"ops-slack\"\n },\n {\n \"events\": [\"restart_exhausted\"],\n \"adapter\": \"pager\"\n }\n ],\n \"adapters\": {\n \"ops-slack\": {\n \"kind\": \"slack\",\n \"webhook_env\": \"CODEWHALE_FLEET_SLACK_WEBHOOK\",\n \"channel\": \"#codewhale-fleet\"\n },\n \"pager\": {\n \"kind\": \"pager_duty\",\n \"routing_key_env\": \"CODEWHALE_FLEET_PAGERDUTY_ROUTING_KEY\",\n \"severity\": \"critical\"\n }\n }\n}\n```\n\nUse dry-run to inspect a redacted adapter payload without sending:\n\n```sh\ncodewhale fleet alert-dry-run \\\n --event stale \\\n --run-id fleet-demo \\\n --worker-id fleet-demo-local-1 \\\n --task-id release-triage \\\n --reason \"worker heartbeat stale since 2026-06-13T02:00:00Z\" \\\n --adapter slack\n```\n\nThe payload includes the run id, worker id, task id, status, short reason, and\nsafe inspection commands such as `codewhale fleet status` and\n`codewhale fleet inspect <worker-id>`. Endpoints, webhook secrets, and\nPagerDuty routing keys are shown as `<redacted:env:...>`.\n\n## Status Surfaces\n\n`codewhale fleet status` shows compact counts for queued, running, completed,\npartial, failed, restarted, escalated, cancelled, stale, and verifier/transport\nfailure sources. `inspect` shows the worker state plus the current task\nobjective, role, host, heartbeat, latest event, artifact refs, latest error, and\nalert state. `logs` prints bounded log artifact contents, and `artifacts` lists\nartifact refs without embedding large payloads.\n\nThe Runtime API exposes the same ledger-backed projection behind the existing\nruntime auth middleware:\n\n```text\nGET /v1/fleet/runs\nGET /v1/fleet/runs/{run_id}\nGET /v1/fleet/runs/{run_id}/workers\nGET /v1/fleet/workers/{worker_id}\nPOST /v1/fleet/workers/{worker_id}/interrupt\nPOST /v1/fleet/workers/{worker_id}/restart\nPOST /v1/fleet/runs/{run_id}/stop\n```\n\nAction endpoints call the same manager controls as the CLI and record their\ndecisions in the fleet ledger.\n\n## Manager-Agent Runbook\n\nManager agents should treat fleet operations as typed, ledgered control-plane\nwork. Start with `codewhale fleet status`, then inspect one run or worker with\n`codewhale fleet inspect <worker-id>`, `logs`, and `artifacts`. Use direct\nreads of `.codewhale/fleet.jsonl`, host logs, or remote files only when the\ntyped CLI/API surface cannot provide the required evidence.\n\nClassify the worker before taking action:\n\n- `transient failure`: stale heartbeat, host timeout, interrupted transport,\n retryable provider/network error, or an adapter status that can plausibly\n recover without changing the task.\n- `task failure`: the worker completed but produced an incorrect result,\n domain failure, missing required artifact, or explicit task-level error.\n- `verifier failure`: the worker result exists, but the scorer/verifier failed,\n timed out, or disagrees with the receipt.\n- `needs-human`: missing authority, secret request, destructive operation,\n repeated restart exhaustion, ambiguous product decision, or conflicting\n evidence that the manager cannot resolve from typed artifacts.\n\nChoose one typed action:\n\n- Restart a worker only when the failure is transient, retry budget remains,\n the task is idempotent or retry-safe, and no permission or secret boundary is\n involved: `codewhale fleet restart <worker-id>`.\n- Interrupt or stop only when the current task is unsafe to continue or the\n operator explicitly asks for cancellation: `codewhale fleet interrupt\n <worker-id>` or `codewhale fleet stop --all`.\n- Do not restart pure task failures by default; preserve artifacts and hand the\n receipt to the task owner unless the task spec says retrying can produce new\n evidence.\n- For verifier failures, inspect scorer inputs and artifact refs first. If the\n verifier cannot be corrected through typed fleet actions, escalate for human\n review.\n- For `needs-human`, draft an escalation instead of sending it unless alert\n config explicitly authorizes sending.\n\nSafe Slack or PagerDuty draft:\n\n```text\nCodewhale fleet needs attention\nRun: <run-id>\nWorker: <worker-id>\nTask: <task-id or unknown>\nClassification: <transient failure | task failure | verifier failure | needs-human>\nReason: <one sentence, no secrets>\nLatest typed evidence: codewhale fleet inspect <worker-id>; codewhale fleet artifacts <worker-id>\nSafe log excerpt: <3 lines max or \"see artifact <ref>\">\nRequested decision: <restart approval | verifier review | task owner review | permission decision>\n```\n\nPost-run summaries should include the run id, workers checked, classification,\ntyped action taken or drafted, expected ledger effect, artifact refs reviewed,\nand next owner. Keep summaries bounded; link artifact refs instead of copying\nfull logs or transcripts.\n\nThe bundled `fleet-manager` skill mirrors this runbook for manager agents. It\nis a first-party system skill and should be discoverable through the normal\nskill registry after system skills are installed or refreshed.\n\n## Host Adapters\n\nThe Runtime host-adapter boundary supports local child processes and explicit\nSSH workers. Host choice is Runtime placement on a worker spec, not fleet member\nidentity or a member selector. It does not authenticate the host or grant\naccess. Adapters expose the same operations: start, read status, read bounded\nlogs, interrupt, restart, stop, and cleanup.\n\nLocal workers run as child processes with stdin closed and stdout/stderr written\nto bounded host-adapter logs. They inherit only a small safe base environment\nsuch as `PATH` and explicitly allowlisted variables.\n\nSSH workers run through the system `ssh` client with `BatchMode=yes` and a\nbounded connect timeout. Remote environment variables are sent with OpenSSH\n`SendEnv`; values are not embedded in the local ssh argv or fleet logs.\n\nHost keys must already be trusted: connections use `StrictHostKeyChecking=yes`\nand never accept a new key automatically. An explicit `known_hosts` file limits\ntrust to that file; when it is omitted, OpenSSH uses its normal known-host stores.\nVerify the host key before adding it to either store. The legacy\n`host_key_fingerprint` field is unsupported and is rejected; migrate it to a\nverified `known_hosts` entry. The `identity` file selects the client login key;\nit does not verify the remote host.\n\nExample SSH worker spec:\n\n```json\n{\n \"id\": \"builder-1\",\n \"name\": \"Builder 1\",\n \"host\": {\n \"kind\": \"ssh\",\n \"host\": \"builder.example.com\",\n \"user\": \"codewhale\",\n \"port\": 22,\n \"identity\": \"~/.ssh/codewhale_fleet\",\n \"known_hosts\": \"~/.ssh/codewhale_fleet_known_hosts\",\n \"working_directory\": \"/srv/codewhale/work\",\n \"env_allowlist\": [\"CODEWHALE_PROFILE\"],\n \"codewhale_binary\": \"/usr/local/bin/codewhale\"\n },\n \"capabilities\": [\"local\", \"linux\", \"tests\"],\n \"max_concurrent_tasks\": 1\n}\n```\n\nDefaults are intentionally conservative:\n\n- no hosted control plane or cloud provisioning is enabled;\n- SSH requires an explicit host, working directory, and Codewhale binary path;\n- secret-like environment names such as `TOKEN`, `SECRET`, `PASSWORD`,\n `API_KEY`, and `PRIVATE_KEY` are rejected from adapter allowlists;\n- secrets should remain in Codewhale config providers or remote host config,\n not in task instructions, argv, or fleet logs.\n\n## Runtime policy and authority are not fleet identity\n\nfleet does not define a project/workspace trust level, filesystem or network\nreach, secret access, approval mode, sandbox, tool set, or execution authority.\nThose belong to delegated-coordination and Runtime policy. This separation is\nload-bearing:\n\n- member resolution considers only member id/name, semantic role,\n provider/model identity, and roster state;\n- the selected identity is frozen before any authority policy is evaluated;\n- the Runtime applies the live parent ceiling when one exists; standalone\n fleet CLI launches instead carry an explicit bounded authority envelope,\n and both paths remain subject to live sandbox and platform enforcement;\n- no trust, permission, capability, secret, sandbox, approval, or tool-policy\n value may select another member or silently change its provider/model route;\n and\n- receipts report requested and effective Runtime posture separately from the\n fleet member identity.\n\nOlder persisted configuration and protocol shapes may still contain fields such as\n`security_policy`, `trust_level`, `permissions`, `capability_grants`, secret\nreferences, host authentication, environment allowlists, or tool profiles.\nThey remain deserializable for ledger replay, but new fleet run creation rejects\n`security_policy` and worker `trust_level` rather than pretending they grant\nauthority. Their presence in old data does not make them fleet variables or\ngrants. The active Runtime remains the final authority and fails closed when a\nrequested operation cannot be enforced.\n\nFor current enforcement behavior, use [Modes](MODES.md),\n[Sub-agents](SUBAGENTS.md), [Agent Runtime](AGENT_RUNTIME.md), and the\n[Command Control Plane](COMMAND_CONTROL_PLANE.md). Keep secret values out of\ntask instructions, arguments, logs, and receipts; adapter and Runtime layers\nmust continue to redact or reject them independently of fleet selection.\n\n## Child grants: 0.10.1 scope and the 0.11 rework (#6298)\n\nA child's authority is one grant object, `ChildGrant`\n(`crates/agent-contract/src/worker_profile.rs`). It shipped in v0.10.0 (966ef974e1,\n#5633). It has `files` (none / read / write), `shell` (none / inspect /\nverify / full), `network`, `desktop`, a tool `surface`, the caller's explicit\n`scope`, and `spawn`. Roles are presets over it (`ChildGrant::for_role`), and\n`ChildGrant::resolve` intersects the preset with the parent-derived profile\nand the caller's scope. The child's tool catalog, its dispatch refusals, and\nits capability envelope all read the same grant, so a tool the child can see\nis a tool it can call. `desktop` is in no preset.\n\n`ShellGrant::Verify` also shipped in v0.10.0: the Verifier preset gets the\nbounded built-in verification surface (default workspace checks, pure test\nselection, bounded Git fetch and merge-tree) instead of a shell grammar.\n\n**Other fixes shipped by v0.10.0** (on top of the grant):\n\n- Children never inherit desktop or computer-control tools (b5e48cd31, #6296).\n- A bounded verify surface for Git: `fetch` against a configured remote name\n and a read-only `merge_tree` (b89349286).\n- Refusals name the sanctioned alternative and tell a child to report a\n blocked probe to its parent instead of working around it (23747acea).\n- One reasoning vocabulary (c2bc1244d). Token budgets are tracked but never\n enforced (a7a8bdb33).\n\n**0.10.1 scope.** This release adds no new grant-model code. #6298 is\nre-scoped to the remainder below, which lands in 0.11 as its own slices.\n\n**0.11 remainder** (one slice at a time):\n\n1. **A `verify` shell mode for builds.** `cargo test`/`check` run under an\n explicit, bounded write scope (`target/`, refs), so a verifier can run the\n builds it is handed without a full shell.\n2. **Classified tool families that fail closed.** MCP and desktop tools form a\n labeled family. A child gets that family only when the spawn grants it with\n a reason, and an unclassified tool is not granted.\n3. **Legible grants.** The role picker, roster, and receipts show the effective\n grant, model, and thinking tier in plain words.\n\nRelated work is tracked in #6015, #5633, #6194, and #6232.\n"
|
||
},
|
||
{
|
||
"path": "docs/MODES.md",
|
||
"sha256": "f07a2fce8d2ef02692fdc9298911ebc99dbd7940af6a3b88d396c039dc7e4608",
|
||
"content": "# Modes and Permission Postures\n\n> 阅读简体中文版:[zh_hans/MODES.md](zh_hans/MODES.md)\n\nCodewhale has three related concepts:\n\n- **TUI mode**: what kind of visible interaction you're in (Plan/Work/Operate).\n- **Permission posture**: how aggressively the UI asks before executing tools.\n- **Workflow**: named steps that coordinate sub-agents, with dependencies and\n results, available in any TUI mode.\n\nModel selection is separate. `--model auto` and `/model auto` route each turn to\na concrete model and thinking level; they are not TUI modes and are not part of\nthe `Tab` cycle.\n\nWorkflow runs named steps through the same sub-agent runtime; Fleet manages\ntheir saved roles and model assignments. A workflow adds ordering, result\nhandoffs, gates and a progress view. The active mode and permission posture\nstill control what each step may execute.\n\nBefore assigning steps, query `agent(action=\"roster\")` for the saved Fleet\nmodels and roles. A plan child can select a listed model with `model`, or use\nthe saved `role`/`profile` assignment. An Exact Fleet fixes each member's route.\n\n## TUI Modes\n\nPress `Tab` to complete composer menus or cycle through the visible modes\nwhen the composer is empty: **Plan → Work → Operate → Plan**. `Tab` never sends\nor queues composer text; use `Enter` to send or queue it.\nPress `Shift+Tab` to cycle permission posture (Ask → Auto-Review → Full Access).\nEvery change of effective permission writes a `Permissions changed to …` line\nin the transcript.\nPress `Ctrl+T` to cycle reasoning effort.\nRun `/mode` to open the mode picker, or switch directly with `/mode work`,\n`/mode plan`, or `/mode operate`.\n\n- **Plan**: design-first prompting. The stable primitive names remain familiar, but the runtime centrally refuses file mutation and shell execution. Read-only inspection and policy-allowed research, including deferred Web search/fetch, remain available. When a Plan turn successfully finishes a nonempty response, including a prose-only plan, the TUI asks how to continue: Work (Ask), Work (Auto-Review), or keep planning. Work sends the exact approved response as a normal follow-up and preserves that turn's existing To-dos, or adds one pending plan item without replacing unrelated work.\n- **Work** (internally `agent`): ordinary multi-step execution. The first-turn toolbox includes `read`, `write`, `edit`, `file_search`, `bash`, `agent`, `workflow`, `todo_write`, and `load_skill`, plus `create_goal`, `get_goal`, `update_goal`, and `task_shell_wait` so goal and background-shell controls are available without discovery. The agent calls `create_goal` when a request is a durable objective; the host never infers a goal from wording, and the contract must not tell the model otherwise. Approval, sandbox, repository law, and managed policy decide what may execute.\n- **Operate**: Work at full strength, the \"ultra\" mode. Same tools and authority as Work; the difference is how they are used. Each substantive request becomes a goal (`create_goal`), the plan stays visible (`todo_write`), multi-part work runs through `workflow` and parallel `agent` workers, non-trivial changes get an independent reviewer before they are called done, long commands keep running in the background, and recurring or watch-type work is offered as an `automation` (created after approval). It keeps going until the goal is verified, blocked on you, or paused.\n\n`Act` and `/mode act` remain compatibility aliases for Work. Saved settings\nstill normalize to the internal value `agent`.\n\n### Tool availability by mode\n\n| Tool family | Plan | Work | Operate |\n|:---|:---:|:---:|:---:|\n| `read` and policy-allowed deferred research tools | yes | yes | yes |\n| `write` and `edit` | visible names; execution denied | approval- and policy-gated | same as Work |\n| `bash` | visible name; execution denied | approval- and policy-gated | same as Work; delegation is preferred when parallelism or isolation helps |\n| `agent` | yes, subject to child-depth authority | yes, subject to child-depth authority | yes, subject to child-depth authority |\n| Deferred native, MCP, and plugin tools | discoverable through `tool_search` when policy permits | same | same |\n| Paid or external-service tools | follows permission posture | follows permission posture | follows permission posture |\n| Access outside the workspace root | explicit trusted paths only | only through trusted paths or trust mode | same trusted-path/trust policy as Work; fleet profiles never widen it |\n\nOperate changes scheduling emphasis, not authority. It neither adds a\nmode-specific tool denial nor bypasses the active approval, sandbox, shell,\nask-rule, repository-law, or managed-policy boundary. Plan remains the\nmode-specific execution boundary for shell and write-capable tools; that\nauthority difference does not require a different primitive vocabulary.\n\n### Operate loop (one screen)\n\n```text\nUser message\n → small / chat / one-file? → parent does it (Work-equivalent tools)\n → multi-step work? → goal → named steps + dependencies + completion checks\n → Workflow phases → independent sub-agents in parallel\n → collect results → check evidence → hand off to the next phase\n → missing required result? → stop dependent work and repair the step\n → one independent task? → one direct sub-agent\n → parent integrates results and reports completed, blocked and next steps\n```\n\nLifecycle claims stay exact: dispatched ≠ settled ≠ verified.\n\n`allow_shell` controls whether `bash` can execute; it does not rename the tool\nor make mode the approval authority. Durable tasks and automation keep\nconservative omitted-field defaults and receive shell authority only when their\nsettings explicitly grant it. Stateful terminal/background controls are\nspecialized deferred tools rather than fields on the small foreground `bash`\nschema. Full Access changes the permission posture: the deterministic\nAuto-Review floor applies in the reviewing postures and is skipped under Full\nAccess, while repository-policy holds remain authoritative.\n\nAction-capable modes can discover the deferred `rlm` family through\n`tool_search`; its `open`, `eval`, `configure`, and `close` actions own persistent\nRLM sessions. The legacy split `rlm_*` spellings remain replay-only aliases.\nInside an RLM Python REPL, `sub_query_batch` fans out 1-16 cheap parallel child\ncalls pinned to `deepseek-v4-flash`.\n\nThe fast `deepseek-v4-flash` / thinking-off path is called Fin in the product\nlanguage. Fin is a seam for routing, summaries, cheap child calls, and\ncoordination work; it does not change approval behavior.\n\nThe orchestration controls remain available without taking over the starting\nscreen: `/auto` turns on Auto-Review so the agent just works, `/goal` keeps one\nobjective across turns, and `/workflow` prepares a repeatable ordered or\nfan-out workflow. They are directly callable and searchable through the full\ncommand palette, but are not pinned to the starter slash menu, idle welcome,\nfooter, or default Hotbar. A bare `/` instead opens the small task-oriented\nstarter set; use `/help` or the command palette for the complete inventory.\n\n`/goal <objective>` sets a session objective with an optional token budget and\nkeeps active objectives visible as Work context. The agent may also create the\ngoal itself when a direct request describes a verifiable end state that will\ntake more than one turn (\"until the tests pass\", \"make X work end to end\"); it\nthen shows one receipt line and you can `/goal pause` or `/goal clear` it. Bare\n`/goal` shows progress (state, elapsed, continuations, and how to continue when\nno turn is running); with no goal and no conversation yet it prints usage.\n`/goal pause` stops goal continuation without changing the objective, `/goal\nresume` resumes and sends the objective back into the turn, `/goal complete`\nmarks it done, `/goal blocked` marks it blocked, and `/goal clear` removes it. Goal state does not change the active TUI mode,\npermission posture, or model route. This remains distinct from `--model auto`, which\nonly controls model and thinking selection.\n\nWorkflow builds on the same separation: a goal can ask the agent to keep\nworking, while Workflow supplies the repeatable workflow/progress surface for\nlarge fanout. In the UI, a Workflow run should be shown as an overlay on the\nmain screen, not as another mode beside Plan, Work, and Operate.\n\nApp-server clients can persist a thread-scoped goal with `thread/goal/set`, read\nit with `thread/goal/get`, and clear it with `thread/goal/clear`. That persisted\nrecord carries `active`, `paused`, `blocked`, `usage_limited`, `budget_limited`,\nor `complete` status plus token/time accounting fields for clients that need\nthread resume semantics.\n\n## Mode Persistence\n\nChoosing a mode interactively also sets the mode a fresh session starts in.\nTab/Shift+Tab cycling, the `Alt+A` / `Alt+P` / `Alt+Y` shortcuts, the hotbar's\nPlan/Work/Operate actions, and `/mode` all write `default_mode` to\n`~/.codewhale/settings.toml`, so switching to Operate survives a restart. The\nwrite happens off the event loop; if it fails, the TUI says so in a warning\ntoast rather than reverting silently on the next launch.\n\nMode, thinking level, and the model picker share one serialized writer, so the\nselection you made last is the one on disk — a burst of Tab presses cannot end\nup persisting whichever write happened to finish last — and a mode write never\nrolls back an unrelated key such as `default_model`.\n\nRestoring a saved session deliberately does **not** rewrite the startup\ndefault: it re-installs the mode that session was in. The legacy `yolo` entry point installs Work\nplus Full Access, and `agent` is what it persists — `yolo` is a permission\nalias, never a startup mode.\n\nRe-selecting the mode you are already in is not a no-op. After a restored\nsession the live mode and `default_mode` routinely disagree, so choosing the\nlive mode again is how you make it durable; Codewhale confirms with a\n\"saved as startup default\" receipt rather than reporting \"already in that mode\".\n\nMode and permissions can change while a turn is running, from any surface\n(Tab, Shift+Tab, `/mode`, the hotbar, `/config mode …`). The change does not\nrestart a response that is already streaming. The engine applies it at its next\nstep in the same turn: before the next tool batch, at every approval gate, and\nbefore the next model request, which carries a new mode notice. A call that was\nstill waiting for approval, or that no approval covers, goes back to the model\nto retry under the new posture; a call you already approved runs only if the\nnew posture is equal or broader. The status line says the change is pending\nuntil that step, and the engine reports `Permissions: … · <mode>` once it\napplies.\n\nModel, thinking level, and provider are still refused while a turn runs, on\nevery surface (`/model`, `/provider`, `/config <key> <value>`,\n`/config preset`): they change the route the running request is on. Press Esc\nto interrupt first.\n\nCodewhale writes `settings.toml` under a lock that spans processes, and replaces\nthe file atomically, so a second Codewhale instance on the same home directory\ncannot lose your selection or read a half-written file. At exit, queued writes\nare flushed before the terminal is restored; anything that failed is printed on\nthe way out instead of disappearing with the alternate screen.\n\n## Compatibility Notes\n\n- Older settings files with `default_mode = \"normal\"` still load as `agent`; saving rewrites the normalized value.\n\n## Escape Key Behavior\n\n`Esc` is a cancel stack, not a mode switch.\n\n- Close slash menus or transient UI first.\n- Cancel the active request if a turn is running.\n- Discard a queued draft if the composer is empty.\n- Clear the current input if text is present.\n- Otherwise it is a no-op.\n\n## Permission Posture\n\nPermission posture controls tool approval and whether a turn may pause for a\nmissing user decision. It is one layer of the full\n[authorization order](AUTHORIZATION_ORDER.md), not a bypass for tool admission,\nrepository law, or sandbox enforcement. Cycle it with `Shift+Tab`, or edit it\nat runtime:\n\n```text\n/config\n# edit the approval_mode row to: suggest | auto | never\n```\n\nLegacy note: `/set approval_mode ...` was retired in favor of `/config`.\n\n- `suggest` (**Ask**, default): tool approvals may interrupt, and Codewhale asks\n when an unresolved user choice materially changes authority, cost, scope, or\n outcome.\n- `auto` (**Auto-Review**): reviews tool calls automatically. Deliberate user\n questions remain available in interactive sessions through `request_user_input`;\n a question parks the turn until answered, canceled, or its configured timeout\n expires. Headless `exec` withholds that tool because it has no responder.\n Tool safety holds remain separate from user questions. Two layers decide approvals. The\n **deterministic floor** (configured block rules plus the built-in safety\n floor) allows proven-safe calls and hard-blocks publish-like actions and\n destructive background/headless work; it is never model-reviewed. Fallback\n holds — calls the deterministic engine could not prove safe — escalate to a\n one-shot **model guardian** (v0.9.8) that returns risk, allow/deny, and a\n rationale. The guardian sees the exact held call and deterministic\n observations in separate JSON fields; conversation history, skill\n instructions, attachments, and expanded model context are excluded. It does\n not infer user intent or compute a generic user-intent score.\n High or critical risk cannot auto-run even if the model says allow. It has no\n tools, remembers no rules, and denies rather than truncates an oversized\n exact call. Exactly one reviewer request is made; incomplete or malformed\n output, timeout, cancellation, or provider failure fails closed. Repeated\n denials stop the turn: after three guardian denials in a row, or ten among\n the turn's last fifty reviews, the turn ends as interrupted with a status\n line, and calls still in that batch are refused without another review.\n Only explicit denials count (not timeouts or failures), an allowed review\n resets the run of three, the count starts fresh with each turn, and a child\n agent counts its own turns. Headless\n adapters use the deterministic-only tier. Repo-law\n holds that explicitly require a person block in Auto-Review rather than\n opening a hidden approval modal.\n\nThe LLM reviewer is closest to OpenAI Codex's experimental Auto-Review at\ncommit [`6fc6b9d6d2580d62622fc9884b5f5707f6505a5e`](https://github.com/openai/codex/tree/6fc6b9d6d2580d62622fc9884b5f5707f6505a5e).\nCodex's [guardian entry point](https://github.com/openai/codex/blob/6fc6b9d6d2580d62622fc9884b5f5707f6505a5e/codex-rs/core/src/guardian/mod.rs)\nreconstructs conversation context and runs a dedicated review session.\nCodewhale deliberately adopts only the exact-action structured decision,\n90-second deadline, fail-closed result, and per-turn denial circuit breaker. It\ndoes not copy Codex's transcript reconstruction, user-authorization score,\nreviewer tools, retries, persistent review session, or denial ledger.\n\nKimi Code at commit\n[`1414d4602898f406e540b23342cb18db23ff9efc`](https://github.com/MoonshotAI/kimi-code/tree/1414d4602898f406e540b23342cb18db23ff9efc)\nalso has no LLM reviewer. Its ordered\n[permission policy](https://github.com/MoonshotAI/kimi-code/blob/1414d4602898f406e540b23342cb18db23ff9efc/packages/agent-core-v2/src/agent/permissionPolicy/permissionPolicyService.ts)\napplies explicit deny rules and then its\n[Auto policy](https://github.com/MoonshotAI/kimi-code/blob/1414d4602898f406e540b23342cb18db23ff9efc/packages/agent-core-v2/src/agent/permissionPolicy/policies/auto-mode-approve.ts)\nreturns `approve` directly. Codewhale uses the deterministic floor and guardian\ndescribed above, while keeping deliberate user questions available.\n\nThe sandbox and escalation baseline is grounded in DeepSeek Harness\n`0.1.0-rc.5` at\ncommit [`47f943859bef60e4160492346772ded9b24f765a`](https://github.com/deepseek-ai/deepseek-harness/tree/47f943859bef60e4160492346772ded9b24f765a):\nits [sandbox contract](https://github.com/deepseek-ai/deepseek-harness/blob/47f943859bef60e4160492346772ded9b24f765a/docs/subsystems/sandbox.md)\ndefines per-call `read-only`, `workspace-write`, and `danger-full-access`\nboundaries and forbids silent unconfined fallback; its\n[approval contract](https://github.com/deepseek-ai/deepseek-harness/blob/47f943859bef60e4160492346772ded9b24f765a/docs/subsystems/approval.md)\ngrants only `allowed-once` and fails closed on rejection, cancellation, or an\nunavailable answerer; and its\n[sandbox result contract](https://github.com/deepseek-ai/deepseek-harness/blob/47f943859bef60e4160492346772ded9b24f765a/packages/shell/bash-sandbox/README.md)\ntells the model to retry a denied command exactly once with the narrowest wider\nmode plus a justification. DeepSeek Harness does not add an LLM reviewer to\nthat path. Codewhale's autonomous posture adds only the single stateless\nguardian request described above; deterministic hard blocks remain\nnon-bypassable in the reviewing postures.\n- `bypass` (**Full Access**): ordinary tool calls do not show approval prompts,\n while deliberate user questions remain available. The deterministic\n Auto-Review floor is skipped — publish-like commands, destructive\n background/headless work, and the Windows npm-launcher platform hold do not\n strand a call the person already granted. Non-bypassable registered holds\n auto-approve instead of opening a contradictory modal. Repository-law and\n managed-policy holds fail closed as hard blocks instead of contradicting\n Full Access with an approval modal.\n- `never`: blocks any tool that is not considered safe/read-only; deliberate\n user questions remain available.\n\nThe effective posture and its question discipline are projected into every\nturn from the same runtime authority that gates tools. A mode/posture change is\ntherefore visible to the next turn. Untrusted runtime-generated input is\nnarrowed before metadata is built and cannot invent approval authority. An\nexplicit Full Access sub-agent handoff preserves the parent's standing posture\nso ordinary child work does not begin prompting again.\n\n### Children (sub-agents and fleet workers)\n\nChildren inherit the session posture faithfully rather than a bare\nauto-approve bit:\n\n- **Auto-Review**: a worker's held call goes through the same deterministic\n policy (proven-safe calls run; publish-like and destructive background work\n is hard-blocked) and, for holds it cannot prove safe, the same one-shot\n model guardian using the child's own session client. No prompt is ever\n opened for a child; an unavailable guardian denies, fail closed.\n- **Ask**: a call the role may delegate runs. A held call is raised as an\n approval prompt in the parent's UI (`agent:<id>:approval:<n>`) when the\n host is an interactive TUI; the worker waits visibly (`waiting for user`)\n and the person's answer is routed back to it, whether the parent turn is\n idle or itself awaiting an approval. Hosts that cannot prompt deny with the\n reason.\n- **Full Access**: calls run, including destructive detached work — the\n Auto-Review floor is skipped exactly as it is for the parent turn. The\n execution envelope still applies and never widens.\n\nRole posture and the execution envelope are checked before and after this\ngate and never widen. Every decision a person did not make at a prompt is\nwritten to the audit log and to the child's transcript as a one-line note\n(`Auto-Review allowed 'bash' (low risk, model guardian): …`), visible when\nthe worker is focused.\n\n## Small-Screen Status Behavior\n\nWhen terminal height is constrained, the status area compacts first so header/chat/composer/footer remain visible:\n\n- Loading and queued status rows are budgeted by available height.\n- Queued previews collapse to compact summaries when full previews do not fit.\n- `/queue` workflows remain available; compact status only affects rendering density.\n\n## Workspace Boundary and Trust Mode\n\nBy default, file tools are restricted to the `--workspace` directory. Enable trust mode to allow file access outside the workspace:\n\n```text\n/trust on\n```\n\nBare `/trust` (like `/trust status`) only *reports* the current setting — it\ndoes not enable anything. Use `/trust off` to restrict access again.\n\nFull Access enables trust mode automatically.\n\n## MCP Behavior\n\nMCP tools are exposed as `mcp_<server>_<tool>` and use the same approval flow as\nbuilt-in tools. Read-only MCP helpers may auto-run in Ask and Auto-Review when\npolicy permits; MCP tools with possible side effects require approval. Full\nAccess does not bypass hard policy holds.\n\nA tool's own MCP annotations count only as far as their source is trusted. A\ntool from a reviewed, enabled plugin that declares `readOnlyHint: true` runs\nlike the built-in read helpers, with no prompt; the same claim from any other\nserver is ignored. A tool that declares `destructiveHint: true` never gets\nthat relaxation, even from a reviewed plugin, and its approval card says the\nserver marked it destructive. Full Access still runs it without a prompt, like\nany other tool that would ask.\n\nSee `MCP.md`.\n\n## Related CLI Flags\n\nRun `codewhale --help` for the canonical list. Common flags:\n\n- `-p, --prompt <TEXT>`: one-shot prompt mode (prints and exits)\n- `codewhale exec --auto --output-format stream-json <PROMPT>`: run the tool-backed non-interactive agent and emit one JSON object per line for harnesses and backend wrappers. `--auto` enables Full Access by default (no Codewhale shell sandbox, trust mode on); add `--sandbox workspace-write` to keep the shell sandbox and file-tool workspace restrictions, with trust mode off. Any `--sandbox` other than `danger-full-access` (or the equivalent `sandbox_mode` in config) keeps trust mode off; `external-sandbox` relies on your own isolation. Exec runs that offer tools are saved in every output format. Exit codes: `0` on success and `1` on failure. Network and timeout errors do not authorize a supervisor to repeat the command: its request may already have run or its user message may already be saved. The terminal stream `metadata` event retains `error_category` for diagnosis; it is not an idempotency receipt.\n- Put exec flags before the prompt. A prompt starting with `-` needs `--` first, for example `codewhale exec -- \"- explain this option\"`. A lone `-` is literal text. Known exec long flags (including `--flag=value`) after the prompt are rejected unless they follow an explicit `--`, which marks them as literal prompt text.\n- `codewhale exec --prompt-file <PATH>` / `cat prompt.txt | codewhale exec --prompt-file -`: read the prompt from a file or stdin instead of argv, for prompts past the OS per-argument limit (~128 KiB on Linux). Conflicts with a positional prompt, and a positional `-` is literal prompt text; `--prompt-file -` is refused with `--parent-death-watch`, which owns stdin\n- `codewhale exec --resume <ID|PREFIX> <PROMPT>` / `--session-id <ID|PREFIX>`: continue a saved session non-interactively\n- `codewhale exec --continue <PROMPT>`: continue the most recent saved session for this workspace non-interactively\n- `codewhale fork <ID|PREFIX>` / `codewhale fork --last`: copy a saved session into a new sibling session; forked sessions retain additive parent-session metadata and show that lineage in session listings\n- `--model <MODEL>`: when using the `codewhale` facade, forward a DeepSeek model override to the TUI\n- `--workspace <DIR>`: workspace root for file tools\n- `-r, --resume <ID|PREFIX|latest>`: resume a saved session\n- `-c, --continue`: resume the most recent session in this workspace\n- `--max-subagents <N>`: clamp to `1..=128`\n- `--full-access`: start with Full Access, no approval prompts and no Codewhale sandbox; the same as `yolo = true`. `serve --acp --full-access` and `--full-access serve --acp` are equivalent.\n- `--mouse-capture` / `--no-mouse-capture`: opt in or out of internal mouse scrolling, transcript selection, right-click context actions, and transcript scrollbar dragging. Mouse capture is enabled by default on non-Windows terminals and on Windows Terminal/ConEmu/Cmder so drag selection copies only transcript text, removes visual wrap-column line breaks from paragraphs, and stays scoped to the transcript pane; hold Shift while dragging or use `--no-mouse-capture` for raw terminal selection. It defaults off on legacy Windows console (CMD without `WT_SESSION` / `ConEmuPID`) and inside JetBrains JediTerm — PyCharm/IDEA/CLion/etc. — where the terminal advertises mouse support but forwards SGR mouse events as raw text (#878, #898). Use `--mouse-capture` to opt in anywhere it's defaulted off. Raw terminal selection may cross the right workbar and include visual wraps because the terminal, not the TUI, owns the selection.\n- `--profile <NAME>`: select config profile\n- `--config <PATH>`: config file path\n- `-v, --verbose`: verbose logging\n\n## Branching and Rollback\n\nCodewhale has three related but intentionally separate recovery paths:\n\n- `codewhale fork <ID>` creates a new saved session from an existing saved\n conversation and records the source session id. This is the safe way to\n explore a different answer path without overwriting the original session.\n- Esc-Esc backtrack rewinds the live transcript to a previous user prompt and\n restores that prompt into the composer for editing. It does not change\n files; `/restore` rolls the folder back to a chosen restore point and needs\n `/trust on` or Full Access.\n- `/restore` and the `revert_turn` tool restore workspace files from side-git\n snapshots. `/restore list [N]` lists more snapshot options before choosing a\n rollback point. They do not rewrite conversation history.\n\nA Pi-style in-file tree browser is a larger UI/data-model project. v0.8.40\nships the bounded fork/backtrack primitives and explicit lineage metadata.\n"
|
||
},
|
||
{
|
||
"path": "docs/public-surface-facts.json",
|
||
"sha256": "0d50e2452380e0acbadd27b4db1f6981b9be4d7c071a48cc555cc55ea2f4b7d8",
|
||
"content": "{\n \"schemaVersion\": 2,\n \"product\": {\n \"name\": \"Codewhale\",\n \"description\": \"Terminal coding agent for supported hosted and local models. One runtime on your machine. Rust, MIT.\",\n \"positioning\": \"Terminal-native, model/provider-neutral, local-first coding agent\",\n \"license\": \"MIT\",\n \"terminology\": {\n \"Fleet\": \"the user's model inventory: who is in the roster and which member is selected\",\n \"Workflow\": \"what order the work follows\",\n \"Lane\": \"one running Workflow instance\",\n \"Runtime\": \"where, how, and with what authority selected work executes\"\n },\n \"sources\": [\n \"Cargo.toml\",\n \"LICENSE\",\n \"README.md\",\n \"npm/codewhale/package.json\",\n \"docs/FLEET.md\"\n ]\n },\n \"sourceCandidate\": {\n \"version\": \"0.10.3\",\n \"providerCount\": 50,\n \"toolCount\": 82,\n \"sandboxBackends\": [\n \"seatbelt (macOS, when available)\",\n \"bubblewrap (Linux, default when installed and working)\"\n ],\n \"sources\": [\n \"Cargo.toml\",\n \"crates/runtime/src/config.rs\",\n \"crates/sandbox/src/lib.rs\",\n \"web/scripts/derive-facts.mjs\"\n ],\n \"providerCountDefinition\": \"Website descriptor identities in this source snapshot, excluding Custom and retired identities; includes protocol/plan variants. Not a count of distinct vendors, ProviderKind identities, live catalogs, or providers in any published release.\"\n },\n \"latestPublishedRelease\": {\n \"tag\": \"v0.10.2\",\n \"version\": \"0.10.2\",\n \"publishedAt\": \"2026-10-24T17:31:14Z\",\n \"url\": \"https://github.com/codewhale-hq/CodeWhale/releases/tag/v0.10.2\",\n \"sources\": [\n \"web/data/latest-published-release.json\"\n ]\n },\n \"install\": {\n \"recommended\": \"curl -fsSL https://codewhale.net/install.sh | sh\",\n \"binaries\": [\n \"codewhale\",\n \"codew\"\n ],\n \"channels\": {\n \"npm\": \"published releases only\",\n \"cargo\": \"published crates only\",\n \"prebuiltArchives\": \"published GitHub Releases only\",\n \"cnbMirror\": \"documented targets only\"\n },\n \"androidTermux\": {\n \"status\": \"preview\",\n \"npm\": \"preview\",\n \"requiresMatchingPublishedAssets\": true,\n \"sourceBuild\": true\n },\n \"sources\": [\n \"README.md\",\n \"npm/codewhale/package.json\",\n \"npm/codewhale/README.md\",\n \"npm/codewhale/scripts/artifacts.js\",\n \"docs/INSTALL.md\",\n \"docs/CNB_MIRROR.md\"\n ]\n },\n \"control\": {\n \"modes\": [\n \"Plan\",\n \"Work\",\n \"Operate\"\n ],\n \"permissionPostures\": [\n \"Ask\",\n \"Auto-Review\",\n \"Full Access\"\n ],\n \"shortcuts\": {\n \"mode\": {\n \"chord\": \"Tab\",\n \"when\": \"composer empty\"\n },\n \"permissionPosture\": {\n \"chord\": \"Shift+Tab\",\n \"when\": \"always (suppressed only under a non-Config modal)\"\n }\n },\n \"sources\": [\n \"docs/MODES.md\",\n \"docs/KEYBINDINGS.md\",\n \"crates/runtime/src/settings.rs\"\n ]\n },\n \"toolSurface\": {\n \"defaultActive\": [\n \"read\",\n \"write\",\n \"edit\",\n \"file_search\",\n \"bash\",\n \"agent\",\n \"workflow\",\n \"todo_write\",\n \"create_goal\",\n \"get_goal\",\n \"update_goal\",\n \"task_shell_wait\",\n \"present\",\n \"load_skill\",\n \"context_archive_get\",\n \"context_archive_search\"\n ],\n \"schemas\": {\n \"read\": [\n \"path\",\n \"offset?\",\n \"limit?\"\n ],\n \"write\": [\n \"path\",\n \"content\"\n ],\n \"edit\": [\n \"path\",\n \"edits\"\n ],\n \"bash\": [\n \"command\",\n \"timeout?\"\n ]\n },\n \"deferred\": {\n \"Web\": [\n \"search\",\n \"fetch\",\n \"wait\"\n ]\n },\n \"compatibility\": {\n \"legacyAliases\": \"hidden-exact\",\n \"modelVisible\": false,\n \"toolSearchDiscoverable\": false\n },\n \"activationCache\": {\n \"maximumNames\": 8,\n \"maximumSchemaBytes\": 16384,\n \"scope\": \"per conversation; each subagent owns an independent policy-filtered cache\"\n },\n \"agentConcurrency\": {\n \"defaultConfigured\": 64,\n \"maximumConfigured\": 128,\n \"maximumAdmitted\": 1024\n },\n \"sources\": [\n \"docs/TOOL_SURFACE.md\",\n \"docs/RUNTIME_SIMPLIFICATION_DESIGN.md\",\n \"crates/engine/src/core/engine/tool_catalog.rs\",\n \"crates/engine/src/tools/registry.rs\",\n \"crates/runtime/src/config/subagent_limits.rs\"\n ]\n },\n \"surfaces\": {\n \"availableInSourceCandidate\": [\n \"TUI\",\n \"codewhale exec\",\n \"Web client\",\n \"Runtime API\",\n \"MCP\",\n \"Fleet\"\n ],\n \"sources\": [\n \"docs/GUIDE.md\",\n \"docs/WEB.md\",\n \"docs/RUNTIME_API.md\",\n \"docs/MCP.md\",\n \"docs/FLEET.md\"\n ]\n },\n \"trust\": {\n \"localRuntime\": \"runtime, workspace state, and audit log stay on the user's machine\",\n \"hostedProviderBoundary\": \"the selected hosted provider receives turn context required for inference\",\n \"localInference\": \"a loopback local-model route can keep inference local\",\n \"telemetry\": \"Codewhale 0.10.3 counts anonymous usage by default and discloses it at first launch (policy notice version 5, naming Codewhale and PostHog as processors), while the earlier 0.9.11 release asked first; turning it off is a saved choice that later versions keep, an opt-out recorded under the earlier opt-in policy stays off, and showing the notice never records any acceptance on the user's behalf; Codewhale does not collect conversations, code, prompts, files, file/repo/branch names, model content, credentials, or per-turn/per-tool timelines; while on, sessions post aggregate counts and closed enums to the first-party endpoint https://telemetry.codewhale.net/v1/telemetry, whose storage has no IP, country, or geo column and a fixed three-month retention; optional PostHog forwarding requires separate operator configuration and verified IP-safe egress, and its retention is a separate project setting; telemetry_endpoint = \\\"\\\" writes only to a local dry-run file; no mandatory hosted relay\",\n \"account\": \"no account required for the local runtime\",\n \"plan\": \"Plan centrally refuses file mutation and shell execution; policy-allowed research may contact external services and local session state can still be persisted.\",\n \"sandbox\": \"Seatbelt is used on macOS when available; Linux bubblewrap is on by default once installed and verified working, and prefer_bwrap = false opts out; Windows currently reports no OS sandbox\",\n \"audit\": \"sensitive events append best-effort to $CODEWHALE_HOME/audit.log (default ~/.codewhale/audit.log); write failures are logged\",\n \"usage\": \"provider token and cache usage is shown locally when available\",\n \"sources\": [\n \"docs/PROVIDERS.md\",\n \"docs/MODES.md\",\n \"docs/SANDBOX.md\",\n \"docs/RUNTIME_API.md\",\n \"docs/WEB.md\",\n \"crates/runtime/src/audit.rs\",\n \"web/app/[locale]/faq/page.tsx\",\n \"web/app/[locale]/install/page.tsx\"\n ]\n },\n \"platforms\": {\n \"prebuilt\": [\n \"Linux x64\",\n \"Linux arm64\",\n \"macOS x64\",\n \"macOS arm64\",\n \"Windows x64\",\n \"Windows arm64\"\n ],\n \"preview\": [\n \"Android/Termux arm64\"\n ],\n \"sources\": [\n \"docs/INSTALL.md\"\n ]\n },\n \"repository\": {\n \"canonical\": \"https://github.com/codewhale-hq/CodeWhale\",\n \"mirrors\": [\n \"https://cnb.cool/codewhale.net/codewhale\",\n \"https://npmmirror.com/package/codewhale\"\n ],\n \"creditSources\": [\n \"CHANGELOG.md\",\n \"docs/CONTRIBUTORS.md\",\n \"web/lib/release-credits.ts\"\n ],\n \"requiredCandidateCredits\": [\n \"@printf033\",\n \"@isaacs\",\n \"@7jrxt42BxFZo4iAnN4CX\",\n \"@SparkofSpike\",\n \"@gaord\",\n \"@Lstarsky0\",\n \"@asto18089\",\n \"@Aboysky\",\n \"@ddlw\",\n \"@laozidabo\",\n \"@Ricardo-M-L\",\n \"@luochen211\",\n \"@gannp\",\n \"@Felixkw12\",\n \"@Gabriel-Degret\",\n \"@soarkit\",\n \"@liuyy2\",\n \"@luestr\",\n \"@M-Maciej\",\n \"@Statter\",\n \"@eugenicum\"\n ]\n },\n \"screenshot\": {\n \"readme\": \"web/public/codewhale-tui-0102-demo.png\",\n \"website\": \"web/public/codewhale-tui-0102-demo.png\",\n \"sourceVersion\": \"0.10.2\",\n \"sourceCommit\": \"bd0c32567024d5bc3efac7d13331c68711de031e\",\n \"sourceSha256\": \"43ebde941cedb690612945b712e9565e92a441045c78646bf8841f3ad43ac369\",\n \"sourceDirty\": true,\n \"sourceDiffSha256\": \"499468bbf066901c9b229c912fe64d7fdcb672674483ce8dbe59db7c66110c45\",\n \"binarySha256\": \"6ac94bff89046b5de4386c4ed39da2b86b2c53233d3c652f09022c83095e14dd\",\n \"terminal\": \"real PTY cell capture; the product page renders native cells as selectable HTML text, and the README image is the same frame exported as SVG by web/scripts/render-terminal-capture.mjs and rasterized as PNG\",\n \"theme\": \"Underwater (default)\",\n \"capture\": \"Captured on 2026-10-08 from the development build of a dirty 0.10.2 candidate based on bd0c32567024d5bc3efac7d13331c68711de031e by the opt-in website_current_terminal_capture test using crates/tui/tests/support/qa_harness. Exact source diff and binary hashes are recorded here and in the capture manifest. A deterministic loopback provider drives the real Engine to edit a JSON exporter, run three passing local Python tests and delegate a source-grounded read-only review. Home, unsent composer, configured provider manager, help, Tasks and Fleet are actual application output with no retouched cells or fixture history. Sealed HOME and Full Access apply only to the isolated demo workspace. This is a local scripted demo, not published-release or external-provider qualification.\",\n \"sources\": [\n \"crates/tui/tests/support/qa_harness\",\n \"crates/tui/tests/cucumber/launch_card_pty.rs\",\n \"web/lib/terminal-captures/manifest.json\",\n \"web/lib/terminal-captures/website-home-100x24.json\",\n \"web/scripts/render-terminal-capture.mjs\",\n \"web/public/codewhale-tui-0102-demo.png\"\n ]\n }\n}\n"
|
||
}
|
||
]
|
||
},
|
||
{
|
||
"id": "constitution",
|
||
"slug": "constitution",
|
||
"sources": [
|
||
{
|
||
"path": "docs/ARCHITECTURE.md",
|
||
"sha256": "0c525a1d7f6e2ae1bf9f0961a92a77c03cc61c54bac8a72f2323a9f30d6e7a41",
|
||
"content": "# Codewhale Architecture\n\n> 阅读简体中文版:[zh_hans/ARCHITECTURE.md](zh_hans/ARCHITECTURE.md)。\n\nCodewhale Engine is the existing Rust execution runtime. The\n[Runtime API](RUNTIME_API.md) is its client interface, and\n[TypeScript mods](EXTENSIONS.md) contribute reviewed extensions.\n\nCurrent boundary note (read the workspace version from `Cargo.toml`; this\nboundary has held since v0.9.1):\n- `crates/tui` is still the live end-user runtime for the TUI, runtime API, task manager, and tool execution loop.\n- Other workspace crates are being split out incrementally, but they are not yet the sole runtime source of truth.\n- `crates/net` owns network policy/auditing, destination and DNS guards, proxy resolution, and Claude import planning. Host tool context, transport grants, redirects and response limits stay with their callers. Pure extension-host settings belong to `crates/config`.\n- `crates/notify` owns shared notification payload redaction, attention/category gates, sound policy, and local audio dispatch. TUI retains terminal sinks and native hosts retain banner submission.\n- `crates/util` owns shared string/path/atomic-write helpers, confined workspace file access, link and file-identity checks, bundle file readers, the `.git` name rule and the exclusive file lock.\n- `crates/dependencies` owns external-tool resolution and bounded subprocess probes (git, python, node, bun); `crates/snapshot` the pre/post-turn side-repo snapshots; `crates/lsp` the language-server client; `crates/repl` the Python REPL runtime and RLM context files.\n- `crates/plugins` owns plugin manifests, discovery, trust review, install and marketplace parsing. Its test-only constructors and policy guards sit behind a dev-dependency-only `test-support` feature; the DSH composition reviewer is registered by the TUI at startup.\n- `crates/skill-install` owns the community-skill installer (GitHub and tarball sources, the curated registry, bounded extraction, install and trust markers), the package content digest and the shared `SKILL.md` frontmatter reader. Its SSRF DNS pin and shortened timeouts are relaxed only under its dev-dependency-only `test-support` feature.\n\n<img src=\"assets/architecture.webp\" alt=\"Codewhale architecture: clients reach the one turn loop, Engine::run_turn, through the CLI or the Runtime API; the engine uses crates/core to build requests, runs tools under approval and sandbox policy, routes to any provider through crates/config, and logs everything model-visible to the session store.\" width=\"1000\">\n\nThe diagram draws the Runtime API and the task manager as their own boxes; today\nboth are hosted inside `crates/tui`, as the note above says. The GPUI desktop\napp and the VS Code extension live outside this repository. The arrow from the\nengine to the session log follows the \"model-visible means logged\" contract\nrather than a traced write path.\n- `crates/tui` is being split into crates sized as agent work partitions:\n contracts, utilities, services, providers, runtime, leaf feature crates,\n TUI views and hosts, in the order `docs/design/TUI_DECONSTRUCTION.md`\n records. The TUI stays the only crate that writes to the terminal. Until a\n module has moved, its path under `crates/tui/src` is still where it lives.\n- The LSP subsystem (`crates/lsp/src/`) is fully wired into the engine's\n post-tool-execution path (`core/engine/lsp_hooks.rs`), providing inline\n diagnostics after `File` write, edit, and patch actions.\n- The swarm agent system was removed in v0.8.5. The active sub-agent surface is\n the single `agent` tool; persistent RLM sessions are available through the\n deferred `rlm` action family.\n No model-visible swarm tool remains in the active codebase.\n\n## High-Level Overview\n\n```\n┌─────────────────────────────────────────────────────────────────┐\n│ User Interface │\n│ ┌─────────────────┐ ┌─────────────────┐ ┌────────────────┐ │\n│ │ TUI (ratatui) │ │ One-shot Mode │ │ Config/CLI │ │\n│ └────────┬────────┘ └────────┬────────┘ └────────┬───────┘ │\n└───────────┼─────────────────────┼────────────────────┼──────────┘\n │ │ │\n ▼ ▼ ▼\n┌─────────────────────────────────────────────────────────────────┐\n│ Core Engine │\n│ ┌─────────────────────────────────────────────────────────┐ │\n│ │ Agent Loop (core/engine.rs) │ │\n│ │ ┌─────────┐ ┌─────────────┐ ┌──────────────────────┐ │ │\n│ │ │ Session │ │ Turn Mgmt │ │ Tool Orchestration │ │ │\n│ │ └─────────┘ └─────────────┘ └──────────────────────┘ │ │\n│ └─────────────────────────────────────────────────────────┘ │\n└─────────────────────────────────────────────────────────────────┘\n │ │ │\n ▼ ▼ ▼\n┌─────────────────────────────────────────────────────────────────┐\n│ Tool & Extension Layer │\n│ ┌──────────┐ ┌──────────┐ ┌─────────┐ ┌────────────────┐ │\n│ │ Tools │ │ Skills │ │ Hooks │ │ MCP Servers │ │\n│ │ (shell, │ │ (plugins)│ │ (pre/ │ │ (external) │ │\n│ │ file) │ │ │ │ post) │ │ │ │\n│ └──────────┘ └──────────┘ └─────────┘ └────────────────┘ │\n└─────────────────────────────────────────────────────────────────┘\n │ │ │\n ▼ ▼ ▼\n┌─────────────────────────────────────────────────────────────────┐\n│ Runtime API + Task Management │\n│ ┌─────────────────────────────┐ ┌──────────────────────────┐ │\n│ │ HTTP/SSE Runtime API │ │ Persistent Task Manager │ │\n│ │ (runtime_api.rs) │ │ (task_manager.rs) │ │\n│ └─────────────────────────────┘ └──────────────────────────┘ │\n└─────────────────────────────────────────────────────────────────┘\n │ │\n ▼ ▼\n┌─────────────────────────────────────────────────────────────────┐\n│ LLM Layer │\n│ ┌──────────────────────────────────────────────────────────┐ │\n│ │ LLM Client Layer (client.rs) │ │\n│ │ ┌──────────────────┐ ┌─────────────────────────────┐ │ │\n│ │ │ OpenAI-compatible │ │ Anthropic / Responses │ │ │\n│ │ │ (chat adapter) │ │ (adapters) │ │ │\n│ │ └──────────────────┘ └─────────────────────────────┘ │ │\n│ └──────────────────────────────────────────────────────────┘ │\n└─────────────────────────────────────────────────────────────────┘\n```\n\n## Module Organization\n\n### Entry Point\n\n- **`crates/cli/src/main.rs`** - The canonical executable entry point. `crates/cli/src/lib.rs` owns its command interface; terminal and headless runtime startup runs in process through the `codewhale_tui` library in `crates/tui/src/lib.rs`.\n\n### Core Components\n\n- **`core/`** - Main engine components\n - `engine.rs` - Engine state, operation handling, message processing\n - `engine/turn_loop.rs` - The existing Engine outer turn loop, shared tool\n planner/executor/result handling, and stream decoder. Its private\n `turn_loop/` phases contain request preparation (`preparation.rs`), model\n dispatch and admission (`model_step.rs`), the ordered continuation ladder\n (`continuation.rs`), inline REPL orchestration (`inline_repl.rs`), and the\n direct model tool batch (`tool_batch.rs`). They borrow the same Engine and\n TurnContext; they introduce no runtime, session, prompt, approval, event,\n or persistence authority. Retry, loop termination, and immediate return\n stay distinct, and only the existing productive paths advance the step.\n - `session.rs` - Session state management\n - `turn.rs` - Turn-based conversation handling\n - `events.rs` - Event system for UI updates\n - `ops.rs` - Core operations\n\n### Configuration\n\n- **`config.rs`** - Configuration loading, profiles, environment variables\n- **`settings.rs`** - Runtime settings management\n\n### Workspace Crates\n\n- **`crates/cli`** - The canonical `codewhale` executable and command interface.\n It owns commands such as `auth`, `metrics` and `update`, and invokes terminal\n and headless modes (`run`, `exec`, `doctor`, `sessions`, ...) in process via\n `codewhale_tui::run(RuntimeOptions, args)`. `crates/tui` is a library; the\n `codew` and legacy release filename aliases contain the same executable.\n- **`crates/tools`** - Shared tool invocation primitives, including tool result/error/capability types used by the TUI runtime.\n- **`crates/agent`** - Model/provider registry (ModelRegistry) for resolving model IDs to provider endpoints.\n- **`crates/agent-contract`** - Fleet role, child authority (`ChildAuthority`, `ChildGrant`) and worker runtime profile types the agent tool resolves before it spawns a child.\n- **`crates/app-server`** - HTTP/SSE + JSON-RPC app server transport for\n headless agent workflows. The canonical executable dispatches\n `app-server --http`/`--mobile` in process to the runtime API hosted by the\n `codewhale_tui` library.\n- **`crates/config`** - Config loading, profiles, environment variable precedence, CLI runtime overrides.\n- **`crates/cloud-facts`** - Fetches the signed Codewhale cloud facts channel\n (`facts/v1`), verifies its Ed25519 envelope, and keeps a verified disk cache;\n never a startup dependency.\n- **`crates/command-contract`** - Prototype command capability and dispatch\n shapes for the staged extraction of TUI commands; shapes only, not yet the\n production dispatch path.\n- **`crates/core`** - Provider-neutral request construction (`request.rs`),\n bounded context fragments, the tool-call parser, and thread/session types.\n It does **not** own the agent loop: the live turn loop is\n `Engine::run_turn` in `crates/engine/src/core/engine/turn_loop.rs`, and\n `crates/engine/src/core/` is a module inside the TUI crate, not this crate. A\n placeholder `engine/` tree here once suggested otherwise — it had no callers\n and emitted `TurnComplete` without contacting a model — and was removed in\n v0.9.11. The source guard follows resolved local phase calls and still\n rejects unlisted loop owners. ACP stdio now projects the existing Runtime\n manager and Engine; it keeps no provider/tool round loop or separate history.\n Recursive RLM and mounted Python RPCs now project captured caller authority\n onto the same Engine producer and Session; no RLM loop exception remains.\n Python retains its context and variables, while each round borrows the\n captured route, Native selection, original code gate, cancellation and\n deadline. Task guidance is bounded and additive to Core policy. Recursive\n history is retained whole; overflow refuses rather than compacting it.\n Persistent `rlm` contexts remain caller-session scoped and `share_session=true`\n explicitly refuses. Child workers also use their captured admission in the\n same Engine; neither nested host retains a turn-loop exception.\n- **`crates/execpolicy`** - Approval/sandbox policy engine for tool execution decisions.\n- **`crates/hooks`** - Event sinks (stdout, JSONL file, webhook, Unix socket)\n for response, tool, job and approval lifecycle events, plus the opt-in\n lifecycle outbox. User-configured shell hooks that run commands around tool\n calls are a separate system in `crates/engine/src/hooks.rs`.\n- **`crates/localization`** - Locale registry for user-facing UI chrome strings\n (`crates/localization/locales/*.json`); it never changes prompts or model\n output language.\n- **`crates/mcp`** - MCP client + stdio server for Model Context Protocol tool servers.\n- **`crates/memory`** - Local, scoped, provenance-bearing memory and\n resumable state (a library, not a second agent loop).\n- **`crates/models`** - Provider request/response models and the offline model\n metadata catalog.\n- **`crates/palette`** - Colour tokens, themes, and contrast math for the\n terminal UI. Its `ratatui` feature (on by default) gates everything that\n renders; theme ids, setting normalizers and hex parsing compile without it,\n which is how the runtime links it.\n- **`crates/paths`** - User-scoped runtime path authority (`CODEWHALE_HOME`\n and platform home resolution).\n- **`crates/protocol`** - Request/response framing and protocol types.\n- **`crates/runtime`** - `codewhale-runtime`, the headless runtime being\n split out of `crates/tui` (`docs/design/TUI_DECONSTRUCTION.md`). Today it\n holds the leaf modules that moved first (retry status, safe labels, sleep\n guard, session tree, ...) and `host_terminal`, the one port through which\n runtime code asks the terminal UI for a terminal effect. It never depends on\n the TUI, `ratatui` or `crossterm`; `scripts/check-command-crate-boundaries.py`\n enforces that and ratchets the runtime -> UI references still in `crates/tui`.\n- **`crates/sandbox`** - Command sandbox policy, platform wrapper preparation,\n read denial, process hardening and the shared shell dispatcher. Runtime\n config and terminal ports remain dependencies; this crate never owns a\n turn loop or depends on the TUI.\n- **`crates/secrets`** - Secret storage. API keys and MCP OAuth tokens live in\n a private file store (`~/.codewhale/secrets/secrets.json`, mode `0600` on\n Unix) by default; the OS keyring is used only when\n `CODEWHALE_SECRET_BACKEND=system` opts in, and a keyring that fails its probe\n falls back to the file store. Account sessions always use the file store.\n The crate also re-exports the shared output sanitizer (`sanitize`) and\n redaction (`redact`) from `crates/sanitize`, which UI and runtime code both\n call.\n- **`crates/state`** - SQLite thread/session persistence layer.\n- **`crates/telemetry`** - Anonymous, user-disableable aggregate usage\n counting; the only crate allowed to build or send a telemetry payload\n (`docs/TELEMETRY.md`).\n- **`crates/workflow`** / **`crates/workflow-js`** - Workflow engine and its\n QuickJS scripting layer (renamed from the whaleflow crates).\n- **`crates/lane`** - Lane runtime: durable, attachable running instances of\n Fleet/Workflow work (`codewhale lane list/status/attach/logs/stop`).\n- **`crates/release`** / **`crates/build-support`** - Release checks and build\n plumbing.\n\n### Embedded turn terminal ownership\n\n`TurnComplete` is the authoritative terminal event; `Error` remains advisory.\nThe engine and protocol projection preserve the operation's original identity:\n\n- Model turns echo their actual `TurnStarted.turn_id` and the submitted\n `submission_id` on completion, failure and interruption. Runtime self-starts\n carry no host submission token.\n- A route-install failure or an `edit_last_turn` rejection before\n `TurnStarted` emits a failed terminal with the rejected `submission_id` and\n no `turn_id`. It does not claim a previous turn. Image and constitution\n rejections in `handle_send_message` still emit only `Error`.\n- Manual compaction terminals carry only their `compaction_id`, including route\n rejection and cancellation. Automatic compaction remains part of its owning\n model turn. Purge operations carry none of these identities.\n\nWire fields are optional and default to absent for older producers. An embedded\nhost must match an owned receipt before closing a pending operation; it must\nnot infer ownership from advisory errors, an absent ID, or whichever request\nhappens to be busy. Hosts also need their own engine-incarnation fence: these\nIDs do not authorize an event from an evicted runtime to mutate its\nreplacement. This contract adds no model prompt/context contributor and no\nsecond turn loop.\n\n### LLM Integration\n\n- **`client.rs`** - The live HTTP client layer: OpenAI-compatible, Anthropic,\n and Responses wire adapters, DeepSeek request-boundary handling, retry\n policy, and streaming. Provider routes land here through the shared config\n and catalog layers.\n- **`llm_client/`** - LLM client trait, retry logic, and error classification\n (`LlmClient`, `RetryConfig`, `with_retry`) consumed by `client.rs`; `mock.rs`\n is test-only (`#[cfg(test)]`).\n- **`crates/models`** (`codewhale_models`) - Data structures for API\n requests/responses; the TUI crate has no local `models.rs`.\n\n#### DeepSeek API Endpoints\n\nDeepSeek exposes OpenAI-compatible endpoints. The first-party route uses:\n- `https://api.deepseek.com/beta` - default DeepSeek base URL (`provider_defaults.rs`)\n- `https://api.deepseek.com/beta/models` - live model discovery and health checks\n\n`https://api.deepseek.com/v1` is accepted for OpenAI SDK compatibility, and\ncan still be configured explicitly to opt out of beta-only features such as\nstrict tool mode, chat prefix completion, and FIM completion. The public\nDeepSeek docs do not document a Responses API path for this workflow; the engine\ndrives turns through Chat Completions.\n\n### Tool System\n\n- **`tools/`** - Built-in tool implementations\n - `mod.rs` - Tool registry and common types\n - `shell.rs` - Shell command execution\n - `file.rs` - File read/write operations\n - `todo.rs` - Checklist tools plus legacy todo aliases\n - `tasks.rs` - Model-visible durable task, gate, background shell, and PR-attempt tools\n - `git.rs` - Read-only `git_status` / `git_diff` inspection wrappers\n - `git_tool.rs` - The canonical action-based `Git` tool (`status | diff | log | show | blame`); per-action legacy aliases were removed in v0.9.3\n - `git_history.rs` - Read-only `git_log` / `git_show` / `git_blame`\n - `github/` - Unified `github` tool family (read-only context plus guarded\n comment/closure actions backed by `gh`); deferred by default and\n discoverable through `tool_search`\n - `automation.rs` - Model-visible scheduling tools over `AutomationManager`\n - `plan.rs` - Planning tools\n - `subagent/` - Sub-agent launch and supervision. `agent` is the one\n creation surface; `subagent/coord.rs` adds narrow coordination tools\n (`agents/list`, `agents/message`, `agents/followup`, `agents/interrupt`,\n `agents/wait`, `agents/coordinate`) over the existing manager. The\n `agent_open`/`agent_eval`/`agent_close` lifecycle surface was retired\n (see the `subagent/coord.rs` module doc)\n - `spec.rs` - Tool specifications\n - `rlm.rs` - Persistent Recursive Language Model (RLM) sessions — persistent local Python REPL subprocesses (environment-scrubbed, not OS-sandboxed) with semantic helper calls and `var_handle` output support\n\n### Extension Systems\n\n- **`mcp.rs`** - Model Context Protocol client for external tool servers\n- **`skills/`** - Skill discovery and registry for local `SKILL.md` files, plus audit and mutation (the installer lives in `crates/skill-install`; plugin loading lives in `crates/plugins`)\n- **`hooks.rs`** - Pre/post execution hooks with conditions\n\n### User Interface\n\n- **`tui/`** - Terminal UI components (ratatui-based; this is a representative\n list, not exhaustive - the module has grown to 80+ focused files):\n - `app.rs` - Application state and message handling\n - `ui.rs` - Event handling, streaming state, and rendering logic\n - `approval.rs` - Tool approval dialog\n - `clipboard.rs` - Clipboard handling\n - `underwater.rs` - Main shell chrome: status chips, mode labels, phase rail\n\n### LSP Integration\n\n- **`lsp/`** - Post-edit diagnostics injection (#136)\n - `mod.rs` - `LspManager` — lazy per-language transport pool + config\n - `client.rs` - `StdioLspTransport` — JSON-RPC over stdio with `didOpen`/`didChange`/`publishDiagnostics`\n - `diagnostics.rs` - Diagnostic types, severity, and HTML-block renderer\n - `registry.rs` - Language detection and the default server map: `rust-analyzer`,\n `gopls`, `pyright-langserver`, `typescript-language-server`, `jdtls`,\n `intelephense` (PHP), `vue-language-server`, `clangd` (`lsp/registry.rs:98-110`)\n - Wired into the engine via `core/engine/lsp_hooks.rs` — called after every successful edit\n\n### Security\n\n- **`crates/sandbox/src/`** - platform sandbox policy preparation and denial reporting\n - `lib.rs` - Sandbox type definitions and manager\n - `backend.rs` - Pluggable sandbox backend abstraction (routes shell\n execution to a remote service, e.g. Alibaba OpenSandbox)\n - `policy.rs` - Sandbox policy configuration\n - `opensandbox.rs` - Alibaba OpenSandbox HTTP backend adapter\n - `seatbelt.rs` - macOS Seatbelt profile generation\n - `bwrap.rs` - Linux bubblewrap command wrapper\n - `process_hardening.rs` - Startup Linux process hardening\n - `read_guard.rs` - Shared protected-path read admission\n - `shell_dispatcher.rs` - Shared shell detection and command construction\n - `process_hardening.rs` - Linux kernel-level hardening for the TUI process\n itself (defense-in-depth; not a child-command sandbox)\n - `windows.rs` - Windows helper contract; not advertised until a Job\n Object process-containment helper exists\n\n### Utilities\n\n- **`utils.rs`** - Common utilities\n- **`logging.rs`** - Logging infrastructure\n- **`compaction.rs`** - Context compaction for long conversations\n- **`purge.rs`** - Agent-driven context purging (surgical message removal/rewriting)\n- **`pricing.rs`** - Cost estimation\n- **`prompts.rs`** - System prompt templates\n- **`runtime_api.rs`** - HTTP/SSE runtime API (`codewhale serve --http`)\n- **`runtime_threads.rs`** - Durable thread/turn/item store + replayable event timeline\n- **`task_manager.rs`** - Durable queue, worker pool, task timelines and artifacts\n\n## Data Flow\n\n### Interactive Session\n\n1. User input received in TUI\n2. Input processed by `core/engine.rs`\n3. Message sent to LLM via `client.rs`\n4. Response streamed back, parsed in `client.rs`\n5. Tool calls extracted and executed via `tools/`\n6. Hooks triggered before/after tool execution\n7. Results aggregated and sent back to LLM\n8. Final response rendered in TUI\n\n### Crash Recovery + Offline Queue\n\n1. Before sending user input, the TUI writes a checkpoint snapshot to `~/.codewhale/sessions/checkpoints/latest.json`\n2. Startup remains fresh by default; prior sessions are resumed explicitly via `--resume`/`--continue` (or `Ctrl+R` in TUI)\n3. While degraded/offline, new prompts are queued in-memory and mirrored to `~/.codewhale/sessions/checkpoints/offline_queue.json`\n4. Queue edits (`/queue ...`) are persisted continuously so drafts and queued prompts survive restarts. Messages restored from an earlier launch or another session are held: they are sent only by `/queue send <n>` and discarded by `/queue clear`\n5. Successful turn completion clears the active checkpoint and writes a durable session snapshot\n6. Action-capable turns also take pre/post-turn side-git workspace snapshots under `~/.codewhale/snapshots/<project_hash>/<worktree_hash>/.git`; `/restore N` and `revert_turn` restore file state without changing conversation history or the user's `.git`\n\n### Tool Execution\n\n1. LLM requests tool via `tool_use` content block\n2. Tool registry looks up handler\n3. Pre-execution hooks run\n4. Approval requested when the effective permission posture and policy require it\n5. Tool executed (possibly wrapped by Seatbelt on macOS or bubblewrap on Linux)\n6. Post-execution hooks run\n7. Result metadata is retained on runtime item records\n8. **LSP post-edit hook**: after a `File` write, edit, or patch action (including a replay-only legacy alias), the engine runs `run_post_edit_lsp_hook()` when LSP is enabled to collect diagnostics\n9. **Diagnostics flush**: before the next API request, `flush_pending_lsp_diagnostics()` injects any collected errors as a synthetic user message\n10. Result returned to agent loop\n\n### Background Tasks\n\n1. Client enqueues task (`/task add ...` or `POST /v1/tasks`)\n2. `task_manager.rs` persists task + queue entry under `~/.codewhale/tasks`\n3. Worker picks queued task (bounded pool), transitions to `running`\n4. Task creates/uses a runtime thread and starts a runtime turn\n5. `runtime_threads.rs` persists thread/turn/item records + monotonic event sequence\n6. Timeline/tool summaries/artifact references are persisted incrementally\n7. Checklist state, verifier gates, PR attempts, and guarded GitHub events are applied from tool metadata to the active task\n8. Final state (`completed|failed|canceled`) is durable and queryable via TUI/API\n\nModel-visible durable task tools are a surface over this same manager. They do\nnot introduce a parallel work system: `task_create` enqueues normal tasks,\n`checklist_*` updates task-local progress, `task_gate_run` and completed\n`task_shell_wait` attach verification evidence, and automation runs enqueue\nordinary durable tasks.\n\n### Runtime Thread/Turn Timeline\n\n1. API/TUI creates or resumes a thread (`/v1/threads*`)\n2. Turn starts on the thread (`/v1/threads/{id}/turns`)\n3. Engine events are mapped to item lifecycle events (`item.started|item.delta|item.completed`)\n4. Interrupt/steer operations apply to the active turn only\n5. Compaction (auto/manual) is emitted as `context_compaction` item lifecycle\n6. Purge (agent-driven) is emitted as `context_purge` item lifecycle\n7. Clients replay history and resume with `/v1/threads/{id}/events?since_seq=<n>`\n\n### Durable Schema Gates\n\n- `session_manager.rs`, `runtime_threads.rs`, and `task_manager.rs` embed `schema_version` on persisted records.\n- On load, newer schema versions are rejected with explicit errors instead of silently truncating/overwriting data.\n- This allows safe forward migrations and prevents corruption when binaries and stored state are out of sync.\n\n## Extension Points\n\n### Adding a New Tool\n\n1. Create handler in `tools/`\n2. Register in `tools/registry.rs`\n3. Add tool specification (name, description, input schema)\n\n### Adding an MCP Server\n\n1. Configure in `~/.codewhale/mcp.json`\n2. Server auto-discovered at startup\n3. Tools exposed to LLM automatically\n\n### Creating a Skill\n\n1. Create skill directory with `SKILL.md`\n2. Define skill prompt and optional scripts\n3. Place in a Codewhale-owned root (`~/.codewhale/skills/` or\n `<workspace>/.codewhale/skills/`), or import from a compatible harness root\n through `/skills`\n\nSee [SKILLS.md](SKILLS.md) for the Skills Manager, audit inventory, and the\nrule that compatible roots (`.claude`, `.agents`, …) are never mutated in place.\n\n### Adding Hooks\n\nConfigure in `~/.codewhale/config.toml`:\n\n```toml\n[[hooks]]\nevent = \"tool_call_before\"\ncommand = \"echo 'Running tool: $TOOL_NAME'\"\n```\n\n## Key Design Decisions\n\n1. **Streaming-first**: All LLM responses stream for responsiveness\n2. **Tool safety**: Ask and Auto-Review require approval according to tool and\n managed policy; Full Access removes ordinary prompts but not hard safety\n holds. Side-effectful MCP tools use the same boundary.\n3. **Extensibility**: MCP, skills, and hooks allow customization without code changes\n4. **Cross-platform**: Core works on Linux/macOS/Windows. Sandbox guarantees\n are platform-specific: macOS uses Seatbelt when available; Linux uses an\n installed bubblewrap executable by default when a probe shows it works; Windows has\n no advertised OS command sandbox. The Windows helper contract is not\n wired into command execution.\n5. **Minimal dependencies**: Careful dependency selection for build speed\n6. **Local-first runtime API**: HTTP/SSE endpoints are intended for trusted localhost access and are served by the `crates/tui` runtime today\n7. **Lock poison**: fail-stop by default. A poisoned lock means a holder\n panicked mid-mutation, so `.expect()` with a message naming the lock is\n the standard posture — never serve half-updated state. Recover with\n `into_inner()` only where stale state is safe (caches, idempotent\n rebuilds), with a comment saying why.\n\n## Configuration Files\n\n- `~/.codewhale/config.toml` - Main configuration (`~/.deepseek/config.toml` is still read as a legacy fallback)\n- `/etc/deepseek/managed_config.toml` - Optional managed defaults layer (Unix)\n- `/etc/deepseek/requirements.toml` - Optional allowed-policy constraints (Unix)\n- `~/.codewhale/mcp.json` - MCP server configuration\n- `~/.codewhale/skills/` - User skills directory\n- `~/.codewhale/sessions/` - Session history\n- `~/.codewhale/sessions/checkpoints/` - Crash checkpoint + offline queue persistence\n- `~/.codewhale/snapshots/` - Side-git pre/post-turn workspace snapshots for `/restore` and `revert_turn`\n- `~/.codewhale/tasks/` - Background task records, queue, timelines, artifacts\n- `~/.codewhale/audit.log` - Append-only security events: credential saves and clears, hook environment key names, compaction passes, goal completions, the terminal's approval routing, Auto-Review verdicts, and outbound network decisions when `[network]` auditing is on. Not an action record: it holds no commands or file changes, and app or `serve` turns write no approvals there. See `docs/RECEIPTS.md` for what a session did\n- `~/.codewhale/sessions/<id>/approval_receipts.jsonl` - Every approval ask and decision for a session, including who decided\n"
|
||
}
|
||
]
|
||
},
|
||
{
|
||
"id": "tools",
|
||
"slug": "tools",
|
||
"sources": [
|
||
{
|
||
"path": "docs/TOOL_SURFACE.md",
|
||
"sha256": "949991cc3ca1b4281507fade514ea5a6fd7dac66706ea1e2859a2ce28b5e30ac",
|
||
"content": "# Tool surface\n\n> 阅读简体中文版:[zh_hans/TOOL_SURFACE.md](zh_hans/TOOL_SURFACE.md)。\n\nThis document describes the current model-facing tool contract. The v0.9.1\ncutover that produced it is recorded in `docs/RUNTIME_SIMPLIFICATION_DESIGN.md`;\nread the workspace version from `Cargo.toml`, not from this line. The registry\nremains larger than the first-turn catalog so\nsaved transcripts can replay and uncommon capabilities can be loaded on demand.\nThe model should learn one canonical name for each common operation.\n\nImplementation sources:\n\n- `crates/engine/src/core/engine/tool_catalog.rs` owns the eager/deferred catalog.\n- `crates/engine/src/tools/registry.rs` registers canonical tools and hidden aliases.\n- `crates/engine/src/tools/{file,file_tool,shell}.rs` own the small foreground\n primitive behavior and schemas; the other native tools remain searchable.\n- `docs/RUNTIME_SIMPLIFICATION_DESIGN.md` records the v0.9.1 cutover and receipt.\n\n## Default-active contract\n\nNew turns start with sixteen eager native names plus synthetic `tool_search`:\n\n1. `read`\n2. `write`\n3. `edit`\n4. `file_search`\n5. `bash`\n6. `agent`\n7. `workflow`\n8. `todo_write`\n9. `create_goal`\n10. `get_goal`\n11. `update_goal`\n12. `task_shell_wait`\n13. `present`\n14. `load_skill`\n15. `context_archive_get`\n16. `context_archive_search`\n17. `tool_search` (synthetic, always active)\n\nThe sixteen native names are `DEFAULT_ACTIVE_NATIVE_TOOLS` in\n`crates/engine/src/core/engine/tool_catalog.rs`, pinned by\n`default_active_contract_keeps_discovery_and_core_tools_eager`. An authority\nboundary may remove `agent` at the maximum child depth, and Plan registers only\nthe foreground shell surface (so no `task_shell_wait`), but route size alone\nmust not change this core vocabulary.\n\nThe direct schemas deliberately stay small:\n\n| Tool | Input | Purpose |\n|---|---|---|\n| `read` | `path`, optional `offset`, optional `limit` | Read a bounded file window with explicit continuation or truncation notices. |\n| `write` | `path`, `content` | Create or replace a file. |\n| `edit` | `path`, `edits` | Apply one or more unambiguous text replacements against one original snapshot. |\n| `file_search` | `query`, optional `path`, `limit`, `extensions`, `exclude` | Find files by name, path fragment or glob and return bounded, ranked matches. Respects ignore files; `exclude` replaces default exclusions. |\n| `bash` | `command`, optional `timeout`, optional `description` | Run one cancellable foreground shell command and return a bounded tail. The optional description is display-only. |\n| `agent` | delegated task and optional scope/context controls | Start or inspect focused child work. |\n| `workflow` | plan/script/source_path plus run controls | Coordinate multi-agent phases with dependencies and completion checks. |\n| `todo_write` | complete replacement list of `{content, status}` items | Keep optional, agent-owned progress notes for genuinely multi-step work. |\n| `create_goal` | objective plus optional budget | Start the session goal the turn works toward. |\n| `get_goal` | none | Read the active goal and its progress. |\n| `update_goal` | terminal status | Mark the goal complete or blocked. |\n| `task_shell_wait` | `task_id`, optional `wait`, `timeout_ms`, `cancel`, `gate` | Poll or stop a background shell task. Completion still arrives as a runtime event, so use it for early output, true barriers, or gate evidence. |\n| `present` | `files`, optional `note` | Declare 1–32 existing workspace files as final deliverables with a read-only SHA-256 handoff receipt; nothing is copied or moved. |\n| `load_skill` | optional `name`, `query` | Load a named skill's `SKILL.md` body and companion-file list; omit `name` or use `name=\"list\"` to list eligible skills, or use `query` to search names and descriptions. |\n| `tool_search` | `query`, optional matching controls | Discover policy-allowed deferred tools and add selected schemas to this conversation's toolbox. |\n\nMode is an authority decision, not a synonym system. Plan, Work, and Operate\nuse the same primitive identities. Plan centrally refuses `write`, `edit`, and\n`bash`; Work and Operate still pass those calls through approval, sandbox,\ntrusted-path, repository-law, and managed-policy gates. Full Access changes\nordinary approval behavior but does not bypass hard safety or repository law.\n\n`update_plan` remains registered only for saved-artifact compatibility and is\nnot model-visible. `tasks`, `Git`, `Run`, `Web`, `remember`, and other\nspecialized capabilities are searchable rather than first-turn ceremony.\n\nOn the TUI's default (calm) shell card the Bash description is the headline,\nshown as written apart from stripped terminal escapes, collapsed whitespace and\na length bound, and the command sits on its own `$` row directly under it in\nevery state and at every width, including automatic runs. A card without a\ndescription leads with the `$` command instead. The labeled card\n(`show_tool_details = true` with `calm_mode = false`) keeps the command in its\nheader and the description as a muted `note:` row. The description never\nreplaces the command and is excluded from execution, exact approval keys and\nAuto-Review input. The original description remains in the recorded tool input\nand exports. Approval prompts, `/jobs`, the activity inspector, ACP and other\nRuntime clients keep showing the command; this change adds no description row\nto those surfaces.\n\n## Local code execution\n\n`code_execution` (Python) and `js_execution` (Node.js) use the same\npermission-aware local launcher as workspace task gates and test runs. Ordinary\napproval retains the effective session policy. To retry a denied exact call\nwith wider permissions, supply `sandbox_permissions` (`workspace-write` or\n`danger-full-access`) and a nonempty `justification`; Ask mode requires explicit\none-shot user approval. The approved policy affects only that call.\n\nBoth tools read source from stdin, return stdout/stderr/return_code, and keep\nthe existing timeout and process-tree cleanup. They are not persistent REPLs;\ntracebacks refer to stdin rather than a temporary script path. The separate\nRLM/REPL kernel is not changed by this launcher integration.\n\nPlatform/backend limits of the shared launcher still apply: no available local\nwrapper means workspace-write remains unenforced, while read-only is refused.\nAn external sandbox session cannot use these local interpreter tools and must\nuse its shell execution path. See [sandbox limits](SANDBOX.md).\n\n## Deferred and dynamic tools\n\n`Web` is conditional and deferred. It is discoverable through `tool_search`\nonly when the active policy and runtime backend permit it. Read-only\nchildren retain its read-only search/fetch evidence path; read-only authority\ndoes not mean \"unable to research.\"\n\nThe durable `github`, `automation`, and `rlm` action families are also deferred\nby default. `rlm` owns `open`, `eval`, `configure`, and `close` actions for a\npersistent local Python session (a subprocess with a scrubbed environment, not\nan OS sandbox). Inline ```` ```repl ```` fences in a reply run in the same kind of\nkernel only when `code_execution` is on the turn's surface (never in Plan mode),\nonly when the fence opens its own line, and only after `code_execution`'s\napproval under the session posture. Feature-gated native tools may be added to\nthe active or deferred catalog only when their implementation and host\ndependencies are available.\n\nMCP tools are dynamic. Successfully connected servers register names such as\n`mcp_<server>_<tool>` from `~/.codewhale/mcp.json`; a failed or disabled server\nmust not be presented as available. MCP and plugin tools are normally deferred. When the composition VM is eager and the turn admits exactly one compact named batch schema, `execute_tools` also accepts `{batch:{tool,arguments}}` instead of JavaScript. The VM forwards the data through the same nested gate, bounds and receipts; the server tool stays deferred unless explicitly requested. The current server routes and turn permissions narrow the shortcut’s member names; its nested dispatch checks the offered members after hooks. The server supplies a bounded guide to their arguments. Other tools stay searchable; `[tools].always_load` still makes individual schemas eager.\n\nFor an unstarted configured server, an MCP-focused `tool_search` first\nperforms bounded discovery under the current turn's server/tool ceiling.\nIt searches real server-provided schemas after connection, rather than\ninventing `mcp_*` definitions. A search inside `execute_tools` describes\nthose schemas without activating them; a direct search uses the existing\nbounded activation cache. Ordinary unrelated searches leave optional servers\nunstarted. The CLI's standalone MCP inspection commands own a separate pool.\n\n### Code mode (`execute_tools`)\n\nFor a known sequence, prefer a tool’s own batch operation. Use `execute_tools` when the next call depends on an intermediate result, and return the verified outcome. Search unknown names and schemas before acting. An unexpected state, refusal, uncertain delivery or direct-only decision ends that sequence.\n\n`execute_tools` is engine-injected, alongside the synthetic interpreter tools.\nIt runs a JavaScript program whose only host surface is\n`await tools.call(name, args)`, and it is the default way to compose several\ntool calls — MCP and plugin tools included — without round-tripping every\nintermediate result through the conversation. It is hidden from Plan mode and\nrefused under a worker authority envelope.\n\n- **One gate.** In a session turn every nested call is sent back to the turn\n loop and planned exactly like a direct call: deny/allow lists, preparation\n (MCP `readOnlyHint`/`destructiveHint`), `tool_call_before` hooks, ask-rules,\n Auto-Review, repository law, and the Computer Use consent refusal. MCP calls\n run through the session MCP pool. Approving the program grants nothing, so\n `execute_tools` itself is auto-approved. If the permission posture changes\n while a program runs, its remaining nested calls are refused (an approved\n call survives only an equal or broader posture, as for a direct call) and\n the model retries them under the new posture.\n- **Approvals suspend the program.** A nested call that needs approval raises\n the normal approval card (named `execute_tools program call: ...`) and the\n program waits; allow resumes it, deny fails only that nested call as an\n exception the program can catch. Time spent waiting on a decision does not\n count against the program's run deadline, which is the turn's remaining\n wall clock.\n- **Receipts.** The result lists every nested call with its decision (`auto`,\n `approved`, `denied`, `refused`) and status (`ok`, `failed`, `refused`,\n `in_flight`). A program that hits its deadline still returns the receipt;\n `in_flight` calls were cancelled and may have partially run. Each nested\n result is `{content, metadata, truncated}`; an oversized result keeps that\n shape, and `truncated` names the original size and the spillover file with\n the full output.\n- **Discovery without re-pinning.** Inside a program,\n `tools.call('tool_search', {query})` returns matching deferred tools with\n their input schemas and does not activate them, so the request's tool array\n and the session-pinned prefix do not change.\n- **Stays direct:** `agent`, `workflow`, `request_user_input`, nested\n `execute_tools`, interactive shells, sandbox escalation, Computer Use\n consent and scripts, and MCP sign-in (`mcp_<server>_authenticate`).\n\nCode mode is on by default (`[features] code_mode = true`), which makes\n`execute_tools` eager from the first request; direct tools and `tool_search`\nstay available either way. Set `code_mode = false` (or run with\n`--disable code_mode`) to go back to deferring `execute_tools` behind\n`tool_search`. The flag is session configuration, so the prompt prefix stays\nstable within a session.\n\nSub-agents get `execute_tools` on the same terms, unless their role is limited\nto the read-only evidence tools or an explicit tool list leaves it out. A\nchild's nested call is planned by the child's own gate, so its grant and its\napproval path apply as for its direct calls, and it is admitted at execution\nagainst the child's write-scope claim and live peers. A child's program also\nstops at the child's tool timeout (time waiting on approvals is not counted)\nor its work deadline, and still returns the receipt. A program in which no\nnested call ran does not count as progress for the child's permission-denial\nguard, but the guard does not yet count a refused nested call as a denial\neither; only the child's step and time budgets end a loop of denied calls\nretried inside programs. Without an engine turn\n(a direct unit call), a program keeps the conservative profile: read-only,\nauto-approved native calls only, no MCP.\n\n### Conversation toolbox cache\n\nA successful search activation is remembered by name for the current\nconversation. Its soft targets are eight deferred names and 16 KiB of added\nprovider-wire schemas, measured after the route's production transformations\nagainst the eager-tool baseline. An adapter that cannot measure the wire reports\nunknown bytes; internal `Tool` serialization is not substituted. To make room it evicts tools that were never called first\n(oldest activation first), then the least recently called; a tool called in the\ncurrent or previous turn is never evicted. When such tools already fill the\ncache, a search still activates its best match over the bound and holds the\nrest; a later activation restores the bound once those tools go unused. Every\nentry is revalidated against the current catalog and policy before it is\nadvertised again. A session sync clears it. The cache cannot resurrect a\nremoved, denied, or newly-eager tool.\n\nWhen a search's activation pushes a previously activated tool out of the\ncache, that search's result lists it under `evicted_tool_references`, so the\nmodel is told the tool is no longer callable rather than finding it missing\nfrom the next request. A held match is listed under\n`unavailable_tool_references`. Both carry `tool_search_input`, an exact regex\nquery for that one name that loads it again. Only tools that actually left the\nactive set are listed; a tool that became eager stays callable and is not\nreported. Each search also returns the current `active_deferred_tools` snapshot\nand wire-byte measurement in `toolbox`; a new admission explicitly supersedes\nolder eviction or unavailable notices for the same name. Other declared toolbox\nchanges, including revalidation and direct first-use activation, append one\ncurrent deferred-toolbox snapshot before the next model request. That snapshot\nsupersedes old availability notices without rewriting earlier history.\n\nEach subagent gets its own policy-filtered deferred catalog, always-present\n`tool_search`, and bounded activation cache. Forked messages and instructions\nremain in context, but the child cache starts empty and discovers tools locally;\nneither forked context nor a cache can become a discovery allowlist. A child can\nstill search every tool its own authority permits, including Web search/fetch\nfor read-only research roles.\n\n## Inspect the model-client request tool payload\n\nRun `/tools` after a model turn to inspect a bounded projection of the exact\ntool field in the latest prepared model-client request. `/tools json` emits the\nsame evidence as bounded machine-readable JSON. Both formats open in a pager;\nthey are not copied into transcript history. `/tool-studio` remains a human-\ncommand compatibility alias; it is not a model tool.\n\nThe snapshot distinguishes an absent tool field from a present empty array. It\nreports the exact model-client tool JSON byte count and SHA-256 digest only when\nmeasurement fits the one-MiB inspection bound; larger payloads stay unavailable.\nProvider adapters may transform, sanitize, or omit those fields while building\na provider-specific wire body, so `/tools` marks provider delivery and the wire\npayload unavailable. The separate per-request receipt described in [CACHE.md](CACHE.md)\nmeasures the production wire schema and joins it with reported provider usage. Capture and rendering are bounded: retained schemas,\ndescriptions, caller lists, catalog rows, turn IDs, and payload measurement all\ncarry explicit truncation, omission, or unavailable receipts. The snapshot stays\nin memory only for the current session and is replaced on each prepared request.\n\nProvider, model, approval, registry provenance, and runtime capability metadata\nare not fields in the request tool schema. `/tools` therefore reports them as\nunavailable instead of joining against mutable state or inferring values. Use\nthe separate route and permission receipts for those facts.\n\n## Modes and permission postures\n\nModes and permission postures are separate controls:\n\n- **Plan** keeps the stable primitive vocabulary but centrally refuses shell\n execution and file mutation.\n- **Work** is ordinary interactive execution.\n- **Operate** uses the same direct-tool authority as Work. Small work stays\n direct; multi-step delegation uses a compact Workflow plan with dependencies,\n bounded scopes, and completion evidence. Fleet manages the same sub-agents\n and roles. One bounded, independent task can use a direct agent; `followup`\n reuses that agent for continued work.\n- **Ask**, **Auto-Review**, and **Full Access** control approval behavior within\n an action-capable mode. They never widen Plan into write or shell access.\n\nSee `docs/MODES.md` for the full mode and posture contract.\n\n## Compatibility names\n\nThe model-facing contract is the lowercase core above. Saved v0.9.x\ntranscripts and protocol clients may still call exact hidden compatibility\nnames such as `File`, `Bash`, and the older single-operation file names. Those\nnames never enter a new model catalog or `tool_search` result.\n\nCompatibility is execution compatibility, not fuzzy aliasing: an exact legacy\ncall must reach the handler for its legacy schema. It must not be rewritten\ninto a small lowercase primitive whose input shape is different. Unknown or\nretired names still fail closed instead of guessing a destination.\n\nSpecialized native families such as `Git`, `Run`, and `Web` are not aliases for\nthe lowercase core. They remain real, policy-filtered deferred tools and are\nloaded through `tool_search` when needed.\n\n## Long-running work\n\n`bash` runs one cancellable foreground command. It does not carry background,\nTTY, wait, interact, or cancel action fields. Stateful process and terminal\ncontrol stays out of the shell schema: `task_shell_wait` is eager for the\nbackground jobs `bash` starts, while interactive terminal control remains\ndeferred behind `tool_search`; neither enlarges the first-turn shell schema.\n\nUse `tasks` when the work itself needs a durable lifecycle, structured gates,\nartifacts, replayable timelines, or a stable task id. Large tool results should\nremain behind bounded handles or artifacts instead of being copied wholesale\ninto the parent transcript.\n\n## Parallel fan-out\n\nThe sub-agent capacity source of truth is\n`crates/tui/src/config/subagent_limits.rs`:\n\n- default configured concurrency: **64**;\n- maximum configured concurrency: **128**;\n- maximum admitted running-plus-queued work: **1024**.\n\nThese are capacity ceilings, not advice to dispatch every available slot. A\nmanager should use the smallest useful fan-out, preserve a single owner for\nfan-in, and verify worker receipts before reporting combined completion.\n\nRLM child-query batching is a different, cheaper cost class. Its\n`sub_query_batch` helper accepts 1–16 one-shot children inside a live `rlm`\nsession; it is not a substitute for tool-carrying `agent` workers.\n\n## Human inspection: `/tools` (`/tool-studio`)\n\n`/tools` renders a **read-only, bounded human projection** of the tool field of\nthe request that was prepared for one `(turn, step)`. It is not a second\nregistry and not an execution surface.\n\n**The seam.** The snapshot is built in `crates/engine/src/core/engine/turn_loop.rs`\nimmediately after `MessageRequest` is constructed, from `request.tools` — the\nsame value the model client is handed. The engine resolves the surrounding\nper-turn data once in `engine.rs` (`ToolSurfaceContext`: flattened registry\nfacts, the MCP pool's own server attribution, the engine-injected catalog names,\nand the resolved model client's receipt) and passes it as plain data, so the\nper-step seam never re-locks the MCP pool or holds a tool object.\n\n**Turn and step identity.** The tool set can differ between steps of a turn, so\neach snapshot is stamped with turn id and step and each seam emits its own. The\nTUI keeps only the latest (`SessionState.last_tool_request_snapshot`). Before\nthe first seam there is no snapshot and `/tools` says so rather than rebuilding\na registry in the UI.\n\nTwo kinds of fact are kept apart:\n\n- **Wire facts** come from the prepared request: name, description, schema,\n `defer_loading` / `strict` / `allowed_callers` / `cache_control`, byte\n accounting, and the catalog digest.\n- **Surface facts** come from the `ToolSurfaceContext`: provenance\n (`builtin` / `plugin` / `mcp` / `synthetic` / `unknown`), MCP server identity,\n declared capabilities, declared approval requirement, and model visibility.\n\nContract:\n\n- **One digest.** `active_tool_catalog_sha256`\n (`crates/engine/src/core/engine/preview.rs`) is the single definition of the\n active-tool-catalog hash. The request manifest publishes it as\n `ToolSurfaceFacts::active_tool_catalog_sha256` and `/tools` reports the same\n value for the same prepared request; neither surface keeps a hash of its own.\n- **Nothing is guessed.** MCP server identity is shown only when the real pool\n attributed that exact model tool name. `McpPool::mcp_model_tool_name` is the\n single definition shared by the model catalog and the human attribution, and\n an ambiguous name (two servers colliding on one model name) resolves to no\n server. Synthetic provenance comes from\n `default_synthetic_catalog_tool_names`, which is asserted against the engine's\n own `is_synthetic_catalog_tool` predicate. A transmitted tool with no registry\n entry reports `capabilities: unknown`, never \"none\".\n- **Provider availability follows the resolved client.** It comes from\n `Engine::tool_surface_provider_receipt`, never from \"a tool registry exists\".\n With no client the receipt is `unavailable` even when the registry is full.\n- **Unknown shrinks, it does not vanish.** `unavailable_for_this_request` always\n contains `provider_wire_payload`: nothing on this path observes what the\n provider adapter finally transmits. It additionally contains `provider` and\n `model` without a resolved client, and `provenance` / `capabilities` /\n `approval` when no surface context was captured.\n- **Absent stays distinct from empty.** A request with no tools field is not a\n request with an empty tools array; an unresolved field is `unknown` with a\n reason, not a default.\n- **Bounded.** Rendering is capped by tool count (32), name, description, schema\n bytes, allowed-caller count, and a payload measurement bound, each with an\n explicit truncation or omission receipt. Registered tools that this request\n does *not* carry are reported as a bounded name list plus an exact count\n rather than expanding the projection.\n- **Inert.** The snapshot lives beside the transcript, never in\n `session.messages`, so it cannot enter a model request or perturb the\n provider's prefix cache. It never executes a tool, never reads credentials,\n never reorders the catalog, and is never registered as a model-callable tool.\n- **Delivery is never claimed.** The capture happens before connection setup, so\n `delivery_status` stays `unknown`.\n\n## Release verification\n\nDo not infer the public surface from handler function names. Verify the model\ncatalog and alias visibility at the exact candidate SHA:\n\n```bash\npython3 scripts/measure-runtime-contract.py\ncargo test -p codewhale-tui --lib --locked core::engine::tests::default_active_contract_keeps_discovery_and_core_tools_eager -- --exact\ncargo test -p codewhale-tui --lib --locked tools::file_tool::tests::primitive_schemas_are_separate_and_small_contract_shaped -- --exact\ncargo test -p codewhale-tui --lib --locked tools::shell::tests::lowercase_bash_schema_is_small_contract -- --exact\ncargo test --locked -p codewhale-tui --lib core::engine::tests::print_mode_tool_catalog_metrics -- --ignored --exact --nocapture\n```\n\nCheck the test names against the source before trusting a green run: `cargo test`\nexits 0 with \"0 passed; N filtered out\" when a filter matches nothing, so a\nmisspelled filter is indistinguishable from a pass. Each `--exact` command\nabove must report `1 passed` (the ignored metrics test reports `1 passed`\nonly because `--ignored` selects it); `0 passed` means the filter matched\nnothing and the check did not run.\n\nThe provider-free receipt must report the thirteen default-active names listed\nabove. A separate repository-wide tool count may include deferred, dynamic,\nfeature-gated, and compatibility-only registrations; it is not the number of\ntools placed in the first-turn model catalog.\n"
|
||
},
|
||
{
|
||
"path": "docs/RUNTIME_SIMPLIFICATION_DESIGN.md",
|
||
"sha256": "c39b7e41d178d464e3aa018065aa43c89490b888ee37e62be1fe9691d96d636c",
|
||
"content": "# Codewhale Runtime Simplification Design\n\n**Status:** Pre-implementation design record for the v0.9.1 cutover, kept for its\n\"Rejected alternatives\" provenance. It is **not** current runtime documentation\nand it shipped differently in two ways:\n\n- Goal 3 below (\"keeping every legacy tool name registered but hidden\") was\n reversed. The per-action file/git/run/web and `exec_shell*` names were\n **removed**, not hidden — `crates/engine/src/tools/registry.rs` asserts they\n must stay unregistered. Only `apply_patch` and the\n `task_*` / `github_*` / `automation_*` / `rlm_*` / `checklist_*` families\n survive as hidden aliases.\n- The default-active policy is nine names, not ten. `update_plan` and `Web` are\n not in `DEFAULT_ACTIVE_NATIVE_TOOLS`\n (`crates/engine/src/core/engine/tool_catalog.rs:44-58`).\n\nFor the current contract see [`TOOL_SURFACE.md`](TOOL_SURFACE.md).\n\n## Goal\n\nMake the model-facing runtime smaller, calmer, and easier for models to use by:\n\n1. Collapsing the long tail of single-purpose file, git, run, and web tools into\n a few canonical action-based tools.\n2. Shrinking the system prompt to durable behavioral invariants and per-turn\n permission deltas.\n3. Keeping every legacy tool name registered but hidden so old transcripts,\n saved sessions, and recorded automation replay without migration.\n\n## Target model-facing surface (default active)\n\n| Tool | Actions / Niche |\n|---|---|\n| `Bash` | `run`, `wait`, `interact`, `cancel` (existing) |\n| `File` | `read`, `list`, `search_name`, `search_content`, `write`, `edit`, `patch` |\n| `Git` | `status`, `diff`, `log`, `show`, `blame` |\n| `Run` | `tests`, `verifiers` |\n| `Web` | `search`, `fetch`, `wait` (deferred unless network is enabled; hidden aliases for legacy names) |\n| `tasks` | durable task family (existing action-based surface) |\n| `github` | durable GitHub family (existing; deferred by default) |\n| `automation` | durable automation family (existing; deferred by default) |\n| `rlm` | durable RLM family (existing; deferred by default) |\n| `agent` | sub-agent dispatch |\n| `remember` | opt-in durable user-memory capture; eager whenever registered |\n| `todo_write` | progress / plan-of-work updates |\n| `update_plan` | plan artifact updates |\n| `tool_search` | on-demand discovery of deferred tools |\n\nDefault-active policy: **10 names** (vs. ~18 before the simplification), with\n`remember` registered only for built-in-memory users and the durable families\nand `Web` discoverable via `tool_search` when needed. `tool_search` itself is a\nsynthetic always-active catalog entry.\n\n## Rejected alternatives\n\n- **Keep every tool but defer the rare ones.** This only changes what is\n advertised, not how many distinct schemas the model must learn. It also\n leaves duplicated guidance in the prompt.\n- **Route search and git through `Bash`.** `grep_files`, `file_search`, and the\n git tools return structured, workspace-aware output and respect sandbox,\n `.gitignore`, and network policy. Shell would force the model to re-parse\n free-form text and lose those guarantees, so dedicated tools win.\n- **One mega `File` tool plus a separate `Edit` tool.** A single `File` tool is\n only slightly larger than a read/edit pair and keeps the boundary the model\n already understands (`read` is cheap, `edit` requires prior read). Splitting\n would re-introduce a two-tool alias for the same underlying operations.\n- **Delete legacy tools.** Saved transcripts and replay tests rely on the old\n names. Removing them would require a config migration and break reproducibility.\n Hidden aliases avoid both.\n\n## Compatibility\n\n- Legacy names (`read_file`, `write_file`, `edit_file`, `list_dir`, `file_search`,\n `grep_files`, `apply_patch`, `git_status`, `git_diff`, `git_log`, `git_show`,\n `git_blame`, `run_tests`, `run_verifiers`, `web_search`, `fetch_url`,\n `wait_for_dev_server`) stay registered with `model_visible = false`.\n- The engine resolves calls by name, so old transcripts replay without changes.\n- `DEFAULT_ACTIVE_NATIVE_TOOLS` is updated to list the new canonical names only;\n hidden legacy tools are ignored by catalog construction.\n\n## Prompt simplification\n\n- Replace the tool-calling recipe sections in `AGENT_MODE` and\n `SUBAGENT_OUTPUT_FORMAT` with short references to the canonical tools.\n- Reduce mode deltas to permission statements (Act = write requires approval,\n Plan = no writes or shell, Full Access = auto-approved, Operate = coordinate from\n ordinary messages).\n- Keep the `BASE_PROMPT` behavioral invariants, `LANGUAGE_PROMPT`, and\n `OUTPUT_PROMPT` intact.\n- Move detailed templates (`COMPACT_TEMPLATE`, sub-agent brief format, planning\n artifact template) out of the stable prefix and into tool schemas or\n conditional blocks.\n\n## Validation\n\n- Provider-free: `scripts/measure-runtime-contract.py` reports active tool count\n and prompt bytes before and after.\n- Behavior-preserving: targeted unit tests for `File`, `Git`, `Run`, and `Web`\n dispatch against legacy inputs.\n- Regression: `cargo fmt`, `cargo clippy --workspace --all-targets --locked`,\n `cargo test -p codewhale-tui --lib --locked`, and\n `cargo test --workspace`.\n\n### v0.9.1 receipt\n\nThe source contract and provider-free metric now exercise the complete policy,\nincluding opt-in `remember`:\n\n| Contract | Before | After |\n|---|---:|---:|\n| Default active tools | 18 | 10 |\n| Agent-mode instruction bytes | 4,064 | 663 |\n| Full system-prompt bytes | 15,842 | 15,368 |\n\nThe final active names are `Bash`, `File`, `Git`, `Run`, `agent`, `remember`,\n`tasks`, `update_plan`, `todo_write`, and `tool_search`. `remember` is present\nonly when built-in memory is enabled; it is eager whenever registered. `File`\nadvertises only read actions in Plan mode, and its `patch` action appears only\nwhen the existing apply-patch feature is enabled. Hidden aliases remain\nexecutable for transcript replay but are absent from the model catalog.\n"
|
||
},
|
||
{
|
||
"path": "docs/LSP_PHP_CUSTOM.md",
|
||
"sha256": "be77d5f68a990d452cc99523b4d9982267c18a7d1b30766d31e5385e0bfed453",
|
||
"content": "# LSP: PHP Support & Custom Language Server Extension\n\n> 阅读简体中文版:[zh_hans/LSP_PHP_CUSTOM.md](zh_hans/LSP_PHP_CUSTOM.md)\n>\n> v0.8.65+ | `codex/lsp-php-custom-servers`\n\n## Overview\n\nThis feature adds **PHP** to the built-in LSP language registry and introduces a\n`[lsp.custom]` config section so users can register arbitrary LSP servers by\nfile extension — covering languages absent from the built-in `Language` enum\n(Ruby, C#, Swift, Lua, etc.).\n\n## Changes\n\n### 1. PHP built-in support\n\n- `Language::Php` variant added to the enum in `crates/lsp/src/registry.rs`\n- `.php` files are detected and routed to `intelephense --stdio` by default\n- User can override via `[lsp.servers].php`\n\n### 2. Custom LSP server extension\n\nNew struct `CustomLspDef` (defined in `crates/lsp/src/lib.rs` and mirrored\nin `crates/config/src/lib.rs`):\n\n```rust\npub struct CustomLspDef {\n pub language_id: String, // LSP languageId for textDocument/didOpen\n pub command: String, // executable to spawn\n pub args: Vec<String>, // arguments (default empty)\n}\n```\n\nNew config field `LspConfig.custom: HashMap<String, CustomLspDef>` — keyed by\nfile extension (without dot), e.g. `\"rb\"`, `\"cs\"`, `\"swift\"`.\n\nIn `LspManager::diagnostics_for`, when the built-in registry returns\n`Language::Other`, the manager checks the user's custom table before giving up.\nCustom servers get their own lazy-spawn transport map and once-per-extension\nmissing-binary warnings (no log spam).\n\n### 3. Transport generalization\n\n`StdioLspTransport::spawn` now accepts `&str language_id` instead of\n`Language`, so both built-in and custom servers share the same transport\nimplementation. The old `Language` import is removed from `client.rs`.\n\n### 4. Polling pipeline extracted\n\n`poll_diagnostics` is a new private method that both built-in and custom\ndiagnostics paths share — eliminating duplicated call/wait/filter/sort/truncate\nlogic.\n\n## Configuration\n\n### Built-in PHP (enabled by default if `intelephense` is on PATH)\n\n```toml\n# No config needed — PHP .php files are detected automatically.\n# Override the server if desired:\n[lsp.servers]\nphp = [\"phpactor\", \"language-server\"]\n```\n\n### Custom language servers\n\n```toml\n[lsp.custom.rb]\ncommand = \"ruby-lsp\"\nargs = [\"--stdio\"]\nlanguage_id = \"ruby\"\n\n[lsp.custom.cs]\ncommand = \"csharp-ls\"\nlanguage_id = \"csharp\"\n\n[lsp.custom.swift]\ncommand = \"sourcekit-lsp\"\nlanguage_id = \"swift\"\n```\n\nKeys are file extensions (no leading dot). The `args` field defaults to empty.\n`language_id` must match what the LSP server expects in `textDocument/didOpen`.\n\n## Architecture\n\n```\nedit_file / write_file / apply_patch success\n │\n ▼\n LspManager.diagnostics_for(file)\n │\n ├── custom_for_extension(file) ── found? ──► transport_for_custom(ext, def)\n │ │\n ├── detect_language(file) ── Other? ──► return None (skip)\n │\n └── transport_for(lang)\n │\n ▼\n poll_diagnostics(file, text, transport)\n │\n ▼\n DiagnosticBlock → injected into session message stream\n```\n\n## Verification\n\n```\ncargo test -p codewhale-tui --lib lsp::\n# 32 tests passed (3 new: detects_php_extension, language_ids_for_php,\n# server_for_php_is_intelephense)\ncargo clippy -p codewhale-tui --lib\n# lsp module: zero new warnings\n```\n\n## Files touched\n\n| File | Change |\n|------|--------|\n| `crates/lsp/src/registry.rs` | +Php variant, detection, server mapping, tests |\n| `crates/lsp/src/lib.rs` | +CustomLspDef, LspConfig.custom, LspManager custom fallback |\n| `crates/lsp/src/client.rs` | spawn accepts &str language_id |\n| `crates/tui/src/config.rs` | LspConfigToml.custom + into_runtime |\n| `crates/config/src/lib.rs` | LspConfigToml.custom + CustomLspDef |\n| `config.example.toml` | PHP docs + custom extension examples |\n"
|
||
}
|
||
]
|
||
}
|
||
]
|