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.
#4817. `[notifications.event_sound]` maps each notification event to a
fixed terminal-bell cue (one or two `\x07` bytes). Functional signals, not
audio design: no assets, no dependencies, and inert on terminals that
ignore BEL, so the whole feature is a platform-safe no-op wherever sound
is unavailable.
Off by default. The decision order is disabled → quiet → not in the
allow-list → `turn-complete` deferred to the existing `completion_sound`
channel (so the two never double-ding) → per-event rate limit → play.
`decide` takes the caller's `now_ms`, so rate limiting is deterministic
and testable rather than wall-clock dependent; the runtime wiring's
epoch-millis clock and its non-monotonicity are documented at the seam.
Wired into `notify_done_to` and initialized from config alongside
`set_completion_sound`. Documents the schema, the full cue table, and the
decision order in docs/CONFIGURATION.md and config.example.toml.
Two changes relative to the reviewed draft, both to keep the process-global
policy honest under test:
- `handle_notification_kind_to` takes an injected sink instead of writing
to `io::stdout()` directly, matching `notify_done_to`. No test path can
BEL a real terminal, and the notification module's existing byte-capture
tests now see any cue that would be emitted.
- The one test that touches the process-global `POLICY` `OnceLock`
serializes on the crate's `lock_test_env()` and restores the default
policy before releasing the lock, so no later test inherits an enabled
policy.
Fourteen sound-policy tests plus two config-parsing tests.
Adds Ca/De/Fr/Id/Hi/Ru/Uk to the Locale enum, tag/display/all/shipped
tables, parse_locale prefix matching, and the thinking-translation and
hidden-translation status strings, then wires them through the surfaces
that enumerate locales: setup, onboarding language picker, config_ui,
settings, the subagent locale seam, the status-picker copy guard, and the
tool card.
Notable behavior changes:
- The onboarding language picker's hotkeys now run 1..=9 then a, b, … so
sixteen options stay single-keystroke selectable; the contiguity test
follows the same rule.
- The German status-picker title is "Statuszeile" — "Status" is a German
word, so that locale gets an exact-match assertion rather than the
"contains no English 'Status'" guard the other locales use.
- Catalan "models" is the correct translation of the English picker
action, so it is exempted from the differs-from-English fallback guard.
- The prompt seam is deliberately unchanged: a new cache-stability test
locks the locale-bookend set to zh-Hans/ja/pt-BR/vi so adding a UI pack
can never silently change model-visible prompt bytes, while the
translation seam names every shipped locale canonically.
- Script-purity tests hold ru/uk apart (no Russian-only letters in uk, no
Ukrainian-only letters in ru, no copied sentences) and require hi to be
genuinely Devanagari.
Harvested from the v0.9.2 localization lane (#3091, #3092, #3093, #4788,
#4789, #4790, #4791).
Run observer and background hooks through bounded supervisors, enforce timeout containment and reaping, and preserve one hook session identity across runtime rebinds. Carry real tool exit codes into after/error events, fire tool-scoped on_error hooks for every settled failure, and keep message-submit stdin and queue recovery exact without parking the terminal loop.
Document the implemented TUI-only event, steering, shell_env, timeout, environment, and audit contracts without implying headless or public-API coverage.
Verified: hook filter 144/144; message_submit 29/29; reported_tool_exit_code 1/1; on_error filter 11/11; TUI locale parity; README locale links; cargo fmt --check; git diff --check.
Preserve @snail-vs's full contributor history from PR #4467 and include the review follow-up that isolates its provider environment test. This local candidate merge is not pushed and remains subject to the combined v0.9.2 release gates.
Let approval cards remember eligible safe shell and file-write approvals as exact allow rules bound to one repository, without weakening deny, ask, or safety-floor precedence.
Refs #1186 and #2242.
Harvested from PR #4761 by @greyfreedom
Co-authored-by: greyfreedom <11493871+greyfreedom@users.noreply.github.com>
Document Zen as a separate API-key provider with model-scoped Responses, Messages, and Chat Completions groups. Record the unsupported Gemini boundary and teach the provider registry drift check about the manual model-aware provider implementation.
Signed-off-by: snailoniu <snailoniu@gmail.com>
* feat(tools): add agent-callable verify critic
Add an opt-out, bounded, toolless adversarial critic that agents can invoke before claiming non-trivial work complete.
Closes#4196.
Harvested from PR #4199 by @Hmbown
Co-authored-by: Hmbown <101357273+Hmbown@users.noreply.github.com>
* fix(tools): make verify evidence and risk accounting complete
Include uncommitted worktree changes when verifying from a base, keep staged diffs valid, and fail safe on medium findings.
Closes#4196.
Harvested from PR #4199 by @Hmbown
Co-authored-by: Hmbown <101357273+Hmbown@users.noreply.github.com>
* chore(web): refresh generated tool facts
* docs(web): refresh public surface tool count
Keep exact K3 routes always-thinking for stale raw Off callers, distinguish dispatched Auto from an omitted provider-default setting, and restore the global Kimi platform links to the .ai domain.
Signed-off-by: Hunter B <hmbown@gmail.com>
Keep bare membership K3 on its lowest thinking tier when Off is selected while leaving Auto to the provider default. Scope direct K3 output metadata, token naming, and fixed-sampling omissions to the exact first-party route.\n\nAdd streaming and non-streaming request-capture coverage, route provenance, neighbor non-regressions, and canonical Kimi platform documentation links.
Signed-off-by: Hunter B <hmbown@gmail.com>
Scope direct K3 behavior to the exact Moonshot endpoint and model, normalize unsupported reasoning tiers, and preserve always-thinking replay. Share final request shaping across stream and non-stream paths with production-boundary coverage for direct, membership, and neighboring routes.
Signed-off-by: Hunter B <hmbown@gmail.com>
Show bare k3 on the Kimi Code membership endpoint, distinguish the direct kimi-k3 route, and document the entitlement-aware 1M context override without removing valid K2.7 compatibility.
Signed-off-by: Hunter B <hmbown@gmail.com>
Require an operator-supplied Metaso key from search configuration or METASO_API_KEY. Fail closed before backend fallback when it is missing, and update configuration guidance to match.
Disable ambient proxies on DNS-pinned fetches, require trusted fake-IP hosts and benchmark CIDRs together, and keep private address classes fail-closed. Enforce web.run cache namespaces, exercise the real redirect and click paths, and replace the broad same-root spam heuristic with the known #964 spam-domain evidence so site-scoped and public-suffix results remain valid.
Signed-off-by: Hunter B <hmbown@gmail.com>
Document disabled, read-only, and reserved managed access semantics for credentials owned by Codex and Grok CLIs. Point Kimi users only to the supported API-key flow and explain Codewhale-owned xAI device-login storage.
Refs #4507
Signed-off-by: Hunter B <hmbown@gmail.com>
Expose the provider's Chat Completions allowlist from shared config and project Grok 4.5 and Kimi K3 through route resolution, registry, live-catalog, and picker paths. Keep Messages-only entries excluded.
Document the current route and cover bare and provider-prefixed selectors without making provider requests.
Fixes#4527
Signed-off-by: Hunter B <hmbown@gmail.com>
Add first-class OpenCode Go configuration, credentials, model selection, routing, billing copy, documentation, and live catalog filtering for the eight models documented on the Chat Completions protocol.
Enforce the Chat-only model cutline centrally so Messages-only models cannot cross-route or bypass validation through a custom base URL.
Harvested from PR #773 by @zhangweiii and PR #1050 by @sternelee.
Refs #1481 by @seanthefuturegorilla.
Co-authored-by: zhangweiii <31117497+zhangweiii@users.noreply.github.com>
Co-authored-by: Sterne Lee <6971752+sternelee@users.noreply.github.com>
Signed-off-by: Hunter B <hmbown@gmail.com>
Integrate the underwater TUI, message-first Operate, Fleet and Workflow reliability, expanded model/provider catalog, exact custom-route restoration, docs-first site, localization, packaging, and release metadata for the v0.9.0 candidate.
Harden endpoint-bound credential provenance, approval and goal UX, Fleet attempt fencing and crash recovery, large-workspace mention discovery, Kimi budgeting, and release asset/version gates. Include the stopship Fleet and Workflow fixtures used by release dogfood.
Verified with workspace fmt/check/clippy/tests on Rust 1.88, release-script and npm suites, 18-crate publish dry run, production web build, Docker build check, secret scan, dependency audit, and protected-state hash validation.
Classify the actual StepFun endpoint as PAYG or Step Plan without persisting raw URLs, then carry that non-secret provenance through turn-end hooks, runtime records, usage aggregation, and offline scorecards.
Price only the exact StepFun PAYG fallback route with the official cache rate. Route-blind background and foreign-provider calls now fail closed so subscription quota cannot be reported as token spend.
Validation: cargo fmt --all -- --check; RUSTFLAGS=-D warnings cargo check -p codewhale-tui --bin codewhale-tui --locked; targeted scorecard/pricing/runtime/hook tests; full TUI suite (6408 passed, 2 ignored before the final narrow gate, then gate-specific regression tests passed).
Signed-off-by: Nightt <87569709+nightt5879@users.noreply.github.com>
Add the OpenAI GPT-5.6 family and Meta Model API's Muse Spark 1.1 across provider metadata, configuration, secrets, CLI selection, catalogs, model limits, pricing, reasoning payloads, and documentation.
Verified with config, agent, secrets, CLI, catalog, and focused TUI tests. This local branch intentionally has no upstream.
API-key path already shipped. Add residual OAuth for [providers.xai]
auth_mode = "oauth": reuse ~/.grok/auth.json (GROK_HOME / GROK_AUTH_PATH),
refresh against auth.x.ai/oauth2/token before expiry, and expose a native
device-code login entry point that prints verification URL + user code
without a localhost callback. API-key remains the reliable fallback when
SuperGrok OAuth returns 403.
Match dynamic Workflow product constraints: the script remains a sandboxed
coordinator (no FS/shell/network), sub-agents do the real work with inherited
tools, write-capable Workflow children auto-accept Suggest-level file edits
while shell/network stay gated, concurrent live children cap at 16, and the
per-run agent lifetime stays at 1_000. Config defaults and docs follow.
Introduce WorkflowConfigToml as the first-class config surface for automatic
Workflow launch, write approval, child/isolation budgets, and completed
activity persistence. Defaults match Section 2.11 product knobs; omitted
tables resolve through Config::workflow_config(), and explicit overrides
round-trip via TOML load/serialize.
Closes#4128
Add xAI as a first-class OpenAI-compatible provider with XAI_API_KEY, XAI_BASE_URL, XAI_MODEL, Grok aliases, catalog entries, and docs/example config coverage.
This intentionally ships the low-risk API-key path from #4257; xAI OAuth/device-code support remains follow-up work.
DEFAULT_HOTBAR now binds mode.operate (not mode.yolo), and assertions
expect Act-labeled YOLO shorthand plus Agent prompt markers so macOS CI
matches the folded mode roster.
Signed-off-by: Hunter B <hmbown@gmail.com>