Audit slice: dead/pretender mode abstractions (item 3).
VerifierVerdictPolicy was a single-value enum ("hunt" only) — one value
is not a setting. Delete the enum and the [verifier].verdict_policy
field. ConfigToml has no deny_unknown_fields, so an old config that
still carries the key keeps loading and the key is read-only dropped
(never re-serialized), matching the retired launch_screen pattern.
harness_profiles (ConfigToml field, resolve_harness_profile helper, and
the whole crates/config/src/harness.rs type family) was accepted and
serialized but had no runtime consumer — harness.rs said so itself
("wired later"). Per the no-framework-without-a-caller rule, remove the
writable schema entry and delete the dead types with it. Old configs
with [[harness_profiles]] tables keep parsing (unknown keys ignored).
The cli bundle exporter drops its harness routing (the key can no
longer occur); the portable bundle's profiles table remains for
importing older bundles.
Docs and config.example.toml drop the matching schema sections. Tests
updated (not weakened): the Hunt assertions become a legacy-config
loads-cleanly test; the twelve harness-only tests are removed with the
feature they encoded.
Gates: cargo fmt --check; nextest codewhale-config 626 passed;
codewhale-tui 11908 passed; codewhale-command-contract 29 passed;
codewhale-cli 332 passed; dead-code budget PASS 425; vocabulary gate
exit 0.
The alternate screen was a startup-only bool that `tui.alternate_screen =
"never"` parsed and ignored. `ScreenMode` (Fullscreen | Inline) is now the
one source of truth: `App` stores it and derives `use_alt_screen()`, so every
pause/resume/teardown site reads the screen the terminal is actually on.
- `/fullscreen` keeps the alternate screen (still the default); `/inline`
paints a stock ratatui `Viewport::Inline` the full height of the terminal
with no alternate screen, so the shell's scrollback survives the session
and stays scrollable after exit.
- `tui.alternate_screen = "never"` starts inline; `auto`/`always` keep the
old meaning.
- The switch rebuilds the terminal over a backend that carries every
negotiated fact (colour depth, palette, theme, measured background) and
adopts it only once `Terminal::with_options` succeeded. A probe the
terminal refuses rolls the alternate-screen escape back and says why.
- The cleanup guard and panic path read the live screen through
`live_alt_screen()`, not the startup choice.
- Inline mode paints the whole transcript inside its viewport; nothing is
committed to host scrollback yet (documented in docs/CONFIGURATION.md).
Evidence:
cargo test -p codewhale-tui --lib -- screen_mode screen_commands startup_screen config_never commands::tests
test result: ok. 91 passed; 0 failed; 0 ignored
scripts/dev-test.sh tui
Summary [ 154.810s] 11856 tests run: 11856 passed (1 slow, 2 leaky), 13 skipped
cargo test -p codewhale-tui --test cucumber --features long-running-tests -- screen_mode_inline_pty
test result: ok. 1 passed; 0 failed (real PTY: DEC 1049 never set on inline start; /fullscreen sets it, /inline clears it)
Signed-off-by: CodeWhale Bot <bot@codewhale.net>
* ci: add Windows x64 build workflow on push to main
* fix: trigger build-windows on any push to main
* fix(custom): support wire = "responses" | "anthropic" | "chat" for kind="openai-compatible"
Custom provider was fixed to ChatCompletions, ignoring providers.<name>.wire.
Now honors per-config wire in both client::provider_wire_format_for_config
and config::provider_capability, keeping Custom::wire_policy default as Chat
for compat. Aliases: responses/openai-responses/responses-api -> Responses;
anthropic/messages/claude -> AnthropicMessages; default -> Chat.
Fixes custom muse-spark-1.2 on opencode.ai/zen/v1 needing Responses.
* fix(opencode-zen): route muse-spark over Responses API
Muse Spark 1.2 contributor-free on https://opencode.ai/zen/v1 only
supports POST /v1/responses (Responses API) and rejects Chat Completions.
Previously the bundled offering roster and ModelAware resolver treated
unknown muse-spark variants as chat or failed closed to unproven, so
CodeWhale sent chat payloads that 404.
- Add muse-spark-1.2, -contributor, -contributor-free to
OPENCODE_ZEN_RESPONSES_MODELS (bundled_offerings)
- Add resolver fallback: any muse-spark* under OpencodeZen resolves
to endpoint_key responses even without exact catalog match
- Update config.example.toml docs (GPT/Muse Spark -> Responses) and
add muse-spark-1.2-contributor-free example
- Add scripts/opencode-chat2responses-proxy.mjs as zero-Rust
chat->responses shim for chat-only clients
Custom gateways can already use wire="responses" (ff504585a);
this fix makes the first-class opencode-zen provider work without
hand-written wire config.
* fix(client): keep codex env-token auth working on custom endpoints
PR #5716 diverted OpenaiCodex credential resolution to the generic key
resolver whenever provider_uses_custom_endpoint() is true, which dropped
an explicit OPENAI_CODEX_ACCESS_TOKEN for custom-base-url setups. The
shared-seam wiremock test proves the regression: the mock only answers
Bearer test-token, so the request came back 404 on all three CI OSes
(client::responses::tests::responses_stream_open_preserves_wire_headers_
through_shared_seam). The manual if-condition formatting also failed the
Lint job's cargo fmt --check.
Restore the pre-PR precedence by trying codex_credentials() first: env
credentials still win on custom endpoints (codex_credentials checks env
before the official-endpoint consent grant), the official endpoint keeps
propagating OAuth errors, and only a custom endpoint with no env token
falls back to deepseek_api_key() — preserving the contributor's goal of
letting a custom endpoint authenticate with its own configured key.
Signed-off-by: CodeWhale Bot <bot@codewhale.net>
* refactor(tui): route wire-dialect reads through one Config helper
The wire= feature read providers.<id>.wire in two places (client wire
resolution and the capability reporter), and provider_capability_with_
wire was exported but never called with a real value — a parallel entry
point that reported Chat for custom providers the client actually speaks
Responses/Messages to.
- Add Config::provider_wire_dialect() as the single trimmed, non-empty
wire reader; use it in provider_wire_format_for_config and the doctor
capability report (provider_capability_with_wire).
- Drop the over-broad '|| normalized.contains("responses")' from
wire_config_prefers_responses in both modules: every listed alias
except the singular 'response'/'response-api' spellings already
contains the substring, so the fallback only admitted unintended
values like 'not-responses'.
- Remove the vestigial 'let _ = provider_kind;' marker in the resolver
arm that now genuinely uses provider_kind.
Signed-off-by: CodeWhale Bot <bot@codewhale.net>
* revert(ci): drop contributor-added build-windows workflow
The PR added a Build Windows x64 workflow triggering on every push to
main. That build is already covered: release-artifacts.yml builds both
x86_64-pc-windows-msvc and aarch64-pc-windows-msvc release binaries,
nightly.yml rebuilds them nightly, and ci.yml runs the full test matrix
on windows-latest. A fourth always-on Windows build only spends CI
minutes on every main push and grants the job an actions:write
permission it does not need. Contributor CI-workflow additions are
outside this feature's scope; restoring main's tree (no such file).
Signed-off-by: CodeWhale Bot <bot@codewhale.net>
---------
Signed-off-by: CodeWhale Bot <bot@codewhale.net>
Co-authored-by: whp233 <whp233@users.noreply.github.com>
Co-authored-by: CodeWhale Bot <bot@codewhale.net>
An agent loop with no finite bound can spend real money forever. Before
this change the parent turn had exactly one hard ceiling — the per-step
stream caps — while the model-step ceiling was `u32::MAX` at every
production call site and no cumulative per-turn wall clock existed.
R1 makes all four bounds finite, overridable, and honest at the limit:
* `max_steps` defaults to 200 (`turn_budget::DEFAULT_MAX_MODEL_STEPS`)
instead of `u32::MAX`. `UNBOUNDED_MODEL_STEPS` is gone; the interactive
TUI, the runtime/API threads, and `EngineConfig::default()` all resolve
from `[tui].max_model_steps` / `CODEWHALE_MAX_MODEL_STEPS`.
* A cumulative per-turn wall clock (`TurnWallClock`, default 3600s,
`[tui].turn_wall_clock_secs` / `CODEWHALE_TURN_WALL_CLOCK_SECS`) is
started once in `Engine::run_turn` and checked at the provider-request
boundary, so a turn that runs out of time stops before authorizing
another billable request. Time blocked on a human approval decision is
excluded, so an unanswered prompt cannot burn the budget and discard
the work the user just approved.
* `exec` without `--max-turns` resolves to the same finite ceiling
instead of `u32::MAX`. A headless run has nobody watching it.
* The per-step stream caps (content bytes, stream duration) keep their
previous values as defaults but are now resolved from config
(`[tui].stream_max_content_mb`, `[tui].stream_max_duration_secs`)
rather than read from module constants.
Hitting a budget is never reported as a clean success. The step ceiling
already ended the turn `Failed` with the limit named (after granting one
bounded final-report turn); the wall clock follows the same contract.
No "0 means unlimited" sentinel: every resolver treats `0` as an invalid
value and falls back to the finite default, and there is no unlimited
value at all — the escape hatch is the documented maximum, which is
large but still terminates. `--max-turns 0` was already rejected by clap.
Signed-off-by: CodeWhale Bot <bot@codewhale.net>
Add GLM-5.3-Flash as a first-class picker row (wire id GLM-5.3-Flash,
OpenRouter z-ai/glm-5.3-flash) so /model can select it. Flash is the
faster/explore sibling of GLM-5.3; the Z.ai default stays GLM-5.3.
Ship the published $0.15/$0.50 list, not the 50% promo.
Add an opt-in, machine-readable lifecycle event outbox for supervisors and
automation harnesses. Unset/empty config = feature OFF = behavior unchanged.
Config ([lifecycle_outbox]):
- path — JSONL outbox file (unset/empty disables the feature)
- webhook_url — optional webhook endpoint; POSTs only when set
- webhook_token — optional bearer token for webhook_url
Writer (crates/hooks/src/lifecycle_outbox.rs):
- One JSONL line per event in the existing RuntimeEventEnvelope shape
(schema_version, seq, event, kind, thread_id, turn_id, item_id,
timestamp, created_at, payload); append + flush per event.
- seq monotonic per file; recovers from the last complete line on open via
a bounded 64 KiB tail scan (torn trailing lines ignored).
- Single non-blocking writer task: emit() enqueues; no tokio runtime
available => drop with warning.
- Payloads only from bounded, pre-redacted fields (headline ≤ 80,
detail ≤ 120, preview ≤ 200 chars; control bytes stripped).
Signed-off-by: M-Maciej <130112810+M-Maciej@users.noreply.github.com>
Promotes the fully gated non-benchmark candidate while preserving the benchmark tree exactly from the prior release-PR head.
Signed-off-by: CodeWhale Bot <bot@codewhale.net>
Two problems, one subsystem.
**Another tool's instruction file was standing authority here.** The canonical
list ranked `.claude/instructions.md` second and `CLAUDE.md` third — above
Codewhale's own `.codewhale/instructions.md` at fourth — `.claude/rules/` was an
auto-discovered rules directory, and `.cursorrules`, `.cursor/rules`,
`.clinerules`, `.windsurf/rules`, `.gemini`, `.github/copilot-instructions.md`
and `.github/muse-instructions.md` were imported into the system prompt with no
opt-in at all. Dropping a `CLAUDE.md` written as law for a different agent into
a repository silently made it law for this one, and outranked the file this
project actually owns. That is an injection surface, not a compatibility
feature.
Codewhale now reads `AGENTS.md`, the cross-agent `.agents/AGENTS.md`, and its
own instruction files by default. Every other agent's format is opt-in by name
through `project_instruction_imports` (env
`CODEWHALE_PROJECT_INSTRUCTION_IMPORTS`), imported files rank *below*
Codewhale's own rather than above them, and a workspace containing an
un-imported format produces a warning naming the exact setting — so this is
discoverable rather than a silent behavior loss. Unknown names in the key are
reported instead of dropped, because a typo otherwise means "import nothing"
and nobody would notice.
**A symlinked candidate directory could read outside the workspace.**
`collect_candidate_files` checked every *file* it found for symlinks but
reached them through `path.is_dir()`, which follows links. A symlinked
`.cursor/rules` pointing anywhere on disk was therefore traversed, and the real
files behind it passed every per-entry check. `project_context.rs` has refused
symlinked rules directories since it gained them, with a comment explaining
this precise escape; `fragments.rs` never got the same guard. Now both loaders
apply it. The regression test fails without the fix and passes with it — I
checked, rather than assuming.
**One budget instead of four.** The chain had 200 KiB, the rules block 500 KiB,
imported fragments 40 KiB, and the global fallback layer was merged in *after*
the chain budget had already closed, so it counted against nothing. No single
number described how much standing instruction text could precede the
conversation. All of it now shares one 48 KiB aggregate ceiling, applied once
after assembly. Instructions claim it before rules, and are trimmed from the
front — dropping the broadest scope first — so the nearest-scope file is the
last thing dropped instead of the first thing stranded, which is what the old
root-first per-segment accounting did. Truncation still leaves an explicit
marker.
The opt-in set is threaded as a parameter rather than read from a global inside
the loader, so callers and tests state which formats are in play instead of
racing on process-wide state.
Preserved: nearest-scope traversal, repository-root stopping, the $HOME clamp,
`O_NOFOLLOW` on unix, truncation markers, `<project_instructions source=…>`
provenance, and the existing duplicate-suppression between the two loaders.
Verified: cargo test -p codewhale-tui --lib -> 10854 passed, 0 failed,
13 ignored. cargo test -p codewhale-core -> 80 passed, 0 failed.
cargo fmt --all clean.
Note for the release notes: this is a behavior change. A repository whose only
instructions live in CLAUDE.md will stop contributing them until
`project_instruction_imports = ["claude"]` is set. The warning names the key.
Implemented with agent assistance.
Signed-off-by: Hunter Bown <hmbown@gmail.com>
Ordinary workspace-write turns built their sandbox with
`network_access: true`, so agreeing to let a session edit this repository also
handed every shell command unrestricted outbound egress.
#273 introduced that grant, and justified it: the seatbelt default denies DNS,
which broke curl, yt-dlp, and package managers, and the comment argued the
application-level NetworkPolicy would remain "the only outbound boundary".
The second half was not true. NetworkPolicy governs fetch_url, web_search, and
MCP HTTP; it never saw a shell subprocess. So the layer meant to compensate for
the wide OS policy did not cover the thing the OS policy had opened, and
workspace-write sessions ran with no outbound boundary at any layer. This is
not tightening a working boundary — it is installing one that was missing.
Workspace-write is now created network-restricted. Egress comes from exactly
three explicit places:
- `sandbox_network_access` in config (env: CODEWHALE_SANDBOX_NETWORK_ACCESS),
- a `danger-full-access` posture, which applies no sandbox at all,
- the existing post-denial elevation prompt, which grants network for one
call after the user sees what was blocked.
Yolo and --yolo/Bypass are unchanged: they resolve to DangerFullAccess, so
their deliberate "no guardrails" contract still reports network. Plan stays
ReadOnly. Writable roots, tmpdir handling, and the git-worktree metadata roots
are untouched — only the network bit moved.
The decision is carried by a typed `SandboxNetworkAccess` rather than another
bool, so the default is stated once at the type instead of at each of the eight
resolver call sites, and "the user asked for network" cannot be transposed with
"some caller passed true".
Two surfaces were lying and now read the flag: `external-sandbox` hardcoded
`network_access: true` even when nothing granted it, and /status printed
"sandbox workspace-write, network on" for every workspace-write policy — true
only by accident of the old default.
Tests. The #273 regression test is retargeted rather than deleted: it now pins
that Agent mode still elevates *writes* while withholding network, which is the
property #273 actually needed. Added coverage for the full posture matrix
(Agent/Ask/Auto-Review/Never x configured overrides, plus Yolo and Plan), the
config key and its camelCase alias, and — the gap the audit surfaced — that the
generated seatbelt profile emits network rules if and only if the policy grants
them, so the OS layer and the application policy are verified to agree instead
of one being assumed to compensate for the other.
Verified: cargo test -p codewhale-tui --lib -> 10850 passed, 0 failed,
13 ignored (RUST_MIN_STACK=16777216, as scripts/dev-test.sh exports).
cargo fmt --all clean.
Platforms with no sandbox backend (default Linux without bubblewrap, and
Windows) still enforce nothing either way; /status and doctor continue to say
so, and that honesty gap is unchanged by this commit.
Implemented with agent assistance.
Signed-off-by: Hunter Bown <hmbown@gmail.com>
The engines already shipped; the remaining gap was visibility. Pin the
orchestration trio at the top of the empty / menu, name them on the idle
welcome and footer, put them in the first three Hotbar defaults, and add
/auto as a host-only Auto-Review switch with when-to-use copy (#5439).
Verified: cargo fmt --all -- --check; locale parity; command-migration
manifest; cargo nextest -p codewhale-config --lib -- hotbar (5);
codewhale-tui lib tests for empty slash pin, /auto, palette root, empty
state, footer, hotbar defaults, and locale sync (32 passed).
Signed-off-by: Hunter Bown <hmbown@gmail.com>
/title was merged into /rename in 24c7dee46 as a discoverable alias sharing
the session name, which made long descriptive session names dominate the
terminal tab and left no way to give parallel windows a short identity.
Restore the independent design from 8b6bcc5e0: /title sets a per-session
tab/window title (persisted on SavedSession.window_title), /title off
clears it, and the root 'title' config key (editable via /config title
... --save) supplies the default. The session *name* stays owned by
/rename. The mid-first-turn recovery added by #5444 is reused so /title
applies immediately on a live first turn too.
Signed-off-by: Hunter Bown <hmbown@gmail.com>
Add localized session controls for notifications, search, prompt handling, thought previews, and related settings. Preserve environment precedence when applying search changes, compose non-persistent edits against live state, and align notification defaults with the documented runtime value.
Verified with focused config-command, notification-composition, search-precedence, thought-preview, localization-parity, help-copy, formatting, and diff checks.
Signed-off-by: Hunter Bown <hmbown@gmail.com>
The bwrap sandbox ran with a bare read-only root bind, so 'foo >/dev/null'
failed with EROFS (the host node under a read-only bind rejects
open(O_WRONLY)) and toolchains expecting /proc or writable /tmp broke —
the #5410 report. Give the invocation the standard container trio:
--dev /dev (fresh private device nodes, redirection works), --proc /proc,
--tmpfs /tmp (writable-but-isolated scratch).
Add the two requested config keys as escape hatches: bwrap_ro_roots
(extra read-only binds, applied last so they can narrow policy-writable
paths) and bwrap_dev_roots (host device nodes bind-mounted read-write;
character/block devices only — never directories — so the key cannot
become a writable-root escape hatch). Non-existent paths skip silently,
same rule as writable roots.
Threaded: config.rs (serde + profile merge), EngineConfig.bwrap_extensions,
SandboxManager.set_bwrap_extensions (+ ShellManager passthrough), engine +
runtime_threads + frame.rs constructors, config.example.toml docs.
Tests (linux-gated, CI ubuntu leg): container trio present; extensions add
ro/dev mounts, skip missing + non-device entries; extension ro-binds apply
after writable binds so narrowing wins. Off-Linux, resolve() is a no-op.
(cherry picked from commit 04239a8f3c)
1. config.example.toml max_depth: default is DEFAULT_SPAWN_DEPTH (3),
clamped to MAX_SPAWN_DEPTH_CEILING (8) in crates/config/src/lib.rs
and tools/subagent/mod.rs (not a hard ceiling of 3).
2. config.example.toml max_subagents: default is DEFAULT_MAX_SUBAGENTS (64),
clamped to 1..=MAX_SUBAGENTS (128) in crates/tui/src/config/subagent_limits.rs.
docs/SUBAGENTS.md already agreed.
3. docs/SUBAGENTS.md output contract: non-scouts use SUBAGENT_OUTPUT_FORMAT
(five headings); scouts use SUBAGENT_SCOUT_OUTPUT_FORMAT (SUMMARY + EVIDENCE)
via FleetRole::system_prompt, pinned by the #5189 F5 scout test.
4. docs/TOOL_LIFECYCLE.md: ToolRegistryBuilder::with_todo_tool keeps
work_update/TodoWrite/todo/checklist_write/checklist_update as hidden
replay aliases of TodoWriteTool; checklist_add/list and todo_add/update/list
are not registered. The "must no longer be callable" comment is
rlm_is_the_only_registered_session_surface, not checklist/todo.
PROSE_MAX_MEASURE=105 capped user/assistant/thinking prose at the
live-transcript render entry points, so wide terminals showed full-width
tool cells beside a 105-column prose rail — the surviving residue of the
frame cap removed for #5322.
Default is now full content width: the cap is gone and each entry point
resolves the effective width via TranscriptRenderOptions::prose_width,
keeping the main cache and the full-screen overlay in agreement.
Escape hatch: [transcript] prose_measure (positive integer) restores a
bounded reading measure; 0 or absent means full width. Values are
validated at config load with a clear transcript.prose_measure error
(negatives, floats, strings, bools all rejected). Tool, diff, and status
cells never inherit the cap.
Prior art: the issue-5322-wide-prose-measure lane (01e5b1814, tui.prose_measure
rail|fill|int, default rail) informed the options-field shape; its default
and key name were deliberately not kept. Kimi renders prose full width
with no setting; Grok ships a config-only ui.max_thoughts_width integer
cap — both support a minimal config-only escape hatch.
/title [name|off] sets a per-session tab/window title, persisted on the
saved session; the root 'title' config key (or a profile overlay)
supplies the default, editable via /config title <name> [--save]. The
prefix renders as [title] in front of every window-title state
(Codewhale / reasoning... / using tool... / done), so parallel sessions
in separate terminal windows are identifiable at a glance. Independent
of /rename, which keeps naming the session in the composer and picker.
- notifications: TITLE_PREFIX state + set_title_prefix (change-detected)
+ decorate_title applied to every OSC-0 write (activity label, done
marker, rest title, interaction reset); empty prefix keeps historical
titles byte-for-byte
- app: window_title (session) / title_default (config), precedence
window_title > title_default > none; render loop syncs each frame via
sync_title_activity
- session: SavedSession.window_title persisted (serde default, omitted
when None); restored on load, cleared on /new, /fork, /clear, and
carried by build_session_snapshot so autosave cannot drop it
- config: root 'title' key parsed and merged (profile overlays included)
- /config title <name> [--save] runtime set + persistence
- locales: CmdTitleDescription across all 15 complete packs
- config.example.toml + crates/tui/CHANGELOG.md entries
- tests: prefix decoration/change-detection, app precedence, render-loop
sync, /title set/clear/report/persist, config parse+merge
GLM-5.3 has been live on the Z.ai Coding Plan since 2026-08-13, so it is
now the default direct Z.ai model: `DEFAULT_ZAI_MODEL` resolves to
`GLM-5.3` in both `codewhale-config` and `codewhale-tui`, the bundled
Models.dev seed marks the GLM-5.3 row `default: true` (matching the
descriptor default as `_meta.default_rows` requires), and it is the first
`/model` row after `/provider zai`.
Only the default moved. Every GLM alias now resolves to its own constant
(`glm-5.2` -> `GLM-5.2`, `glm-5.3` -> `GLM-5.3`) instead of routing
through `DEFAULT_ZAI_MODEL`, so a saved `model = "GLM-5.2"` keeps sending
GLM-5.2; a new CLI provenance test pins that. GLM-5.2 rows, aliases, and
capability metadata are unchanged; GLM-5.3 still inherits limits and
reasoning options from GLM-5.2 until Z.ai publishes distinct numbers, and
no USD price is claimed.
Left on 5.2 deliberately: OpenCode Go (`glm-5.2` is the only GLM row that
gateway documents), Model Studio plan rows, and the OpenRouter
`z-ai/glm-5.2` mirror (not a provider default; both 5.2 and 5.3 mirrors
stay registered with the same fast-sibling pairing).
Docs, config.example.toml, and the 0.9.8 changelog entry now describe the
new default truthfully.
[workshop] read_result_max_bytes and tool_result_max_bytes raise the
model-visible floor and never lower the compile-time defaults. Hard cap
is 2MiB (#5367).
Make grok-4.6 the first-party xAI default and move the grok alias onto
it, while keeping grok-4.5 explicitly selectable. Capabilities come from
the Models.dev-shaped catalog: 500K context, text/image input, official
low/medium/high/xhigh efforts, no fabricated output limit or flat price.
The Chat Completions wire sends reasoning_effort only on exact
https://api.x.ai/v1 + grok-4.6. Usage-aware 200K pricing stays
provider-scoped to direct xAI.
DeepSeek's callable ID remains deepseek-v4-pro. Docs now note the live
backend label DeepSeek-V4-Pro-0813 without remapping aliases or claiming
a dated 0813 changelog post.
No OpenCode Go, Zen, Model Studio, or OpenRouter grok-4.6 rows were
added. Priority processing is not implemented.
Verified: cargo fmt; codewhale-agent and codewhale-config suites;
cargo test -p codewhale-tui --lib --locked (10303 passed);
cargo test --workspace --locked; cargo clippy --workspace --all-targets
--locked -- -D warnings; cargo build --release -p codewhale-cli -p
codewhale-tui. Isolated rerun of
exec_persistent_service::failed_exec_kills_pending_service_and_exits_nonzero
passed after one parallel flake. Source budget recorded at 688357.
Agent-assisted implementation.
Adds OrcaRouter as a first-class provider alongside the existing
OpenRouter wiring: ProviderKind, provider! registration, default
base URL https://api.orcarouter.ai/v1, default model
deepseek/deepseek-v4-pro, flash model deepseek/deepseek-v4-flash,
auto-routing model orcarouter/auto, ORCAROUTER_API_KEY env mapping,
model normalization, ProvidersToml/EnvRuntimeOverrides config, CLI
--provider selector, TUI ApiProvider picker + key/model mappings,
reasoning-effort pass-through, agent model registry entries, docs,
and web facts. Keys start with sk-orca-.
Verified: fmt, clippy (touched crates), provider-registry and web
facts drift checks, config tests, and live L3 calls against the
OrcaRouter endpoint (models catalog, chat completions on the default
and orcarouter/auto models, and auth rejection for an invalid key).
Agent assistance: integration prepared with agent tooling; noted in
plain body per repo policy (no Co-authored-by trailer).
Harvested from PR #5321 by @XiaoHuo888-hue
Scope Mistral's polymorphic reasoning and replay behavior to exact first-party HTTPS routes, preserve stored thinking across real prompt construction, and keep DeepSeek's sanitizer from injecting a second dialect into tool-call history.
Align the current model registry, provider-scoped model override, generated facts, docs, and focused route-isolation tests. Split the large stream decoder test module so the source-structure gate remains below budget.
Signed-off-by: CodeWhale Bot <bot@codewhale.net>
Wire Mistral AI / la Plateforme into the shared provider registry, TUI
provider enum, provider-scoped config/env overrides, static model
registry, context-window metadata, reasoning wiring, docs, and
examples. The route uses Mistral's OpenAI-compatible Chat Completions
endpoint at https://api.mistral.ai/v1 with 'mistral-code-latest' as
the default model (Codestral coding model, 256K context).
Model IDs verified live against https://api.mistral.ai/v1/models: the
static registry ships 'mistral-code-latest' (accepts 'codestral-latest'
as alias for backward compatibility), 'mistral-medium-latest',
'mistral-small-latest', 'magistral-small-latest', and
'mistral-large-latest'. All models report 262144 (256K) context on
/v1/models except mistral-code-latest at 256000; earlier drafts of
this PR had those windows reversed.
Reasoning is wired end-to-end for the three models that advertise
'reasoning: true' on /v1/models — mistral-medium-latest,
mistral-small-latest, and magistral-small-latest. Codewhale sends
'reasoning_effort' (Mistral currently accepts 'none' or 'high' only;
intermediate tiers return HTTP 400 code 3051), parses the polymorphic
'content: [{type: thinking, thinking: [{type: text, text: ...}],
closed: bool}, {type: text, text: ...}]' shape emitted by reasoning
models, and replays the thinking trace back into multi-turn history
per docs.mistral.ai/capabilities/reasoning. Non-reasoning models
(mistral-code-latest, mistral-large-latest) never receive the field
because Mistral would reject it. FIM (/v1/fim/completions) is not
wired.
Provider aliases: mistral-ai, mistralai, la-plateforme. Env vars:
MISTRAL_API_KEY, MISTRAL_BASE_URL, MISTRAL_MODEL. Auth via API key
from https://console.mistral.ai/api-keys, config, or 'codewhale auth
set'.
Test env-poisoning: EnvGuard captures/removes/restores MISTRAL_* so
tests stay reproducible when a user has these vars exported in their
shell.
Validation:
- cargo fmt --all -- --check
- cargo clippy --workspace --all-targets --all-features --locked (with
the documented allow list) -- No issues found
- cargo test --workspace --all-features --locked -- 22 pre-existing
failures in crates/tui git-shell tests (worktree init failing on
'git commit' in isolated tempdirs), verified identical count on
origin/main at 91bca01a9 and unrelated to this change
- python3 scripts/check-provider-registry.py -- passed
- codewhale --provider mistral --model mistral-medium-latest exec
against api.mistral.ai returned a correct reasoning-mode response
- codewhale --provider mistral --model mistral-large-latest exec
succeeded without HTTP 400 code 3051 (verifies the model-aware
reasoning gate)
- TUI smoke previously validated: /status shows mistral +
mistral-code-latest, /provider lists Mistral, tool call end-to-end
Assisted by Codex CLI for implementation and multiple Oracle review
passes (correctness + convention + Hunter's inline review) that
surfaced the ProviderArg clap enum gap, the ModelRegistry silent
fallthrough to DeepSeek, the Codestral context-window regression, the
EnvGuard env-poisoning flake, and the model-ID / context-window /
reasoning-support mistakes from the initial docs-slug pass now
corrected against the live /v1/models catalog.
One visible fast exploratory role: Scout. The agent tool schema no
longer advertises `model_strength` (parsing survives for compatibility
and maps onto the Scout policy); the workflow tool schema text and
config.example.toml copy now speak in Scout terms.
- fleet/scout.rs: resolve_scout_route with an explicit order — a pinned
Scout always wins and survives operator changes; an unpinned Scout
gets the provider's documented fast sibling (the existing
provider_router_candidates tables: DeepSeek pro/flash, Z.ai → GLM-5-
Turbo, Claude → Haiku, provider-specific wire spellings) VERIFIED
against the merged catalog before it is ever suggested; no verified
companion means deliberate inheritance, never an invented fallback;
no session route at all is Unavailable with a precise reason.
- The Fleet detail view shows the resolved Scout route before a run:
`scout → provider/model (pinned | catalog suggestion | inherits
session route)`.
- Receipts: legacy `faster`/`fast` values still parse and resolve
through the same Scout policy.
Tests: 5 scout resolution tests (pin wins + survives operator change,
verified companion, inheritance with no sibling, unavailable reason,
catalog-verification honesty gate) and the schema-vocabulary test now
asserts model_strength's absence while keeping the closed role enum.
cargo test -p codewhale-tui --bin codewhale-tui: 9800 passed, 0 failed,
9 ignored. cargo fmt clean.
Three lies in one comment block, all the same family the tool sweep just
closed:
- `raw = true` was documented as a per-call bypass for output routing. It is
not. The adaptive router takes it as `_raw_bypass` and ignores it; it is
honoured only under the legacy CODEWHALE_CLASSIC_OUTPUT_ROUTING switch, and
no tool advertises it. A user setting it got routing anyway, silently.
- The per-tool override example keys off `exec_shell`, `grep_files`, and
`web_search`, all retired. An override written from this example matches
nothing and does nothing, with no error.
Names the live tools instead and says plainly that the escape hatch is not
one. `raw` itself is left alone pending a decision on whether it should exist.
The TUI has told users about new releases since #3961/#14, but it asked
GitHub on every single launch and always advertised `codewhale update` --
which is the wrong command for most installs and actively harmful for some.
This adds the "throttled" half of #5053 and fixes the wording.
Throttling. `codewhale-release::check` caches the answer in
~/.codewhale/update-check.json and reuses it for `check_interval_hours`
(default 24). The cache stores the *tag we last saw*, not a "checked
recently" flag: a user on a stale binary still sees the notice on every
launch while the network is touched once a day. Caching only a timestamp
would have hidden the notice for the whole interval, which is the opposite
of the point. A failed check is deliberately not cached, so an outage does
not suppress the notice until tomorrow.
Suppression. Checks are skipped without touching the network in CI
(CI, GITHUB_ACTIONS, GITLAB_CI, ...) and on CODEWHALE_NO_UPDATE_CHECK or
NO_UPDATE_NOTIFIER. Values of "", 0, false, no, off do not count as set, so
a `CI=false` export does not disable checks for ordinary users. The
decision is factored into a pure `resolve_version_check_source` so this
repo's own CI run does not change the answer under test.
Install-method awareness. `codewhale-release::install` classifies the
running binary from its path -- npm (node_modules), Homebrew (Cellar /
linuxbrew), cargo (~/.cargo/bin), or a plain release binary -- and the
notice now names that manager's command. Package-managed installs also get
an explicit warning against `codewhale update`: overwriting a binary
Homebrew or npm owns leaves the manager describing a version that is no
longer on disk, and its next upgrade silently reverts the user. `codewhale
update` itself prints the same warning before proceeding; it warns rather
than refuses, since the download still yields a working binary and refusing
would break workflows that have relied on it. Homebrew intentionally points
at the legacy `deepseek-tui` formula -- no `codewhale` formula is published
yet, and naming one that does not exist would hand the user a failing
command.
Nothing is installed without the user asking. The check remains
fire-and-forget: it never delays startup and never blocks a turn.
Still open on #5053: the one-chord update-and-relaunch. Left out rather
than half-wired -- running a package manager on the user's behalf from
inside the TUI needs a confirmation surface and a clean re-exec path that
this change does not build.
Tests: 6 new in codewhale-release (cache freshness, clock skew, atomic
round-trip, corrupt cache, install detection), 5 new in the TUI (CI
suppression, cache hit answers offline, failure is not cached,
install-specific wording), 1 in the CLI updater.
`config.example.toml` shipped `memory_path = "~/.codewhale/memory.md"` and
three docs implied that file is what gets written. Under the Native backend
— the only backend — the filename is discarded and the store is re-rooted to
`<parent>/memory/global/MEMORY.md`. Users who pointed the setting at the
native layout path double-nested the tree.
States the re-rooting explicitly in the example config, CONFIGURATION.md and
MEMORY.md, and names the resolved path for the shipped default.
The first-party ingest service is deployed at
https://telemetry.codewhale.net/v1/telemetry (Cloudflare Worker, source in
telemetry-ingest/). Until now `telemetry_endpoint` resolved to `None` no
matter what, so even a user who answered Enable at the first-run notice was
writing to `dryrun.jsonl` and contacting nobody. Wire the default.
`DEFAULT_TELEMETRY_ENDPOINT` is applied in `resolve_runtime_options`, not as
a serde default on `ConfigToml`, so `get_value`/`list_values` still report an
unconfigured key as unconfigured and the four config verbs round-trip
unchanged. Precedence is unchanged in shape: environment, then config file,
then — new — the shipped default.
This changes *where* an enabled session's batches go, never *whether* a
session collects. Telemetry is still opt-in and off by default; the endpoint
is read only after `telemetry` resolved true, which requires the first-run
notice to have been answered with Enable. `CODEWHALE_TELEMETRY=0`,
`telemetry = false`, and a recorded decline are all upstream of this line and
all still hard floors.
The local dry-run sink stays reachable through an explicitly *empty*
endpoint, in the config file or the environment. That required dropping the
env-layer `.filter(non-empty)`: with a default behind it, discarding an
emptied `CODEWHALE_TELEMETRY_ENDPOINT=` would have fallen through to the
shipped endpoint — the exact opposite of what anyone typing it means.
Three tests pin the behavior: the default by literal value (so it cannot
drift), a config-file and environment value each beating it, and empty
resolving to `None` from both sources. `TelemetryEnvGuard` now also clears
the endpoint variables, so an ambient value in a developer's or CI's
environment cannot make the default assertion vacuous.
docs/TOOL_SURFACE.md carried four claims the runtime's own tests contradict:
1. "The default-active policy contains exactly these ten names" listing
`update_plan`. `DEFAULT_ACTIVE_NATIVE_TOOLS`
(crates/tui/src/core/engine/tool_catalog.rs:44-58) has eight entries and
`update_plan` is not among them — it appears nowhere in tool_catalog.rs. The
policy is nine (those eight plus synthetic `tool_search`), eight with memory
disabled. `update_plan` is registered (crates/tui/src/tools/plan.rs:401) but
reachable only through `tool_search`; the tool table now says so.
2. "A memory-disabled or Moraine-fallback runtime". There is no Moraine
fallback — docs/MEMORY.md:11-13 records the removal, and
crates/tui/src/prompts.rs:2445-2449 is a test asserting MEMORY_GUIDANCE must
not contain the word.
3. A "Replay-only aliases" table promising "saved transcripts, sessions, and
recorded automation replay without migration" for 23 names, 16 of which are
asserted REMOVED at crates/tui/src/tools/registry.rs:2066-2088 ("{retired}
must stay removed") and 6 more at :2290-2304 ("{alias} must be removed").
Split into a "Removed spellings" section (with the registry.rs:313-316 note
that resolve has no fuzzy step, so those calls fail rather than dispatch) and
a "Replay-only aliases" section holding only what is still registered:
apply_patch, task_*, github_*, automation_*, rlm_*, checklist_*/todo_*.
4. A "Release verification" block whose three cargo filters name tests that do
not exist (`rg` finds those three strings only in that doc). `cargo test`
exits 0 with "0 passed; N filtered out" on a filter that matches nothing, so
a release engineer following it got three green checkmarks having verified
nothing. Replaced with the real names —
`shell_surface_contains_only_the_canonical_bash_tool` (registry.rs:2290) and
`runtime_task_families_expose_only_canonical_tools` (registry.rs:2333) — plus
the receipt test, and a warning about the silent-pass failure mode.
docs/RUNTIME_SIMPLIFICATION_DESIGN.md repeats errors 1 and 3 and is designated
authoritative by docs/TOOL_LIFECYCLE.md:3-7, but carries no status marker. Given
a status banner naming both divergences and pointing at TOOL_SURFACE.md; the
"Rejected alternatives" provenance is worth keeping, so not deleted.
docs/SUBAGENTS.md:
- "a bounded queue of up to 200 running plus queued sub-agents by default" —
`MAX_SUBAGENT_ADMISSION` is 1024 (crates/tui/src/config/subagent_limits.rs:21),
which is what docs/TOOL_SURFACE.md:182 already said. The 64/128 concurrency
figures on the same page were correct and are untouched.
- The memory section described a `memory.md` that does not exist and omitted the
`scope` parameter. crates/tui/src/tools/remember.rs:165 states the legacy
single-file path was removed in v0.9.4; writes go through
`NativeMemoryStore::remember(scope, workspace_id, note)` (remember.rs:77-108).
config.example.toml documented two key sets that do not exist. Neither struct has
`deny_unknown_fields`, so both were silently discarded rather than rejected:
- `[advisor] max_tool_pairs` / `system_prompt`. `AdvisorConfigToml`
(crates/config/src/lib.rs:2369-2394) has enabled, max_tool_calls (default 10,
clamped 1-50 — the doc said 8, max 32), rate_limit_secs, dedup_window_secs,
and model. `model` was undocumented; now it is.
- `[fleet.profiles.*.permissions] allow_tools` / `deny_tools`.
`FleetProfilePermissions` (lib.rs:1966-1977) has allow_shell, trust,
approval_required. `rg 'allow_tools|deny_tools' crates/` finds nothing. The
example value was `"exec_shell"`, itself a removed tool name.
docs/CONFIGURATION.md: deleted the "Parsed but currently unused" section. Its one
entry, `tools_file`, is not parsed by anything — the field was removed in
346bfe3b6 and the doc bullet was orphaned. Repo-wide `rg` finds the string only
in that section, and nothing links a #parsed-but-currently-unused anchor.
docs/TTC_DESIGN.md said implementation "is deferred beyond v0.9.0". The `verify`
tool shipped and is default-on (crates/tui/src/tools/verify.rs,
features.rs:262, registry.rs:1040-1041 with verify_tool_enabled defaulted true).
Retitled as landed-in-part; capability (B) is still genuinely deferred, so the
doc stays. Its interface line said `with_verify(critic)`; the real signature is
`with_verify_tool(client, model)` (registry.rs:886).
docs/skills/README.md advertised `gh-plan-issues`, deleted in 18de2ebc0, and
credited these skills to "the v0.8.61 release" at a 0.9.4 release.
docs/architecture/provider-model-settings-v091.md pinned
`provider_is_configured` to config.rs:8625-8669; it is at :10160 and that region
is now unrelated code. Replaced with the symbol name, since config.rs is under
active edit.
docs/architecture/command-dispatch.md:133 claimed EPIC-002 was "ready for PR".
The PR (#3706) merged and #2870 closed 2026-08-01. Line 145 was an empty
"Current Evidence (Draft)" heading with no content; removed.
.gitignore: `git check-ignore -v` attributes .claude/settings.json,
scheduled_tasks.lock, worktrees/, and *.local.* to the blanket `.claude/` at
line 126, not to the specific rules above them. Dropped the redundant ones and
annotated why the HANDOFF_/CODEMAP_ patterns are deliberately kept.
`a_run_scoped_kill_switch_preserves_a_consenting_users_state` runs the shipped
binary three times with `CODEWHALE_TELEMETRY` set to each spelling of "off"
against a seeded, consenting home, and asserts the directory comes back
byte-identical with no tombstone — then writes `telemetry = false` to the same
home and asserts that one *does* wipe. Both halves matter: a test where the two
switches are merely both silent would pass on the old, destructive behavior.
`config.example.toml` and `docs/CONFIGURATION.md` each repeated the "permanent
tombstone" claim `docs/TELEMETRY.md` made and could not keep. They now say what
is true and testable — the tombstone stands for as long as the `false` that
produced it stands, the config key outranks `--telemetry true` and
`CODEWHALE_TELEMETRY=1`, and the environment variable erases nothing.
`every_event()` gets the note it has needed since it was written: it is
hand-maintained, every red-line walk starts from it, and nothing in this file
can make the compiler extend it. `Event::is_bounded`'s exhaustive match is what
actually catches a new variant, and the note says so rather than implying a
guarantee the fixture list does not carry.
`html[lang="zh"] h1/h2/h3` relaxed `overflow-wrap: anywhere` so Chinese
headings stop stranding punctuation on a line of its own. ja needs exactly the
same rule and never had it — capping the ocean headings made it visible, with
`コマンド 1 つで始める。` breaking between `1` and `つ`. ko wants it too: it has
real word boundaries and should break on them rather than anywhere.
Extended rather than duplicated, per the rule that CJK overrides are extended
and never routed around.
Verified by eye at /ja and /ko, 1440px and 390px; no horizontal page scroll in
en/zh/ja/ko at 390px. npm test (235), lint, check:locales, check:docs.
Both findings come from @vFONGv's Windows beginner guide (PR #5229), verified
against the code before landing.
config.example.toml claimed "Shift+Tab in the TUI cycles between off / high /
max". That is stale: crates/tui/src/tui/app.rs:2370 emits the notice
"Shift+Tab now cycles permissions — reasoning effort moved to Ctrl+T". A user
following the config comment would cycle their permission posture while trying
to change reasoning depth, which is the more consequential of the two. His
guide had it right and our own example config had it wrong.
docs/PROVIDERS.md gains his China-region Moonshot finding: a China-region key
needs base_url = "https://api.moonshot.cn/v1" or it fails authentication on the
default international host, and editing base_url alone does not take effect
until `codewhale auth set` is re-run. `api.moonshot.cn` appeared nowhere in the
repo, so this is new information. Recorded as attributed user field evidence
rather than a tested route — we have no China-region key to verify it.
Harvested from PR #5229
Co-authored-by: vFONGv <21223725+vFONGv@users.noreply.github.com>
Owner directive: add GLM-5.3 everywhere GLM models appear.
Scope is deliberately narrower than "everywhere glm-5.2 appears", and the
reason is the whole point of this commit. GLM-5.3 is NOT live on the Z.ai
API — the owner's own credential was used to query it and the live roster
returns glm-4.5, glm-4.5-air, glm-4.6, glm-4.7, glm-5, glm-5-turbo, glm-5.1,
glm-5.2 and nothing further. Zhipu has published no GLM-5.3 identifier,
endpoint, limit, rate, or capability list.
So this wires the model where we can be honest about it, and nowhere else:
- First-party Z.ai row (GLM-5.3) and its OpenRouter mirror (z-ai/glm-5.3):
catalog, aliases, model registry, picker lists, context/output limits,
reasoning classification, and the tiered-effort wire path.
- Every capability and limit is INHERITED field-for-field from the verified
glm-5.2 row (1M context, 131072 output, reasoning with effort high/max).
Nothing is invented.
- No pricing. Z.ai has published no GLM-5.3 rate, and inheriting 5.2's would
fabricate one, so every price surface reports unknown. Pinned by
glm_5_3_has_no_hardcoded_price, which says in its own comment not to "fix"
it by copying 5.2's row.
- GLM-5.2 remains the default for every provider, profile and fleet role.
Adding a model does not move anyone's route.
- One greppable marker in models_dev.bundled.json `_meta.pending_release_metadata`
records the inheritance and the scope, so correcting the id or the limits
when Z.ai ships is a single-place edit.
Deliberately NOT added: OpenCode Zen, OpenCode Go, Alibaba Model Studio, and
TelecomJS rosters. Those tables transcribe what a third-party gateway
publishes. Metadata inheritance is not evidence of third-party availability —
there is no glm-5.2 value to copy, because the fact in question is roster
membership, not a limit. An honesty audit caught six such claims after the
first pass (including docs wording that read as a promise that these gateways
serve it); all six were removed rather than re-valued, and each roster now
carries a dated comment naming the evidence that would justify adding it.
The TelecomJS arm is additionally annotated as a frozen pre-refresh snapshot
(it still lists a GLM-5.0 we do not otherwise model) that must be refreshed
wholesale, not hand-extended.
`is_exact_zai_glm_5_2_route` became `is_exact_zai_tiered_effort_route`, since
the tiered top-level reasoning_effort path is now a family property rather
than one model's.
Receipts, all exit 0: cargo fmt --all --check; cargo test -p codewhale-config;
cargo test -p codewhale-tui --bin codewhale-tui; cargo test -p codewhale-workflow;
cargo test -p codewhale-cli; cargo test -p codewhale-agent;
cargo clippy -p codewhale-config -p codewhale-cli.
End-to-end: `codewhale model list` shows GLM-5.3 alongside GLM-5.2 with the
default unchanged, and `model resolve` maps the alias set correctly.
* Initial plan
* feat(config): add multiple named operator-scoped Fleet configurations (#5039)
Adds support for multiple named durable Fleet configurations in the config TOML,
each scoped to an operator identity. The existing [fleet] table remains the
backward-compatible default.
New public types:
- `NamedFleetConfigToml`: a [fleets.<name>] entry with a required `operator`
field plus independent trust/role/profile/exec settings. Exposes
`resolve_role()` and `as_fleet_config()` for unified usage.
- `FleetResolutionError`: typed, actionable errors (UnknownFleet,
UnknownOperator, AmbiguousOperator) with human-readable Display messages
that list available options rather than failing silently.
New methods on `ConfigToml`:
- `resolve_fleet(name)`: returns the named fleet or `UnknownFleet` error with
available names listed.
- `resolve_fleet_for_operator(operator)`: returns the unique fleet owned by an
operator; `UnknownOperator` if none matches, `AmbiguousOperator` if more
than one matches (caller must name a fleet explicitly).
New `fleets` field on `ConfigToml`: `BTreeMap<String, NamedFleetConfigToml>`,
serialized under [fleets.*] keys. Skipped when empty so legacy configs are
byte-for-byte unchanged.
config.example.toml updated with full named-fleet documentation and examples,
including the selection-precedence comment.
14 new tests added to crates/config/src/tests.rs covering: legacy-only,
mixed (legacy + named), multiple named fleets, resolve_fleet/operator success
and error paths, error message content, as_fleet_config view, and round-trip
serialization. All 489 tests pass.
---------
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
A per-step create_message call that exceeded step_api_timeout went
straight to Interrupted with zero retries, so one live-but-slow provider
call killed an entire child (dogfood: a 6-agent fan-out wiped out one by
one at the 120s wall, FINISH-0.9.4 entries #39/#40). Fold the timeout
arm into the retry machinery:
- SUBAGENT_API_TIMEOUT_MAX_RETRIES (5) per-step timeout budget with
exponential backoff (1s base, x2, 30s cap, +/-20% jitter via the
llm_client UUID-entropy idiom) and the same ModelWait progress event
style as the transient-provider retry path.
- After exhaustion, behavior is unchanged: Interrupted with the
checkpoint preserved for continuation (api_timeout).
- DEFAULT_SUBAGENT_API_TIMEOUT_SECS 120 -> 600 and clamp ceiling
1800 -> 3600; doc comments, config.example.toml, and docs updated
(including the stale ~/.deepseek/config.toml reference). The resolved
default heartbeat rises to 630s via the existing api+30s floor.
- Tests: deterministic backoff sequence + jitter bounds, retry-then-
success and retry-then-exhaustion integration coverage (new
always_delayed_chat_client helper; backoff shrinkable in tests),
config clamp literals (600 default, 3600 accepted, 3601 clamped).
Verified each claim against the code before editing:
- [capacity]: the 15-key controller was documented in CONFIGURATION.md
and config.example.toml but has zero matches anywhere under crates/
— silently ignored on parse. Removed, along with the link to the
nonexistent docs/capacity_controller.md and two prose mentions.
- [context] seam keys: verbatim_window_turns, l1/l2/l3_threshold and
seam_model are all 'Ignored (was: …)' in crates/tui/src/config.rs
(2036-2048); docs presented them as a live opt-in. Both files now
document only the live keys (enabled, project_pack) and mark the
seam keys ignored.
- Settings keys: show_thinking / thinking_default_expanded /
cost_currency were misfiled in config.example.toml; they belong to
~/.codewhale/settings.toml (settings.rs:344,414) and Config has no
deny_unknown_fields, so users got silently ignored settings.
Replaced with a pointer comment.
- Hotbar default was inverted in docs: KEYBINDINGS.md and
config.example.toml claimed fresh configs show the default bar;
since #3807 a missing hotbar key renders no bar (sidebar.rs:192-195).
- MCP tool naming: docs/MCP.md claimed mcp_deepseek_shell; the rule
is mcp_{server}_{tool} (mcp.rs:3022) with default server name
codewhale, i.e. mcp_codewhale_shell.
- MODES.md said 'DeepSeek-TUI'; memory_path bullet described the
deleted legacy single-file fallback — both fixed for the native
store (see 1135a1e65).
Evidence: cross-surface-tech-debt-audit-2026-08-03.md findings 74-80;
§11.3 docs-truth row.
Integrate Turisla’s verified permissions listing and snapshot-bound removal flow into the v0.9.2 release candidate while preserving the contributor commit and review history.
Show configured and effective provider context windows in /config and its audit/help paths, including the route-limit source. Document Kimi plan-tier caps and prove a 256K override drives compaction, the context meter, and preflight input budgeting from the same resolved limits.
Add /permissions listing with active source, matcher, scope, and current workspace applicability while keeping /config ask-rules compatible.
Gate removal behind a snapshot token and serialize append/remove through the same atomic permissions lock. Reload the live user ruleset without clearing session approvals.
Refs #1186
Expose the setting in the shipped example and configuration/accessibility references, including its interaction with show_thinking and the Space toggle.
Composes the #4797 cost-truth repair (HEAD) with the provider-truth
harvest (codex/v092-ptruth-harvest). Both lanes close truth-critical
blockers; where they touched the same seam the rule applied was: one
receipt type per job, classification computed from the dispatched
receipt, and the fail-closed answer wherever the two disagreed.
route_billing.rs
- Kept the harvest's single `classify(provider, identity, base_url,
product)` and its `capture_product`/`RouteProduct` credential truth.
It fully subsumes the cost lane's `minimax_billing`, which only read
`mode`; the harvest reads the same mode plus non-secret key-shape
provenance and never opens the keyring. Dropped `minimax_billing`,
`stepfun_billing`, `uses_zai_coding_plan`.
- Added `subscription_plan` to the MiniMax plan modes so the cost
lane's documented spelling is not silently discarded as unprovable.
- `for_endpoint_without_config` is now a thin wrapper over `classify`
with no identity and an unproven product, instead of a second copy of
the endpoint rules. Same fail-closed contract, one implementation.
- Moved the cost lane's endpoint gate into `classify`'s catch-all
(`endpoint_shaped_payg_billing`): a first-party or aggregator provider
on an unrecognized host is Unknown, not metered-by-provider-name
(#4318). This also fixes the harvest's noted hole where an empty
endpoint fell through to metered.
- `billing_surface_for_dispatch` kept as-is (three live callers) and now
benefits from the harvest's `for_route`.
core/events.rs, core/engine.rs
- `TurnRoute` carries both layers, documented at the definition because
they are captured at different instants and answer different
questions: `base_url`/`billing_product`/`provider_identity` are the
DispatchedReceipt frozen at client-freeze (readable from TurnStarted),
while `billing: Option<RouteBillingEnvelope>` is the wire-boundary
envelope that must be structurally absent for an undispatched route.
- The envelope's `billing_mode` is now classified from that same frozen
receipt via `for_dispatched_receipt` rather than a second ambient
`for_route` read, so the two halves cannot disagree.
subagent mailbox/mod, subagent_routing
- Kept the cost lane's `source_id` + `route: EffectiveRouteEnvelope` on
`MailboxMessage::TokenUsage` and dropped the harvest's parallel
`billing: Option<ChildBillingProvenance>` field. The envelope is the
child's dispatch receipt: the client it ran on froze provider,
identity, endpoint fingerprint, billing surface and billing mode at
construction, and `RouteBillingMode` has the same variant set as
`ChildBillingProvenance` plus strictly more evidence. Child provenance
still wins; it just travels on the richer receipt.
- The turn-end mailbox barrier (seal/drain/await before TurnComplete)
and its exactly-once detached-child accounting are unchanged.
tui/tool_routing.rs
- Kept the cost lane's path: bill from the child's own
`EffectiveRouteEnvelope`, rehydrated from the complete `child_*`
metadata emitted by all three real producers (review, verify, rlm).
The harvest's reader was explicitly unwired ("no tool producer emits
the keys yet") and its parent-inheritance fallback is contradicted by
the cost lane's tested contract
(`legacy_child_usage_metadata_fails_closed_without_parent_route_fallback`),
which is the stronger, fail-closed one: incomplete child metadata is
Unknown and reported as missing spend, never inherited.
- Consequently the harvest's `ActiveTurnMetadata` receipt mirror
(`billing_identity`/`billing_product`/`billing_base_url` and
`dispatched_receipt()`) had no production consumer and was removed;
`TurnRoute::cost_envelope()` is the same receipt one layer down and is
already consumed.
Dead code after composition (no -D warnings, nothing silently kept)
- `ChildBillingProvenance`, `static_subscription_label`,
`for_child_route_receipt`, `ChildParentRoute`, `ChildRouteClaim` are
now `#[cfg(test)]`-gated with a note at each definition explaining
that the wired child receipt is `EffectiveRouteEnvelope`. Their tests
are kept as the executable record of the serialization and
identity-comparison contracts.
Tests changed, and why
- `child_route_billing_fails_closed_for_every_ambiguous_provider`: the
cost lane expected Metered for PAYG aggregators and an exact
subscription label for OpenaiCodex/OpencodeGo children. The harvest's
`for_child_route` returns Unknown for every non-local cross-provider
child without provenance. The harvest's contract is stronger — a
provider name is not evidence of what a turn billed, and Unknown
(unlike a subscription label) keeps the turn in `/cost`'s money
coverage denominator — so the weaker expectations were updated, and a
same-provider inheritance case was added.
- `minimax_requires_an_explicit_saved_billing_mode`: expected label
changed from the generic "MiniMax subscription plan" to the harvest's
"MiniMax Token Plan quota", which names the actual product.
docs/PROVIDERS.md: kept the cost lane's StepFun billing-route setup row
and the harvest's MiniMax product-split row; the harvest's Moonshot
product-split and K3-clamp rows auto-merged.
Verification: route_billing 48, pricing 64, cost 87, receipt 157,
subagent 507, subagent_routing 18, tool_routing 11, prompt_suggestion
23, engine 461, model_inventory 17, tui::app 383, ui::tests 653,
config:: 458, mailbox 34 — all 0 failures. `cargo fmt --all -- --check`
clean; CI clippy (--workspace --all-features --locked, five -A allows)
clean; check-tui-locale-parity.py PASS.
Note: `failed_paused_dispatch_preserves_app_checkpoint_state_and_engine_gate`
overflows the default 2 MiB test stack under batch parallelism and
passes with RUST_MIN_STACK=32M. It is an unmodified HEAD test and a
pre-existing stack-depth papercut, not a merge regression.
Documentation caught up with the provider-truth behaviour this branch lands, so
the docs stop describing guarantees the code no longer makes (or never did):
- CONFIGURATION: `CODEWHALE_BASE_URL` is the **active** route's endpoint. A
pinned request resolves provider table → provider-scoped variable → provider
default and never inherits the session host; a custom route with no
`base_url` fails closed on the loopback placeholder. The legacy root
`base_url` stays shared between the DeepSeek identities when the user wrote
it, and belongs to one identity when the environment did.
- PROVIDERS/Moonshot: `kimi-for-coding-highspeed` joins the membership roster,
the mutual endpoint rejection is stated, and billing is described as the
endpoint-decided split it now is — metered direct platform, Kimi Code quota on
the exact membership endpoint, `cost: unknown` for gateways and neighbouring
Kimi paths — including that an imported token with no configured `base_url`
still bills as membership quota, and that a finished turn is billed from its
own dispatch receipt rather than a later config re-read.
- PROVIDERS/MiniMax: billing comes from the credential product, not the
endpoint; keyring-held keys are deliberately not read and leave the route
`cost: unknown` rather than assuming pay-as-you-go.
- PROVIDERS/reasoning: both exact K3 routes clamp `off` to `low`, but for
different reasons — the membership roster declares K3 always-thinking, while
the direct-platform clamp is defensive because the API documents no `off`
state and the live entitlement is unknown. Stated as rationale only: this
build does not yet emit distinct status-line receipts for the two.
- config.example.toml lists the high-speed membership id.
Locale parity: PASS (en 1177/1177, zh-Hans 1177/1177 complete; no new keys).
Harvested from the provider-truth lane.