Compare commits

..

343 Commits

Author SHA1 Message Date
CodeWhale Bot 518db2b37e debug: probe python availability on Windows runner 2026-08-11 22:05:33 -07:00
CodeWhale Bot 05efea2109 fix(tui): gate POSIX-only bash stream test to unix; bump source budget
lowercase_bash_returns_one_ordered_stream uses printf/redirection
syntax that Windows cmd cannot execute; it ran on Windows CI only
after the pi_output compile gap was fixed and failed deterministically.
Gate it to unix like its neighbor. The approval-test failure on
Windows is being rerun to separate a flake from a real defect.
Source budget follows the added lines (687594).
2026-08-11 21:36:18 -07:00
CodeWhale Bot d970495929 fix(subagent): keep route receipts under the admission limit on CI builds
The receipt embeds the build commit, which CI stamps with the full
40-hex GITHUB_SHA while local builds carry the literal 'unknown'.
A full-length sha pushed the serialized receipt to 394 bytes over the
384-byte admission cap, so every CI build refused child launches and
the receipt regression tests failed on GitHub runners (they passed
locally only because 'unknown' is short). Some legitimate routes also
reached 386 bytes even without the sha.

- Truncate the embedded build commit to 12 hex chars; version + short
  sha still identify the build for attribution.
- Raise the cap to 448 bytes with headroom for route growth.
- Remove the temporary debug workflow used to capture the runner
  failure output.

Verified with the exact CI condition reproduced locally: a build
with DEEPSEEK_BUILD_SHA set to a full 40-hex sha now passes the
receipt tests, and the full lib suite is green (10301 passed).
2026-08-11 20:47:30 -07:00
CodeWhale Bot 66df75a5a3 debug: run receipt tests with --nocapture on a GitHub runner 2026-08-11 20:35:17 -07:00
CodeWhale Bot b28d66e98b fix(tui): make catalog pinning tests hermetic to host tool availability
The exact-surface tests hardcoded tools whose registration is gated on
local backends: image_ocr (tesseract/native OCR) and pandoc_convert
(pandoc binary). CI runners and Linux containers lack those, so the
pinned catalogs drifted and CI has been red for these tests all day.
Expected sets now follow ocr_available()/resolve_pandoc(), and the
image_ocr allowed-tool assert is conditional the same way.

Verified in both directions: macOS host (tesseract + pandoc present)
and rust:1.97.1 Linux container (neither) — all four tests pass.
Also raises the source-structure budget by 4 lines for the rename.
2026-08-11 20:34:06 -07:00
CodeWhale Bot 3920028c6c refactor(tui): align shell and file internals with contract naming
Rename the lowercase bash tool struct and its bounded output
accumulator, plus the file read/write helper symbols and their
tests, to contract-based names. Tool names, schemas, and behavior
are unchanged; doc and test references updated to match; the
model-visible catalog text and budget files are untouched.

Verified: cargo test -p codewhale-tui --lib (shell/registry/file/
file_tool/tool_catalog filters) 308 passed; web
public-surface-contract 13/13 passed.
2026-08-11 19:14:00 -07:00
CodeWhale Bot 27583e61e6 fix(tui): add pi_output to windows-only shell test literal
bf6def00d added the Pi-compatible bounded output accumulator
(pi_output) to BackgroundShell, but the #[cfg(windows)] test
initializer in shell/tests.rs was not updated. Windows CI test
builds fail with E0063 (missing field pi_output) while macOS/Linux
builds never compile that test. Add pi_output: None — the test
shell has no live process pipe, so collect_output falls back to
the stdout buffer path.
2026-08-11 19:03:16 -07:00
CodeWhale Bot 865bc79408 fix(web): sync public-surface test with the seven-name toolbox
bf6def00d added todo_write to the model-facing toolbox
(read/write/edit/bash/agent/todo_write/tool_search) and updated
docs/TOOL_SURFACE.md to 'exactly seven model-facing names', but
web/lib/public-surface-contract.test.ts still asserted six. The
release-candidate and Web Frontend proof runs fail on that stale
assertion at the frozen SHA; align the contract test with the
documented surface. Verified: vitest public-surface-contract (13/13).
2026-08-11 18:52:56 -07:00
CodeWhale Bot 3726669a89 chore(release): refresh v0.9.6 packaged changelog slice
Sync to CNB / sync (push) Has been cancelled
Regenerate crates/tui/CHANGELOG.md from the root CHANGELOG.md so the
embedded changelog matches the finalized 0.9.6 notes (sync-changelog.sh).
Verified with ./scripts/release/check-versions.sh --require-dated-release
(workspace/npm/lockfile in sync; OHOS gates green).
2026-08-11 18:44:00 -07:00
CodeWhale Bot bf6def00d7 fix(tui): keep progress and shell work reachable 2026-08-11 18:21:09 -07:00
CodeWhale Bot 4d1fe43116 docs(release): finalize 0.9.6 change notes 2026-08-11 16:28:08 -07:00
CodeWhale Bot b60262fa81 refactor(tui): deduplicate child route projection 2026-08-11 16:22:59 -07:00
CodeWhale Bot 4ed1869a97 feat(subagent): persist child route receipts (#5305) 2026-08-11 16:19:18 -07:00
CodeWhale Bot 9935832d9b ci(web): surface pending manual deploys
Annotate green main builds with the exact manual deployment command while preserving the credential-free, approval-gated Cloudflare boundary.
2026-08-11 15:59:04 -07:00
CodeWhale Bot 2ba40aeab8 feat(web): default to keyless Firecrawl
Use the native Firecrawl adapter as the zero-config search route, preserve visible DuckDuckGo and Bing fallback, and document explicit China-provider choices without inferring geography.
2026-08-11 15:57:53 -07:00
CodeWhale Bot 1fceff0c5c feat(web): add keyless Firecrawl search 2026-08-11 15:22:40 -07:00
CodeWhale Bot 1ed798db56 fix(release): honor SOURCE_DATE_EPOCH in bundles (#5312)
Derive archive mtimes from the pinned source commit in release artifacts, retain reproducible archive bytes and executable modes, and cover metadata plus input validation.
2026-08-11 15:02:30 -07:00
CodeWhale Bot a60d805e09 fix(exec): skip excluded MCP startup
Apply a provably native-only exec allowlist before engine feature setup so MCP servers that cannot contribute a tool are not connected. Keep unknown names and wildcard rules conservative, and cover both paths with focused catalog tests.
2026-08-11 14:34:06 -07:00
CodeWhale Bot e33681da89 fix(runtime): scope reasoning replay to wire contracts
Treat provider reasoning continuity as typed state instead of readable transcript text. Preserve OpenAI Responses encrypted items only for the exact originating provider, API, and model; strip them on route changes; keep DeepSeek, Kimi K3, Model Studio, Mistral, and Anthropic contract regressions intact; and stop generic model-name suffixes from authorizing replay.
2026-08-11 13:10:14 -07:00
CodeWhale Bot 4a487f3cea refactor(tui): remove obsolete full diff wrapper
The bounded renderer now owns both capped previews and the exact transcript path, so remove the dead compatibility wrapper exposed by all-target compilation.
2026-08-11 13:04:50 -07:00
CodeWhale Bot a77b12876f ci(release): publish npm through trusted OIDC
Bind npm publication to the exact release SHA after the public asset freshness gate, without a long-lived registry token. Document the npm-side publisher binding and interactive 2FA recovery path.\n\nCloses #5299
2026-08-11 12:52:11 -07:00
CodeWhale Bot 254cb5148b fix(tui): bound live diff rendering
Retain only each live view's wrapped-row budget while scanning the full diff for truthful omission counts. Keep transcript detail exact and remove the unreachable legacy DiffPreview cell.\n\nCloses #5087
2026-08-11 12:51:42 -07:00
CodeWhale Bot de4fc3caa9 fix(tui): preserve deferred tool retry results
Scope tool-call result integrity to each assistant turn so a provider can reuse an identifier on a later retry without the completed result being quarantined. This prevents deferred plugin tools from looping after successful MCP execution.\n\nVerified with the focused repair suite, the full cucumber acceptance target, all-feature Clippy, and source/runtime/dead-code budgets.
2026-08-11 07:04:44 -07:00
CodeWhale Bot 037aab9cf3 docs: align the v0.9.6 public surface
Document Work and To-do as the two product concepts, synchronize every shipped locale, publish the six-tool and typed-image contracts, and keep static web generation offline while live GitHub chrome refreshes after deployment.
2026-08-11 06:16:30 -07:00
CodeWhale Bot 2d6fe4fde6 feat(tui): finish the v0.9.6 runtime contract
Adopt the six-tool lowercase surface, keep To-do state out of repeated provider prompts, and store Codex-style compaction checkpoints in ordinary history. Preserve provider-specific reasoning continuity, typed image tool results, role capability boundaries, ACP tool turns, and exact regression coverage across the runtime.
2026-08-11 06:16:13 -07:00
CodeWhale Bot cc4e8a734f fix(acp): enforce native safety across editor tool turns
Route ACP calls through shared hook and policy admission, keep sandbox and override authority aligned with native turns, preserve receipts across cancellation/provider errors, and freeze the per-prompt system prefix. Stateful terminal/background forms remain unavailable on the ACP surface.
2026-08-10 19:42:49 -07:00
CodeWhale Bot 82c7e3eb40 Merge pull request #5225 from rafaelcavalheri/feature/acp-tool-execution 2026-08-10 19:21:03 -07:00
CodeWhale Bot 87addc9a7a fix(tui): refresh v0.9.6 interface contracts 2026-08-10 19:09:25 -07:00
CodeWhale Bot 4c3a2b17a4 fix(acp): align tool turns with stable runtime contracts
Compose ACP prompts through the canonical headless builder so editor-driven turns receive the same project instructions, configured instructions, memory, locale, and route-budget context as the current runtime. Preserve client-specific JSON-RPC response IDs while tools are running and make the Bash cancellation regression deterministic.

Fail closed by requiring the client terminal capability, the explicit headless shell opt-in, and the stable ShellTool feature before registering Bash. Apply the current Agent/Suggest sandbox policy and gate ApplyPatch through its feature flag. ACP continues to reuse the shared ToolRegistry and executes tool calls sequentially.

The stale v0.8.68 PowerShell build script described in the original contribution is intentionally not carried into v0.9.6.

Co-authored-by: Rafael Cavalheri <144138270+rafaelcavalheri@users.noreply.github.com>
2026-08-10 18:26:13 -07:00
rafaelcavalheri 2d4ff2570a fix(acp): restore the shell safety gate and fix a flaky cancel test
Review feedback on #5225 (Hunter):

1. build_acp_tool_registry set context.auto_approve = true, which
   short-circuits the SafetyLevel::Dangerous check in
   tools/shell.rs (only runs `if !context.auto_approve`), so every
   command an ACP client's model emits ran unreviewed. ACP has no
   session/request_permission round-trip yet to fall back on. Drop
   the line and let ToolContext::new's default (auto_approve: false)
   stand — matching mcp_server.rs's trust posture over a different
   transport. A blocked command already surfaces as a normal
   `success: false` "BLOCKED: ..." tool result fed back to the model
   (execute_tool_calls_with_cancellation already round-trips tool
   results), not a silent failure, so there's no UX regression from
   restoring the gate.

2. agentic_turn_cancels_while_a_tool_is_running scripted a tool call
   named "exec_shell", which with_shell_tools() never registers
   (renamed to "Bash" in v0.9.3). The lookup miss made the tool
   future resolve to an immediate error instead of actually running
   SLOW_SHELL_COMMAND, so the test's `select!` raced two already-ready
   futures and asserted PromptOutcome::Cancelled on a coin flip.
   Renamed to "Bash" so the 5-second command genuinely runs and the
   cancel path genuinely preempts it. Also swept the remaining
   `exec_shell` references (doc comments, a test name/message) left
   over from the pre-v0.9.3 tool spelling.

Verified: cargo test -p codewhale-tui acp_server (34/34) and
route_budget (11/11) pass; the renamed cancel test passes 15/15 runs
in isolation (was ~50/50 before the rename). cargo fmt and the
project's workspace clippy gate (fmt + clippy --workspace
--all-features -D warnings, CONTRIBUTING.md allow-list) are clean
except one pre-existing, unrelated lint in mcp.rs.

Blocker 3 (build_system_prompt, deleted in a98b184f5) is Hunter's to
carry per the review; not touched here.

Drafted with agent assistance (Claude Code); build-verified by the
human author before pushing.
2026-08-10 18:26:12 -07:00
rafaelcavalheri 1496a1b776 feat(acp): expose file/search/git/patch/shell tools over session/prompt
The ACP session/prompt path only streamed text; it never executed the
tool calls a model requested, so editors driving CodeWhale over ACP
(Zed, and third-party bridges like acp-deepseek-adapter) got a
chat-only agent with no real code-editing capability. This wires the
existing ToolRegistry into the ACP turn loop instead of duplicating a
new one:

- run_agentic_prompt_turn drives multi-round tool_use/tool_result
  turns (capped at MAX_ACP_TOOL_ROUNDS) over the same file/search/git/
  patch/shell tools the TUI uses, and reuses response_id_policy so
  every tool-round response still gets the client-specific id
  translation (Zed/avante.nvim) the existing streaming path relies on.
- Shell access is gated on the client declaring `terminal` support at
  `initialize` (default false/restrictive); MAX_ACP_SESSIONS caps
  concurrent sessions with true insertion-order eviction (VecDeque,
  not HashMap iteration order).
- Tool-call cancellation signals a CancellationToken and waits for the
  running tool (including a child shell process) to actually stop
  before returning, rather than abandoning it.
- max_tokens for the ACP path now resolves through the same
  route-limits machinery the TUI/CLI use (effective_max_output_tokens_for_route)
  instead of a fixed 4096 fallback.
- scripts/build.ps1: release build script for Windows PowerShell 5.1,
  used to produce the ACP binary tested against Zed on Windows.

34 unit tests cover the turn loop, tool execution against a real
workspace, cancellation mid-tool, and concurrent sessions with
independent registries, all against in-memory streams (no live
provider needed).

Drafted with agent assistance (Claude Code); build-verified and
reviewed by the human author before submission.
2026-08-10 18:26:11 -07:00
CodeWhale Bot a090439c38 fix(web): show Work mode and build provenance 2026-08-10 18:24:39 -07:00
CodeWhale Bot 4127662e98 fix(web): refresh v0.9.6 repository facts 2026-08-10 18:21:05 -07:00
CodeWhale Bot e3638ea223 Merge pull request #5317 from ousamabenyounes/fix/issue-5253 2026-08-10 18:19:14 -07:00
CodeWhale Bot f62d113166 chore(gates): record the promoted v0.9.6 source budget 2026-08-10 18:17:06 -07:00
CodeWhale Bot 7e5b25f1e2 docs(release): finalize the v0.9.6 changelog 2026-08-10 18:13:01 -07:00
CodeWhale Bot 00aae33b44 fix(release): assemble container images from native artifacts 2026-08-10 18:12:14 -07:00
CodeWhale Bot e3e9123d34 chore(tui): remove verified-dead execution surfaces 2026-08-10 18:11:44 -07:00
CodeWhale Bot 6d49b281b5 docs(subagents): clarify inherited depth schema 2026-08-10 18:11:29 -07:00
CodeWhale Bot 54a18ce158 style(rust): apply pending formatter output 2026-08-10 18:10:55 -07:00
CodeWhale Bot 58d91d01b2 feat(fleet): add configured-pool setup advisory 2026-08-10 18:10:34 -07:00
CodeWhale Bot f82b2fa8a4 fix(mcp): reject relative config path ambiguity 2026-08-10 18:10:24 -07:00
Ben Younes 85d4827e46 fix(subagents): cap nested max_depth by inherited budget
A descendant subagent could widen the absolute recursion budget inherited
from its root session by passing an explicit max_depth on a nested spawn.
child_max_spawn_depth_for_spawn dropped the inherited budget for the
explicit-request arm, so child_max_spawn_depth_for_spawn(2, 2, Some(8), None)
returned 8 even though the root selected an absolute maximum of 2 — the
descendant could then keep spawning past the intended boundary.

Take the min with the inherited budget in the explicit-request arm, mirroring
the profile-hint arm that already did so. A request or hint may only narrow,
never widen, the root/session's chosen absolute depth. The global
MAX_SPAWN_DEPTH_CEILING added in #3931 stays the outer bound.

Adds a dedicated regression test for the issue scenario and updates the two
assertions in test_child_max_spawn_depth_profile_hint_only_narrows that had
encoded the old widen-up-to-ceiling behavior.

Fixes #5253

Implemented with AI-assisted tooling; authored and reviewed by the contributor.

(cherry picked from commit 4e5ac2ba39)
2026-08-10 17:35:32 -07:00
CodeWhale Bot c88d1b8e09 feat(tui): rename the Act mode to Work
The mode dial (what the agent does: Work / Operate / Plan) and the
permission dial (how approvals happen: Ask / Auto-Review / Full
Access) both carried A-words — "Act" and "Ask" — and error copy
conflated them. Rename the mode's user-facing name to Work in every
locale; "work" parses everywhere "act" does, and "act" stays as a
back-compat alias for configs and muscle memory.

Internal identifiers (AppMode::Agent) are unchanged; aligning the
backend enum names with the frontend vocabulary is tracked as the
v0.9.7 vocabulary unification.
2026-08-10 17:34:22 -07:00
CodeWhale Bot 9238ac83e0 feat(approval): auto-approve non-bypassable tools in Full Access
#3866 made start_mcp_server and rlm eval fail closed in Full Access
because that posture opens no approval modal — which stranded the
calls: an operator who had granted full access could not run the tool,
could not approve it, and had to leave the posture to proceed. Full
Access already grants everything these calls can do (the shell can
spawn the same processes), so the gate protected nothing while
blocking the documented flow.

Owner decision 2026-08-10: Full Access auto-approves. The resolver
now returns Allow for non-bypassable holds under auto-approve/Yolo;
every posture that can open the modal (default suggest, never) still
prompts or denies exactly as before, and repo law still overrides.
2026-08-10 17:34:21 -07:00
CodeWhale Bot 59029fcdca fix(mcp): import the moved stale-session classifier at the module root
The #3310 split moved is_mcp_stale_session_body into mcp/wire.rs and
updated the callers the branch could see, but the v097 lane had grown
a stdio-reader call site in root mcp.rs after the branch point. The
merge kept both halves; the import now names both classifiers.
2026-08-10 17:34:19 -07:00
CodeWhale Bot d39324820a Merge #4079 project_context.rs module split into v0.9.7 lane
Three verbatim moves from the isolated agent worktree: types into
project_context/types.rs, the workspace pack pipeline, and the
constitution loader into project_context/constitution.rs. Root
re-exports keep every existing path resolving; assembled system
prompts unchanged.
2026-08-10 17:13:56 -07:00
CodeWhale Bot e2cd593508 Merge #3310 mcp.rs module split into v0.9.7 lane
Three verbatim moves from the isolated agent worktree: HTTP transport
into mcp/http.rs, shared wire-format helpers into mcp/wire.rs. Every
MCP transport now sits behind the same boundary; no behavior change.
2026-08-10 17:13:51 -07:00
CodeWhale Bot aab06c71b8 chore(tui): drop the never-wired LargeOutputRouter::synthesis_prompt
The follow-up that was supposed to call this prompt builder never
landed: sibling wrap_synthesis did get wired, so the routing path
shipped without ever asking for a synthesis prompt. The doc's "public
so callers outside this crate can unit-test it" was false twice over —
no such test exists and the tools module is private. estimate_tokens,
EvidenceRouting, and wrap_synthesis all have callers and stay.

Deletion-work-order group 11; proof: RUSTFLAGS=-Dwarnings cargo test
-p codewhale-tui --lib large_output_router.
2026-08-10 17:12:27 -07:00
CodeWhale Bot 1ab8f8e829 feat(workflow): resolve the search ceiling from the Fleet concurrency seam
#5060: experimental search re-hardcoded a 16-worker ceiling instead of
reading the Fleet seam, so a deliberately small pool still admitted
16-wide batches and a larger pool could never use its width. Every
validation entry point now has a _with_limit twin that takes the
resolved Fleet ceiling ([workflow] max_concurrent and a profile's
delegation.max_concurrency, the lower present bound winning), and the
frozen receipt records which of Fleet limit or crate fallback actually
bounded the run — kept out of the preregistration hash on purpose,
because pool admission is an operational fact about the run, not a
scientific input.
2026-08-10 17:12:26 -07:00
CodeWhale Bot ea4c868b07 feat(tools): tui_help gives the model the command and key map
#1708: the model could not answer "what can I press here" without
guessing — the command catalog and keybinding table were human-only.
tui_help reads both registries back out of the same sources /help and
the help overlay render from (commands::command_infos, the user
registry, tui::keybindings::KEYBINDINGS), so the model-facing
reference cannot drift from the human one. Per-section caps keep an
unscoped dump from costing more context than any answer it contains.
2026-08-10 17:12:26 -07:00
CodeWhale Bot 238bf173f9 feat(commands): /update checks for and installs releases from the TUI
The update path asked users to leave the session, open a shell, and
remember the subcommand; the update notice said so. /update (alias
/upgrade) now drives the existing codewhale update binary — check by
default, install only on explicit request — and the update notice
points at both paths.

Deliberate limits: package-managed installs (Homebrew, npm, cargo)
get instructions rather than an updater run, so no manager's metadata
is left describing a version that is no longer on disk; and /update
never relaunches — telling the user to restart is the honest slice.
2026-08-10 17:09:29 -07:00
CodeWhale Bot a5df72ec7b feat(tui): OSC 8 file links for named markdown paths
A [main.rs](/repo/src/main.rs) in model output now opens with the
terminal's Cmd/Ctrl-click gesture: named markdown link destinations
that are absolute paths gain a file:// OSC 8 target alongside the
existing HTTP(S) path. Relative destinations stay inert — this layer
has no workspace root to resolve them against — and control bytes or
file://host/ remote forms are rejected rather than reinterpreted.
Prose is never scanned for path-shaped text, so bare paths in running
sentences do not linkify.
2026-08-10 17:09:29 -07:00
CodeWhale Bot 985a4c8be1 feat(tui): headline the dispatch name over the whale nickname
#5287: operators dispatch sub-agents by name and think in that name,
but every identity surface — sidebar rows, the work-surface agent
column, agent details, the pending-work indicator, the /subagents
view — spelled the row from the generated whale nickname instead.
sidebar::dispatched_agent_name reads the session name the manager was
given (an agent-id seed is reported as absent), and it now leads every
chain; the whale names only the agents dispatched without one.
2026-08-10 17:09:28 -07:00
CodeWhale Bot 06ea7a8039 feat(approval): configurable default selection for approval cards
#5293: v0.9.6 made a fresh approval card highlight Deny so a reflexive
Enter refuses a call the user has not read. That is the right default,
but operators who trusted the pre-v0.9.6 Enter-to-approve flow deserve
a knob instead of a surprise. [approval] default_selection accepts
deny (default) or allow_once; it moves the highlight only — which
calls are prompted stays approval_policy plus permissions.toml rules.

Documented in CONFIGURATION.md with the muscle-memory rationale.
2026-08-10 17:09:27 -07:00
CodeWhale Bot 376c69c9a2 feat(subagents): record requested-vs-effective route on spawn receipts
#5305: a spawn receipt that names only the resolved Fleet profile
invites false model attribution — the reader cannot tell whether the
child kept the session route or took the profile's. The start receipt
now carries child_route: requested provider/model (absent when nothing
was pinned) alongside the effective provider/model and the precedence
rule that chose them, all captured at the spawn seam so a later
session-level model switch cannot rewrite a launched child's receipt.

child_route stays inside the compact receipt's 1KB budget rather than
being exempted: five short identifiers cost ~150B, and omitting the
route is the misattribution this fixes.
2026-08-10 17:09:26 -07:00
CodeWhale Bot 2f05b07e91 feat(tools): content-hash edit guards for write, edit, and patch
#3979: an edit computed against a stale read could still match and
overwrite work that landed between the read and the write — the race
that bites shared worktrees hardest. File read now reports a
sha256-prefixed content_hash over the whole file (streamed, so large
windowed reads keep their memory bound) in the model-visible content,
and write/edit accept an optional expected_hash verified before any
match or write. File patch guards the patch target — the explicit path
or the first touched file — and refuses the whole patch, writing
nothing, on a stale hash.

Absent expected_hash keeps pre-#3979 behavior exactly. The FileTool
schema byte budget rises 3000 → 3100 for the new parameter: a decision
recorded in the test, not drift.
2026-08-10 17:09:26 -07:00
CodeWhale Bot b1d33331df feat(tui): honest context-window rungs for unresolved routes
#5239: a route that never resolves (auto selection, endpoint-keeping
model switch, failed resolution) used to erase an operator-configured
context window and print a borrowed 128K claim. Add
resolve_context_window for candidate-less hosts — configured override,
then offering/catalog limits, then the capability fallback — and carry
the window as one ContextWindowResolution so a report can never
attribute one rung's tokens to another rung's label.

The fallback rung is a guess made because nothing described the model:
doctor and /context now mark it unverified instead of asserting a
"128K default" the capability matrix may not hold, and every rung
round-trips through its own label.
2026-08-10 17:09:25 -07:00
CodeWhale Bot 3d4ff41761 refactor(tui): move the constitution loader into project_context/constitution.rs
Third move of #4079. The .codewhale/constitution.json pipeline —
workspace-upward discovery to the git root, parsing, the rendered
<codewhale_repo_constitution> authority block, and the mechanically
enforceable write holds compiled for crate::repo_law — is a
self-contained concern and now owns its own module.

The loader keeps reading through the shared context helpers it always
used (context_candidate_exists, find_git_root, join_relative_components,
load_context_file); callers reach it through a root re-export, so the
assembled system prompt is unchanged. Pure move, no logic edited.

Implemented with Claude Code agent assistance.
2026-08-10 16:57:05 -07:00
CodeWhale Bot 0e74dc503e refactor(mcp): move shared wire-format helpers into mcp/wire.rs
Third move of #3310. The frame/response size ceilings, SSE event
framing and field parsing, and the stale-session/closed-connection
error-text classifiers were duplicated across the transports that
consume them; they now live once in mcp/wire.rs as pub(super)
helpers, with sse.rs, stdio.rs, streamable_http.rs, and the root
mcp.rs reading from there. tests.rs imports the moved classifiers
explicitly.

Pure move: no ceiling moved and no classifier rewritten, so transport
behavior is unchanged.

Implemented with Claude Code agent assistance.
2026-08-10 16:57:02 -07:00
CodeWhale Bot 7a28bb4906 feat(client): one DeepSeek effort tier table shared by Chat and Responses
DeepSeek spells the same four thinking tiers differently on its two
wires, and until #5055 each spelling lived inline at its call site — a
documented mapping change meant archaeology across two files instead of
one edit. client::deepseek_effort is now the single annotated table
(Off/Low/High/Max with both wire spellings); the Chat Completions path
and the Responses path are two spellings of it, and a test fails if the
two wires ever disagree with the table.

DeepSeek documents that the Pro actual-effort mapping changes in early
August 2026; when it does, the table is the one place to edit.
2026-08-10 16:55:27 -07:00
CodeWhale Bot 4f829bc7d1 feat(tui): report token deltas in compaction receipts
Message counts alone do not show the win the user cares about: a
compaction that drops few but enormous messages reads as a no-op. The
emergency path already reports tokens; manual and auto compaction now
match, printing ~before → ~after tokens alongside the message counts.
2026-08-10 16:55:12 -07:00
CodeWhale Bot e511e10738 docs(core): describe the actual state of the engine move
The #5261 module doc claimed the turn loop already lived in
crates/core; it does not. Only request-building and fragments have
moved, and crates/tui/src/core/engine/turn_loop.rs is what every
interactive and headless turn runs today. Say so, so the next mover
lands against the real boundary instead of an imagined one.
2026-08-10 16:55:11 -07:00
CodeWhale Bot a4b5e78204 fix(tui): hold back incomplete UTF-8 sequences in shell preview deltas
A preview poll can land mid-character: the caller decodes each delta as
UTF-8, so a truncated multibyte sequence rendered as replacement glyphs
and corrupted the next delta's leading byte too — the streaming-client
bug from #1675, alive in the shell preview path. Leave an unfinished
trailing sequence in the buffer for the next poll; genuinely invalid
bytes still pass through so binary output cannot stall the cursor, and
the final result reads the whole buffer.

Tested with a split CJK character across two deltas and a lone 0xFF.
2026-08-10 16:55:09 -07:00
CodeWhale Bot 31842a96b6 refactor(tui): move the project context pack into its own module
Second move of #4079. The pack is a self-contained pipeline — walk the
tree breadth-first under a depth/entry budget, sort by priority then
case-folded path, classify config vs source, excerpt the README, serialize
— and it shares nothing with instruction-file loading except the workspace
path. It had no reason to sit in the middle of the loader.

`project_context/pack.rs` takes the eight `PACK_*` budgets and the whole
`ProjectContextPack`/`ReadmePack` chain, plus the five tests that pin its
determinism: stable sorting, the ignore lists for agent state and binary
noise, breadth-first fairness to later top-level directories, cross-platform
sort equivalence, and `..` rejection in relative paths. Those tests need no
loader fixtures, which is the acceptance criterion the issue asked for.

`generate_bounded_project_overview` becomes `pub(crate)` because the
ephemeral-context fallback still calls it from the loader half;
`generate_project_context_pack` keeps its visibility and is re-exported
from the root, so init.rs and context_report.rs are untouched.

Pure move: no logic edited, so the assembled system prompt is unchanged.

Implemented with Claude Code agent assistance.
2026-08-10 16:37:49 -07:00
CodeWhale Bot a44a6dc225 refactor(mcp): move the HTTP transport into mcp/http.rs
`HttpTransport`, `HttpTransportMode`, `McpHttpAuth`, and
`mcp_headers_have_authorization` were the last transport still living in the
root `mcp.rs` alongside connection, pool, and config code. They move verbatim
into `crates/tui/src/mcp/http.rs`, joining stdio, SSE, and streamable HTTP in
their own module, so every transport now sits behind the same boundary.

The grouping is the transport's own surface: the Streamable-HTTP-first send
path with its SSE fallback, the GET session preflight, and the header/bearer/
OAuth resolution that only the HTTP-flavoured transports consume. Items are
`pub(super)` rather than `pub` — this is an internal split, and `sse.rs`,
`streamable_http.rs`, and `tests.rs` are the only outside callers.

The root re-exports `HttpTransport` and `McpHttpAuth` so `super::` paths in
sibling transports keep resolving; `tests.rs` now imports them and
`StreamableHttpTransport` explicitly, matching how it already imports the
header helpers. No behavior change.

Implemented with Claude Code agent assistance.
2026-08-10 16:37:45 -07:00
CodeWhale Bot cacb7bc424 refactor(tui): split project_context types into their own module
First move of #4079. `project_context.rs` is 2,892 lines mixing four
concerns; this lifts out the smallest, most self-contained one so the
later moves have somewhere to hang their shared vocabulary.

`project_context/types.rs` now owns the two things every other group
passes around rather than computes: `ProjectContextError` (the read/size/
symlink failure enum) and `ProjectContext` itself, including
`as_system_block()` — the function that decides the constitution block
precedes `<project_instructions>` and that rules trail it. `merge_contexts`
moves with the struct it folds, and its unit test moves with it.

`ProjectContextError` becomes `pub(crate)` because the loader half that
constructs it now lives in a sibling module; nothing else changed
visibility. The root re-exports `ProjectContext` so `crate::project_context::
ProjectContext` keeps resolving for session.rs and project_context_cache.rs.

Pure move: no logic edited, so the assembled system prompt is unchanged.

Implemented with Claude Code agent assistance.
2026-08-10 16:24:11 -07:00
CodeWhale Bot ddc4e231a2 chore(release): drop the unreferenced verify-workspace-version gate
`scripts/release/verify-workspace-version.sh` had zero references anywhere:
no workflow under `.github/workflows/`, no `.cnb.yml` job, no runbook
(`docs/RELEASE_RUNBOOK.md`, `docs/RELEASE_CHECKLIST.md`), no other script,
and no entry in the private ops repo. A repo-wide ripgrep across all file
types (hidden files included, `target/` excluded) matched only the file's
own path.

Its job is fully subsumed by two gates that are wired in:
- `require-release-tag-checkout.sh:9-30` refuses to publish unless HEAD is
  exactly `refs/tags/v<workspace_version>` on a clean tree, which is the
  tag-vs-workspace agreement this script re-derived from `GITHUB_REF`.
- `check-versions.sh` check #1 forbids literal per-crate `version =` values,
  so `cargo metadata` versions cannot diverge from the workspace version in
  the first place.

Proof: `bash scripts/release/check-versions.sh && bash scripts/release/require-release-tag-checkout.test.sh`
Result: pass — "Version state OK: workspace=0.9.6, npm=0.9.6, lockfile in
sync." then "Release checkout gate OK: clean v0.9.6 at 3815bd705." and
"require-release-tag-checkout tests passed" (exit 0).

Implemented with Claude Code agent assistance.
2026-08-10 15:30:09 -07:00
CodeWhale Bot 3815bd7051 feat(tui): open an agent's transcript directly from every agent row
One agent, one destination (#5270/#5287 lane): activating a running or
completed child-agent row from the Work strip, the sidebar, or the
sub-agents view now opens that agent's transcript instead of a details
screen that hides it behind a second action. Agent Details stays reachable
as the secondary action (Alt+V) and is unchanged internally; the stale doc
comment claiming the default route intentionally omits the transcript is
corrected.

Verified: cargo test -p codewhale-tui --lib (10140 passed), --test pty
work_bar (5 passed, including the new
finished_agent_row_opens_its_transcript_and_alt_v_reaches_details),
cargo fmt --check.

Implemented with Claude Code agent assistance.
2026-08-10 14:44:54 -07:00
CodeWhale Bot b9dad36472 feat(telemetry): report observed active installs with trend, freshness, and caveats
Operationalize the owner report around the honest metric: observed active
installs = distinct rotating anonymous install ids with a session_start
ingested on a UTC day. The canonical report:active-installs command prints
the daily series, a complete-days 7-vs-7 trend, event freshness, and an
always-printed coverage-caveat block; report:dau remains as a pure re-export
compat alias. Exclusion guarantees are now pinned by tests (index1 only
inside count(DISTINCT), no content/identity/network columns anywhere in the
report path), and the output is forbidden from claiming DAU/unique users.

Known floor semantics recorded in docs: day attribution is ingest-day (events
carry no per-event timestamps), distinct counts cannot be sampling-corrected,
and id rotation can double-count across a trend window — all framed as the
lower-bound caveats they are.

Verified: telemetry-ingest npm test (109/109), npm run typecheck, CLI smoke
against fixtures.

Implemented with Claude Code agent assistance.
2026-08-10 14:15:04 -07:00
CodeWhale Bot 32d136b35f chore(npm): drop the orphaned codewhale-tui bin alias
The single-binary sweep (be676502d, #5259) removed codewhale-tui from the
wrapper's bin map but left bin/codewhale-tui.js behind, where the files glob
still ships it as an executable npm never links. Nothing references it;
delete it to complete the sweep. Users invoking a previously-linked
codewhale-tui shim already get the deprecation from their installed v0.9.4
wrapper, not from this file.

Implemented with Claude Code agent assistance.
2026-08-10 14:08:32 -07:00
CodeWhale Bot a8af16d3eb chore(plugins): make inactive-capability and OAuth wording version-neutral
Enable/review/OAuth error strings still said "v0.9.1" ("v0.9.1-inactive
capabilities", "the v0.9.1 review limit", "disabled in v0.9.1",
"oauth=disabled-v0.9.1"). The boundary is unchanged in shape since v0.9.1
but the strings read as stale branding once the docs state it as of v0.9.6.
Reworded to version-neutral copy; behavior unchanged.

Verified: cargo test -p codewhale-tui --lib -- plugins oauth (186 passed),
cargo fmt --check.

Implemented with Claude Code agent assistance.
2026-08-10 14:06:56 -07:00
CodeWhale Bot 7abd6edb87 docs(plugins): reconcile the bundle contract with the shipped v0.9.6 boundary
PLUGIN_BUNDLES.md was still written against v0.9.1 while PLUGINS.md
described the v0.9.4 /plugin lifecycle. The bundle doc now states the
boundary as of v0.9.6, documents both manifest encodings the runtime
actually parses (native plugin.json and legacy plugin.toml), describes the
real accept/reject behavior for inactive manifest sections (inventoried,
shown in review, enable fails closed naming them), notes that
capabilities.network_hosts is enforced today, and declares ownership between
the two docs. PLUGINS.md's plugin.toml-only install claim is corrected to
match the installer.

Every behavioral claim verified against crates/tui/src/plugins/ (manifest.rs,
agent_plugin.rs, registry.rs, install/) before restating.

Implemented with Claude Code agent assistance.
2026-08-10 14:04:27 -07:00
CodeWhale Bot a97b66876a fix(tui): give session-picker resume a durable transcript receipt
Resuming from the /resume picker reported only a transient status toast,
which the next footer update replaces — unlike /load, whose receipt lands in
the transcript. The same asymmetry class as the v0.9.6 idle-compaction
receipt fix. The picker path now writes the loaded receipt (with message
count) to the transcript, and the picker route-identity PTY regression pins
it.

Verified: cargo test -p codewhale-tui --test pty
release_session_picker_restores_route_identity; --lib session filter
(493 passed); cargo fmt --check.

Implemented with Claude Code agent assistance.
2026-08-10 13:59:56 -07:00
Ben Younes 4e5ac2ba39 fix(subagents): cap nested max_depth by inherited budget
A descendant subagent could widen the absolute recursion budget inherited
from its root session by passing an explicit max_depth on a nested spawn.
child_max_spawn_depth_for_spawn dropped the inherited budget for the
explicit-request arm, so child_max_spawn_depth_for_spawn(2, 2, Some(8), None)
returned 8 even though the root selected an absolute maximum of 2 — the
descendant could then keep spawning past the intended boundary.

Take the min with the inherited budget in the explicit-request arm, mirroring
the profile-hint arm that already did so. A request or hint may only narrow,
never widen, the root/session's chosen absolute depth. The global
MAX_SPAWN_DEPTH_CEILING added in #3931 stays the outer bound.

Adds a dedicated regression test for the issue scenario and updates the two
assertions in test_child_max_spawn_depth_profile_hint_only_narrows that had
encoded the old widen-up-to-ceiling behavior.

Fixes #5253

Implemented with AI-assisted tooling; authored and reviewed by the contributor.
2026-08-10 20:41:49 +00:00
CodeWhale Bot add98ef1c6 test(gates): add the v0.9.6 runtime continuity suite runner
One command for the provider-neutral invariants the v0.9.6 release repaired:
parent-request compaction pressure, replace-not-stack summaries, the stable
cache prefix, queue-behind-turn manual compaction, durable compaction
receipts, truthful reasoning display, and live shell-wait progress.

Verified: ./scripts/continuity-suite.sh — all three gates PASS.

Implemented with Claude Code agent assistance.
2026-08-10 13:06:08 -07:00
CodeWhale Bot 1b6e77f6f0 test(tui): pin route identity across every interactive resume surface
The v0.9.6 known issue says a resumed session can display the startup
provider/model instead of the restored route. Three new PTY regressions pin
the full contract with a cross-provider, cross-endpoint, cross-model
scenario (env-configured DeepSeek startup route vs a persisted named custom
provider route): persisted metadata, the displayed header identity, and the
outbound request endpoint + body model must agree after resume, and the
startup route must receive nothing after the switch.

Covered surfaces: /load, startup --resume, and the /resume session picker.
All three pass — the interactive drift described in the known issue did not
reproduce against v0.9.6's route-restore path, so these tests hold the line
rather than fix a live defect. Headless exec --resume remains unpinned
(no displayed identity exists there).

Verified: cargo test -p codewhale-tui --test pty restores_route_identity
(3 passed, repeated), cargo fmt --check.

Implemented with Claude Code agent assistance.
2026-08-10 13:06:05 -07:00
CodeWhale Bot 33d4bf7179 fix(tui): queue manual /compact behind a saturated engine mailbox
The v0.9.6 known issue promised that manual /compact during an active turn
queues instead of refusing. The ordinary path already queued; the refusal
the owner reproduced live was the bounded 32-slot op mailbox filling during
a long turn (the turn loop never drains rx_op mid-turn), which mapped
TrySendError::Full to the sticky "engine is busy" error.

A full mailbox now defers the request client-side: the user gets the same
queued receipt as the ordinary behind-a-turn path, the event loop retries
the send each iteration until a slot frees, and a compaction that starts or
settles in the meantime supersedes the deferred request (releasing the
queued flag so /compact cannot latch "already in progress").

The three release-runtime QA compaction scenarios deferred from v0.9.6 were
blocked by a harness artifact, not the runtime: load_session waited for a
needle that line wrap and the scrollbar glyph split across rows. The wait
now normalizes frame chrome, the three tests are re-enabled, and the
full-mailbox liveness regression asserts the queue-behind-pressure contract
plus the single-pass repeat receipt.

Verified: cargo test -p codewhale-tui --lib (10134 passed), --test pty
(72 passed, 4 compaction scenarios green), cargo fmt.

Implemented with Claude Code agent assistance.
2026-08-10 12:39:55 -07:00
Hunter Bown ad37fc85d3 Merge pull request #5315 from Hmbown/release/0.9.6
chore(release): ship v0.9.6
2026-08-10 03:52:18 -07:00
CodeWhale Bot 59882c9f27 chore(gates): record the post-fix source budget for 0.9.6
Advance the aggregate owned-Rust ceiling from 684375 to the measured
685062 lines after the post-review fix set (rail/todo visibility, ⌥V
row details, telemetry notice rewording, screencapture stabilization,
fleet read-only web parity, and their regression tests). Module and
large-module ceilings are unchanged; the structure gate passes at the
recorded values.
2026-08-10 03:16:53 -07:00
CodeWhale Bot f3a3963350 docs(release): record the 0.9.6 post-review fixes and the compaction deferral
Adds changelog entries for the owner-dogfood fix set — the rail keeping
the to-do list visible beside the agent register, the ⌥V details chord
honoring the selected work row, the reworded first-run telemetry
question across all locales, macOS screencapture stabilization, and
read-only/recon Fleet members keeping Web search/fetch — and documents
the owner-approved deferral of the /compact busy-queue behavior to
v0.9.7 under Known issues. Regenerates crates/tui/CHANGELOG.md.
2026-08-10 02:47:49 -07:00
CodeWhale Bot 92cd2b2317 test(fleet): align the ceiling deny-list assertion with the Web read-only exception
The read-only/recon web parity change keeps the `Web` family name
reachable (so search/fetch survive) while every reaching spelling stays
denied. Update the workflow ceiling assertion to match: `Web` must NOT
appear in disallowed_tools; web.run/web_search/fetch_url/
wait_for_dev_server/mcp* must.

Companion to feat(fleet): give read-only/recon members read-only web
search; landed from the web-access builder's worktree (wall-time budget
elapsed before it could commit this test update).
2026-08-10 02:45:36 -07:00
CodeWhale Bot 9bd112e6c2 feat(fleet): give read-only/recon members read-only web search
Fleet-dispatched members under a `network_tool = false` ceiling lost the
`Web` family entirely: the ceiling deny list matched the family name via
the `web*` glob and the `Web` entry, and even an exact-name list would
have blocked `search`/`fetch` through their legacy aliases (`web_search`,
`fetch_url`). An ordinary `agent`-tool scout keeps `Web {search, fetch}`;
a Fleet recon member got nothing, a parity gap.

Fix, in two halves that must stay together:

- `NETWORK_TOOL_DENYLIST` no longer denies the `Web` family name. The
  two narrow globs `web_*` / `web.*` replace the `web*` glob so every
  other browsing spelling (`web.run`, `web_run`, `web_search`,
  `web.fetch`, `web_fetch`, `fetch_url`, `wait_for_dev_server`) is still
  denied by prefix or exact name, while `Web` itself survives. The
  sentinel (`fetch_url`) is untouched, so the capability envelope's
  network bit and `network_is_denied()` read exactly as before.
- `SubAgentToolRegistry::is_action_allowed` carves out `Web {search,
  fetch}` for network-denied children past the denied aliases. The
  family name denial still wins outright, `wait` stays denied through
  `wait_for_dev_server`, and `reject_network_reaching_input` still
  refuses a URL-addressed `fetch` at dispatch — the carve-out grants the
  read-only shape, not the reach.

`full`/network ceilings are unchanged (empty deny list, whole `Web`
enum + `web.run`). Everything else a network denial seals — `web.run`,
`fetch_url`, `github`, `mcp*`, `rlm_open`/`rlm_eval` — stays sealed, and
the URL-input guard remains deny-closed for any tool we did not
explicitly allow.

Tests: extend the exact-fleet registry tests to the new contract (Web
visible as exactly search/fetch, reaching spellings denied, URL-addressed
fetch refused at dispatch, full member untouched) and add a dedicated
recon-member test; add a deny-list test covering every network-denied
preset; update the posture-sealing test for the `web_*` glob. FLEET.md
documents the read-only exception.

Security note: nothing new is granted beyond the Web family's
search/fetch actions for network-denied members; every destructive or
reaching surface stays denied, and the sentinel-backed envelope plus
URL-input guard remain the fail-closed backstop for unenumerated tools.
2026-08-10 02:45:23 -07:00
CodeWhale Bot a51eaab55c fix(tui): stabilize macOS screencapture screenshots the moment they arrive
macOS parks capture-UI screenshots under /var/folders/…/Temporary
Items/NSIRD_screencaptureui_*/ and deletes them minutes later, so a
screenshot referenced in a message was gone by the time the agent read
it (owner hit this repeatedly during the 0.9.6 dogfood).

Inbound message text now passes through
stabilize_screenshot_references at the single queued-message choke
point: any reference (quoted paste, @-mention, [Attached …], or bare
whitespace-split paste) to an existing file under a screencapture
Temporary Items dir is copied once to ~/.codewhale/attachments and the
reference is rewritten to the stable path. Detection requires both a
"Temporary Items" component and a screencaptureui-named component, so
ordinary paths are never touched; copies are idempotent and a failed
copy silently keeps the original reference.

Landed from the screenshot-fixer builder's worktree (its wall-time
budget elapsed before it could commit); verified with its seven
included unit tests (spaces, unicode, idempotence, fail-safe).
2026-08-10 02:45:05 -07:00
CodeWhale Bot 21a30e4715 Rework telemetry notice into a clear opt-in/opt-out question
The first-run modal previously read like a statement ("Anonymous usage
counting") with two passive choices. Turn it into an explicit question
("Help improve Codewhale?") whose choices are unambiguous: "Yes, keep
anonymous counts" / "No, turn off tracking".

- notice.rs: headline + body rewritten; keeps every factual claim
  (what is counted, what is never collected, random on-machine ID
  replaced every 90 days, persistent opt-out command) and the schema
  link. telemetry/tests.rs assertions updated to match the new wording
  while still asserting each real fact.
- en.json + all 14 shipped locale packs: native translations for the
  headline, body, both choices, and the two keep-on receipts (the old
  "Keep-on" noun no longer matches any visible label). Compact body and
  disabled receipts intentionally unchanged.
- telemetry_notice.rs: render tests assert the new question and choice
  labels; the verbatim schema-owned test now also pins NOTICE_HEADLINE.
- qa_pty.rs: PTY harness waits on the new headline and choice labels.

Consent semantics are untouched: telemetry stays unarmed until a choice,
and the disable path still deletes the random ID.
2026-08-10 02:28:48 -07:00
CodeWhale Bot 31044240c1 test(tui): defer the compaction busy-queue QA scenarios to 0.9.7
The three release-runtime compaction scenarios fail against a real bug:
manual /compact during an active turn is refused with "engine is busy"
instead of queueing behind the turn (owner reproduced it live on
2026-08-10). The fix is non-trivial — a full builder attempt exhausted
its budget without landing it — and the owner approved deferring it so
0.9.6 can ship with the fixes that are ready.

Mark the three tests #[ignore] with the tracking reason rather than
weakening or deleting them; they encode the exact required lifecycle
(stream/compact/stream ordering, labels past toast expiry, successor
summary). Re-enable with the 0.9.7 fix. Full context in the private
codewhale-ops v0.9.7 ledger (P0 — compaction busy-queue).
2026-08-10 02:24:27 -07:00
CodeWhale Bot 86f43d0f78 fix(tui): keep the to-do list visible with the agent register; honor ⌥V on the selected rail row
Two owner dogfood regressions from the 0.9.6 rail work:

- Clicking the "Subagents N" header switched to the Agents panel, which
  projected sub-agent rows only — the to-do list disappeared with no way
  back. The Agents panel now keeps the durable to-do checklist under its
  own Tasks heading alongside the full register (same rule as Pinned: a
  panel preference is not consent to lose durable work), and the register
  header is a two-way door: clicking it inside the Agents panel returns to
  Tasks.

- ⌥V advertised row details but always opened the transcript tool-details
  pager, so a selected checklist row showed the latest reasoning cell
  instead of its own content. The focused rail now answers the details
  chord with the selected row's primary action; the transcript pager keeps
  the chord when no work row is selected.

Covered by three new work-surface regression tests (both visible, two-way
door, ⌥V row details); full TUI library suite green (10,123 passed).

Found and verified during the owner's 0.9.6 release dogfood.
2026-08-10 02:24:13 -07:00
CodeWhale Bot e81ed33fcc fix(release): finish the v0.9.6 candidate
Move first-run usage disclosure into the native TUI, keep telemetry unarmed until the decision, and order all telemetry writes and delivery against persistent opt-out with fail-closed setup-state handling.

Replace the narrow wide-terminal rail with a responsive full-screen ocean canvas, preserve readable prose measure, and remove per-call padding inside grouped tool activity.

Verified with formatting, clippy, locale/version/budget gates, the 10,120-test TUI library suite, telemetry/config suites, focused UI/PTy coverage, and independent release/UI review. The sandbox-only loopback acceptance rerun remains explicitly environmental.

Agent assistance: Claude and Codex were used for implementation analysis and verification.
2026-08-09 22:41:53 -07:00
CodeWhale Bot 6ef4467329 fix(cli): simplify public help copy 2026-08-09 19:46:21 -07:00
CodeWhale Bot 291599707b test(tui): accept the centered session rail 2026-08-09 19:32:44 -07:00
CodeWhale Bot 12d4f7b845 chore(gates): record the reconciled 0.9.6 source budget
Advance the explicit maximum-module and aggregate Rust ceilings to the measured release-candidate tree after the v0.9.6 lane reconciliation. The structure gate remains one-way and passes at the recorded values.
2026-08-09 19:11:08 -07:00
CodeWhale Bot c76f2d70bf docs(release): finish the 0.9.6 public surface
Replace duplicated implementation-heavy notes with a complete user-facing release ledger, sync the embedded changelog, and align compaction math documentation with the shipped trigger.\n\nExpose the existing Skills and Plugins guides in the website documentation index while retaining v0.9.5 as the latest published release until 0.9.6 is public.
2026-08-09 19:11:00 -07:00
CodeWhale Bot db1a3d6f25 feat(tui): center wide sessions on a readable rail
Use one shared 112-column shell for the header, transcript, work strip, composer, and footer while leaving compact terminal geometry unchanged.\n\nVerified with focused layout regressions, a debug build, and compact, wide, and ultrawide VHS captures.
2026-08-09 19:10:41 -07:00
CodeWhale Bot af3ba0e1d1 merge: reconcile remaining v0.9.6 worktree histories
Tree-preserving reconciliation: account, reasoning, and web lanes are patch-equivalent to release commits; the core request lane is superseded by 16b597dd2 on the tested release line.
2026-08-09 17:48:52 -07:00
CodeWhale Bot 7b16ca61d7 merge: reconcile v0.9.6 release lane 2026-08-09 17:34:46 -07:00
CodeWhale Bot 499c2fbb61 test(cli): assert default-on telemetry help 2026-08-09 17:17:34 -07:00
CodeWhale Bot 8118e4ce92 test(tui): isolate PTY usage counting 2026-08-09 17:12:13 -07:00
CodeWhale Bot eef5be4e29 fix(telemetry): keep diagnostics state-free
Do not arm anonymous usage counting for doctor, session-diagnostics, or setup --status. These read-only commands must not create CODEWHALE_HOME as a side effect.\n\nVerified with the telemetry surface unit regression and all 13 read-only diagnostic process tests.
2026-08-09 16:58:19 -07:00
CodeWhale Bot cedcccc337 test(tui): stabilize shared release gates
Serialize the prompt-host environment test and box the large runtime API future so parallel full-suite execution does not race process environment or exhaust the test-thread stack.\n\nVerified with both focused regressions.
2026-08-09 16:40:59 -07:00
CodeWhale Bot 9055f8e9cf fix(tui): avoid capturing launch git output
Probe worktree support via exit status with stdout and stderr discarded. The launch path only needs success or failure and should not retain command output.
2026-08-09 16:40:52 -07:00
CodeWhale Bot 46aec12a1b fix(tui): keep wire placeholders out of transcripts
Persist only reasoning emitted by the provider. Keep route-specific compatibility placeholders in request serialization and hide placeholders already present in restored sessions.\n\nVerified with focused engine, history, Anthropic, and chat replay regressions.
2026-08-09 16:40:40 -07:00
CodeWhale Bot 687dfde086 feat(telemetry): add observed DAU report 2026-08-09 16:30:54 -07:00
CodeWhale Bot 5b738bc65c feat(telemetry): make anonymous usage counting opt-out 2026-08-09 16:24:17 -07:00
CodeWhale Bot cb69741b66 fix(remote-control): make account relay crash-safe 2026-08-09 15:58:17 -07:00
CodeWhale Bot 73c4b81538 feat(remote-env): disclose source handoff boundary 2026-08-09 15:58:12 -07:00
CodeWhale Bot 011f1f2726 fix(remote-control): make account relay crash-safe 2026-08-09 15:58:04 -07:00
CodeWhale Bot 6444630ce7 feat(remote-env): disclose source handoff boundary 2026-08-09 15:57:52 -07:00
CodeWhale Bot 616af71c90 fix(model-calls): inherit route policy across internal consumers 2026-08-09 15:52:41 -07:00
CodeWhale Bot d25a8ba0da Fix reasoning affordance ownership
Bind the per-cell Space hint to the exact cached transcript action owner, with a destructive identity epoch so same-index replacements cannot inherit stale actions. Derive fold actionability from the rendered reasoning analysis and preserve truthful narrow-width, copy, and localized behavior.\n\nKeep mouse selection, viewport retargeting, filtering, streaming, interruption, restore, and transcript lifecycle mutations aligned with that rendered owner. Add cache, lifecycle, interaction, and terminal-width regressions for issue #5291.
2026-08-09 15:31:19 -07:00
CodeWhale Bot 16b597dd26 refactor(core): own primary request preparation
Move the production MessageRequest DTO closure into codewhale-core while preserving the historical TUI path through compatibility re-exports.

Route both the streaming turn loop and read-only preview through one pure primary-turn constructor, and prove its prepared body matches bytes sent by the production transport.

Provider-specific dialect shaping and HTTP transport remain in the TUI for the next extraction slice.

Agent assistance: implemented and independently reviewed with CodeWhale sub-agents.

Refs #5261
2026-08-09 15:31:15 -07:00
CodeWhale Bot 26cb5196ca fix(model-calls): use resolved route budgets for internal tasks 2026-08-09 15:27:18 -07:00
CodeWhale Bot 8fd0cd1e64 fix(compaction): isolate live context pressure from billing 2026-08-09 14:27:08 -07:00
CodeWhale Bot e9bce3db9d fix(web): ground FAQ and getting-started copy in the 0.9.5/0.9.6 runtime contract
- FAQ: replace the two-binary codewhale/codewhale-tui answer with the
  one-runtime contract — codew is a byte-identical short name, the updater
  refreshes legacy codewhale-tui paths from the same bytes, and the tui
  crate now compiles into codewhale-cli; drop the separate codewhale-tui
  cargo install line (EN+ZH).
- FAQ: add Mistral AI to the provider sample list (EN+ZH).
- FAQ: codewhale doctor prints its report to stdout — the claimed
  ~/.codewhale/doctor.log file does not exist (EN+ZH).
- FAQ: drop the audit.log sentence; no runtime writer for it exists (EN+ZH).
- FAQ: OpenRouter setup uses the documented route form — --provider
  openrouter plus OpenRouter's own model slugs; no provider/model prefix
  parsing exists (EN+ZH).
- getting-started: step 4 becomes 'Set up your ideal fleet' — add every
  provider, then /fleet setup, the documented authoring wizard; drop
  codewhale fleet init, which only prints the ledger path.
- fleet docs: drop codewhale fleet init from the CLI verb block.
- home dictionaries + guide: align the four-step lede with the new step 4
  (EN+ZH).
2026-08-09 13:58:32 -07:00
CodeWhale Bot c7dcf8bb51 fix(compaction): visible lifecycle receipts, replace-not-stack summaries, window-percent trigger
The v0.9.6 release blocker: /compact ran and committed engine-side, but
every lifecycle state was toast-only and the engine's turn-complete status
landed in the same UI drain batch, replacing the completion toast before a
frame was drawn — a successful compaction looked like a no-op. Outcomes
(completed/failed/queued/duplicate/full-mailbox) now land in the transcript;
a terminal event with no tracked start no longer wedges is_compacting.

Auto-compaction retriggered nearly every turn because each pass appended its
summary to the successor system prompt while keeping the previous ones: the
stable prefix grew by a full summary per pass and pressure re-latched. The
prior committed summary is now injected into the summarization request as a
coalescing bridge and the commit replaces the old block. The compact prompt
tells the model to summarize the task, not the checkpoint machinery.

The trigger percentage now means percent of the context window (matching the
meter), clamped to the spendable input ceiling:
  trigger = min(window x percent, window - output reservation - headroom)
Pressure is measured with the uninflated estimate or the provider-billed
prompt tokens of the current turn, whichever is higher; the 1.5x-inflated
estimator stays for overflow protection only. Previously 80% on a 1M window
with a 262K output reservation fired near 30% of real usage.

The summary request no longer hard-codes temperature 0.3; like ordinary
turns it sends no sampling params, so routes with fixed-sampling contracts
(Kimi Code membership) stop rejecting the compaction pass.

Regressions: repeated-compaction replacement (engine), billed-pressure and
window-percent trigger math (unit), and a real-TUI PTY test proving the idle
/compact outcome survives as a transcript receipt and a second /compact
carries the coalescing bridge.

Verified: cargo test -p codewhale-tui --lib (10035 passed), the four
compaction PTY tests, cargo fmt --check, cargo clippy --workspace
--all-targets -D warnings. Full workspace gate and rebuilt-binary dogfood
run alongside this commit.

Implemented with agent assistance (Claude) under founder direction.
2026-08-09 13:49:16 -07:00
CodeWhale Bot 48c4122820 fix(web): ground FAQ and getting-started copy in the 0.9.5/0.9.6 runtime contract
- FAQ: replace the two-binary codewhale/codewhale-tui answer with the
  one-runtime contract — codew is a byte-identical short name, the updater
  refreshes legacy codewhale-tui paths from the same bytes, and the tui
  crate now compiles into codewhale-cli; drop the separate codewhale-tui
  cargo install line (EN+ZH).
- FAQ: add Mistral AI to the provider sample list (EN+ZH).
- FAQ: codewhale doctor prints its report to stdout — the claimed
  ~/.codewhale/doctor.log file does not exist (EN+ZH).
- FAQ: drop the audit.log sentence; no runtime writer for it exists (EN+ZH).
- FAQ: OpenRouter setup uses the documented route form — --provider
  openrouter plus OpenRouter's own model slugs; no provider/model prefix
  parsing exists (EN+ZH).
- getting-started: step 4 becomes 'Set up your ideal fleet' — add every
  provider, then /fleet setup, the documented authoring wizard; drop
  codewhale fleet init, which only prints the ledger path.
- fleet docs: drop codewhale fleet init from the CLI verb block.
- home dictionaries + guide: align the four-step lede with the new step 4
  (EN+ZH).
2026-08-09 13:08:54 -07:00
Hunter Bown 3c84e7609d Merge pull request #5313 from Hmbown/fix/v096-shell-wait-progress
chore(release): prepare v0.9.6
2026-08-09 08:53:17 -07:00
CodeWhale Bot a11c6f3b91 fix(shell): preserve background result compatibility
Keep the established Background task started prefix while retaining explicit session-lifetime and persist guidance for services.
2026-08-09 07:54:46 -07:00
CodeWhale Bot 1369099e9a fix(ci): keep service guidance budget-neutral
Compress the temporary-background versus persistent-service distinction into the original schema byte budget, retaining both required flags and the session-exit warning without increasing any runtime-contract metric.
2026-08-09 07:49:15 -07:00
CodeWhale Bot 0a820e1506 fix(shell): distinguish services from background work
Make the Bash schema and managed-background result explicit that ordinary background jobs are terminated at session exit, and point services that must survive a successful headless exec to the existing persist:true ownership-transfer path. Keep regression assertions within the release source budget.
2026-08-09 07:37:02 -07:00
CodeWhale Bot 5f8df4b5e0 fix(ci): keep Unix gating within source budget
Preserve immutable terminal state on non-Unix builds and shadow it mutably only for the Unix transfer path. Tighten adjacent whitespace so the release correction remains within the no-growth source-structure ceiling.
2026-08-09 07:00:24 -07:00
CodeWhale Bot ceb45781be fix(tui): gate persistent services on Unix
The v0.9.6 persistent-service transfer is Unix-only, but its stream variant, mutable terminal state, and process-id helper were still compiled on Windows. Gate those pieces consistently and keep the Windows-only shell test fixture aligned with the new ownership field.
2026-08-09 06:43:31 -07:00
CodeWhale Bot be5c8f8bfd chore(release): prepare 0.9.6
Bump the workspace, every published crate, the npm CLI package and its
codewhaleBinaryVersion, the runtime SDK, and the VS Code extension to 0.9.6 —
the four version sources the release workflow cross-checks before it will
tag.

The changelog entry describes v0.9.6 as what it is: a subtractive release.
The guards that interrupted live work, the per-mode prompt doctrine, and the
deterministic second compaction system are gone; a truncated provider
response can no longer be recorded as a finished answer. Most of these were
found by running v0.9.5 against Terminal-Bench 2.1 beside Pi 0.8.41 on the
same model, effort, endpoint, and task digests, then reading the trials
Codewhale lost — so the entry names those trials rather than describing the
fixes in the abstract.

Mistral AI ships in this release; Xavier Pestel (@xavierpestel-ai) is
credited in the contributors section for #5295.
2026-08-09 01:02:51 -07:00
CodeWhale Bot 5798de8b39 build(release): native musl for Linux ARM64, and reconciled copy
Three v0.9.5 benchmark tasks could not launch Codewhale at all:
mteb-leaderboard, mteb-retrieve, and pytorch-model-recovery run older ARM64
images, and the aarch64-unknown-linux-gnu artifact built on ubuntu-24.04-arm
requires GLIBC_2.39.

Release and nightly now build aarch64-unknown-linux-musl on the native ARM
runner, alongside the x64 musl build that has shipped since v0.8.65. Both
gain a static check (no ELF INTERP) plus a launch smoke on the matching
native runner, so a dynamically linked or non-starting binary fails the
build rather than the user's install. docs/INSTALL.md drops the arm64 glibc
floor section and describes the v0.9.6 matrix.

Also reconciles the copy the earlier commits invalidated: setup ratification
text and all 15 locales stop attributing execution doctrine to "mode
prompts", docs/MODES.md states Auto-Review's actual deterministic
allow/deny behavior, and the source-structure, dead-code, and
runtime-contract budgets are re-measured — the last locking down 25
decreased ceilings and the new mode-agnostic prompt-stage digests.
2026-08-09 00:52:42 -07:00
CodeWhale Bot f993077d54 fix(providers): a truncated response is a failure, not an answer
The turn loop read usage from MessageDelta and discarded its stop reason.
Three v0.9.5 benchmark trials spent their whole ~65k output allowance on
reasoning, emitted no answer, and were recorded status=completed,
termination_reason=resolved with reward 0.

The stop reason is now retained end to end. The Responses adapter preserves
the provider's incomplete_details reason instead of flattening it to
max_tokens, so an unknown future reason cannot be mistaken for a finished
answer. On an incomplete stop the runtime charges the billed usage, keeps
the visible fragment as interrupted rather than recording it as a completed
assistant message, closes every opened tool lifecycle without executing the
call, and fails the turn with the provider's own reason. The same rule
covers the RLM root and bridge, and sub-agents — whose consecutive-truncation
retry counter is gone, since retrying a truncated response was never the
recovery it looked like.

Step-budget exhaustion gets its own ErrorCategory::Budget so it reduces to
BudgetExhausted instead of an untyped failure, and a terminal error category
now outranks historical tool/approval evidence when classifying a run.
2026-08-09 00:52:42 -07:00
CodeWhale Bot 63a5ba8c47 feat(shell): blocking wait by default, and explicit service ownership
Two shell contracts the v0.9.5 benchmark lane showed were wrong.

`action=wait` computed blocking from a separate `wait` boolean that
defaulted to false, so `{"action":"wait","task_id":...,"timeout_ms":600000}`
returned immediately and ignored the timeout — the action named the
intent and the schema contradicted it. wait now blocks by default; pass
wait=false for a nonblocking snapshot. task_shell_wait keeps its documented
nonblocking default, including when wait/block arrive as null.

Background processes were killed on manager drop, so a service the model
started and verified died when the headless exec that started it exited
successfully: kv-store-grpc built its server, confirmed port 5328, and the
external verifier then got connection refused. `persist:true` (Unix, real
headless exec, explicit danger-full-access, background only) stages a
service with null stdio in its own process group; a successful exec
transfers ownership and emits a receipt. Failure, cancellation, signal, and
engine-channel EOF kill it and exit nonzero. Ordinary background jobs keep
kill-on-drop.
2026-08-09 00:52:41 -07:00
CodeWhale Bot 3eeda34cf0 refactor(prompts): one universal prompt, no mode doctrine
Plan, Agent, and Operate each shipped a mode-delta prompt asserting its own
worldview — Agent mandating todo_write before any three-step task, Operate
carrying eight numbered doctrine clauses about dispatch and fan-in, Plan
restating the read-only rules the runtime already enforces. Composition
prepended the delta to the constitution, so a mode change rewrote the stable
prefix and the model read a paragraph about its mode before the rules that
mode modulates.

Modes differ in permissions and available tools. Runtime policy and the live
tool catalog already express both, concretely, per turn. Prompt prose
describing the same thing is a second source of truth that can only drift.

All three modes now compose the same prompt. Headless hosts get a compact
constitution stating the cross-cutting contract the runtime cannot express:
assist someone, begin from possibility, invent no urgency, tools and
workspace are how you see, failure is information, honor intent and active
authority, check before concluding and never call unverified work complete.
Interactive hosts keep the full base. Explicit embedder and base-prompt
overrides still take precedence over both.

Extracts the pr-prompt and telemetry-surface tests to crates/tui/src/tests/
to stay under the source-structure budget.
2026-08-09 00:52:41 -07:00
CodeWhale Bot b39cf56505 refactor(engine): delete the stuck, read-repeat, and coaching guards
Three systems watched the model and intervened when its behavior matched a
pattern:

- stuck_guard fingerprinted steps by tool name and arguments and warned,
  then stopped, after enough consecutive matches. Because the fingerprint
  had no result digest, a `Bash action=wait` on a live job looked identical
  every poll. The v0.9.5 benchmark lane shows it killing active work:
  filter-js-from-html was stopped while the task it was waiting on went on
  to pass 2/2, and llm-inference-batching-scheduler and mcmc-sampling-stan
  were stopped mid-optimizer and mid-compile.
- read_repeat_guard coalesced same-batch duplicate reads onto one execution
  and, from the fifth occurrence, replaced the result with a receipt
  pointing at a prior tool_use_id. A model that asks to read a file twice
  gets to read it twice; a synthetic receipt in place of the content it
  asked for is a worse answer than the content.
- Tool errors were rewritten to append fallback strategy ("after one retry,
  switch to a direct URL path...") and a degradation hint fired after two
  consecutive error steps. The model can read a raw error.

Errors now return as the tool produced them. max_steps, declared tool
budgets, and cancellation remain the real limits.
2026-08-09 00:52:40 -07:00
CodeWhale Bot 789a7bf1ff test: pin step-budget, cancellation accounting, and service ownership
Three gaps the v0.9.6 work could regress silently:

- max_steps exhaustion must be Failed/BudgetExhausted and must never
  release a pending persistent service. Writing this surfaced a real bug:
  a goal continuation injected on the last step relabeled an already
  delivered answer as a step-budget failure, so exhaustion is terminal
  only when the model still owes work.
- Cancellation arriving after the provider reported terminal usage must
  still charge the turn.
- Real-process persistent services: a successful headless exec releases the
  explicitly persisted service and it outlives the exec; a failed exec and
  a terminating signal both kill the pending service and exit nonzero.
  These drive the actual binary against a wiremock provider with real
  child processes, not a mocked manager.
2026-08-09 00:52:40 -07:00
CodeWhale Bot f2febee96b fix(providers): reject incomplete responses in every model consumer
The turn loop, one-shot exec, RLM, and sub-agents already refuse a
provider-declared incomplete response. The remaining direct consumers still
parsed whatever text arrived and reported success:

- compaction committed a truncated summary as the session's history
- the review and verify tools returned a partial critique as a verdict
- the MCP thread handler stored a fragment and answered with it
- purge could execute a complete-looking purge_context call from a
  truncated response
- the advisor, auto-route classifier, fleet router, and both setup drafts
  parsed fragments
- `codewhale review` wrote a receipt and printed success:true

Each now checks is_incomplete_stop_reason before parsing, persisting,
executing, or reporting success, after the billed usage has been recorded.
Truncation is a failure with the provider's own stop reason in the message,
not a smaller answer.

Extracts the exec-exit-semantics tests to crates/tui/src/tests/ to stay
under the source-structure budget.
2026-08-09 00:52:39 -07:00
CodeWhale Bot 1536ed852f fix(goal): stop bounding the user's goal from inside the runtime
Three restrictions treated an active goal as something to be contained
rather than pursued:

- MAX_GOAL_CONTINUATIONS_PER_TURN capped continuation passes at 3 per turn
  and ended the turn with a "runaway loop" status. max_steps already bounds
  a turn; this was a second, quieter ceiling on the same thing.
- Three identical critical verifier gap sets auto-paused the goal for
  "no progress". Repeated gaps are information the model should act on,
  not grounds for the runtime to stop the user's work.
- The continuation prompt told the model to stop at any unanswered
  question and report itself blocked.

Codex's goals/continuation.md takes the opposite posture: the goal persists
across turns, the objective stays whole, and ending a turn does not require
shrinking success to what fits now. Match that. The cross-turn circuit
breaker ([goal] max_continuations, default unlimited) and terminal
complete/blocked status remain the ways a goal run ends.

todo_write loses its upkeep coaching for the same reason — the list is
optional support for the user's view, and instructions to "keep it live"
and "never batch completions" bought list management instead of work.
2026-08-09 00:52:38 -07:00
CodeWhale Bot de30396e7c refactor(compaction): rebuild on the Codex model
Codewhale's compactor had grown a second, deterministic compaction system
alongside the model summary: a continuation-contract extractor with decision
and evidence marker lists, a workflow-context scanner, an anchors file
reader, a path regex plus working-set derivation feeding pin planning, a
fixpoint tool-call-pair enforcer, and a three-rung summary-input ladder with
a degenerate-output resampler. Every layer was a guess about what the next
agent would need, and each one could drop or mangle the thing it was trying
to save.

Replace it with what Codex does (codex-rs/core/src/compact.rs):

- One summary request that IS the live conversation plus a final user
  message asking for a handoff summary, so the provider's prefix cache
  covers everything already sent.
- A committed summary block introduced by Codex's summary_prefix text.
- A replacement history of the recent plain user messages, newest-first
  within a 20k-token budget, restored to transcript order.
- On context-window overflow, drop the oldest history item and retry
  (Codex's history.remove_first_item), instead of re-rendering the input
  at three progressively lossier rungs.

Kept because they are Codewhale contracts, not ceremony: mechanical
tool-result pruning before paying for a summary, retained-message
sanitization, the conservative reclaimability guard that stops
auto-compaction from firing on every tool step, the exact captured
successor reanchor, and re-stating the user's /anchor file after the
summary (the /anchor command promises those facts survive compaction).

extract_compaction_summary_prompt now recognizes both the new marker and
the pre-0.9.6 one, so sessions saved under the old format still restore
their committed summary on reload.
2026-08-09 00:42:16 -07:00
Hunter Bown 7aa5c6bac0 Merge pull request #5308 from Hmbown/fix/cnb-release-download-url
fix(release): use CNB asset download URLs
2026-08-08 21:58:06 -07:00
CodeWhale Bot 11f6c99bec test(cnb): make failure fixtures root-safe
Signed-off-by: CodeWhale Bot <bot@codewhale.net>
2026-08-08 20:53:48 -07:00
CodeWhale Bot 5162341fd1 ci(cnb): match workspace test stack
Signed-off-by: CodeWhale Bot <bot@codewhale.net>
2026-08-08 19:59:20 -07:00
CodeWhale Bot b899c42be9 ci(cnb): bound Rust gate memory
Signed-off-by: CodeWhale Bot <bot@codewhale.net>
2026-08-08 19:18:47 -07:00
Hunter Bown fde3cb0ff0 Merge pull request #5306 from Hmbown/codex/5298-crate-publish-order
fix(release): validate crate publication order
2026-08-08 19:01:29 -07:00
CodeWhale Bot cb1e994829 fix(release): use CNB asset download URLs
Signed-off-by: CodeWhale Bot <bot@codewhale.net>
2026-08-08 18:26:32 -07:00
CodeWhale Bot 5763000c92 test(release): cover invalid crate inventories
Signed-off-by: CodeWhale Bot <bot@codewhale.net>
2026-08-08 18:06:07 -07:00
CodeWhale Bot 4a6933754e fix(release): validate crate publication order
Signed-off-by: CodeWhale Bot <bot@codewhale.net>
2026-08-08 17:58:32 -07:00
Hunter Bown f864ca67f7 Merge pull request #5295 from xavierpestel-ai/codex/add-mistral-provider
feat: add Mistral AI as a first-class provider route
2026-08-08 17:57:51 -07:00
CodeWhale Bot 9bac910fc6 fix(web): align Mistral provider count
Keep the checked source-candidate contract in sync with the generated 41-provider registry so the full web suite validates the new first-class route.

Signed-off-by: CodeWhale Bot <bot@codewhale.net>
2026-08-08 17:27:16 -07:00
CodeWhale Bot f11657e77f fix(mistral): isolate the reasoning wire contract
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>
2026-08-08 17:23:14 -07:00
Xavier Pestel f157d34a51 feat: add Mistral AI as a first-class provider route
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.
2026-08-09 02:00:32 +02:00
Hunter Bown aa47e142c9 Merge pull request #5301 from Hmbown/codex/v096-compact-hotfix
fix(tui): make compaction live and pressure-aware
2026-08-08 16:39:23 -07:00
CodeWhale Bot f6beb4464a fix(tui): make compaction live and pressure-aware 2026-08-08 16:01:30 -07:00
CodeWhale Bot 49fb811ad1 Fix reasoning affordance ownership
Bind the per-cell Space hint to the exact cached transcript action owner, with a destructive identity epoch so same-index replacements cannot inherit stale actions. Derive fold actionability from the rendered reasoning analysis and preserve truthful narrow-width, copy, and localized behavior.\n\nKeep mouse selection, viewport retargeting, filtering, streaming, interruption, restore, and transcript lifecycle mutations aligned with that rendered owner. Add cache, lifecycle, interaction, and terminal-width regressions for issue #5291.
2026-08-08 13:43:06 -07:00
CodeWhale Bot eef8542e0c refactor(core): own primary request preparation
Move the production MessageRequest DTO closure into codewhale-core while preserving the historical TUI path through compatibility re-exports.

Route both the streaming turn loop and read-only preview through one pure primary-turn constructor, and prove its prepared body matches bytes sent by the production transport.

Provider-specific dialect shaping and HTTP transport remain in the TUI for the next extraction slice.

Agent assistance: implemented and independently reviewed with CodeWhale sub-agents.

Refs #5261
2026-08-08 10:54:36 -07:00
Hunter Bown 91bca01a9d Merge pull request #5297 from Hmbown/codex/v095-postrelease-facts
docs(web): publish the v0.9.5 release snapshot
2026-08-08 10:27:36 -07:00
CodeWhale Bot 519a0d6162 docs(web): publish the v0.9.5 release snapshot
Advance the separately modeled public-release record only after the immutable GitHub release and 34-asset gate are live. Regenerate the checked-in web facts so install pages and deployed receipts resolve v0.9.5 instead of the prior release.
2026-08-08 09:50:47 -07:00
Hunter Bown 853cb707bb Merge pull request #5296 from Hmbown/codex/v095-telemetry-parallel-fix
Sync to CNB / sync (push) Has been cancelled
Release / resolve (push) Has been cancelled
Release / parity (push) Has been cancelled
Release / artifacts (push) Has been cancelled
Release / docker (push) Has been cancelled
Release / release (push) Has been cancelled
Release / homebrew (push) Has been cancelled
test(telemetry): serialize process fixtures
2026-08-08 07:34:52 -07:00
CodeWhale Bot 7ef5cd3846 test(telemetry): serialize process fixtures
The consolidated integration target was launching every telemetry contract child at once. On the exact-main Ubuntu runner, two real-turn children produced no useful process or loopback evidence under that load even though both tests pass alone and under local stress.

Hold one module-local permit for each fixture lifetime because these tests cover isolated telemetry behavior, not launch concurrency. Also require successful child status before the existing model, batch, and privacy assertions so future infrastructure failures report the exit status and captured streams instead of masquerading as telemetry regressions.

Verified with both tests alone, the 15-test telemetry subset, four concurrent subset binaries, the full 263-test integration target, strict all-target/all-feature TUI Clippy, source/dead-code budgets, formatting, and diff checks.
2026-08-08 07:03:44 -07:00
Hunter Bown a79014025c Merge pull request #5294 from Hmbown/codex/v095-telemetry-optout-fix
fix(telemetry): flush only at shutdown
2026-08-08 06:40:29 -07:00
CodeWhale Bot 8f01171a37 fix(telemetry): flush only at shutdown
Remove the impossible startup-recovery path: arming deliberately truncates every pre-consent buffer, so the startup predicate could only race with events queued by the current process and send them before a mid-session opt-out.

Make the single shutdown flush structural by removing the non-final actor message and public flush API. Strengthen the process-level contract to prove an armed session sends nothing before shutdown, while preserving the shutdown consent re-check and bounded deadline.

Verified with the telemetry unit suite, five focused process-level race runs, the full 263-test integration target, strict workspace all-target/all-feature Clippy, source/runtime/dead-code budgets, formatting, and diff checks.
2026-08-08 05:55:49 -07:00
Hunter Bown fdac09b00e Merge pull request #5292 from Hmbown/codex/v095-release-contract-fix
chore(release): prepare v0.9.5
2026-08-08 05:31:35 -07:00
CodeWhale Bot cd4bf73215 fix(ci): target consolidated PTY acceptance
Run the isolated Skills Manager scenario through the pty integration-test binary and use its module-qualified name so --exact executes one test instead of zero. Lock the command into the workflow contract and update the durable TUI verification examples.

Verified with the exact ignored PTY command, the workflow contract test, actionlint YAML/expression checks, and git diff --check.
2026-08-08 05:03:43 -07:00
CodeWhale Bot 8d4a9f0356 test(tui): make Scout cwd proof deterministic
Keep pwd as a separately admitted read, but prove the ordinary Scout dispatch workspace by reading unique sentinel bytes through a bounded relative cat operand. This avoids comparing platform-specific pwd renderings and removes the temporary Windows-only pwd flag expansion.

Verified with the focused Scout test, strict all-target/all-feature TUI Clippy, formatting, source-structure budget, and diff checks.
2026-08-08 04:30:42 -07:00
CodeWhale Bot 131bf525f2 fix(tui): normalize Windows evidence paths
Reject rooted current-drive operands at the read-only Scout boundary, render absolute artifact footer paths with canonical forward slashes, and make the Scout cwd proof use Git-for-Windows pwd output without weakening its semantic path comparison.

Verified with the four Windows-failing TUI unit regressions, the related artifact test, the strict all-target TUI Clippy gate, formatting, source-structure budget, and diff checks.
2026-08-08 04:07:48 -07:00
CodeWhale Bot 9a3015892d docs(web): advance published release snapshot
Record GitHub v0.9.4 as the latest published release while keeping the workspace and website source candidate at v0.9.5. Regenerate the build-time facts fallback so the manual Cloudflare deploy receipt compares against the same published tag already served from KV.

Verified with the focused facts and deploy contracts, the complete 256-test web suite, ESLint, TypeScript, facts/docs drift checks, and a 288-page production build.
2026-08-08 03:28:59 -07:00
CodeWhale Bot f93d7ab0b9 docs(tui): keep safety examples internal
The single-binary library conversion made the private command-safety module visible to rustdoc while its examples still imported it as a public API. Render those examples as behavior tables instead of publishing an internal policy module solely for doctests.\n\nVerified with cargo fmt and the codewhale-tui all-features doctest target.
2026-08-08 03:20:35 -07:00
CodeWhale Bot 519c42f050 chore(gates): record final release fixes
Reconcile the aggregate Rust ledger at 685165 lines after the release-gate repairs for session persistence, Scout authority, compact layout, provider-test isolation, and fetched relative links. Package, binary, large-module count, and largest-module ceilings do not grow.

Verified with the source-structure checker and JSON parsing through the checker.
2026-08-08 03:09:17 -07:00
CodeWhale Bot df65a60595 fix(web): resolve fetched relative links
Resolve relative anchor destinations against the fetched HTTP(S) page through htmd parsed elements before Markdown conversion. Absolute, fragment, non-HTTP, and malformed destinations retain the converter existing behavior.

Verified with all 19 extraction tests, including readable page chrome removal and relative-link preservation.
2026-08-08 03:09:08 -07:00
CodeWhale Bot a2d3920a25 test(tui): isolate provider credential setup
Scope both config-path environment variables through the shared guard, and run the xAI API-key confirmation against a canonical temporary Codewhale home with the file secret backend. This removes ambient config/keychain coupling and the macOS /var symlink failure from the full parallel suite.

Verified with the focused xAI provider confirmation test.
2026-08-08 03:09:01 -07:00
CodeWhale Bot 754bf9a6f5 fix(tui): shed work indicator before ambient content
Budget the pinned background-work row only from space left after the Work strip and measured composer growth. Compact terminals now hide the redundant chip before it steals rows from chat or the idle ocean.

Verified with all nine work-surface rail, resize, paint, and ambient-floor regressions.
2026-08-08 03:08:55 -07:00
CodeWhale Bot f2dad3d6cd fix(fleet): restore bounded scout delegation
Keep canonical Bash.run available to Scout and Reviewer through the hardened read-only executor without reviving the retired exec_shell alias. Restore nested agent visibility below the configured depth ceiling, and align roster/setup assertions with the seeded worker, planner, and custom members.

Verified with 420 subagent tests, 16 Fleet roster tests, and the focused setup override regression.
2026-08-08 03:08:49 -07:00
CodeWhale Bot 1fee0325b3 fix(session): preserve canonical resume and newer history
Remove the stale /sessions resume alias now that /resume is a first-class command. At the persistence disk boundary, reconcile a non-empty compatibility projection back into the append-only journal before serialization so direct SavedSession callers cannot save a newer transcript that the next load silently replaces with an older journal branch.

Verified with both command registry invariants, stale-checkpoint recovery, long-history save/load, and checkpoint round-trip tests.
2026-08-08 03:08:39 -07:00
CodeWhale Bot 7f60a6a200 chore(gates): record final 0.9.5 ownership
Rebaseline the paused persistence receipt after eliminating duplicate queued history: retained payload drops from 16,924,032 bytes in the broken candidate to roughly 8,528,000 bytes. The ceiling is under 0.5% above the pre-journal schema and includes 0.015% headroom over the largest of repeated clean samples.

Record the final net source delta at 86 lines above the prior review ceiling with no new package, binary, or thousand-line module. Verified with both budget checker suites, repeated clean measurements, JSON validation, and the source-structure gate.
2026-08-08 02:39:10 -07:00
CodeWhale Bot 2d4a9cb58c fix(runtime): close OAuth and API lifecycle gaps
Own and abort the MCP OAuth callback task so cancelled flows release fixed ports. Preserve omitted-versus-null semantics for nullable MCP PATCH fields while rejecting a server with no endpoint, and make replacement thread goals begin a fresh lifecycle instead of inheriting usage.

Agent-assisted implementation; primary review restored the existing create-validation test and added persisted-state coverage for an invalid final-endpoint clear. Verified with focused OAuth, MCP management, and thread-goal tests, rustfmt, and strict all-target TUI Clippy.
2026-08-08 02:35:35 -07:00
CodeWhale Bot ef6104957d fix(release): close consolidated runtime contract gaps
Keep provider-neutral auto selection in the provider-aware TUI and launch workflow lanes from the exact running executable. Repair CNB and installer alias contracts, refresh legacy website-installed TUI bytes during upgrades, and make account pull reject an unimplemented local import truthfully.\n\nVerified with focused CLI/config tests, strict Clippy, workflow contracts, shell syntax checks, and hermetic web installer tests.
2026-08-08 02:24:47 -07:00
CodeWhale Bot 9f22e2a59f fix(i18n): complete session and todo translations
Register the tree, branch, and resume descriptions in the complete-locale contract and provide native copy in every shipped language. Keep the todo command name code-owned through a placeholder, and remove internal issue wording from the fork description.\n\nVerified with the localization and behavioral-tip test suites plus JSON parsing for every locale pack.
2026-08-08 02:20:55 -07:00
CodeWhale Bot 0f98274960 fix(persistence): queue one canonical session history
Keep only the journal-backed active history in queued snapshots, then materialize the legacy messages projection in a temporary copy at the disk boundary. This removes the near-2x paused-channel retention introduced by the session journal while preserving 0.9.4 readers and checkpoint recovery.

When load-time tool-history repair changes the active projection, append the repaired suffix as a sibling branch instead of letting the stale journal overwrite it or rewriting prior evidence.

Verified with the paused measurement, all persistence-actor and session-manager tests, focused append-only repair regressions, rustfmt, and strict all-target codewhale-tui Clippy. The residual 0.46% journal schema overhead and aggregate source ledger are reconciled separately after the active release lanes land.
2026-08-08 02:19:08 -07:00
CodeWhale Bot 001921d056 fix(ci): target persistence measurement library test
Run the ignored persistence backlog receipt test from the TUI library after the integration target move. Pin the exact Cargo command in a hermetic harness test and reject successful zero-test or missing-receipt runs so future test relocations fail clearly.
2026-08-08 01:56:19 -07:00
CodeWhale Bot 51ae6ae6b2 fix(runtime): consolidate duplicate mobile routes
Axum rejects duplicate method/path registrations while constructing the runtime router. Keep skill toggle plus uninstall and MCP list plus create on their respective single route definitions.

Add an explicit mobile router-start regression so future overlaps fail in the focused runtime API unit lane.

Verified with the mobile, MCP management, skill lifecycle, and skill-state tests; codewhale-tui all-targets clippy; and the runtime-contract budget gate.
2026-08-08 01:52:49 -07:00
CodeWhale Bot fd28361c87 chore(gates): remove unreached release scaffolding
Bring the dead-code ledger back to its existing ceiling by deleting helpers that were added for unfinished follow-up slices and by modeling the OAuth callback listener as intentional RAII state. The core Engine keeps its public constructor shape but no longer stores fields that are not yet part of the runtime.

Verified with the dead-code and source-structure gates, the runtime-contract gate, focused core/TUI unit tests, rustfmt, and strict Clippy for codewhale-core and codewhale-tui.
2026-08-08 01:39:12 -07:00
CodeWhale Bot a701490dde fix(release): smoke the two published commands
The npm wrapper exposes codewhale and codew, while v0.9.5 keeps codewhale-tui only as legacy asset filenames for old updater compatibility. Stop npx from resolving a nonexistent codewhale-tui package during the release smoke and make the runbook describe the single compiled runtime accurately.

Verified with the full local npm wrapper smoke against the exact 0.9.5 release binary, node syntax checking, and the website documentation contract.
2026-08-08 01:20:37 -07:00
CodeWhale Bot 4863fb6db9 Merge pull request #5258 from SparkofSpike/codex/session-title-fix
Record Shizuku's exact current PR head as v0.9.5 ancestry. The verified release tree already contains the equivalent session-title repair; the ours merge preserves the contributor's commit and GitHub provenance without replaying the older release integration.
2026-08-08 01:14:13 -07:00
CodeWhale Bot 89c8d7da75 Merge pull request #5257 from skyzhao1223/feat/auto-model
Record SKY ZHAO's exact current PR head as v0.9.5 ancestry. The verified release tree already contains the equivalent prompt-based auto-model routing; the ours merge preserves the contributor's commits and GitHub provenance without duplicating the patch.
2026-08-08 01:14:13 -07:00
CodeWhale Bot cf84784332 Merge pull request #5256 from bistack/feature/mcp-discovery
Record Sun Zhenyuan's exact current PR head as v0.9.5 ancestry. The verified release tree already contains the equivalent incremental MCP Registry behavior; the ours merge preserves the contributor's commit and GitHub provenance without duplicating the patch.
2026-08-08 01:14:13 -07:00
CodeWhale Bot 851563da7f Merge pull request #5255 from aboimpinto/feat/FEAT-012-layer-5-3-palette-completion-and-discovery-filte
Record Paulo Aboim Pinto's exact current PR head as v0.9.5 ancestry. The verified release tree already contains the equivalent command-discovery behavior; the ours merge avoids replaying stale main while preserving the contributor's commits and GitHub provenance.
2026-08-08 01:14:13 -07:00
CodeWhale Bot 12888571e1 docs(release): finalize the 0.9.5 liveness notes
Record the unlimited-by-default headless and goal policies, removal of every legacy 20-resume ceiling, inspectable errors, and the bounded Scout evidence and note-taking surface. Use the actual 2026-08-08 publication date and regenerate the packaged TUI changelog slice.
2026-08-08 01:13:00 -07:00
CodeWhale Bot de256a794a chore(gates): reconcile the Scout evidence boundary
Record the measured 684,975-line production source boundary for the shared normal/headless Scout authority contract. The same update locks in the existing largest-module improvement from 17,700 to 17,680 lines and removes main.rs from the thousand-line allowlist.

No package, binary, or large-module count is added. Verified with scripts/check-source-structure-budget.py.
2026-08-08 01:12:50 -07:00
CodeWhale Bot 16445405d7 fix(engine): remove hidden continuation ceilings
Delete the shared 20-resume counter from queued steering, child completion, REPL feedback, late completion, and goal continuation paths. Explicit configured limits and the dedicated empty-loop/read-repeat/stuck guards remain in force.

Lock the legacy counter and status markers out of turn_loop.rs and retain the existing 20-tool-round behavioral regression. Verified with both focused engine tests and cargo fmt.
2026-08-08 01:11:33 -07:00
CodeWhale Bot e0f0285b97 fix(fleet): give scouts a bounded evidence shell
Transport explicit read-only shell and bounded-verification caps across Fleet, intersect them with parent shell/network ceilings, and project the same evidence-only catalog in normal and headless workers.

Execute the admitted shell subset as direct argv with strict command/option, workspace-path, executable, environment, and GitHub-host guards. Keep child Todo state as the only editable Scout notes surface and cover catalog, dispatch, transport, path-shadow, helper, and role-isolation regressions.
2026-08-08 01:09:20 -07:00
Paulo Aboim Pinto 96f6ba9451 Merge remote-tracking branch 'origin/main' into feat/FEAT-012-layer-5-3-palette-completion-and-discovery-filte
# Conflicts:
#	scripts/source-structure-budget.json
#	web/app/[locale]/docs/tools/page.tsx
2026-08-08 09:58:29 +02:00
CodeWhale Bot 2556a55ecf fix(goal): leave continuation unlimited by default
Remove the implicit 100-pass terminal ceiling from persistent goals. Token and time budgets remain advisory telemetry, while users who want a circuit breaker can still opt into [goal] max_continuations explicitly.

Verified with the focused goal-loop and configuration tests. Agent assistance was used for implementation and review.
2026-08-07 23:26:43 -07:00
CodeWhale Bot 8a222e9a17 fix(tui): make complete errors directly inspectable
Advertise a full-error pager on every engine error, prioritize the newest visible error over adjacent tool cards, and preserve the exact source message for detail and clipboard surfaces so narrow terminal wrapping cannot split recovery commands or URLs.
2026-08-07 23:08:48 -07:00
CodeWhale Bot 3966b014c9 fix(web): keep the canonical brand-first identity
Preserve the exact Codewhale-first identity sentence in metadata and social previews. Render it as the Open Graph heading itself so accessible and visual surfaces say the brand once without the previous duplicated prefix.
2026-08-07 23:02:07 -07:00
CodeWhale Bot b8aaec21f4 chore(release): finalize 0.9.5 metadata
Date the approved 0.9.5 release, freeze its compare links, record unlimited-by-default headless execution, and refresh the embedded changelog slice.

Verified with prepare-release 0.9.5, version coordination, the OHOS linker/dependency contracts, and diff checking.
2026-08-07 22:43:31 -07:00
CodeWhale Bot b5cf91ec7f fix(exec): leave headless turn budgets opt-in
Run headless agent loops without a finite model-step ceiling unless the caller explicitly supplies --max-turns. Keep finite values validated and preserve the separate Fleet worker budget.

Remove the verifier harness's implicit 100-turn flag so long benchmark rollouts are not silently truncated. Verified with the focused TUI regression, all nine verifier harness tests, cargo fmt, targeted strict Clippy, and diff checking.
2026-08-07 22:42:40 -07:00
CodeWhale Bot 9c9d385e1d test(cli): migrate telemetry dispatch coverage in-process
The single-runtime consolidation removed sibling TUI dispatch, leaving the telemetry kill-switch integration test pointed at a fake binary the product no longer invokes.

Drive a keyless features-list command through the real dispatcher instead. The local dry-run sink proves the positive control reaches the in-process runtime, while an explicit or malformed environment kill switch must leave no telemetry state.

Verified with the focused integration test, rustfmt, diff check, and targeted Clippy with warnings denied.
2026-08-07 22:41:59 -07:00
CodeWhale Bot 6d4ff224fc chore(release): cut release/0.9.6 lane from release/0.9.5 (0.9.4 -> 0.9.6)
Bumps the workspace package version to 0.9.6 and refreshes Cargo.lock.
Branches off release/0.9.5 (which carries the 0.9.5 foundation work:
single-binary packaging, session tree, /rc + managed login, and the
engine->crates/core scaffolding) so v0.9.6 can take on the deferred feature
layer tracked in milestone v0.9.6 (#60, 122 open issues).

cargo check --workspace passes at v0.9.6.
2026-08-07 22:41:16 -07:00
CodeWhale Bot 5bdfeb7146 fix(web): keep localized navigation interactive
Move translated layouts onto the compact navigation until xl, reserve wide companion labels for 2xl, and keep every masthead control inside the viewport.

Portal the compact menu to a true viewport modal with inert background roots, contained keyboard focus, an in-dialog close control, and immediate cleanup when a resize crosses the desktop breakpoint.

Verified with all 254 web tests, ESLint, TypeScript, responsive width probes, and live English, Chinese, and Spanish pointer, keyboard, focus, resize, and navigation checks.
2026-08-07 22:17:12 -07:00
CodeWhale Bot e54b8043df fix(gates): measure the library runtime contract
Run each ignored receipt test against its exact library path and reject Cargo's successful zero-test result explicitly. Keep the standalone tool-catalog measurement and current verification guidance on the same target, with hermetic command and failure regressions wired into CI.

The repaired measurement exposed a real duplicate AGENTS.md injection from the bounded fragment importer. Keep canonical project-context sources single-owned while still importing additional rule formats through the typed, capped fragment boundary; this restores the checked-in prompt identity without raising the budget.

Verified with the full 55-metric runtime-contract checker, focused core and TUI regressions, both Python harness suites, cargo fmt, and all-target clippy for codewhale-core and codewhale-tui.
2026-08-07 21:56:00 -07:00
CodeWhale Bot 471c6d8bb0 fix(ci): align nightlies with the single runtime
Build only the codewhale executable, then stage codewhale and codew from the same bytes for every retained nightly target. Native jobs smoke only the executable that Cargo actually produced, while tagged-release TUI bridge filenames remain out of the nightly command contract.\n\nExtend the workflow contract test to lock the six-target, twelve-artifact inventory, byte-identity check, native smoke selection, and 14-day retention.
2026-08-07 21:52:11 -07:00
CodeWhale Bot 636b84610e docs(release): reconcile 0.9.5 contributor credit
Record the four human contributors whose work is present in the 0.9.5 candidate, update the public credit matrix and website snapshot, and add the missing canonical identity for PR #5257. The candidate heading and compare links remain explicitly pre-tag until publication approval.
2026-08-07 21:52:06 -07:00
CodeWhale Bot 41232cb3be refactor(tui): split release-gate test ownership
Move the roster shadow/trust coverage and telemetry counter coverage into dedicated test-only modules. This restores the one-way source-structure ceilings without raising the budget or changing runtime behavior.

Verified with the source-structure budget gate, cargo fmt --check, a TUI library check, and both extracted test groups.
2026-08-07 21:48:15 -07:00
CodeWhale Bot dd84af2297 chore(release): prepare 0.9.5 candidate
Bump every tagged package and internal dependency pin to 0.9.5, refresh Cargo and npm lock records, regenerate the packaged changelog and web facts, and record the user-visible candidate contract. The changelog deliberately remains marked Unreleased candidate until the rebuilt binary is dogfooded and publication is explicitly approved.
2026-08-07 21:47:47 -07:00
CodeWhale Bot d90b6eda3c fix(npm): make source fallback match one runtime
The glibc preflight no longer promises Cargo-installed binaries that do not exist. It installs the sole codewhale implementation and shows how to create the optional codew alias, with a regression that rejects the removed TUI install hint.
2026-08-07 21:46:26 -07:00
CodeWhale Bot 93b14c4dde fix(web): publish truthful software schema versions
Derive SoftwareApplication softwareVersion only from the published-release receipt backing the install URL. Omit the field when no published release is known so a source candidate is never presented as downloadable.

Verified with focused schema tests, ESLint, TypeScript, and diff checking.
2026-08-07 21:46:02 -07:00
CodeWhale Bot 6fb9a9e27b fix(web): deploy one exact OpenNext bundle
Build once through the OpenNext adapter before preview or deploy, and remove Wrangler custom-build recursion so cache population and upload use the same bundle. Keep the manual main-only exact-SHA workflow and post-deploy receipt gate intact.

Verified with deploy-preflight tests, ESLint, a complete 288-page OpenNext build, Wrangler 4.113.0 deploy --dry-run, and diff checking.
2026-08-07 21:46:01 -07:00
CodeWhale Bot ade598d054 fix(web): keep shared social identity singular
Keep the identity phrase brand-free, derive one shared Open Graph alt string, and render the visual brand from the same SITE_NAME constant. Add a regression against repeated branding and use the supported neutral es-419 date locale.

Verified with metadata and dictionary tests, locale checks, ESLint, and diff checking.
2026-08-07 21:46:01 -07:00
CodeWhale Bot eb5faf754b web: sharpen EN copy, real nav icon buttons, native locale rewrites
Three fixes on the newspaper-ocean site:

- Nav: baseline-align the Han secondary labels with the Latin primary
  (inline-flex items-baseline + matching line-height), and give the
  masthead real brand buttons — GitHub mark before the star count,
  Discord logo icon-only — with a .site-discord-link rule matching
  .site-github-link and a shared .brand-mark size.
- EN copy: hero/meta/footer sharpened around "Codewhale dives into the
  deep so you don't have to"; every "local-first" claim dropped in
  favor of "any model, on your machine". heroIntro keeps the {brand}
  token the lede split and dictionary tests require.
- Locales: home/chrome rewritten natively in all nine non-English
  packs (zh, ja, ko, vi, ru, uk, es, id, pt-BR) instead of machine
  translation. zh hero uses the 一入码门深似海 allusion per community
  feedback. Key parity, template tokens, and ru/uk script purity all
  hold; check:locales and dictionaries.test.ts pass.

Verified: npm ci && prebuild && check:facts && check:docs &&
check:locales && vitest (250/250) && eslint && tsc --noEmit && build.

Agent-assisted (Kimi Code); copy reviewed against the en reference.
2026-08-07 21:46:00 -07:00
CodeWhale Bot 679e681d41 fix(web): align installs with the single runtime
Advertise codewhale and the release/npm codew convenience name without exposing the retired codewhale-tui install surface. Keep Cargo truthful: codewhale-cli installs only codewhale unless the user defines an alias.

Verified with focused public-surface tests, facts/docs/locale checks, ESLint, and diff checking.
2026-08-07 21:45:59 -07:00
CodeWhale Bot 2f5a824c19 fix(release): bump every tagged package
Release preparation now updates the runtime SDK and VS Code extension manifests and locks alongside the Rust and npm wrapper versions. The normal version gate checks the same records that the tag workflow requires, and the transactional fixture proves the expanded bump and rollback set.
2026-08-07 21:44:24 -07:00
CodeWhale Bot 6989574363 fix(update): migrate aliases from primary binary
Download and validate one codewhale release asset, then refresh the primary path plus existing codew and legacy codewhale-tui paths from those exact bytes. This keeps direct alias invocation safe and prevents a 0.9.4 three-command install from leaving codew stale.

Remove stale source and mirror hints for the retired TUI implementation asset, and cover fallback, Android, and alias update behavior.

Verified with cargo test -p codewhale-release --locked; cargo test -p codewhale-cli --lib --locked update::tests; and cargo clippy -p codewhale-cli -p codewhale-release --lib --locked -- -D warnings.
2026-08-07 21:37:43 -07:00
CodeWhale Bot 6915769297 style(tui): restore PTY test formatting
Apply the repository rustfmt configuration to the four PTY test files that made the clean release branch fail cargo fmt --check.
2026-08-07 21:36:40 -07:00
CodeWhale Bot 7689e0de58 fix(tui): advertise complete releases hourly
Align startup release completeness with the current two-command 27-asset contract so v0.9.5 and later releases are not hidden by removed TUI assets. Reduce the default network-check cache to one hour while retaining cached notices, CI suppression, and explicit opt-out behavior.\n\nVerified with all 37 codewhale-release tests and the focused v0.9.5 startup-notice inventory regression.
2026-08-07 21:36:28 -07:00
CodeWhale Bot e291350b4c fix(release): bridge the 0.9.5 single runtime
Build codewhale once, expose the verified bytes as codew across release channels, and retain seven TUI-named release aliases solely so shipped v0.9.4 clients can discover and cross the transition. Current installers and containers expose only codewhale and codew.\n\nVerified with the npm asset suite, exact 34-asset assembly, workflow contract, Homebrew renderer, release-body, dogfood installer, and shell syntax tests.\n\nRefs #5259
2026-08-07 21:36:17 -07:00
CodeWhale Bot 2d95a5d09c fix(tui): let productive tool turns reach completion
Ordinary tool-result steps are model-visible progress, so exclude them from the synthetic no-user-input resume backstop. Keep the existing child, REPL, and goal continuation guards intact.

Cover 20 successful distinct tool rounds followed by a 21st provider request and final assistant text.

Refs #5267
2026-08-07 21:16:05 -07:00
CodeWhale Bot 686bc164c0 fix(core): correct leaf-history count in the #5261 session scaffolding test
leaf_is_moved_not_rewritten appended two journal entries (header `a`,
user `b`) then asserted journal.len() == 3 after branching back to `a`.
The protocol Journal::len() is entries.len(); branch_to only moves the
leaf cursor and never rewrites history (protocol's own
branching_only_moves_leaf test confirms this). Two appends leave two
entries, so the post-branch count is 2, not 3 — the scaffolding test had
an arithmetic/copy-paste error.

The assertion's intent ("history never rewritten") holds with == 2:
branching kept both entries and only moved leaf_id. This leaves the
#5261 scaffolding green, per the overlay's "every slice leaves the tree
green" rule.

Verified: RUST_MIN_STACK=16777216 cargo test -p codewhale-core
-p codewhale-protocol (144 passed, 0 failed).

Refs #5261
2026-08-07 20:51:56 -07:00
CodeWhale Bot 6857c1f0bb feat(tui): pinned background-work indicator above the composer (#5286)
Salvage from a paused worktree, verified and landed (test helper fix
applied on landing).

When the main turn is waiting on background shells, durable tasks, or
running sub-agents, a single chip row renders directly above the
composer so the user sees — exactly where they are looking — that the
model is blocked and on what. It auto-updates as items start/finish and
collapses to zero rows when nothing is pending.

- background_indicator: PendingWork snapshot built from the same state
  the Work strip and /jobs surface read (App::task_panel for background
  shells/tasks; subagent_cache Running + agent_progress for sub-agents).
  No new registry, no lock in the render path. Per-item label cap
  (ITEM_LABEL_MAX_WIDTH) pre-truncates long commands/names so one item
  cannot eat the row before whole-line truncation.
- ui/frame: reserve one extra layout row between the pending-input
  preview and the composer, carved from the auxiliary budget (compact
  terminals shed the chip before chat/composer space). The row is pinned
  — the transcript scrolls away from it — and renders only when work is
  in flight.

Verified: RUST_MIN_STACK=16777216 cargo test -p codewhale-tui
background_indicator (8 passed); cargo clippy -p codewhale-tui
--all-targets -- -D warnings clean.

Closes #5286
2026-08-07 20:44:16 -07:00
CodeWhale Bot 1a878a2a4a feat(fleet): unify built-in dispatch postures through the roster (#5285)
Salvage from a paused worktree, verified and landed.

Every named `type:` dispatch now resolves through a Fleet roster profile
instead of a parallel hidden enum. The built-in postures — worker,
planner, and custom — become seeded roster members (worker/planner/
custom seed alongside the existing scout/reviewer/builder/verifier/
consultant seeds), so there is no dispatch posture the roster cannot
see.

- config: add FleetSlot::Planner; seed worker/planner/custom roster
  members so canonical dispatch postures are roster-visible.
- fleet/roster: built_ins()/load() surface the new seeded members;
  type-dispatch flows through the roster (no parallel enum).
- fleet/worker_runtime: preserve backward compat for `type:` calls — a
  type-resolved member that does not pin a concrete route keeps its
  legacy model options, so seeding worker/planner/custom does not newly
  reject previously-valid calls. Only a member that actually binds a
  provider/model (or an explicitly-named `profile:` member outside the
  General slot) is route-bound and rejects overrides.
- tools/subagent: apply_spawn_profile now resolves every *named* type
  dispatch (incl. worker/planner/custom) through the roster; only the
  fully-unnamed default skips roster resolution. `general`/`default`
  alias to the canonical `worker` posture.

Verified: RUST_MIN_STACK=16777216 cargo test -p codewhale-tui fleet::
(306 passed) and tools::subagent (473 passed), 0 failed.

Closes #5285
2026-08-07 20:41:47 -07:00
CodeWhale Bot 123c7643a9 test(subagent): align shared-contract test with the scaled-down scout output
test_agent_type_prompts_include_shared_output_contract_once demanded the
full "## Output contract (mandatory)" header plus "### BLOCKERS" for
every FleetRole, but the Scout role intentionally ships a scaled-down
"## Output contract (scout)" (SUMMARY+EVIDENCE only) per #5189 F5 —
scouts are read-only explorers and drop the CHANGES/RISKS/BLOCKERS
ceremony. The prompt divergence is the documented design, not a bug.

Rewrite the assertion to honor both: every role still shares exactly one
"## Output contract" spine with "### SUMMARY"; non-scout roles keep the
mandatory contract + BLOCKERS; Scout uses the scout contract and omits
BLOCKERS. This was a pre-existing failure at the branch tip, unrelated
to the #5285 roster salvage; clearing it unblocks the tools::subagent
verification gate.

Verified: RUST_MIN_STACK=16777216 cargo test -p codewhale-tui
test_agent_type_prompts_include_shared_output_contract_once (1 passed).

Refs #5189
2026-08-07 20:40:06 -07:00
CodeWhale Bot 39197e58b9 test(cli): rewrite diagnostic_dispatch_read_only for in-process dispatch
The origin/main merge landed #5259 single-binary argv0 dispatch, which
deleted the DEEPSEEK_TUI_BIN sibling-binary delegation this stale test
asserted against (fake-binary receipts). The dispatcher now runs doctor
and setup --status entirely in-process via run_tui_in_process ->
codewhale_tui::run, so there is no receipt to read.

Rewrite to assert the in-process behavior while keeping every read-only
invariant: success exit for doctor / doctor --json / doctor --context-json
/ setup --status; doctor --context-json emits a machine-readable
{"entries":[...]} context source map; and no secret migration, no legacy
settings rewrite, and no state created under a sealed HOME.

Verified: RUST_MIN_STACK=16777216 cargo test -p codewhale-cli
--test diagnostic_dispatch_read_only (1 passed).

Refs #5259
2026-08-07 20:31:45 -07:00
CodeWhale Bot df0afabbda fix(gates): land the owed workspace clippy baseline (-D warnings) and fmt
Full-workspace clippy --all-targets --all-features --locked -- -D warnings
was owed since session start and was red at every layer:

- codewhale-tui lib: dropped dead codewhale_core re-exports from the
  private core module (consumers import the crate directly); collapsible
  ifs -> let-chains in branch.rs, session_manager.rs, session_tree.rs;
  identical-if and map_or -> is_some_and/is_ok_and in runtime_api.rs;
  dead-code allows on the #5264 fragment recognizers (in-flight consumer);
  missing spawn_depth/journal/leaf_id fields in 10 test initializers
  (E0063 after the #5262/#5265 struct merges).
- codewhale-tui pty tests: qa_harness was loaded as a module 4x via
  #[path] in one binary (clippy::duplicate_mod) — declared once at the
  crate root, scenario modules use crate::qa_harness paths.
- codewhale-cli lib + lib test: orphaned doc blocks from the #5259
  single-binary merge; dead lane_process_spec_from_command/command_env/
  telemetry_test_resolved; map-over-inspect and option-map-unit in the
  run/exec dispatch; nested-unsafe + noop &str clones in ScopedEnvVar
  drop; 5 duplicated #[test] attributes; unused import.
- codewhale-tui lib test: collapsible if in mcp_registry.rs.
- cargo fmt --all normalizes the #5261 scaffolding (fragments.rs).

Verified: cargo clippy --workspace --all-targets --all-features --locked
-- -D warnings -> exit 0. Full test suite runs in parallel (owed gate,
results reported separately).

Refs #5259, #5262, #5265, #5247
2026-08-07 20:19:02 -07:00
CodeWhale Bot 3fccdc775f fix(core): clear clippy -D warnings on the #5261 scaffolding
Four lints failed the mandated workspace gate on release/0.9.5:
- engine/mod.rs: drop unused CoreSessionId import; mark Engine config/state
  fields allow(dead_code) with a note that the #5261 slice consumes them
- engine/thread/events.rs: underscore the not-yet-wired session_id param
- fragments.rs: collapse nested if into a let-chain (edition 2024)

Verified: cargo clippy -p codewhale-core --all-targets --all-features
--locked -- -D warnings exits 0.
2026-08-07 19:15:54 -07:00
CodeWhale Bot 2ba1b45fa5 chore(fmt): cargo fmt over the #5261 scaffolding files
The engine/request scaffolding and the protocol lib.rs mod block landed
unformatted; session_tree.rs drifted with the merge. Pure rustfmt output,
no semantic change.
2026-08-07 19:07:52 -07:00
CodeWhale Bot cb134ec30f Merge origin/main into release/0.9.5 (16 commits: 0.9.4 release-gate fixes)
Resolutions:
- crates/tui/src/main.rs: kept the release shim (codewhale_tui::run, #5259).
  Main's signal-arming repair c07f00c28 was aimed at the old monolith, so it is
  ported here into crates/tui/src/lib.rs: TerminatingSignals::register() now
  installs SIGINT/SIGTERM/SIGHUP handlers synchronously before the spawn, and
  spawn_signal_cleanup_task() moves ahead of telemetry arming/notice. Without
  this port the branch would silently keep the Ctrl-C kill-window #1583/#5282
  fixed on main.
- scripts/source-structure-budget.json: kept branch ceilings (both sides
  rebaselined); single full rebaseline lands at the end of the 0.9.5 stack.
- web docs tools page: took main's public-surface contract fix (update_plan is
  not a default-active tool).

Brings in: builder shared-shell liveness fix (b596b6bfa), fetch-cache test
serialization, nanoid GHSA pin, runtime-contract/source-structure rebaselines,
README refresh. Verified: cargo check -p codewhale-tui green post-merge.
2026-08-07 19:02:01 -07:00
CodeWhale Bot 6c63aacbbc WIP(core): wire #5261 scaffolding modules into lib roots + fix session-tree compile
Uncommitted in-flight work recovered from the working tree:
- crates/core: expose engine/ids/journal/request/session modules (files
  landed in cae5626e6 but were never wired into lib.rs)
- crates/protocol: expose ids/op/event_msg/journal modules
- tui: fix BranchSummary pattern (parent_branch_id field), fork_from_session
  Option arg, branch_to borrow in update_session, mcp_server_management cap
- workspace: add tokio-util dep for crates/core

Verified: cargo check -p codewhale-core -p codewhale-protocol -p codewhale-tui
passes (warnings only). Tests and clippy not yet run.

Refs #5261
2026-08-07 18:49:20 -07:00
Hunter Bown c20386d29c Merge pull request #5284 from Hmbown/fix/builder-shared-shell
Sync to CNB / sync (push) Has been cancelled
Release / resolve (push) Has been cancelled
Release / parity (push) Has been cancelled
Release / artifacts (push) Has been cancelled
Release / docker (push) Has been cancelled
Release / homebrew (push) Has been cancelled
Release / release (push) Has been cancelled
fix(subagent): stop counting finished children as shared-checkout contenders
2026-08-07 18:04:35 -07:00
Hunter Bown d0bbb7b375 Merge pull request #5283 from Hmbown/docs/readme-fleet-positioning
docs(readme): lead with mixed fleets — any model in any role
2026-08-07 17:24:12 -07:00
CodeWhale Bot b596b6bfa3 fix(subagent): stop counting finished children as shared-checkout contenders
A builder sub-agent could not run `echo x > file` in the workspace. Every
`Bash` write came back with "cannot prove a bounded file target for this
shared-workspace write claim", and the advice — use worktree isolation —
puts the work in a sibling checkout the operator never looks at. Writing
the same path through `File` was allowed the whole time, so the gate was
not protecting anything the child could not already do.

The gate asked whether *this* agent holds a shared write claim. The risk
it exists for is a *peer* overwriting the same paths, and claims outlive
the agents that register them: a test workspace with six `Completed`
agents still held four standing claims, three of them non-isolated. So a
lone builder was refused on account of children that had finished long
ago, and a workspace got more restrictive the more it was used.

`has_peer_shared_write_claim` now asks the real question: is another
child, still `Running`, writing in this shared checkout. Worktree-isolated
peers are excluded because they cannot contend for these paths, and an
owner missing from the agent map stays contended — a claim that predates
this session should fail closed.

Concurrent writers are unaffected:
`child_write_tool_fails_closed_outside_registered_scope` still passes
unchanged, because it registers a live peer. A new test,
`lone_shared_writer_keeps_unbounded_shell`, pins the case that was broken.

Verified live against the release binary, in a workspace carrying those
four stale claims: the builder ran `echo shell_fix2_ok > shell_fix2.txt`
via `Bash`, exit 0, and the file landed in the workspace root — not in a
worktree.

Assisted by Claude Code.
2026-08-07 17:07:39 -07:00
CodeWhale Bot b9e85906f7 docs(readme): drop a resume claim the ledger does not support
The previous commit added "including which role ran on which model" to the
resume bullet. Testing a real fleet run showed that is not true of the
Fleet ledger: `FleetTaskState` in `crates/tui/src/fleet/ledger.rs` carries
`entry`, `status`, `lifecycle_seq`, `leased_to`, `leased_at`, and
`completed_at` — no model, no provider, no route. A grep for `pub model` /
`pub provider` in that file returns zero.

The route *is* recorded, but by the sub-agent state
(`.codewhale/state/subagents.v1.json` holds
`runtime_profile.model = {fixed: "deepseek-v4-flash"}` and
`provider = "deepseek"`), which is a different system from `fleet.jsonl`.
Conflating the two put a claim in the README that the named artifact does
not back.

The rest of the fleet positioning stands and was verified in the same run:
spawning with only `type=scout` and no model resolved the scout profile's
pinned `deepseek-v4-flash` / `deepseek` route, with `permissions.write =
false`.

Reverted in English and all nine translations; re-stamped.

Assisted by Claude Code.
2026-08-07 16:31:26 -07:00
Hunter Bown f65a60d9f4 Merge pull request #5282 from Hmbown/fix/v094-release-gates
fix(release): clear the four CI blockers holding v0.9.4
2026-08-07 16:26:14 -07:00
CodeWhale Bot d00b0fe404 docs(readme): lead with the thing that is actually different — mixed fleets
The README sold "any model, any provider" as *switching*: pick a provider,
pick a model, change it mid-task with `/model`. That undersells what the
runtime does. A saved role records its `provider`, `model`, and reasoning
tier explicitly, so roles in one fleet can run on different models from
different vendors in a single run — a cheap fast model directing an
expensive reasoning one, a GLM builder beside a Kimi reviewer.

That capability was documented in docs/FLEET.md (which even ships a
`provider = "zai"` / `model = "glm-5.2"` example) and absent from the front
door. `/fleet` was described as "runs a team of workers", which reads like
a thread pool rather than a team you compose.

Four changes, all prose:

- An intro paragraph stating the idea directly: you pick the model per
  role, and they don't have to match.
- The first "What it does" bullet now says roles pin their route
  explicitly, so a fleet can span vendors and a role's route does not
  depend on whichever provider happens to be active.
- A new bullet for the other half — roles and the constitution are files
  you author, so the harness matches your practice instead of ours.
- `/fleet` and the resume bullet name the per-role model.

Claims verified against `FleetProfileDraft` (`model`, `provider`,
`reasoning_effort` are per-profile fields) and docs/FLEET.md, not written
from the product pitch.

All nine translations updated in step and re-stamped; no new sections,
code blocks, or URLs, so the structural gates hold. Verified:
check-readme-translations.py, check-readme-locales.sh, 250 web tests,
check:docs.

Assisted by Claude Code.
2026-08-07 16:08:56 -07:00
CodeWhale Bot ce42330c29 fix(ci): correct the source-structure ceiling for the cache-guard lines
The signal-arming rebaseline set the aggregate ceiling to 676657 and then
the fetch-cache test guard added 13 more lines, so the Lint lane failed on
a ceiling this branch had itself raised — 676670 > 676657. Rebaseline once,
at the end, to the number the branch actually lands: 676670.

The ledger note now accounts for both parts of the +66: 53 lines of
terminating-signal registration in main.rs and 13 for the shared guard
that stops the two global-fetch-cache tests from resetting the cache under
each other.

Verified: check-source-structure-budget.py passes at 676670.

Assisted by Claude Code.
2026-08-07 15:38:41 -07:00
CodeWhale Bot 21f4299786 test(tui): serialize the fetch-cache tests that reset global state
`FETCH_CACHE` is a process-global LRU and `reset()` empties it for every
thread, but both tests that call it ran in parallel with each other. When
one test's `reset` lands between the other's `insert` and its assertion
the entry is simply gone, and
`cache_is_scoped_by_session_and_accept_header` fails on
`get("session-a", ...).is_some()` — reading as a cache-scoping bug rather
than the test collision it is.

It surfaced as the single failure in an otherwise green
`cargo test --workspace --all-features` run (9940 passed, 1 failed). It
does not reproduce running the module alone (25/25 green); the window
only opens under full-suite thread pressure, which is exactly where a
release lane runs.

Both resetters now take a shared guard, following the
`retry_status::test_guard` pattern already used for this in the workflow
tests. No production code changes — the cache stays unaware of tests.

Verified: 3/3 clean full-binary runs after the change.

Assisted by Claude Code.
2026-08-07 14:58:10 -07:00
CodeWhale Bot 98ea81dc20 docs(changelog): record the signal-arming, docs-tool-list, and nanoid fixes
Adds the three user-visible entries from this release-gate batch to the
0.9.4 section — the startup Ctrl-C window, the docs tool list naming two
uncallable tools, and a new Security heading for the nanoid advisory —
and regenerates the embedded slice with sync-changelog.sh.

Assisted by Claude Code.
2026-08-07 14:41:37 -07:00
CodeWhale Bot 7e0e369186 chore(budget): rebaseline source-structure for the signal-arming fix
Splitting terminating-signal registration from the await costs 53 lines in
main.rs: a struct and two cfg-gated impls replace one free async fn, plus
the comment recording why registration cannot be lazy. Aggregate 676604
-> 676657, max module 17631 -> 17684. Ledger note added alongside the
others; no new modules or packages.

Assisted by Claude Code.
2026-08-07 14:39:44 -07:00
CodeWhale Bot a0f9df7795 fix(web): pin nanoid past GHSA-2v37-7h3g-55p8
`npm audit` in web/ reported one high-severity advisory: nanoid <3.3.17
loops indefinitely when a custom generator is given size zero. It reaches
us transitively through postcss, which is already an override entry, so
the fix follows the pattern the file established for exactly this case
rather than waiting on an upstream postcss bump.

nanoid now resolves to 3.3.18 and `npm audit` reports 0 vulnerabilities,
restoring the release contract's zero-advisory requirement.

Verified: 0 vulnerabilities, 250 web tests, eslint, tsc --noEmit, and
next build all pass against the reinstalled tree.

Assisted by Claude Code.
2026-08-07 14:38:55 -07:00
CodeWhale Bot c07f00c285 fix(tui): arm terminating signals before the spawn, and before telemetry
`spawn_signal_cleanup_task` registered SIGINT/SIGTERM/SIGHUP *inside* the
task it spawned. A `tokio::spawn`ed task does not run until the scheduler
first polls it, so between the call and that first poll the signals still
had their default disposition: a Ctrl-C landing there killed the process
outright — no exit code, no terminal restore, no `session_end`. That is
precisely the outcome #1583 added this handler to prevent, and the window
widens exactly when the machine is busy.

It was reproducible: `ctrl_c_exits_while_a_second_process_holds_the_lock`
failed 3 of 20 local runs with `status.code()` `None` (terminated by
signal) instead of `Some(130)`, and failed the macOS lane on the 0.9.4
release candidate.

Two changes close it:

- `TerminatingSignals::register()` installs the streams synchronously and
  hands them to the spawned task, so the OS disposition has changed by the
  time the call returns rather than at some later scheduling point. The
  Windows branch registers `tokio::signal::windows::ctrl_c()` the same way
  instead of awaiting the lazy `tokio::signal::ctrl_c()`.
- The call moves ahead of `arm_telemetry` and the telemetry notice.
  Arming creates the telemetry buffer — the first externally observable
  thing the process does — and the notice can sit waiting on a human;
  a Ctrl-C in either window has to be handled too. Recording a
  `session_end` from the signal path is a no-op until arming runs, so
  installing earlier collects nothing.

Verified: 40/40 runs of the previously-flaky test, the full 15-test
telemetry_contract suite, and a `cargo check --target
x86_64-pc-windows-msvc` of the Windows branch in isolation (the full
Windows cross-build is blocked locally by `ring`, so the Windows lane in
CI remains its first end-to-end check).

Assisted by Claude Code.
2026-08-07 14:38:46 -07:00
CodeWhale Bot d2bdb9d1cf fix(web): follow the todo_write rename through the public-surface contract
21ed173cf made `todo_write` the canonical work-progress tool and moved
`work_update` to a hidden compat alias, updating
docs/public-surface-facts.json and docs/RUNTIME_SIMPLIFICATION_DESIGN.md
but not the two places that still asserted the old name. The Web Frontend
lane has been red since that commit.

- public-surface-contract.test.ts pinned `work_update` in the nine-name
  default-active list and in the design-doc sentence it greps for. Both
  now read `todo_write`, matching the files they are checking.
- The docs tools page listed `update_plan · work_update` as coordination
  tools. Neither is model-visible — `update_plan` replays older Plan
  artifacts only and `work_update` is a hidden alias — so the page named
  two tools a reader cannot call and omitted the one they can. It now
  lists `todo_write`, in both the English and Chinese copy.

Verified: 250 web tests, eslint, tsc --noEmit, and next build all pass.

Assisted by Claude Code.
2026-08-07 14:38:13 -07:00
Paulo Aboim Pinto eae3b7ffb9 Merge remote-tracking branch 'origin/main' into feat/FEAT-012-layer-5-3-palette-completion-and-discovery-filte
# Conflicts:
#	scripts/runtime-contract-budget.json
#	scripts/source-structure-budget.json
2026-08-07 18:39:45 +02:00
CodeWhale Bot 23be47ab6b chore(budget): bump aggregate ceiling for session-tree 5262 (683000 -> 684000) 2026-08-07 06:29:29 -07:00
CodeWhale Bot cae5626e6c feat(session-tree): append-only entry journal + /tree /branch /fork /resume (#5262)
- Every session entry carries id + parentId, leafId tracks active position,
  in-memory tree projects from journal, context rebuilds root->leaf.
- Tree operations as commands: /tree (render), /branch (move leaf only,
  never rewrites history), /fork (new session from any node, interactive
  picker per #576 via /fork picker), /resume (picker + foreign-session
  import/export container).
- branch_summary and compaction entries are first-class SessionEntryKind
  variants (data shape lands now, strategies deferred).
- Spawn-depth tracking on SessionMetadata and Journal; fork increments.
- Foreign-session import/export via SessionImportContainer so /resume can
  ingest sessions from other agents.
- SavedSession journal migration: old linear messages -> journal entries
  with linked parent chain, leaf = last; new sessions write both journal
  and derived messages for compat. Atomic write/fsync/crash-checkpoint
  and MAX_SESSIONS=50 preserved.
- Shares entry shape with compaction (same SessionEntry envelope).

Co-depends on #5261 engine split (core journal placeholder already
landed in parallel work on same branch).
2026-08-07 06:29:10 -07:00
CodeWhale Bot 0918686b19 feat(rc): /rc remote control + managed login (5265)
Implement end-to-end dogfood for the account-owned web remote control
in the single-binary TUI consolidated by 5259, plus the managed login
account layer twin (Hmbown/cwc 187).

Remote control (crates/tui/src/remote_control.rs):
- Survives token TTL: proactive JWT 60s refresh plus single 401 retry
  with refresh then reconnect, no 401 loop (cwc 140 pattern). On revoked
  re-enrolls via device flow. Heartbeat 25s, sync 1.2s, typed relay
  Prompt/Approval/Control, bounded validation, ownership-restore.
- Enrollment persisted in global 0600 OS keyring/file store (not repo
  local), validated absolute control-plane base.

Managed login (crates/cli/src/cloud.rs, crates/secrets/src/account.rs):
- codewhale cloud login device flow, status, logout, keys (BYOK vault)
  persists refreshable account session in durable global secret storage
  (OS keyring, CODEWHALE_CLOUD_ALLOW_FILE_SESSION_STORE opt-in file).
  CODEWHALE_CLOUD_API_BASE origin validation (HTTPS except loopback for
  staging dogfood 5062) and verification URL trust.
- Bearer custody: never in config.toml (5226). OS keyring slot
  codewhale-cloud-auth-v1-sha256(profile+api_base) with token-free
  RuntimeAccountInfo in runtime_api.rs.
- Cloud settings pull/push --dry-run: explicit only, never automatic,
  field-level last-writer-wins via GET /api/me and PATCH If-Match 412,
  offline stays file-only. Hydrates shared settings document (5226).

Blockers:
- 5047: reject relative CODEWHALE_HOME/CONFIG_PATH at path authority,
  unify read/write, loud plaintext fallback refusal (already on lane).
- 5243: adopt minted OAuth token without second picker trip (011b9a986).
- 2984: Codex OAuth route via Responses wire protocol, UsageMeter.
- 5062: loopback staging dogfood ready.

Budget: cloud.rs 995 -> 1060 newly allowed large module, 178 -> 179,
max 17652 -> 17700, aggregate 680000 -> 683000.

Fixes #5265
Refs #5062 #5226 #5047 #5243 #2984 #5259
2026-08-07 06:19:20 -07:00
CodeWhale Bot d1d7b65fb7 feat(packaging): winget generate helper + RELEASE_RUNBOOK 27-asset (5260)
- Allow packaging/winget/*.sh via .gitignore (global *.sh ignore)
- Track packaging/winget/generate-winget-manifest.sh (bump version/SHA + sync .winget mirror)
- docs/RELEASE_RUNBOOK: 34-file -> 27-file single-binary inventory (codewhale+codew only)
  7x1 matrix verified, FreeBSD source-build note stays in release-artifacts.yml
  and docs/INSTALL.md (#1097) and winget manifests resolve #1561.

Co-Authored-By: internal-model
2026-08-07 06:16:15 -07:00
CodeWhale Bot be676502df feat(release): single-binary packaging follow-ups (5259/5260)
Complete 5259 single-binary sweep for packaging/docs: remove
codewhale-tui from .cnb.yml, nix, npm bin, installer, bundles,
locales, and docs/INSTALL matrix; add .winget + packaging/ manifests
(single-binary 27-asset inventory, FreeBSD source-build note) and
update release-artifacts comments from 34 to 27 assets. 27-asset
inventory verified via assemble-release-assets --verify.

Co-Authored-By: internal-model
2026-08-07 06:14:45 -07:00
CodeWhale Bot fdc336ec2b fix(docs): resolve broken intra-doc link to stamp_subagent_summary
The Documentation CI job (cargo doc -Dwarnings, which runs on dispatch
and schedule but not push) failed on an unresolved intra-doc link in
subagent_done_sentinel's doc comment. stamp_subagent_summary exists but
isn't in scope by bare name from that comment (different module in the
file); render it as a plain code span so rustdoc no longer errors.
2026-08-07 06:07:29 -07:00
CodeWhale Bot 6d7add26c9 chore(budget): tighten single-binary ceilings (5259) 2026-08-07 06:04:02 -07:00
CodeWhale Bot 4b728a1d84 feat(release): single-binary install.sh + Dockerfile (5260)
install.sh: copy codewhale + codew only, build hint cargo install codewhale. Dockerfile: build -p codewhale-cli only, ship codewhale + codew (no codewhale-tui), update header. Completes 5260 packaging sweep for shell + Docker.
2026-08-07 06:03:22 -07:00
CodeWhale Bot 927024f80e feat(release): single-binary npm + CI artifacts (5260)
npm: ASSET_MATRIX 3->2 per platform, CNB_BINARY 3->2, drop tui field (pair[1] shim). Backwards compat: old tui artifact check no longer needed. 7x1 matrix already done. Fixes part of 5260 packaging sweep.
2026-08-07 06:02:54 -07:00
CodeWhale Bot 69ddf337b4 feat(release): 7x1 single-binary matrix — drop codewhale-tui asset (5260)
Release CI 7 targets x 3 binaries -> 7 x 1. Removes tui_binary/tui_artifact from matrix, cargo build -p codewhale-cli only, drops tui smoke/stage/upload and windows installer copy. Single codewhale binary + codew shim only. Part of 5260 packaging sweep (I1 -> I2).
2026-08-07 06:02:27 -07:00
CodeWhale Bot 5e5608cee2 fix: fmt/clippy follow-ups for single-binary + deps shrink 2026-08-07 06:01:14 -07:00
CodeWhale Bot 59e710271e fix(ci): rebaseline runtime-contract + source-structure budgets for 0.9.4
These two CI-enforced budgets drifted during the 0.9.4 ship and were
masked because cargo fmt was red (the fmt failure skipped every later
Lint step, so neither budget check ran until fmt was fixed):

- runtime-contract-budget.json: the todo_write sole-progress-surface
  change (work_update -> todo_write in the plan tool catalog) and the
  +276-byte base-prompt growth were never accompanied by a snapshot
  regeneration, so the identity check failed. Regenerated from the live
  receipt (budget_from_receipt); the only enforced drift was the
  plan.full.tool_names identity.

- source-structure-budget.json: aggregate owned Rust grew 676325 ->
  676604 (+279 lines) with the final 0.9.4 ship items. Bumped
  max_total_owned_rust_lines and recorded a reviewed _todo note; no new
  1000-line modules, max_module unchanged.

Both are legitimate 0.9.4 baselines; pay the aggregate down in 0.9.5.
2026-08-07 05:48:55 -07:00
CodeWhale Bot 4772354da2 fix(single-binary): stray test attribute after delegate removal 2026-08-07 05:47:45 -07:00
CodeWhale Bot 0cb8ce7423 chore(single-binary): remove codew second binary file (5259 follow-up) 2026-08-07 05:39:00 -07:00
CodeWhale Bot 1b77edd814 feat(single-binary): consolidate cli + tui into one codewhale (5259)
- add [lib] to crates/tui exposing codewhale_tui::run(args) -> ExitCode
- thin crates/tui/src/main.rs to call library
- remove codew second binary, add argv0 dispatch in crates/cli/src/main.rs
- replace delegate_to_tui/build_tui_command* /tui_spawn_error/locate_sibling/xAI bail and every Command::new(&tui) site with in-process codewhale_tui::run
- sweep two-process assumptions (path resolution, env-forwarding, session-file handoff) while keeping persistence_actor
- update default-members to single binary and budgets
- fix syntect load_defaults_newlines for 5.3
2026-08-07 05:38:44 -07:00
CodeWhale Bot aa9d88121f deps(tui): shrink 708-package graph — dedupe, prune features, drop redundant stacks (#5248)
Epic #5249 build-time lane. Baseline 708 (measured 710 via cargo
metadata --offline, 91 normal duplicate entries, 56 cargo-deny warnings
→ 674 packages after shrink (-36, -5% on Cargo.lock, -27 normal dups).
Remaining duplicates are explicitly ratcheted in deny.toml (warn/dry-run).

What changed, why it is safe, and what was measured:

* http: tiny_http 0.12 (blocking, 3 packages: tiny_http+ascii+chunked_transfer)
  removed from codewhale-tui. OAuth loopback (crates/tui/src/mcp/oauth.rs)
  now uses tokio::net::TcpListener + minimal HTTP/1.1 parsing. The previous
  server was only for the OAuth redirect (single GET /callback?code=…); the
  new code keeps the same parse_oauth_callback contract and writes the same
  200/400 bodies. tiny_http is kept as [dev-dependencies] for the two
  integration tests that still use it (skill_cli, plugin_e2e_acceptance) so
  cargo test --workspace still compiles. Saves 3 normal packages.
* html: readability 0.3 (html5ever 0.26 / markup5ever 0.11 stack, 16
  packages: readability+phf 0.10+string_cache 0.8+tendril 0.4+xml5ever 0.17
  etc.) removed from crates/tui. crates/tui/src/tools/web/extract.rs now
  uses the existing fallback_main_html regex for cleaned_html and
  html_to_plain_text for text, keeping the meaningful_text≥32/≥5-words
  signal. htmd 0.5 (html5ever 0.38) is retained as the single HTML→Markdown
  stack. The two stacks were compiling incompatible html5ever trees; the
  fallback path was already the JS-required fallback, so behavior is
  preserved. Saves 16 packages (phf 0.10, string_cache 0.8, etc. gone).
* jsonschema: workspace 0.48 → 0.46 to match schemaui 0.12 (which pins
  ^0.46). crates/workflow-js now uses the same 0.46 validator (validator_for
  exists in both). Saves jsonschema 0.48.5 + referencing 0.48 etc. (6).
* tower-http: workspace 0.7 → 0.6 to match reqwest 0.13 (which depends on
  0.6). CorsLayer API is identical for the single use (cors::CorsLayer).
  Saves tower-http 0.7.
* lru: crates/tui 0.18 → 0.16 to match ratatui-core =0.1.0 (which pins
  0.16). LruCache::new(NonZeroUsize) + get/put API is identical.
* base64: crates/tui 0.23.0 → 0.22.1 to match oauth2 5.0 (0.22). Engine
  API (general_purpose::STANDARD) is identical since 0.21.
* reqwest: crates/tui removes unused gzip feature (compression-codecs
  + async-compression). http2/keep-alive, blocking (fleet/alerts,
  xai_oauth), stream (fetch::bytes_stream), form (OAuth) are kept and
  verified via cargo check. Saves 3 packages.
* cargo-deny ratchet: [bans] multiple-versions stays warn (dry-run per
  AGENTS.md) but every surviving duplicate is now in skip/skip-tree with a
  blocker comment (oauth2 5.0 → reqwest 0.12/sha2 0.10, portable-pty 0.9 →
  filedescriptor 0.8 → thiserror 1, rust-i18n 4.2 → toml 0.8 stack,
  windows-* split, etc.). cargo deny check now reports 0 duplicate warnings
  (was 56) and will warn on any new duplicate. The gate stays warm until
  Hunter approves deny.

Metrics (cargo metadata --offline / cargo tree -d -e normal --workspace /
cargo deny check / scripts/check-source-structure-budget.py, all --offline
where applicable, on a clean release/0.9.5 checkout):

  before: 710 packages, 91 duplicate entries (44 crates), 56 deny dups,
          28 build scripts, 680592 owned Rust lines, 178 large modules,
          max module 17631 (main.rs)
  after:  674 packages (-36), 64 duplicate entries (-27), 0 deny dups,
          680653 owned Rust lines (+61, still < 680700), 178 large modules,
          max module still 17631. Budget PASS.

  cargo check -p codewhale-tui --offline  PASS
  cargo check -p codewhale-cli --offline PASS (wrapped set_var/remove_var
    in unsafe for Rust 1.97)
  cargo test -p codewhale-tui --no-run PASS
  cargo deny check PASS (0 duplicate warnings)
  scripts/check-source-structure-budget.py PASS

The HTTP server evaluation (axum 0.8 vs tiny_http) and the HTML evaluation
(readability vs htmd) are documented above: axum is kept for the main TUI
runtime; tiny_http is dropped for the OAuth loopback in favor of a ~80-line
Tokio listener. readability is dropped in favor of the fallback + htmd
single stack; keeping both was compiling two html5ever trees.

Fixes #5248.

Co-authored-by: codewhale --provider deepseek --model deepseek-v4-flash exec (deps analysis)
EOF
)
2026-08-07 05:36:47 -07:00
CodeWhale Bot 4e3087b0e2 fix(test): align fleet-contract tests with 0.9.4 named-role model pin
0.9.4 pins each named fleet role to its configured model/route and
rejects a dispatcher-supplied model on non-general slots (#5046). Two
tests predated that contract and failed on every CI lane (macOS + Windows)
plus `cargo fmt`:

- custom_fleet_profile_also_rejects_model_override asserted the mismatch
  error contained "pre-configured route", but a profile that pins a model
  hits the more specific "pins model 'X', but the caller requested 'Y'"
  branch. Assert on "pins model" instead.

- workflow_run_dispatches_task_through_subagent_manager dispatched a
  scout (`type: explore`) child with an explicit model, which the contract
  now rejects (named roles bind their route). Drop the `type` so the child
  launches as a prompt-only general worker: the explicit model still
  drives routing (route_source stays "task.model"), no fleet profile is
  bound (profile stays null), and no write scope is required. The dispatch
  mechanism under test is unchanged.

Also applies rustfmt to subagent/tests.rs
(isolated_fleet_roster_with / apply_spawn_profile_promoted_alias_rejects_model_mismatch),
resolving the `Lint` job failure.

Production behavior is unchanged; only stale tests and formatting are
corrected so 0.9.4 CI is green.
2026-08-07 05:04:27 -07:00
CodeWhale Bot 8e60f7a5f7 fix(subagents): scale down output contract for scouts (5189 F5)
Builder/verifier keep 5-section spine (SUMMARY/EVIDENCE/CHANGES/RISKS/BLOCKERS) for parseable handoffs; scouts now use SUMMARY+EVIDENCE only — 3-5 tool calls cap dropped in favor of stop-condition. Adds SUBAGENT_SCOUT_OUTPUT_FORMAT and selects by SubAgentRole::Scout.
2026-08-07 05:01:39 -07:00
CodeWhale Bot 307c26d641 build: consolidate tui integration-test binaries (5247)
26 root-level *.rs binaries each linked the full codewhale-tui graph
plus cucumber/wiremock/rio-vt — ~26 large link jobs per
cargo test -p codewhale-tui (30-minute suite, #4991).

Consolidate into 3 directory harnesses so crate:: for the
path-included production modules resolves at the harness crate root:

- tests/integration/main.rs — 16 plain #[test]/#[tokio::test] suites
  (adaptive_evidence, cache_guard, coordination, diagnostic_read_only,
   dotenv_authority, eval_harness, exec_stream_drop, exec_turn_usage,
   integration_mock_llm, palette_audit, protocol_recovery,
   reasoning_content, skill_cli, telemetry_contract,
   verifiers_harness, workflow_tool_stream). Shares wiremock/tempfile
   and links the TUI once.

- tests/cucumber/main.rs — 6 Gherkin runners
  (core_session_command_extraction, directory_listing,
   epic_acceptance_harness, eval_smoke, plugin_e2e, tool_lifecycle).
  Each World is per-mod via cucumber 0.23 inventory, so merging cuts
  6 link jobs to 1.

- tests/pty/main.rs — 4 real-PTY suites (qa_pty, release_runtime_qa,
  terminal_matrix_qa, work_bar_subagents_pty). #[cfg(unix)],
  portable-pty/rio-vt linked once.

ls crates/tui/tests/*.rs | wc -l 26 -> 0
cargo test -p codewhale-tui --tests --no-run (warm incremental)
  31.54s (26 binaries + bin) -> 6.20s (3 harnesses + bin)
cargo test -p codewhale-tui --tests -- --list
  263 (integration) + 14 (cucumber) + 101 (pty) = 378 integration
  + 10008 bin unit tests. Filtering still works:
  cargo test -p codewhale-tui --test integration adaptive_evidence

README updated with harness table and filtering examples.

Also repair crates/tui/src/runtime_api/tests.rs broken by the
stacked 0.9.5 PRs (548b8b52d, 5c228c5bd, 5dcd26464, 8cf9280e2) which
left conflict markers and interleaved thread_goal/memory/fleet/skill
bodies (7687, 7820). Restored from 649a9a1bb and re-appended the
4 fleet, 7 memory, 4 MCP, 15 skill lifecycle tests from their
clean feature branches (10723dc8e, 3130b49a6, d16b83284, 9864e2d71).

Fixes #5247
2026-08-07 04:58:17 -07:00
CodeWhale Bot 011b9a9860 fix(auth): adopt minted OAuth token automatically without second picker trip (#5243)
After a device OAuth completes, the token is captured/adopted in the
same chord — no follow-up 'e' press and no second trip to the provider
picker. Validates external credential files at grant time (existence +
freshness) instead of lexically normalizing the path and failing at
first request (auth:oauth-consented-select-to-check). Adds one-chord
'e' from the provider list and auto-adopts a fresh external token when
the user presses Enter on a provider that already has one (xAI via
Grok CLI, ChatGPT/Codex via Codex CLI). Pattern fix for both providers.

Fixes #5243
2026-08-07 04:48:32 -07:00
CodeWhale Bot a6fd150bda chore(budget): bump for 5243 OAuth wip (+200) — 5243 adds 128 lines, keep PASS 2026-08-07 04:45:00 -07:00
CodeWhale Bot b847ba56aa fix(context): silence dead_code for new fallback helpers (5244 follow-up) 2026-08-07 04:42:20 -07:00
CodeWhale Bot f6972f3e0a fix(tests): remove stray conflict markers from stacked runtime_api tests
Stacking 5133/5132/5131/5130/5129 left 9 marker lines (>>>>>> 3130b49a6, >>>>>>> 9864e2d71, <<<<<<< HEAD) in crates/tui/src/runtime_api/tests.rs causing mismatched delimiter compile error (thread_goal_crud... unclosed). Cleaned by dropping marker lines, keeping both test suites. Cargo check now passes.
2026-08-07 04:41:42 -07:00
CodeWhale Bot 555b1e9003 fix(context): loud fallback for unknown models (5244) 2026-08-07 04:38:52 -07:00
CodeWhale Bot 4862176f09 chore(budget): re-baseline for 0.9.5 PR stack
Aggregate 678400 -> 680500 (+2100) for stacked PRs #5258, #5205, #5256, #5257, #5255 plus copilot runtime APIs (goal, verifier, memory with native_memory.rs new 1017 large, mcp, skill). Large 177->178.

Passes check-source-structure-budget.
2026-08-07 04:35:47 -07:00
copilot-swe-agent[bot] 5dcd264640 feat: add skill lifecycle routes to runtime API (install, update, uninstall, trust, audit)
- Add POST /v1/skills/install for installing from remote sources
- Add POST /v1/skills/{name}/update for updating by name
- Add DELETE /v1/skills/{name} for uninstalling
- Add POST /v1/skills/{name}/trust for marking skill as trusted
- Add GET /v1/skills/{name}/audit for read-only inspection receipts
- Add `skill_lifecycle: bool` to RuntimeCapabilities in protocol crate
- Advertise skill_lifecycle=true in GET /v1/runtime/info
- Add ApiError::forbidden for network-policy-denied responses
- Add 15 API tests covering success, not-found, invalid scope, digest drift, and auth

The trust note preserves exact advisory wording from the TUI: "advisory and
digest-bound; records your review intent but does not sandbox or auto-authorize
scripts."

Closes #5070
2026-08-07 04:35:24 -07:00
copilot-swe-agent[bot] 5c228c5bdb feat: add bounded MCP server management endpoints to runtime API
- Add `mcp_server_management: true` capability to `RuntimeCapabilities`
  in the protocol crate so clients can discover support via
  `GET /v1/runtime/info`.

- Expose 7 new routes on the runtime API:
  - `POST   /v1/apps/mcp/servers`              create
  - `GET    /v1/apps/mcp/servers/{name}`        read (redacted)
  - `PATCH  /v1/apps/mcp/servers/{name}`        update (partial)
  - `DELETE /v1/apps/mcp/servers/{name}`        delete
  - `POST   /v1/apps/mcp/servers/{name}/enable`    enable
  - `POST   /v1/apps/mcp/servers/{name}/disable`   disable
  - `POST   /v1/apps/mcp/servers/{name}/reconnect` drop pool / re-init

- Credential redaction: `McpServerDetail` response type never returns
  header values, env variable values, bearer-token env var names, or
  OAuth client secrets; callers see only key names and boolean flags.

- Make `validate_mcp_transport` public so it can be called from the
  new route handlers.

- Add 4 tests covering: full CRUD lifecycle, input validation (400 on
  missing command/url, 400 on missing name, 409 on duplicate), credential
  redaction, and capability advertisement.

Closes #5071
2026-08-07 04:35:18 -07:00
copilot-swe-agent[bot] 548b8b52df feat: expose bounded memory inspection and lifecycle controls via Runtime API
Implements GET/v1/memory (list with scope/search/limit), GET /v1/memory/{id}
(inspect), POST /v1/memory (create, auth-gated), and DELETE /v1/memory (clear
by scope) backed by the existing NativeMemoryStore.

Key design decisions:
- Raw file-system paths are never exposed; entries carry scope ("global" /
  "workspace") and workspace_id (SHA-256 digest of origin URL, not a path)
- Summaries are bounded to 300 chars to prevent private data exfiltration
- Workspace scope lookups are silently empty when no git origin is configured
  (same behavior as the existing get_for_workspace boundary)
- DELETE /v1/memory requires explicit scope= param, rejecting absent/empty values
- memory: true is advertised in GET /v1/runtime/info capabilities

Also adds NativeMemoryStore::list_all() for ordered listing without FTS, and
updates the RuntimeCapabilities struct + test in the protocol crate.

Closes #5072
2026-08-07 04:35:11 -07:00
copilot-swe-agent[bot] 8cf9280e25 feat: expose verifier evidence via fleet receipts API
Add three new read-only endpoints to the Runtime API under the fleet
run resource:

  GET /v1/fleet/runs/{run_id}/receipts
    Lists all durable receipts for every completed task in the run.
    Each entry includes: run_id, task_id, worker_id, attempt,
    terminal_seq, completed_at, result, failure_kind, failure_class
    (human-readable), retry_eligible, score, artifacts summary, and
    evidence_available flag.

  GET /v1/fleet/runs/{run_id}/receipts/{task_id}
    Returns the same receipt detail for a single task.  Returns 404
    when the run or task has no durable receipt yet.

  GET /v1/fleet/runs/{run_id}/receipts/{task_id}/evidence
    Reads the Receipt-kind artifact file (bounded to 64 KiB) and
    returns its structured JSON content alongside path, checksum,
    size_bytes, and a truncated flag.  Returns 404 when no receipt
    artifact exists or the file is not yet written.

Key design points:
- retry_eligible is true only for transport failures; verifier and
  task failures require human review or a code change.
- failure_class provides a plain-English description of each
  FleetTaskFailureKind so managed clients can explain a failure
  without hard-coding enum labels.
- Evidence is kept behind an explicit inspection endpoint and capped
  at MAX_RECEIPT_EVIDENCE_READ_BYTES (64 KiB); raw artifact paths are
  exposed for off-band retrieval.
- All three endpoints are read-only; no action (rerun/cancel) is
  wired here — those delegate to the existing execution owner.

Tests added:
- fleet_receipt_json_pass_result_has_no_failure_fields
- fleet_receipt_json_verifier_failure_is_not_retry_eligible
- fleet_receipt_json_transport_failure_is_retry_eligible
- fleet_receipt_json_receipt_artifact_sets_evidence_available
- fleet_receipt_api_list_and_get_round_trip (integration: list,
  get, evidence, and 404 for missing task)

Closes #5073
2026-08-07 04:35:02 -07:00
copilot-swe-agent[bot] 649a9a1bb9 feat: expose persistent goal-loop state and completion controls via HTTP API
Add five new endpoints to the runtime API for managing thread goals:

  GET    /v1/threads/{id}/goal         — read goal (objective, status, usage,
                                         budget, continuation count, timestamps)
  PUT    /v1/threads/{id}/goal         — create or update goal (objective +
                                         optional token_budget)
  DELETE /v1/threads/{id}/goal         — clear goal, emits cleared event
  POST   /v1/threads/{id}/goal/complete — transition to complete (409 if already
                                          terminal)
  POST   /v1/threads/{id}/goal/block   — transition to blocked  (409 if already
                                          complete)

Goals are stored durably in the RuntimeThreadStore (goals/ subdirectory, one
JSON file per thread). Every write emits a replayable SSE event
(thread_goal_updated / thread_goal_cleared) so subscribers get the same
durable update that engine-driven changes produce.

The GET/PUT/DELETE handlers verify the thread exists in the runtime store
before touching the goal; unknown-thread requests return 404.

Lifecycle authority is preserved: PUT always resets to Active status; the
complete and block actions are the only paths to those states.

  • Add goals_dir + save_goal/load_goal/delete_goal to RuntimeThreadStore
  • Add get_goal/save_goal/remove_goal + goal event helpers to
    RuntimeThreadManager
  • Add thread_goals: bool to RuntimeCapabilities in codewhale-protocol
    (default false for old deserializers; set to true in runtime_api)
  • Four new integration tests covering CRUD, invalid transitions, auth,
    and capability advertisement

Closes #5074
2026-08-07 04:34:45 -07:00
Paulo Aboim Pinto a88d2f018f fix(tui): assert normalized footer paths in spillover tests
8f2b622dc normalized the model-facing artifact footer to forward slashes
(platform-independent). Two unit tests still asserted the raw OS path
against that footer, failing on the Windows lane:

- truncate::adaptive_evidence_footer_names_artifact_path_and_recovery
- subagent::subagent_tool_results_spill_to_disk_and_stay_bounded_inline
  (upstream fa7c4b055)

Assert the normalized form via format_artifact_relative_path, matching
what the footer actually emits.
2026-08-07 04:34:33 -07:00
Paulo Aboim Pinto 301b2ca1d9 chore(budget): re-baseline source-structure ceiling for Layer 5.3 acceptance harness
FEAT-012 adds the shared discovery-shadowing contract (discovery.rs, 235
lines) and the Gherkin acceptance harness (epic_discovery_acceptance.rs,
750 lines) mirroring the accepted FEAT-011 pattern. Aggregate owned Rust
source 673375 -> 674554 (+1179 lines). No new 1000-line modules.

Pay down in v0.9.5 per the existing budget TODO notes.
2026-08-07 04:34:32 -07:00
Paulo Aboim Pinto 3339b43cc6 fix(tui): make git-repo-root no-repo test layout-independent
- git_repo_root_reports_attempted_paths_when_no_repo_found created its
  harness in the checkout's parent dir; when the checkout is nested inside
  another git repo (e.g. a workspace repo with sibling checkouts), the
  harness itself resolved to that parent repo and the no-repository path
  was never exercised
- Use the system temp dir with deep nesting beyond the parent-search limit,
  mirroring the sibling create_isolated_worktree no-repo test

Boy Scout repair found by the FEAT-012 Phase 8 full workspace gate; test-only,
no behavior change to git_repo_root itself.
2026-08-07 04:34:24 -07:00
Paulo Aboim Pinto eb70916a30 feat(FEAT-012): add Gherkin discovery-filtering acceptance harness
- New tests/features/feat-012-discovery-filtering.feature: 7 scenarios
  (AC1-AC6 + AT-010 alias-aware unification) covering all six FEAT-012
  acceptance criteria and EPIC AT-008/009/010
- New commands/epic_discovery_acceptance.rs: scenario-level cucumber worlds
  bound to live palette builder, live slash completion, and live dispatch;
  fail_on_skipped + non-zero passed-step assertions per scenario
- Registered module in commands/mod.rs
- docs/architecture/command-dispatch.md: module map row for shared
  discovery.rs owner
- feat012 selector: 7/7 scenarios pass, zero skipped, non-zero steps each
- Guards: discovery 13/13, palette 34/34, completion 24/24; strict clippy
  0 warnings

Generated with Claude Code
2026-08-07 04:34:19 -07:00
Paulo Aboim Pinto a9702c496d feat(FEAT-012): unify slash completion onto shared discovery contract
- widgets/mod.rs: builtin_visible_for_completion_match and push_command_entry
  now consume commands::discovery predicates; local duplicate
  user_command_shadows_builtin_canonical/_alias definitions deleted
- New completion test: slash_completion_accepted_user_alias_claims_builtin_canonical_token
  (user alias claiming a built-in canonical token suppresses the built-in
  suggestion and surfaces the user command)
- Completion suite: 24 passed (23 existing + 1 new); discovery 13/13 and
  palette 34/34 guards green; strict clippy 0 warnings; no ranking/dedup/
  file-move changes; slash_completion_hints stays in widgets/mod.rs

Generated with Claude Code
2026-08-07 04:34:19 -07:00
Paulo Aboim Pinto 44524b9d8d feat(FEAT-012): unify command palette onto shared discovery contract
- command_palette.rs: canonical-shadow check now uses
  commands::discovery::user_command_shadows_builtin_canonical over a
  collected metadata slice instead of user_registry.get(command.name)
- palette_description_for_unshadowed_aliases now consumes
  discovery::unshadowed_builtin_aliases (order-preserving projection)
- Removed temporary #[allow(dead_code)] markers from discovery.rs (all three
  predicates now have consumers); module doc note removed
- New palette tests: visible canonical shadow (exactly one user-owned /help
  row with user metadata/action), accepted-alias suppression of built-in
  canonical row, hidden canonical shadow (no discovery row), alias-only
  shadow preserving canonical row without the claimed alias
- Palette suite: 34 passed (30 existing + 4 new); shared 13/13; completion
  23/23 guard green; strict clippy 0 warnings

Generated with Claude Code
2026-08-07 04:34:18 -07:00
Paulo Aboim Pinto ead5d43aac feat(FEAT-012): add shared discovery-shadowing contract with unit tests
- New crates/tui/src/commands/discovery.rs: user_command_shadows_builtin_canonical,
  user_command_shadows_builtin_alias, unshadowed_builtin_aliases (order-preserving)
- Semantics ported from slash-completion predicates (widgets/mod.rs) which are the
  explicit alias-aware reference; palette consumes them in Phase 3
- 13 unit tests: canonical claims, accepted-alias claims, hidden ownership,
  rejected-alias omission, alias projection order, registry-lookup agreement guard
- Registered pub mod discovery in commands/mod.rs
- Temporary #[allow(dead_code)] on the three predicates until Phase 3/4 consumers
  land (recorded in planning-analysis-report.md); MUST be removed there

Generated with Claude Code
2026-08-07 04:34:17 -07:00
CodeWhale Bot 580668bd3e docs: document auto model in config.example.toml and CHANGELOG
When model = auto, dispatcher analyses prompt and selects pro vs flash.
2026-08-07 04:34:12 -07:00
zhaotian1 70191981ac feat(config): add model = auto for prompt-based tier selection 2026-08-07 04:33:49 -07:00
Sun Zhenyuan eed131721f feat(mcp): background incremental registry sync
registry_sync now returns instantly from the local snapshot and
refreshes it in the background: incremental via updated_since, with a
full pagination only when the snapshot is missing or older than a
month, and atomic cache replacement.
2026-08-07 04:33:46 -07:00
copilot-swe-agent[bot] feacbd1e55 WIP: stabilize Tabby IME redraws 2026-08-07 04:33:31 -07:00
CodeWhale Bot 18769cdbe8 fix(release): 0.9.4 stall, budget, and UI polish for session-title fix
- runtime-contract: regenerate tool_catalog for todo_write sole surface (plan/act/operate full/active now list todo_write, not work_update); bumps bytes/sha + prompt stages (agent plugins work)
- web: public-surface-contract expects todo_write (matches docs/RUNTIME_SIMPLIFICATION_DESIGN already)
- source-structure: 676325 -> 676652 (+327) — 321 for bf69e7ff5 session-title fix plus 6 for stall/UI tweaks; durable test asset
- engine: raise no_user_input_continues 12 -> 20 (6 sites) to stop false 'hit (12)' stops on long todo_write loops; preserves anti-runaway
- subagent: GENERAL/PLAN intros now say todo_write, not work_update (child priming fix)
- palette: WHALE_TEXT_HINT #8491AA -> #8A99B3 (+0.4 contrast)
- tui: add BehavioralTipTodoWrite + 15 locale keys (hint: track with todo_write)
- subagent tests: fmt fixes for isolated_fleet_roster_with + assert! expansion

Refs: efcf47a1d, 21ed173cf, ec5747f7d, #5258
2026-08-07 04:32:55 -07:00
Shizuku ecb6c5b53f fix(tui): stop stale cached session title from pinning New Session
build_session_snapshot restored the title from the in-memory cache before
the disk lifecycle merge, and the cache is only refreshed at the end of
the function. A snapshot taken before the first user message therefore
pinned the placeholder title forever: every later snapshot overwrote
the conversation-derived title with the stale cached copy.

Title now resolves in priority order:
1. disk record, when the session already exists (user renames survive
   autosave, #2934/#4397);
2. in-memory cache, when no disk record exists for the session yet;
3. the title computed from the conversation (first user message).

A placeholder that survived from an earlier snapshot yields to the
computed title once a user message exists, healing both fresh and
pre-existing sessions. The placeholder string is centralized in
DEFAULT_SESSION_TITLE so the healing rule cannot drift from the
generator.

Regression tests: stale cached placeholder no longer overrides the
generated title; a persisted placeholder record yields to the computed
title. Existing picker-rename tests (rename survives autosave) still
pass. Full codewhale-tui suite: 9708 passed; 10 failures all verified
pre-existing on main (6) or parallel-flaky (4, pass in isolation).

Reviewed by a sub-agent reviewer: no Critical/Major findings; Minor
findings addressed (comments corrected, placeholder centralized, cache
assertions completed); one documented edge (a session deliberately
renamed to the literal placeholder title yields to the computed title).
2026-08-07 04:32:48 -07:00
CodeWhale Bot 5de9b71697 fix(release): 0.9.4 stall, budget, and UI polish for session-title fix
- runtime-contract: regenerate tool_catalog for todo_write sole surface (plan/act/operate full/active now list todo_write, not work_update); bumps bytes/sha + prompt stages (agent plugins work)
- web: public-surface-contract expects todo_write (matches docs/RUNTIME_SIMPLIFICATION_DESIGN already)
- source-structure: 676325 -> 676652 (+327) — 321 for bf69e7ff5 session-title fix plus 6 for stall/UI tweaks; durable test asset
- engine: raise no_user_input_continues 12 -> 20 (6 sites) to stop false 'hit (12)' stops on long todo_write loops; preserves anti-runaway
- subagent: GENERAL/PLAN intros now say todo_write, not work_update (child priming fix)
- palette: WHALE_TEXT_HINT #8491AA -> #8A99B3 (+0.4 contrast)
- tui: add BehavioralTipTodoWrite + 15 locale keys (hint: track with todo_write)
- subagent tests: fmt fixes for isolated_fleet_roster_with + assert! expansion

Refs: efcf47a1d, 21ed173cf, ec5747f7d, #5258
2026-08-07 04:18:50 -07:00
Shizuku bf69e7ff54 fix(tui): stop stale cached session title from pinning New Session
build_session_snapshot restored the title from the in-memory cache before
the disk lifecycle merge, and the cache is only refreshed at the end of
the function. A snapshot taken before the first user message therefore
pinned the placeholder title forever: every later snapshot overwrote
the conversation-derived title with the stale cached copy.

Title now resolves in priority order:
1. disk record, when the session already exists (user renames survive
   autosave, #2934/#4397);
2. in-memory cache, when no disk record exists for the session yet;
3. the title computed from the conversation (first user message).

A placeholder that survived from an earlier snapshot yields to the
computed title once a user message exists, healing both fresh and
pre-existing sessions. The placeholder string is centralized in
DEFAULT_SESSION_TITLE so the healing rule cannot drift from the
generator.

Regression tests: stale cached placeholder no longer overrides the
generated title; a persisted placeholder record yields to the computed
title. Existing picker-rename tests (rename survives autosave) still
pass. Full codewhale-tui suite: 9708 passed; 10 failures all verified
pre-existing on main (6) or parallel-flaky (4, pass in isolation).

Reviewed by a sub-agent reviewer: no Critical/Major findings; Minor
findings addressed (comments corrected, placeholder centralized, cache
assertions completed); one documented edge (a session deliberately
renamed to the literal placeholder title yields to the computed title).
2026-08-07 03:53:28 -07:00
CodeWhale Bot efcf47a1d1 fix(tui): interactive mid-stream network resume, paste dedup, and fleet type/model wiring for 0.9.4
- Preserve partial assistant output on interactive network/timeout stream
  drops, append a runtime continuation message, and re-issue the request
  bounded by MAX_STREAM_RETRIES.
- Stop sending large pasted text to the model both inline and as a backup
  .md file; submit only the file @-mention.
- Promote `agent { type: "builder", model: "..." }` to a matching fleet
  roster profile when the explicit model matches the profile's pinned
  route; reject with a clearer message when it does not.
- Update CHANGELOG and sync crates/tui/CHANGELOG.

Targeted tests and clippy pass.

Generated with Devin (https://devin.ai)
2026-08-07 03:49:08 -07:00
Paulo Aboim Pinto 5d93dd00e0 fix(web): sync public-surface contract with todo_write canonical naming
Upstream 21ed173cf renamed work_update -> todo_write (canonical; old names
stay hidden replay-only aliases) and updated docs/public-surface-facts.json,
docs/TOOL_SURFACE.md, and docs/RUNTIME_SIMPLIFICATION_DESIGN.md — but left
web/lib/public-surface-contract.test.ts expecting work_update, breaking the
Lint & Type Check gate on every branch:

- defaultActive array: work_update -> todo_write (matches facts file)
- RUNTIME_SIMPLIFICATION_DESIGN expectation: same rename
- web docs tools page: user-visible copy now names todo_write

Full web suite: 250/250 passing.
2026-08-07 12:41:06 +02:00
Paulo Aboim Pinto 3a27a2d813 Merge remote-tracking branch 'origin/main' into feat/FEAT-012-layer-5-3-palette-completion-and-discovery-filte
# Conflicts:
#	scripts/dead-code-budget.json
2026-08-07 12:28:26 +02:00
CodeWhale Bot 21ed173cf1 fix(tui): pre-release repair batch for 0.9.4 — stall honesty, idle wakes, truncation recovery, wait ergonomics
- turn_loop: a mid-stream chunk-timeout now counts toward the stream-error
  budget (stall with nothing streamed retries transparently; an exhausted
  budget fails the turn with the real reason instead of ending Completed
  over a frozen block).
- idle engine: a finished background shell task wakes and starts an ordinary
  runtime turn even without an active goal; a dead provider route claims the
  completion once and reports where the output lives.
- subagent: over-budget final reports spill to a session artifact and the
  truncation footer names the retrieve_tool_result ref; write failures
  degrade to the honest no-ref footer. Test-only wrappers marked cfg(test).
- waits: agents/wait and agent action=wait default to 30 s and cap at 120 s
  (blocked waits deafen the session; settled children report back as
  sentinels). Bash action=wait honors timeout_secs/timeout aliases and
  block; result metadata reports the real wait_timeout_ms.
- todo_write canonical naming: constructor is new(); work_update/TodoWrite/
  todo stay hidden compat aliases; user-visible copy and docs updated.
- behavioral tips: DurableStateWritten fires on successful remember calls;
  enum allow removed. voice.rs and work_surface model use let-chains.
- test: Windows path-separator tolerant artifact footer assertion.
- changelog: 0.9.4 additions (Agent Plugins v1.0.0, send_later, /advisor,
  quiet mode, automation forms, resume_from, transport resilience,
  durability, zh-Hant, update chip, RLM groundwork, stall/wake/truncation/
  wait fixes). Dead-code budget re-baselined to 452.
2026-08-07 02:45:59 -07:00
Paulo Aboim Pinto ac07033abb ci: retrigger windows lane (flaky pwsh detection, no code change) 2026-08-07 11:35:01 +02:00
Paulo Aboim Pinto 1b3600229c ci: retrigger platform test lanes after runner cancellation 2026-08-07 10:55:48 +02:00
Paulo Aboim Pinto 0e61855743 chore(budget): re-baseline source-structure ceiling after rustfmt reflow
The rustfmt pass on the footer-path test assertions (093186a1a) added 4
lines to production truncate.rs, pushing the aggregate to 677475, 4 over
the 677471 ceiling. Tighten ceiling 677471 -> 677475 (measured).
2026-08-07 10:39:28 +02:00
Paulo Aboim Pinto 093186a1a4 fix(tui): assert normalized footer paths in spillover tests
8f2b622dc normalized the model-facing artifact footer to forward slashes
(platform-independent). Two unit tests still asserted the raw OS path
against that footer, failing on the Windows lane:

- truncate::adaptive_evidence_footer_names_artifact_path_and_recovery
- subagent::subagent_tool_results_spill_to_disk_and_stay_bounded_inline
  (upstream fa7c4b055)

Assert the normalized form via format_artifact_relative_path, matching
what the footer actually emits.
2026-08-07 10:23:57 +02:00
Paulo Aboim Pinto 43a55ce87b chore(budget): re-baseline runtime contract for upstream todo_write rename
Upstream ec5747f7d ("fix: todo_write sole progress surface + §3d/4a test
alignment", 0.9.4) renamed work_update -> todo_write in the tool catalog,
and the 0.9.4 WIP prompt tightening (b6585ea99) grew the system prompt and
representative-stage identities. Neither re-baselined
scripts/runtime-contract-budget.json, leaving origin/main itself red on
this gate.

Sync all 49 drifted contract metrics to the measured receipt:
- tool_catalog tool_names/identity digests/bytes/tokens for plan, act,
  operate x active/full (work_update -> todo_write; +125 bytes per surface)
- system_prompt bytes/tokens for all modes (prompt tightening)
- representative_context stage identities and byte counts

The contract budget is a snapshot of the code; this locks the new identity
per the gate's own "explicit maintainer decision" rule. No FEAT-012
changes contribute to the drift.
2026-08-07 09:50:23 +02:00
Paulo Aboim Pinto f0e4d8a266 chore(budget): re-baseline dead-code ceiling for upstream WIP growth
Upstream commit b6585ea99 (WIP: 0.9.4 fence, turn liveness, model picker,
budget, and contributor credit, merged 2026-08-06) added three
#[allow(dead_code)] attributes without bumping the dead-code budget,
leaving both origin/main and this branch 3 over the 451 ceiling:

- stuck_guard.rs: StepFingerprint::waiting_for_subagents (test-only ctor)
- turn_loop.rs: should_hold_turn_for_subagents (test-only, #3216)
- behavioral_tips.rs: enum BehavioralTip (3 of 6 variants unconstructed)

None are removable without deleting test-only constructors or enum
variants, so re-baseline 451 -> 454 per the gate's own guidance. Our
FEAT-012 code contributes 0 net allows (Phase 2 added 3, Phase 3 removed
them). Pay down in the #4785 sweep.
2026-08-07 09:36:14 +02:00
Paulo Aboim Pinto c9a45fc382 Merge remote-tracking branch 'origin/main' into feat/FEAT-012-layer-5-3-palette-completion-and-discovery-filte
# Conflicts:
#	scripts/source-structure-budget.json
2026-08-07 09:30:45 +02:00
Paulo Aboim Pinto 8f2b622dc5 fix(tui): normalize artifact footer path separators for Windows
Fixes an upstream Windows-lane test failure introduced by #5212
(commit f0a6898c3, "fix(tui): honest large-output truncation + recovery
path").

#5212 flipped the adaptive-evidence contract so the model-facing
truncation footer MUST name the on-disk artifact path, but the footer
kept building that path with absolute_path.display().to_string(), which
on Windows emits backslashes (\artifacts\) while the acceptance test
asserts the POSIX form (/artifacts/).

- truncate.rs: the truncated_preview recovery_path (both the adaptive
  evidence path and the legacy spillover fallback) now goes through
  crate::artifacts::format_artifact_relative_path, which normalizes
  separators to '/' — the same normalization the artifact_relative_path
  metadata field already used.
- Consumers that read the footer path back (tool_routing, retrieval,
  UI preview) construct PathBuf from the string, and PathBuf::from
  accepts forward slashes on Windows, so no behavior change beyond the
  model-facing text being platform-independent.

The failing test (headless_bash_success_and_failure_are_distinct_
bounded_exact_evidence) fails identically on upstream main without this
PR's changes; this commit lands the repair inside the Layer 5.3 PR to
unblock the Windows lane.

Paulo Aboim Pinto
2026-08-07 09:09:41 +02:00
Sun Zhenyuan 1c67aa38cb feat(mcp): background incremental registry sync
registry_sync now returns instantly from the local snapshot and
refreshes it in the background: incremental via updated_since, with a
full pagination only when the snapshot is missing or older than a
month, and atomic cache replacement.
2026-08-07 14:00:19 +08:00
zhaotian1 4847397f57 docs: document auto model in config.example.toml and CHANGELOG 2026-08-07 13:56:58 +08:00
zhaotian1 3273a7ae26 feat(config): add model = auto for prompt-based tier selection 2026-08-07 13:33:49 +08:00
CodeWhale Bot d57ce9d06f fix(clippy): needless borrow in work_surface model 2026-08-06 20:37:24 -07:00
CodeWhale Bot ec5747f7d7 fix: todo_write sole progress surface + §3d/4a test alignment (0.9.4)
- canonical progress tool is todo_write only (not 4 names): work_update/TodoWrite/todo are hidden compat aliases (model_visible=false) for replay
- prompts/text.rs AGENT_MODE/PLAN_MODE now say call todo_write (not work_update)
- todo.rs CANONICAL_PROGRESS_TOOL=todo_write, description and DEFAULT_ACTIVE_NATIVE_TOOLS updated
- registry with_todo_tool registers work_update as alias (no duplicate), tool_category and missing_tool hints updated
- fix 6 prompt/registry/engine tests + 5 follow-on failures (default_active, missing_tool, tool_category, compressed invariant, todo metadata)
- fix subagent liveness: list_filtered now shows current terminals + prior Running without handle, test helpers get live handle via leaked runtime

RUST_MIN_STACK=16777216 cargo test -p codewhale-tui --bin codewhale-tui: 9919 passed, 0 failed
cargo build --release -p codewhale-tui: ok
2026-08-06 20:27:42 -07:00
CodeWhale Bot e733c00892 fix(turn): honest REPL failures and tool-error streak (NOTE §9-10)
- REPL init/refresh failure now sets turn_error -> Failed not Completed
- reset consecutive_tool_error_steps on no-tool steps
2026-08-06 18:48:00 -07:00
CodeWhale Bot 1699a7cf71 fix(roster/liveness): live counts and ticking (4a/4b)
- list_filtered(false) now only live Running with task_handle and heartbeat (4a)
- SubAgentResult gains started_at for live elapsed; snapshot_for_listing copies it
- work_surface agent_elapsed_ms derives from started_at at render when Running
- patched all manual SubAgentResult literals to include started_at: None
- wait default 300->30s and guidance prefers ending turn (4c)
2026-08-06 18:46:23 -07:00
CodeWhale Bot 180271efaf fix(web/docs): sync 0.9.4 contributor credits and restore media plan placeholder
- add @mky and @cacdcaecawae to web release-credits and docs/CONTRIBUTORS
- restore docs/releases/v0.9.2-media-plan.md placeholder after move to ops
2026-08-06 18:37:27 -07:00
CodeWhale Bot 9e2c929bf1 fix(audit): bump js-yaml in extensions/vscode (GHSA-5p4m-2wfm-xmqj) 2026-08-06 18:34:34 -07:00
CodeWhale Bot b6585ea990 WIP: 0.9.4 fence, turn liveness, model picker, budget, and contributor credit
- fix rlm/turn.rs build (build_metadata_message) and honest/empty guard
- fold Unreleased� 0.9.4 dated 2026-08-07 and sync changelog
- fix Meta facts (  (3 and,�, contrib) and provider picker fallback
- tighten prompts (backticks for work_update)
- stick guard: Warn/Stop across flav...
2026-08-06 18:30:13 -07:00
Paulo Aboim Pinto 24e4ac9b9c chore(budget): re-baseline source-structure ceiling for Layer 5.3 acceptance harness
FEAT-012 adds the shared discovery-shadowing contract (discovery.rs, 235
lines) and the Gherkin acceptance harness (epic_discovery_acceptance.rs,
750 lines) mirroring the accepted FEAT-011 pattern. Aggregate owned Rust
source 673375 -> 674554 (+1179 lines). No new 1000-line modules.

Pay down in v0.9.5 per the existing budget TODO notes.
2026-08-07 03:06:13 +02:00
Paulo Aboim Pinto 0527358f86 fix(tui): make git-repo-root no-repo test layout-independent
- git_repo_root_reports_attempted_paths_when_no_repo_found created its
  harness in the checkout's parent dir; when the checkout is nested inside
  another git repo (e.g. a workspace repo with sibling checkouts), the
  harness itself resolved to that parent repo and the no-repository path
  was never exercised
- Use the system temp dir with deep nesting beyond the parent-search limit,
  mirroring the sibling create_isolated_worktree no-repo test

Boy Scout repair found by the FEAT-012 Phase 8 full workspace gate; test-only,
no behavior change to git_repo_root itself.
2026-08-07 02:52:04 +02:00
Paulo Aboim Pinto 070eebfad7 feat(FEAT-012): add Gherkin discovery-filtering acceptance harness
- New tests/features/feat-012-discovery-filtering.feature: 7 scenarios
  (AC1-AC6 + AT-010 alias-aware unification) covering all six FEAT-012
  acceptance criteria and EPIC AT-008/009/010
- New commands/epic_discovery_acceptance.rs: scenario-level cucumber worlds
  bound to live palette builder, live slash completion, and live dispatch;
  fail_on_skipped + non-zero passed-step assertions per scenario
- Registered module in commands/mod.rs
- docs/architecture/command-dispatch.md: module map row for shared
  discovery.rs owner
- feat012 selector: 7/7 scenarios pass, zero skipped, non-zero steps each
- Guards: discovery 13/13, palette 34/34, completion 24/24; strict clippy
  0 warnings

Generated with Claude Code
2026-08-07 02:52:04 +02:00
Paulo Aboim Pinto 37160d6f8d feat(FEAT-012): unify slash completion onto shared discovery contract
- widgets/mod.rs: builtin_visible_for_completion_match and push_command_entry
  now consume commands::discovery predicates; local duplicate
  user_command_shadows_builtin_canonical/_alias definitions deleted
- New completion test: slash_completion_accepted_user_alias_claims_builtin_canonical_token
  (user alias claiming a built-in canonical token suppresses the built-in
  suggestion and surfaces the user command)
- Completion suite: 24 passed (23 existing + 1 new); discovery 13/13 and
  palette 34/34 guards green; strict clippy 0 warnings; no ranking/dedup/
  file-move changes; slash_completion_hints stays in widgets/mod.rs

Generated with Claude Code
2026-08-07 02:52:04 +02:00
Paulo Aboim Pinto 267c6dc10f feat(FEAT-012): unify command palette onto shared discovery contract
- command_palette.rs: canonical-shadow check now uses
  commands::discovery::user_command_shadows_builtin_canonical over a
  collected metadata slice instead of user_registry.get(command.name)
- palette_description_for_unshadowed_aliases now consumes
  discovery::unshadowed_builtin_aliases (order-preserving projection)
- Removed temporary #[allow(dead_code)] markers from discovery.rs (all three
  predicates now have consumers); module doc note removed
- New palette tests: visible canonical shadow (exactly one user-owned /help
  row with user metadata/action), accepted-alias suppression of built-in
  canonical row, hidden canonical shadow (no discovery row), alias-only
  shadow preserving canonical row without the claimed alias
- Palette suite: 34 passed (30 existing + 4 new); shared 13/13; completion
  23/23 guard green; strict clippy 0 warnings

Generated with Claude Code
2026-08-07 02:52:04 +02:00
Paulo Aboim Pinto 795d9542a3 feat(FEAT-012): add shared discovery-shadowing contract with unit tests
- New crates/tui/src/commands/discovery.rs: user_command_shadows_builtin_canonical,
  user_command_shadows_builtin_alias, unshadowed_builtin_aliases (order-preserving)
- Semantics ported from slash-completion predicates (widgets/mod.rs) which are the
  explicit alias-aware reference; palette consumes them in Phase 3
- 13 unit tests: canonical claims, accepted-alias claims, hidden ownership,
  rejected-alias omission, alias projection order, registry-lookup agreement guard
- Registered pub mod discovery in commands/mod.rs
- Temporary #[allow(dead_code)] on the three predicates until Phase 3/4 consumers
  land (recorded in planning-analysis-report.md); MUST be removed there

Generated with Claude Code
2026-08-07 02:52:04 +02:00
Paulo Aboim Pinto 9accef6092 fix(tui): repair baseline clippy errors found by FEAT-012 Phase 0 lint gate
- structcopy.rs: simplify nonminimal boolean in next_absolute_path_start (no behavior change)
- latex_render.rs: remove always-true '|| true' last-row guard in parse_rows, which emitted a spurious empty row after a trailing row separator; add regression test

Boy Scout repairs of pre-existing issues on origin/main so the configured strict lint gate is green.
2026-08-07 02:52:03 +02:00
CodeWhale Bot 7242381022 docs: move internal planning out of the public repo
The public repo carried maintainer process that is not contributor-facing
contract: perishable lane state, the release queue, the issue-triage
standard, dated audits and state matrices, per-release completion ledgers,
QA evidence, and design specs. All of it moves to the private
`codewhale-ops` repo, which already holds this class of document.

Moved: docs/ops/CURRENT.md, RELEASE_QUEUE.md, AGENT_READY_ISSUES.md,
MODEL_PROVIDER_AUDIT.md, CONSTITUTIONAL_KERNEL_AUDIT.md, the dated
TUI_DOG_008 state matrix, TUI_METAMORPHOSIS.md,
RECURSIVE_SELF_IMPROVEMENT.md, TTC_DESIGN.md, and the docs/releases/,
docs/evidence/, and docs/superpowers/ trees.

Two were moved and put back. `PREVIEW_REQUEST.md` is cited from
request_manifest.rs, client.rs, and engine/preview.rs, and
`RUNTIME_SIMPLIFICATION_DESIGN.md` is listed in
docs/public-surface-facts.json, which the web vocabulary tests pin. Those
are load-bearing references, not planning notes.

Every surviving link was repointed rather than left dangling: AGENTS.md,
crates/tui/AGENTS.md, CONTRIBUTING.md, docs/ISSUE_TRIAGE.md,
docs/CATALOG_REFRESH.md, docs/AGENT_RUNTIME.md. `npm run check:docs`
passes.

Also re-baselines the source-structure budget for the [Unreleased] work
merged this session (673375 -> 676325 aggregate, 17596 -> 17631 max
module, 175 -> 176 large modules) and declares plugins/agent_plugin.rs as
an allowed thousand-line module. Unrelated to the doc move; the gate simply
had not been re-run since Agent Plugins landed.
2026-08-06 17:24:27 -07:00
CodeWhale Bot 8b5df3f669 docs(ops): record the desktop-app fork and the visual identity contract
Adds two sections a successor needs before touching either surface.

The VS Code fork: cloned at /Volumes/VIXinSSD/CW/vscode, product.json
rebranded, and — the part most likely to be undone by someone helpful —
`extensionsGallery` pointed at Open VSX. The Microsoft Marketplace ToU
restrict it to Microsoft products and a fork aimed at it violates them
without failing loudly. Also states the honest cost: icons, toolchain,
signing, and then rebasing on upstream forever, which is the real expense.

Visual identity: the mark is Signal Current, defined in web/components/
whale.tsx from the managed product contract, and its two path strings now
live in three files — which is exactly how the extension ended up shipping
a different whale. Names the duplication so the next person collapses it
instead of adding a fourth copy.

Includes the real TUI palette from crates/tui/src/palette/tokens.rs so
matching the extension to the product is a table lookup rather than taste,
with two cautions: webviews must still respect the user's editor theme via
var(--vscode-*) or they look broken in light mode, and the TUI's ambient
touches are characterful rather than decorative — port the restraint, and
give anything animated a reduced-motion path. Notes CWC should take the
same palette so the four surfaces stop diverging.
2026-08-06 17:20:29 -07:00
CodeWhale Bot 4ac9bc61d3 fix(vscode): use the Signal Current mark, not a one-off whale
The extension shipped its own slate/sky whale that existed nowhere else in
the product. The canonical mark is Signal Current — `web/app/icon.svg` and
the WHALE_BODY / WHALE_CURRENT constants in `web/components/whale.tsx`,
described there as "from the managed Codewhale product contract."

Replaced with the canonical paths (verified byte-identical to icon.svg) on
the product color tokens: signal gold #F6C453 and current cyan #48D7FF.
A comment names the two other copies so the next person keeps all three in
step rather than inventing a fourth.
2026-08-06 17:18:52 -07:00
CodeWhale Bot a75a475b99 docs(ops): add the publish handoff
Records the state a successor needs: main is 22 commits past what the
v0.9.4 notes describe, what is verified locally versus never seen by CI
(RUST_MIN_STACK in particular), the three blockers before publishing, and
the traps that cost this session real time.
2026-08-06 17:13:22 -07:00
CodeWhale Bot e9b6afd87c Merge branch 'codex/website-polish' (motion)
Scroll reveals, hover/focus states, restrained depth, and state-change
feedback — each with a reduced-motion path, no animation library, and
paint-only properties so layout shift stays zero.
2026-08-06 16:42:10 -07:00
CodeWhale Bot dd61f99b1d feat(web): motion and depth within the editorial design language
Entrance reveals on scroll, considered hover/focus states, restrained depth
on the terminal and card surfaces, and feedback on state changes — sized to
a print object coming to life rather than a landing page.

Every effect has a reduced-motion path written alongside it, not bolted on:
all four components check `prefers-reduced-motion` and the CSS carries its
own block. That block also avoids the usual reduced-motion bug — freezing
the ticker would strand every entry past the fold, so the track stops
moving and becomes scrollable instead of simply halting.

No animation library. `package.json` is untouched; this is CSS transitions,
the Web Animations API, and IntersectionObserver.

Nothing animated triggers layout. The properties in play are transform,
color, border-color, and background-size (underline draws) — all paint, so
cumulative layout shift stays zero. The brief asked for transform/opacity
only; the paint-only additions keep that intent.

The accessibility work from the previous pass is intact and re-checked:
the white/55 contrast value, the roving tabindex and aria-controls wiring,
and the mobile-menu focus return all survive.

Known, judged acceptable: focus stays inside the menu during its 170ms exit
fade and returns to the toggle on unmount; pointer interaction is disabled
for that window.

Drafted by Kimi K3 in Codewhale exec on an isolated worktree. Gates re-run
here: 250 tests, eslint clean.
2026-08-06 16:42:09 -07:00
CodeWhale Bot fd1489c818 Merge branch 'codex/agent-plugins'
Agent Plugins v1.0.0 consume, publish, and name slugification.

Discovery prefers `plugin.json` and falls back to `plugin.toml`; both parse
into the existing PluginManifest, so registry, trust, staging, and skills
are untouched. Codewhale-only fields round-trip through
`extensions["net.codewhale"]`, unknown namespaces are dropped rather than
rejected — the point of the standard — and `mcp_servers` map to and from a
sibling `mcp.json`, since plugin.json's root is closed. `/plugin export`
emits a spec-valid bundle into a fresh directory without touching the
source.

The implementation corrected four things the design doc got wrong about
this codebase, which is why it is worth reading before the next pass:

- PluginManifest was NOT a superset of the standard. It had no `homepage`,
  `repository`, `license`, or `keywords`, and models `author` as a bare
  string, so those were added and the structured author is mapped both ways.
- The name rules genuinely conflict. Codewhale historically allows `--` and
  bans dots; the standard bans `--`/`..` and allows dots. Holding
  `plugin.toml` to the standard rule would have stopped existing plugins
  like `a--b` loading at all — and made export-time slugification
  unreachable, since no registry could then hold a non-conforming name. So
  toml keeps its historical rule, json enforces the standard, and export
  slugifies between them with the original kept as a display name.
- The mcp.json transport discriminant was never specified; `type` is now
  emitted and consumed explicitly, inferred from command-vs-url when absent.
- `/plugin export` had no destination in the spec, so it takes an explicit
  target directory — writing into the source bundle would be the migration
  half, which is deliberately out of scope.

Known interop limit, stated rather than discovered later: Codewhale's trust
model still applies to third-party bundles. Literal `env` values and literal
headers are rejected (env must be exact `${VAR}` placeholders) and
`capabilities.network_hosts` must cover remote MCP hosts, so some in-the-wild
plugins will need those two idioms adjusted before they load here.

Built by Kimi K3 in Codewhale exec on an isolated worktree. Verified here:
147 plugin tests, full suite 9941 passed, fmt clean.
2026-08-06 16:35:55 -07:00
CodeWhale Bot c44cd9e96c feat(tui): Agent Plugins v1.0.0 consume, publish, and name slugification
Implement the vendor-neutral agent-plugins.org format per
docs/superpowers/specs/2026-08-06-agent-plugins-design.md:

- discovery prefers plugin.json and falls back to legacy plugin.toml;
  both parse into PluginManifest, so nothing downstream of discovery
  changes. Codewhale-only fields (commands, agents, hooks, lsp, native,
  capabilities, when, display_name) round-trip through
  extensions["net.codewhale"]; unknown extensions namespaces are
  ignored, never rejected. mcp_servers map to/from a sibling mcp.json
  with stdio / streamable-http / sse transports; PLUGIN_ROOT and
  PLUGIN_DATA are reserved env names.
- /plugin export <name> <dir> publishes a spec-valid bundle
  (plugin.json + mcp.json when servers exist + the skills/ tree) into a
  fresh directory; every emission is re-validated against the
  standard's shape before writing; custom skills layouts normalize to
  the standard skills/ root, with collisions as errors.
- names invalid under the standard are slugified on publish and the
  original is preserved as the display name; a slug colliding with an
  existing plugin is an error, never a silent rename. plugin.toml keeps
  its historical name rule so existing bundles (e.g. `--` runs) keep
  loading; plugin.json is held to the standard rule.
- install/staging/runtime-receipt lanes resolve either manifest name;
  tarball installs accept a dual-published bundle root. The plugin.toml
  content-hash domain stays byte-identical; plugin.json starts a fresh
  receipt family. On-disk auto-migration is intentionally not included.

Agent-implemented per the approved design doc.
2026-08-06 16:30:31 -07:00
CodeWhale Bot 411ec84739 fix(vscode): stop reporting Connected to a runtime that will reject every call
`/health` and `/v1/runtime/info` are intentionally unauthenticated, so a
token-protected runtime answers both with 200. The extension read that as
success and showed "Connected" — then every `/v1/*` fetch failed with 401
and the view sat there empty with no explanation.

The info body already carries the real signal (`auth_required`, see
crates/tui/src/runtime_api.rs). Read it: no token plus `auth_required`
now reports auth-required and names the setting that fixes it.

Audited the rest and found it sound: all six contributed commands are
registered and implemented, activationEvents match, `serve --http --host
--port --auth-token` all exist in the real CLI, the default port matches,
the docs URL target exists, timeouts degrade to an honest offline state,
and the webview CSP/nonce/escaping hold.

Verified by Kimi K3 against a live runtime rather than a passing compile:
no token -> auth-required; with token -> connected, version 0.9.4; nothing
listening -> offline; threads parse with keys matching the TS interface.
`vsce package` produces a clean 12-file vsix, rebuilt here to confirm.

Not verified anywhere: rendering inside a real VS Code host. The data
paths and command wiring are exercised, but webview layout and status-bar
placement need `code --install-extension` on a machine with the CLI.
2026-08-06 16:19:49 -07:00
CodeWhale Bot 9ccfddeba1 Merge branch 'codex/website-polish'
Website defect fixes ahead of relaunch: mobile-menu focus management,
the ARIA tabs pattern behind the terminal player's tablist, a WCAG AA
contrast failure (4.32:1 -> 5.78:1), SoftwareApplication JSON-LD sourced
only from repo-proven facts, and the missing Indonesian og:locale.
2026-08-06 16:11:33 -07:00
CodeWhale Bot 74b5192110 fix(web): accessibility, structured data, and metadata defects
Defect fixes ahead of relaunch — no redesign, no new copy, no media.

Accessibility:
- The mobile menu declares `aria-modal`, which promises the dialog owns
  interaction, but focus never entered it and never came back. Focus now
  moves inside on open and returns to the toggle on close. The toggle node
  is captured before cleanup rather than read from the ref inside it, which
  would race React clearing it.
- The terminal player had `role="tablist"` with none of the pattern behind
  it. Adds arrow-key/Home/End movement, a roving tabindex, and
  aria-controls/aria-labelledby wiring between each tab and the panel.
- Context text ran at white/45 on ink — 4.32:1, below the 4.5:1 WCAG AA
  floor for normal text. Now white/55, 5.78:1.

Metadata:
- SoftwareApplication JSON-LD on the home page. Every field traces to a
  repo-sourced fact already rendered on the page; `softwareVersion` is
  omitted rather than guessed when facts carry none.
- `id` was missing from the OpenGraph locale map, so Indonesian pages
  emitted no og:locale.

Drafted by Kimi K3 in Codewhale exec, reviewed here. The claimed contrast
ratio was recomputed independently (5.78:1 vs its stated 5.75:1 — a
rounding difference against the exact ink token, and the before value does
genuinely fail AA). Gates re-run here: 250 tests, eslint clean.

Note for follow-up, not introduced by this change: `npm audit` in web/ now
reports 1 high-severity js-yaml advisory. The release contract requires 0.
2026-08-06 16:11:31 -07:00
CodeWhale Bot 43f933ca5d Merge embedder-owned sub-agent state roots
Harvested from PR #5252 by @cacdcaecawae

Adds an optional `EngineConfig::subagent_state_root` so an embedding host
can own delegated-agent lifecycle storage instead of sharing
`<workspace>/.codewhale/state` with every other conversation bound to the
same project. The worker ledger, transcript artifacts, coordination lock,
cleanup, and `resume_from` reads move behind the selected root; child
execution cwd, file authority, and receipts are unchanged.

Unset leaves the legacy default byte-for-byte, so nothing changes for
anyone who does not opt in.

The PR is explicit that it is a partial building block for #4416 and
deliberately does not implement durable session ownership, cross-session
history union, or write arbitration — and therefore does not close it. It
also documents that distinct state roots are distinct coordination domains
and do not arbitrate writes to a shared execution workspace, which is the
limitation most likely to be misread as isolation.
Co-authored-by: cacdcaecawae <109055297+cacdcaecawae@users.noreply.github.com>
2026-08-06 16:04:35 -07:00
CodeWhale Bot 147e407464 Merge FreeBSD build fix
Harvested from PR #5254 by @mky

rquickjs ships no pre-generated bindings for FreeBSD, so `rquickjs-sys`
failed to compile there:

  rquickjs probably doesn't ship bindings for platform
  `x86_64-unknown-freebsd(n/a)`. try the `bindgen` feature instead.

Adds the `bindgen` feature for `cfg(target_os = "freebsd")`, mirroring the
NetBSD and Android blocks already directly above and below it — same
shape, same rationale, same comment form.
Co-authored-by: mky <817223+mky@users.noreply.github.com>
2026-08-06 16:04:11 -07:00
CodeWhale Bot 6dba4934c3 fix(client): make SSE header stalls retryable instead of fatal
An SSE open failure returned a bare `anyhow!` string, so it never became an
`LlmError` and `is_retryable()` never saw it. The shared retry layer
therefore treated a transport stall as a permanent failure and killed the
turn.

Sub-agents survived it anyway, because they carry their own classifier that
text-matches the message
(`transient_provider_classifier_matches_sse_header_timeout`) and retry with
backoff and a checkpoint. Root turns had no such path. So the same transient
network event that a child shrugged off would destroy a long root run — and
did: a delegated Kimi K3 agent lost a half-finished edit to exactly this,
leaving a non-compiling file behind.

All three open failures (H2 stall, H1-pinned stall, H1 fallback error) are
now `LlmError::NetworkError`, which the shared layer already classifies as
retryable. The guidance text is unchanged, so `CODEWHALE_FORCE_HTTP1=1`
still surfaces for the proxy/Windows case. The existing single-retry H1
twin is untouched — this is about what happens when that is exhausted.

A test now pins that a header stall downcasts to a retryable LlmError, so
the classification cannot silently regress to a string again.
2026-08-06 15:58:53 -07:00
CodeWhale Bot 091f675350 Merge branch 'codex/rlm-bindings'
Sub-agent billing attribution: the routing bug and the blindness that hid it.

- `FleetLoadout::Fast` mapped to `ModelRoute::Faster`, the "cheap sibling",
  which routes a child off the parent's model. A scout takes `Fast` by
  default, so a parent turn on a deliberately-priced route spawned children
  billed as something else. A loadout says how much work a role should do,
  not what it costs; `Fast` now inherits.
- Nothing surfaced it. In stream-json, `AgentSpawned` hit a catch-all that
  emitted nothing, so a live run showed six agent events and exactly one
  `model` field — the parent's. There is now an `agent_spawned` event
  carrying the child's model, depth, parent, and route source.
- Also lands the RLM block-intent scanner: static extraction of what a
  Python block intends to do, so the REPL can eventually act under one
  informed approval instead of per-call prompts or a blind yes. It fails
  toward disclosure — computed arguments are reported as undecidable rather
  than omitted.

Found because a human noticed a number on an invoice, not because any test
or check in this repo caught it.
2026-08-06 15:49:02 -07:00
CodeWhale Bot 497a426d05 feat(exec): report the model every sub-agent actually ran on
In stream-json mode `Event::AgentSpawned` fell into a catch-all that
emitted nothing:

    Event::AgentSpawned { .. } | AgentProgress { .. } | AgentComplete { .. } => {}

So a delegated child was invisible to anything reading the stream. A live
run proved it: parent on muse-spark-1.2-contributor, six agent events, a
scout child, and exactly one "model" field in the entire output — the
parent's.

That blindness is what let the Fast loadout re-price scout children onto a
cheaper sibling unnoticed until it showed up on an invoice. The routing bug
is fixed; this closes the reason it went unseen.

Adds an `agent_spawned` stream event carrying the child's id, the model it
was installed with, spawn depth, parent run id, and — where the spawn path
resolved one — the route source. The model is read at the spawn seam from
the child's own record, so a later `/model` switch cannot rewrite a
launched child's attribution.

`route_source` is honestly absent on the manager spawn path rather than
guessed: provenance is resolved on the workflow seam
(WorkflowTaskSpawnMetadata), and the half that determines billing is
present either way.

Verified live on the same scenario that exposed the gap:
  {"type":"agent_spawned","id":"agent_02ed07f3",
   "model":"muse-spark-1.2-contributor","spawn_depth":1}

Started by Kimi K3 in Codewhale exec (it found the ExecStreamMeta naming
and drafted the event fields); its run died on an SSE timeout mid-edit and
was finished here.
2026-08-06 15:48:49 -07:00
CodeWhale Bot 2e0a3ab3c0 fix(fleet): stop the Fast loadout silently re-pricing child work
A `Fast` loadout mapped to `ModelRoute::Faster` — the "cheap sibling" —
which routes a child off the parent's model onto whatever the provider's
cheaper alternative is. A scout sub-agent takes `Fast` by default, so a
parent turn deliberately running a specifically-priced route spawned
children billed as something else entirely.

Nothing surfaced it. Verified against a live run: the parent's model
appears in `exec --output-format stream-json` as
`"model":"muse-spark-1.2-contributor"`, six `agent` events and a `scout`
child follow, and the child's model appears nowhere in the output. The
only place the re-pricing showed up was the invoice.

A loadout is a statement about how much work a role should do, not
authority to re-price it. `Fast` now inherits the parent route like every
other default. `Auto` still resolves to the cheap sibling, so opting into
one remains possible — but only deliberately.

The five tests that asserted the old behavior now assert the new contract,
including the router-parity test that specifically pinned "fleet fast
loadout resolves to the provider cheap sibling".

Still open and worth fixing separately: sub-agent model attribution is
absent from structured output, so any future routing surprise would be
equally invisible.
2026-08-06 15:28:32 -07:00
CodeWhale Bot bb8f6d8ca1 feat(rlm): static intent extraction for code blocks
Groundwork for letting the RLM REPL act, not just read. Prime Intellect's
RLM has file operations, shell, and tool use happening through code;
Codewhale's REPL has context management and sub-queries but no way to act,
so it can reason and delegate and not much else.

The blocker to adding action bindings is approval. Every gate in Codewhale
keys on tool identity — ApprovalRequirement per ToolSpec, execpolicy and
command_safety on shell, sandbox policy per tool. One Python block can
perform ten gated operations, so prompting per call makes the surface
unusable while approving a blind block gives up the policy layer that
Prime's own README says it does not have.

This reads a block before it runs and reports what it intends to do, so one
approval can still be an informed one. It fails toward disclosure: literal
arguments are reported exactly, computed ones (f-strings, variables, calls,
concatenation) are recorded as undecidable rather than omitted. A manifest
that under-reports would turn "I don't know" into "nothing will happen".

Over-reporting is treated as its own failure — binding names inside
comments and strings are not calls, because a manifest listing operations
that never happen trains people to approve without reading.
2026-08-06 15:04:32 -07:00
CodeWhale Bot eca3d0e21a docs(changelog): record the memory-maintenance and visibility work under [Unreleased]
The 0.9.4 section is dated and closed, so this work belongs to the next
release even though it now sits on main. v0.9.4 is therefore tagged at
6b7eb20ef — the commit its CI validated and its notes describe — not at
main's head.
2026-08-06 14:57:31 -07:00
CodeWhale Bot cafa4e5e15 Merge branch 'codex/visibility-tips'
Model-facing memory maintenance, the audit trail for durable state, and the
first tip that tells a user any of it is happening.

- memory gains `revise` and `retire` beside `append`, both requiring the
  exact target note and the evidence for the change; append-only memory
  decays as corrections stack up behind what they contradict.
- every in-place memory edit and every harness refine/remove is journalled
  with before/after/evidence. Harness removal previously left no record at
  all, and the entry leaves state entirely.
- the journal is excluded from indexing: it lives in the memory tree, so
  `collect_markdown` was picking it up and re-injecting retired notes into
  prompts under their `before:` lines.
- `memory_path` pointed at an already-native store no longer nests a second
  one inside it and writes to the wrong file.
- `muse`/`muse-spark` resolved to 1.1 in the agent registry while config had
  defaulted to `muse-spark-1.2`; the registry now carries 1.2 and the
  contributor variant.
- `BehavioralTip::DurableStateWritten` tells the user the first time
  Codewhale saves something durable, in all fifteen complete locale packs.

These land under [Unreleased], not 0.9.4 — that section is dated and closed.
v0.9.4 must therefore be tagged at 6b7eb20ef, the commit its CI validated and
its notes describe, not at main's head.
2026-08-06 14:57:05 -07:00
CodeWhale Bot 8c05b84379 feat(tips): surface durable state the first time the model writes it
`remember` and the `harness` tool write state that persists across
sessions and shapes later prompts, and nothing ever told the user it
happened. The tip catalog covered planning, receipts, cleared input, MCP,
and hotbar — nothing about the state Codewhale keeps about you. The moment
it first saves something is the moment to say so.

Adds `BehavioralTip::DurableStateWritten` ("Saved · /memory to inspect"),
translated into all fifteen complete packs. The command stays literal and
composed in code.

Drafted by Meta muse-spark-1.2-contributor running in Codewhale exec
--auto; reviewed and verified here. The Korean particle was spaced as a
separate word (`{command} 에서`); Korean particles attach to the preceding
noun, so it is now `{command}에서`.
2026-08-06 14:52:36 -07:00
Marek Krawczyk 5eb0385e8f Generate rquickjs bindings for FreeBSD at build time. 2026-08-06 23:28:07 +02:00
CodeWhale Bot 50cb6df2f2 fix(config): stop nesting a second native store inside a configured one
`memory_path` is historically a legacy single-file setting
(`$CODEWHALE_HOME/memory.md`), and the native store is derived from its
parent directory. That is right for the default and for anyone still
carrying the old value.

It is wrong for the obvious reading of the name. Pointing `memory_path`
(or `CODEWHALE_MEMORY_PATH`) at a native store produced
`…/memory/global/memory/global/MEMORY.md` — a second store nested inside
the first — so writes silently landed somewhere other than the file the
user named, and the file they named stayed stale. Found while testing the
memory work against a hand-built store.

Honour an already-native path as itself; keep deriving from the parent
otherwise. The default is unchanged.
2026-08-06 14:24:28 -07:00
CodeWhale Bot c329d37fc4 feat(memory): let the model revise and retire its own durable notes
`remember` could only append. Durable memory that only grows decays: a
corrected fact sits behind the wrong one it contradicts, both get injected
next session, and the prompt block drifts toward noise. The model could
notice a note had gone stale and had no way to act on it.

Adds `revise` and `retire` alongside the default `append`. Both name the
exact note they target and both require evidence — what in the session
justified the change. Ambiguity fails closed: a target matching zero or
several notes is an error rather than a guess, because guessing silently
rewrites the wrong durable fact.

Every in-place edit is journalled to `memory/JOURNAL.md` with before,
after, and evidence, so a retired note stays recoverable and drift is
reviewable after the fact. The same trail now covers the continual
harness, where `refine` and `remove` previously left no record at all —
removal especially, since the entry leaves state entirely.

The journal is excluded from indexing. It is Markdown in the memory tree,
so `collect_markdown` picked it up and its `before:` lines re-entered the
searchable set — putting every retired note straight back into the prompt
under a new name. An audited memory that resurrects what it retires is
worse than an unaudited one; a test now pins this.

Verified end-to-end against Meta `muse-spark-1.2-contributor`: given a
changed fact and an obsolete one, the model revised the first and retired
the second unprompted, and the journal recorded both with evidence.
2026-08-06 14:18:51 -07:00
CodeWhale Bot 6187221e0f docs(spec): Agent Plugins v1.0.0 support design
Adopt plugin.json as the native manifest format, with plugin.toml
legacy-readable and auto-migrated inside Codewhale's managed plugin root.
Codewhale-specific manifest fields move under extensions["net.codewhale"];
mcp_servers moves to a sibling mcp.json because plugin.json's root is
additionalProperties: false.

Records the two hazards worth knowing before implementation: the standard's
name pattern rejects uppercase/underscores/spaces so existing names need
slugification, and migration must not rewrite manifests living under someone
else's version control.
2026-08-06 13:45:11 -07:00
CodeWhale Bot 6b7eb20ef1 ci: give test threads the stack the product gives itself
main.rs runs the owner thread and every tokio worker at
CODEWHALE_MAIN_STACK_BYTES (16 MiB) because the engine and runtime-thread
futures are genuinely deep — 8c98bedc75 landed the worker half of that
after a debug `agent` dispatch measured a 2.25-2.5 MiB high-water mark and
died on the guard page.

`#[tokio::test]` builds its own runtime and never sees that. So the test
lanes ran the same code on ~2 MiB, and ~1 MiB on Windows: a configuration
that never ships. That is what aborted the entire Windows test binary with
STATUS_STACK_OVERFLOW in start_turn_accepts_dynamic_tools_and_environment_
id, taking ~9.7k unreported results with it and masking every other
Windows failure — the same masking 78afd8d3d4 called out when it Box::pin'd
that one frame. Box::pin fixed the frame; the environment mismatch stayed,
so the abort returned as soon as codegen moved frame sizes again.

Set RUST_MIN_STACK to the same 16 MiB on the CI test lane and on
release.yml's parity gate. std reads it for any thread spawned without an
explicit size, which covers both libtest's per-test threads and tokio's
workers, so the whole suite gets production's stack instead of the
harness default.

This is not a bigger hammer for a deep-recursion bug: 16 MiB is exactly
what the product already guarantees this code, so the tests now measure
the shipped configuration rather than a stricter one no user runs.
2026-08-06 10:00:57 -07:00
CodeWhale Bot 70d34e1f22 chore(budget): re-baseline aggregate Rust ceiling for the Windows lane repair
673296 -> 673375 (+79 lines), the cost of the test-lane fixes that unblock
the v0.9.4 ship.

The growth is a pinned sandbox backend in the underwater test fixture and
one new test covering the "(unenforced)" rendering the fixture no longer
exercises, plus the rooted-mention and native-separator fixes in
file_mention and their comments. No new packages, binaries, or
thousand-line modules; the max module and large-module count are
unchanged.

The budget's standing note asks that v0.9.4 stop treating aggregate growth
as normal and pay it back in v0.9.5. This bump is a ship blocker, not new
surface, and it is small — but it belongs on that ledger.
2026-08-06 09:54:54 -07:00
CodeWhale Bot 9678e0c1fc Merge branch 'fix/windows-test-lane-0.9.4'
Unblock the v0.9.4 release train: 12 failing tests and a broken rustdoc
build, all pre-existing on main and none caused by the harvest merges.

The 12 failures were read as Windows-only. Seven are not — they fail on
any host with no OS sandbox backend, which includes the Ubuntu runner
(bubblewrap is opt-in). They looked Windows-only because the Ubuntu lane
only runs tests on workflow_dispatch, so no push ever exercised them off
macOS. That mattered for more than CI: release.yml's parity job runs the
full suite on ubuntu-latest, so tagging would have failed the release
itself, not just the CI gate.

Two of the fixes are product bugs, both Windows-only and both real:

  * a rooted mention like `@/absent/guide.md` is not `is_absolute` on
    Windows, so it fell through to the completion index and silently
    attached an unrelated same-basename file's contents to the model's
    context.
  * index-resolved mention paths were joined from `/`-separated display
    strings, rendering as `C:\ws\ops/f.md` in the payload and the context
    inspector.

The rest are test defects where the product behavior is correct: header
layout tests that inherited the host's sandbox availability, a receipt
assertion hard-coding `/`, and a stdin test running `cat` on a host whose
shell is PowerShell.

Also unbreaks `cargo doc --workspace --no-deps` under -Dwarnings, which
had been failing on main. The Documentation job only runs on schedule and
workflow_dispatch, never on push, so it went unnoticed.

Verified locally: full codewhale-tui --all-features suite, cargo fmt
--all --check, the release parity clippy invocation, CI's exact cargo doc
command, and check-versions.sh --require-dated-release.
2026-08-06 09:07:32 -07:00
CodeWhale Bot 77462eaac2 docs: link internal items as code spans, not intra-doc links
Sync to CNB / sync (push) Has been cancelled
The remaining rustdoc failures after the previous commit. `codewhale-tui`
is a binary crate, so rustdoc documents no private items and every
intra-doc link into its internals is unresolvable by construction:

  * `wire_model_for_provider_route` (config_ui.rs)
  * `SharedModelClient` (rlm/bridge.rs)
  * `looks_relative` (workflow/redaction.rs) — private, so
    `rustdoc::private_intra_doc_links` denies it

None of these can be made to resolve without `--document-private-items`,
which is not what CI runs. They read the same as code spans, so demote
them rather than paper over the lint.

`RUSTDOCFLAGS=-Dwarnings cargo doc --workspace --no-deps` — CI's exact
command — now completes clean.
2026-08-06 08:50:21 -07:00
CodeWhale Bot 0ff42dc268 docs(tui): unbreak the rustdoc build
`cargo doc --workspace --no-deps` with `-Dwarnings` has been failing:

  * three bare URLs in mcp_registry.rs (harvested with #5238) — rustdoc
    does not linkify these, so `rustdoc::bare_urls` denies them. Wrap them
    in `<...>` automatic links.
  * `'<value>'` in a workflow.rs doc comment parsed as an unclosed HTML
    tag. It is quoting an error string, so make it a code span.
  * a redundant explicit link target in image_attach.rs, where the label
    already resolves to the same destination.

The Documentation job only runs on schedule/workflow_dispatch, never on
push, so this never blocked the release lane and went unnoticed — the
weekly scheduled run is where it was failing.
2026-08-06 08:47:32 -07:00
CodeWhale Bot 0b173fa87e test(tui): use the platform echo-stdin command in the stdin alias test
`every_advertised_stdin_spelling_reaches_the_command` piped stdin to
`cat`. The dispatcher runs PowerShell or `cmd` on Windows, where `cat` is
either absent or an alias for `Get-Content` — which reads a file, not
stdin — so the test failed there for a reason unrelated to what it holds.

`echo_stdin_command()` already exists one screen up for exactly this and
is what `test_write_stdin_streams_output` uses. Use it here too. The
assertion is unchanged: every advertised spelling must reach the command.
2026-08-06 08:40:36 -07:00
CodeWhale Bot b2f1d8e4ab test(tui): build the fleet save receipt tail with the platform separator
The receipt names the path as `Path::display` writes it, which is `\`-
separated on Windows. The test hard-coded `.codewhale/fleets/fleet-c.toml`,
so `save_writes_the_file_and_receipt_names_the_path` only held on Unix.

Build the expected tail the same way the receipt does. The product is
right here — a receipt a human reads on Windows should use Windows
separators — so the assertion moves, not the rendering.
2026-08-06 08:40:35 -07:00
CodeWhale Bot 0e6ae746b6 fix(tui): make @-mention index resolution correct on Windows
Two Windows-only defects in `resolve_mention_in_completion_index`, both
real behavior and not just test noise.

Rooted mentions fell through to the index. The guard that keeps an exact
location from being "fixed up" to a same-basename file elsewhere tested
`Path::is_absolute`, which is false for `/definitely/absent/guide.md` on
Windows — that path is rooted on the current drive but carries no drive
prefix. So a rooted miss silently attached an unrelated file from the
index. Test the root marker directly. `\` counts only on Windows, where
it is a root marker; on Unix it is an ordinary leading filename byte.

Resolved paths came back with mixed separators. Index display strings are
`/`-separated, and `root.join("ops/f.md")` keeps that slash verbatim on
Windows, so the resolved path rendered as `C:\ws\ops/f.md` — in the
payload handed to the model and in the context inspector. Rejoin the
components with the platform separator first.

Fixes on Windows: absolute_mention_miss_never_uses_index,
mention_miss_resolves_via_unique_index_basename,
context_references_reflect_index_resolution. No behavior change on Unix,
where `/` is already `is_absolute` and already the native separator.
2026-08-06 08:40:34 -07:00
CodeWhale Bot b2b13851ec test(tui): stop underwater header tests probing host sandbox availability
`filesystem_scope_label` is deliberately honest about enforcement: with no
OS sandbox backend it renders "files: workspace (unenforced)" instead of
"files: workspace" (the 2026-08-04 audit). That is 12 extra columns in the
permission chip.

`test_app()` took whatever backend the host happened to have, so these
tests only held on a machine with one. They passed on macOS (seatbelt is
always available) and failed everywhere else: all of Windows, and Linux
without bubblewrap, which is opt-in. Seven tests broke — one on the exact
chip text, six on width budgets the longer chip blows:

  configured_session_tokens_follow_underwater_header_width_priority
  header_shows_exact_named_custom_provider
  ocean_header_keeps_goal_chip_in_cramped_layouts
  ocean_header_keeps_workflow_chip_in_cramped_layouts
  ocean_header_names_a_paused_goal
  permission_chip_reports_the_same_effective_scope_as_execution
  underwater_header_keeps_session_tokens_opt_in

Header rendering is not a probe of the host's sandbox, so pin the backend
in the fixture and keep the layout assertions platform-stable. The
unenforced rendering is worth holding too, so it gets its own test rather
than being an accident of the runner.

This is the same class of breakage af874d776 fixed for the /status
safety-policy test; these were missed because the Ubuntu lane only runs
tests on workflow_dispatch, so a push never exercised them off macOS.
2026-08-06 08:40:17 -07:00
liuyang 71033d9a9e feat(subagents): allow isolated runtime state roots
Separate delegated-agent persistence and coordination state from the execution workspace while retaining the legacy default. Keep child cwd and file authority unchanged, and cover isolated ledgers, transcripts, locks, and resume reads.

Refs Hmbown/CodeWhale#4416 (partial).

Signed-off-by: liuyang <3078108050@qq.com>
2026-08-06 16:59:46 +08:00
rafaelcavalheri 9146c4f63f fix(acp): restore the shell safety gate and fix a flaky cancel test
Review feedback on #5225 (Hunter):

1. build_acp_tool_registry set context.auto_approve = true, which
   short-circuits the SafetyLevel::Dangerous check in
   tools/shell.rs (only runs `if !context.auto_approve`), so every
   command an ACP client's model emits ran unreviewed. ACP has no
   session/request_permission round-trip yet to fall back on. Drop
   the line and let ToolContext::new's default (auto_approve: false)
   stand — matching mcp_server.rs's trust posture over a different
   transport. A blocked command already surfaces as a normal
   `success: false` "BLOCKED: ..." tool result fed back to the model
   (execute_tool_calls_with_cancellation already round-trips tool
   results), not a silent failure, so there's no UX regression from
   restoring the gate.

2. agentic_turn_cancels_while_a_tool_is_running scripted a tool call
   named "exec_shell", which with_shell_tools() never registers
   (renamed to "Bash" in v0.9.3). The lookup miss made the tool
   future resolve to an immediate error instead of actually running
   SLOW_SHELL_COMMAND, so the test's `select!` raced two already-ready
   futures and asserted PromptOutcome::Cancelled on a coin flip.
   Renamed to "Bash" so the 5-second command genuinely runs and the
   cancel path genuinely preempts it. Also swept the remaining
   `exec_shell` references (doc comments, a test name/message) left
   over from the pre-v0.9.3 tool spelling.

Verified: cargo test -p codewhale-tui acp_server (34/34) and
route_budget (11/11) pass; the renamed cancel test passes 15/15 runs
in isolation (was ~50/50 before the rename). cargo fmt and the
project's workspace clippy gate (fmt + clippy --workspace
--all-features -D warnings, CONTRIBUTING.md allow-list) are clean
except one pre-existing, unrelated lint in mcp.rs.

Blocker 3 (build_system_prompt, deleted in a98b184f5) is Hunter's to
carry per the review; not touched here.

Drafted with agent assistance (Claude Code); build-verified by the
human author before pushing.
2026-08-04 08:07:23 -03:00
rafaelcavalheri 73dd36514a feat(acp): expose file/search/git/patch/shell tools over session/prompt
The ACP session/prompt path only streamed text; it never executed the
tool calls a model requested, so editors driving CodeWhale over ACP
(Zed, and third-party bridges like acp-deepseek-adapter) got a
chat-only agent with no real code-editing capability. This wires the
existing ToolRegistry into the ACP turn loop instead of duplicating a
new one:

- run_agentic_prompt_turn drives multi-round tool_use/tool_result
  turns (capped at MAX_ACP_TOOL_ROUNDS) over the same file/search/git/
  patch/shell tools the TUI uses, and reuses response_id_policy so
  every tool-round response still gets the client-specific id
  translation (Zed/avante.nvim) the existing streaming path relies on.
- Shell access is gated on the client declaring `terminal` support at
  `initialize` (default false/restrictive); MAX_ACP_SESSIONS caps
  concurrent sessions with true insertion-order eviction (VecDeque,
  not HashMap iteration order).
- Tool-call cancellation signals a CancellationToken and waits for the
  running tool (including a child shell process) to actually stop
  before returning, rather than abandoning it.
- max_tokens for the ACP path now resolves through the same
  route-limits machinery the TUI/CLI use (effective_max_output_tokens_for_route)
  instead of a fixed 4096 fallback.
- scripts/build.ps1: release build script for Windows PowerShell 5.1,
  used to produce the ACP binary tested against Zed on Windows.

34 unit tests cover the turn loop, tool execution against a real
workspace, cancellation mid-tool, and concurrent sessions with
independent registries, all against in-memory streams (no live
provider needed).

Drafted with agent assistance (Claude Code); build-verified and
reviewed by the human author before submission.
2026-08-03 10:13:31 -03:00
608 changed files with 87812 additions and 50855 deletions
+17 -9
View File
@@ -19,14 +19,22 @@
.rust_workspace_gates_stage: &rust_workspace_gates_stage
name: rust workspace gates
# The all-feature TUI test crate is large enough that concurrent rustc and
# clippy processes or disposable test debug metadata can exceed the shared
# CNB runner's memory. Keep the full gate surface, but serialize Cargo,
# omit test-only debug tables, and use the established workspace-test stack
# size so deep runtime API tests do not abort on the platform default.
timeout: 45m
script: |
set -eu
export CARGO_BUILD_JOBS=1
export CARGO_PROFILE_TEST_DEBUG=0
./scripts/release/check-versions.sh
./scripts/release/check-ohos-deps.sh
cargo fmt --all -- --check
cargo check --workspace --all-targets --locked
cargo clippy --workspace --all-targets --all-features --locked -- -D warnings
cargo test --workspace --all-features --locked
RUST_MIN_STACK=16777216 cargo test --workspace --all-features --locked
# Parity gates as first-class steps so drift surfaces as a named failure,
# not a buried workspace-test entry. Mirrors release.yml's parity job.
cargo test -p codewhale-protocol --test parity_protocol --locked
@@ -60,12 +68,12 @@
set -eu
# The release profile uses full LTO and one codegen unit. Bound Cargo's
# parallelism so the final links cannot exhaust a shared CNB runner.
cargo build --jobs 2 --release --locked -p codewhale-cli -p codewhale-tui
cargo build --jobs 2 --release --locked -p codewhale-cli
cp target/release/codewhale target/release/codew
export PATH="$PWD/target/release:$PATH"
node scripts/release/npm-wrapper-smoke.js
./target/release/codewhale --version
./target/release/codew --version
./target/release/codewhale-tui --version
.linux_release_preflight: &linux_release_preflight
name: linux release preflight
@@ -100,12 +108,12 @@
set -eu
# Keep the production release profile intact while avoiding a burst of
# concurrent rustc/linker processes on the shared release runner.
cargo build --jobs 2 --release --locked -p codewhale-cli -p codewhale-tui
cargo build --jobs 2 --release --locked -p codewhale-cli
cp target/release/codewhale target/release/codew
export PATH="$PWD/target/release:$PATH"
node scripts/release/npm-wrapper-smoke.js
./target/release/codewhale --version
./target/release/codew --version
./target/release/codewhale-tui --version
main:
push:
@@ -141,13 +149,13 @@ $:
./scripts/release/check-ohos-deps.sh
cargo build --jobs 2 --release --locked \
--target x86_64-unknown-linux-musl \
-p codewhale-cli -p codewhale-tui
-p codewhale-cli # single binary
mkdir -p target/cnb-release
BIN_DIR="target/x86_64-unknown-linux-musl/release"
cp "$BIN_DIR/codewhale" target/cnb-release/codewhale-linux-x64
cp "$BIN_DIR/codew" target/cnb-release/codew-linux-x64
cp "$BIN_DIR/codewhale-tui" target/cnb-release/codewhale-tui-linux-x64
cp "$BIN_DIR/codewhale" target/cnb-release/codew-linux-x64
cp "$BIN_DIR/codewhale" target/cnb-release/codewhale-tui-linux-x64
strip \
target/cnb-release/codewhale-linux-x64 \
target/cnb-release/codew-linux-x64 \
@@ -188,7 +196,7 @@ $:
echo "Assets:"
echo "- codewhale-linux-x64"
echo "- codew-linux-x64"
echo "- codewhale-tui-linux-x64"
echo "- codewhale-tui-linux-x64 (v0.9.4 compatibility alias)"
echo "- codewhale-artifacts-sha256.txt"
} > target/cnb-release/CNB_RELEASE.md
+3
View File
@@ -19,3 +19,6 @@
# AtlasCloud OpenAI-compatible endpoint
# ATLASCLOUD_API_KEY=
# Mistral AI (la Plateforme) — https://console.mistral.ai/api-keys
# MISTRAL_API_KEY=
+5
View File
@@ -183,6 +183,9 @@ heloanc = heloanc <61081755+heloanc@users.noreply.github.com>
heloanc@users.noreply.github.com = heloanc <61081755+heloanc@users.noreply.github.com>
bistack = Sun Zhenyuan <9128763+bistack@users.noreply.github.com>
zhenyuan.sun@163.com = Sun Zhenyuan <9128763+bistack@users.noreply.github.com>
skyzhao1223 = SKY ZHAO <15373810+skyzhao1223@users.noreply.github.com>
zhaotian1 = SKY ZHAO <15373810+skyzhao1223@users.noreply.github.com>
zhaotian1@wps.cn = SKY ZHAO <15373810+skyzhao1223@users.noreply.github.com>
vFONGv = Matthew.Fong <21223725+vFONGv@users.noreply.github.com>
fangb0987612345@gmail.com = Matthew.Fong <21223725+vFONGv@users.noreply.github.com>
luismateusvargas = Luis Mateus Vargas <289766246+luismateusvargas@users.noreply.github.com>
@@ -209,3 +212,5 @@ adity982 = ADITYA <59918965+adity982@users.noreply.github.com>
vibecoding-skills = Harsh Dattani <209214219+vibecoding-skills@users.noreply.github.com>
XhesicaFrost = XhesicaFrost <142909332+XhesicaFrost@users.noreply.github.com>
ffaacceelee = ffaacceelee <11267580+ffaacceelee@users.noreply.github.com>
mky = mky <817223+mky@users.noreply.github.com>
cacdcaecawae = cacdcaecawae <109055297+cacdcaecawae@users.noreply.github.com>
+249 -4
View File
@@ -9,6 +9,7 @@ const {
allAssetNames,
allReleaseAssetNames,
BUNDLE_ASSET_NAMES,
LEGACY_TUI_BRIDGE_ASSET_NAMES,
} = require(path.join(repoRoot, "npm", "codewhale", "scripts", "artifacts"));
function read(relativePath) {
@@ -20,11 +21,24 @@ function valuesForKey(source, key) {
return [...source.matchAll(expression)].map((match) => match[1]);
}
function namedStep(source, name) {
const marker = ` - name: ${name}\n`;
const start = source.indexOf(marker);
assert.notEqual(start, -1, `missing workflow step: ${name}`);
const next = source.indexOf("\n - ", start + marker.length);
return source.slice(start, next === -1 ? source.length : next);
}
const ci = read(".github/workflows/ci.yml");
const nightly = read(".github/workflows/nightly.yml");
const candidate = read(".github/workflows/release-candidate.yml");
const artifacts = read(".github/workflows/release-artifacts.yml");
const release = read(".github/workflows/release.yml");
const releaseDockerfile = read("packaging/docker/Dockerfile.release");
const cnb = read(".cnb.yml");
const bundles = read("scripts/release/create-release-bundles.sh");
const archiveInstaller = read("scripts/release/install.sh");
const cliDispatcher = read("crates/cli/src/lib.rs");
const runbook = read("docs/RELEASE_RUNBOOK.md");
assert.match(ci, /^ workflow_dispatch:\n inputs:\n expected_sha:/m);
@@ -37,6 +51,68 @@ for (const output of ["heavy", "workflow", "mobile", "actions"]) {
}
assert.match(manualForceBlock[1], /#EXPECTED_SHA.*-ne 40/s);
assert.match(manualForceBlock[1], /actual.*EXPECTED_SHA/s);
assert.match(
ci,
/run: cargo test -p codewhale-tui --test pty qa_pty::skills_opens_manager_owned_then_compatible -- --ignored --exact/,
"CI must run the isolated Skills Manager acceptance from the consolidated PTY target",
);
assert.doesNotMatch(ci, /--test qa_pty\b/, "CI must not name the removed qa_pty target");
const expectedNightlyTargets = [
"x86_64-unknown-linux-gnu",
"aarch64-unknown-linux-musl",
"x86_64-apple-darwin",
"aarch64-apple-darwin",
"x86_64-pc-windows-msvc",
"aarch64-pc-windows-msvc",
].sort();
assert.deepEqual([...new Set(valuesForKey(nightly, "target"))].sort(), expectedNightlyTargets);
assert.deepEqual(
[
...valuesForKey(nightly, "primary_artifact"),
...valuesForKey(nightly, "alias_artifact"),
].sort(),
[
"codewhale-linux-x64",
"codew-linux-x64",
"codewhale-linux-arm64",
"codew-linux-arm64",
"codewhale-macos-x64",
"codew-macos-x64",
"codewhale-macos-arm64",
"codew-macos-arm64",
"codewhale-windows-x64.exe",
"codew-windows-x64.exe",
"codewhale-windows-arm64.exe",
"codew-windows-arm64.exe",
].sort(),
);
assert.match(
nightly,
/cargo build --release --locked --target \$\{\{ matrix\.target \}\} -p codewhale-cli/,
);
assert.match(nightly, /startsWith\(matrix\.target, 'x86_64-'\).*runner\.arch == 'X64'/s);
assert.match(nightly, /startsWith\(matrix\.target, 'aarch64-'\).*runner\.arch == 'ARM64'/s);
const nightlyArmMuslSetup = namedStep(nightly, "Install Linux ARM64 musl toolchain");
assert.match(nightlyArmMuslSetup, /matrix\.target == 'aarch64-unknown-linux-musl'/);
assert.match(nightlyArmMuslSetup, /apt-get install -y binutils musl-tools/);
assert.match(nightlyArmMuslSetup, /rustup target add --toolchain stable aarch64-unknown-linux-musl/);
const nightlyArmStaticSmoke = namedStep(
nightly,
"Verify static Linux ARM64 binary and launch",
);
assert.match(
nightlyArmStaticSmoke,
/matrix\.target == 'aarch64-unknown-linux-musl' && runner\.arch == 'ARM64'/,
);
assert.match(nightlyArmStaticSmoke, /readelf -l "\$\{bin_path\}"/);
assert.match(nightlyArmStaticSmoke, /grep -Fq 'INTERP'/);
assert.match(nightlyArmStaticSmoke, /"\$\{bin_path\}" --version/);
assert.doesNotMatch(nightly, /codewhale-tui/);
assert.doesNotMatch(nightly, /target\/[^\n]*\/codew(?:\.exe)?/);
assert.match(nightly, /cp "\$\{bin_path\}" "\$\{dir\}\/\$\{artifact\}"/);
assert.match(nightly, /cmp -s[\s\S]*nightly-primary[\s\S]*nightly-alias/);
assert.equal((nightly.match(/retention-days: 14/g) || []).length, 2);
assert.match(candidate, /^ workflow_dispatch:\n inputs:\n expected_sha:/m);
assert.doesNotMatch(candidate, /^ (push|pull_request|schedule):/m);
@@ -101,7 +177,7 @@ assert.match(artifacts, /^ workflow_call:/m);
assert.match(artifacts, /^permissions:\n contents: read$/m);
const expectedTargets = [
"x86_64-unknown-linux-musl",
"aarch64-unknown-linux-gnu",
"aarch64-unknown-linux-musl",
"aarch64-linux-android",
"x86_64-apple-darwin",
"aarch64-apple-darwin",
@@ -110,6 +186,27 @@ const expectedTargets = [
].sort();
assert.deepEqual([...new Set(valuesForKey(artifacts, "target"))].sort(), expectedTargets);
const releaseMuslBuild = namedStep(artifacts, "Build static Linux binaries (musl)");
assert.match(releaseMuslBuild, /endsWith\(matrix\.target, '-unknown-linux-musl'\)/);
assert.match(releaseMuslBuild, /apt-get install -y binutils musl-tools/);
assert.match(releaseMuslBuild, /rustup target add --toolchain stable \$\{\{ matrix\.target \}\}/);
assert.match(
releaseMuslBuild,
/cargo build --profile dist --locked --target \$\{\{ matrix\.target \}\} -p codewhale-cli/,
);
const releaseStaticSmoke = namedStep(
artifacts,
"Verify static Linux binaries and launch on matching native runners",
);
assert.match(releaseStaticSmoke, /endsWith\(matrix\.target, '-unknown-linux-musl'\)/);
assert.match(
releaseStaticSmoke,
/startsWith\(matrix\.target, 'aarch64-'\) && runner\.arch == 'ARM64'/,
);
assert.match(releaseStaticSmoke, /readelf -l "\$\{bin_path\}"/);
assert.match(releaseStaticSmoke, /grep -Fq 'INTERP'/);
assert.match(releaseStaticSmoke, /"\$\{bin_path\}" --version/);
const builtAssetNames = [
...valuesForKey(artifacts, "cli_artifact"),
...valuesForKey(artifacts, "shim_artifact"),
@@ -118,10 +215,18 @@ const builtAssetNames = [
assert.equal(builtAssetNames.length, 21);
assert.deepEqual(
[...new Set(builtAssetNames)].sort(),
allAssetNames().filter((name) => name !== "codewhale.bat").sort(),
[
...allAssetNames().filter((name) => name !== "codewhale.bat"),
...LEGACY_TUI_BRIDGE_ASSET_NAMES,
].sort(),
);
assert.match(
artifacts,
/stage_binary "\$\{\{ matrix\.cli_binary \}\}" "\$\{\{ matrix\.tui_artifact \}\}"/,
"legacy TUI bridge assets must be staged from the one compiled codewhale binary",
);
const bundleInvocations = [...bundles.matchAll(
/^bundle (\S+) \\\n\s+\S+ \S+ \S+ (tar\.gz|zip) (""|portable)$/gm,
/^bundle (\S+) \\\n\s+\S+ \S+ (tar\.gz|zip) (""|portable)$/gm,
)].map((match) => {
const variant = match[3] === "portable" ? "-portable" : "";
return `codewhale-${match[1]}${variant}.${match[2]}`;
@@ -133,6 +238,13 @@ assert.match(artifacts, /codew-windows-arm64\.exe/);
assert.match(artifacts, /CodeWhaleSetup\.exe/);
assert.match(artifacts, /assemble-release-assets\.js --verify release-assets/);
assert.match(artifacts, /CODEWHALE_SMOKE_ASSETS_DIR/);
const bundleStep = namedStep(artifacts, "Create and checksum platform archives");
assert.match(bundleStep, /git show -s --format=%ct "\$\{\{ inputs\.source_sha \}\}"/);
assert.match(
bundleStep,
/SOURCE_DATE_EPOCH="\$\{source_date_epoch\}"[\s\\]+bash scripts\/release\/create-release-bundles\.sh artifacts bundles/,
);
assert.doesNotMatch(bundleStep, /\bdate\b/, "bundle timestamps must come from the pinned source commit, not wall-clock time");
assert.equal(allReleaseAssetNames().length, 34);
assert.match(release, /^ artifacts:\n/m);
@@ -148,10 +260,143 @@ assert.equal(
assert.match(release, /overwrite_files:\s*false/);
assert.match(release, /fail_on_unmatched_files:\s*true/);
assert.match(release, /^ docker-build:\n/m);
assert.match(release, /^ docker:\n/m);
assert.match(release, /runner: ubuntu-latest\n\s+platform: linux\/amd64/);
assert.match(release, /runner: ubuntu-24\.04-arm\n\s+platform: linux\/arm64/);
assert.match(release, /cli_artifact: codewhale-linux-x64/);
assert.match(release, /cli_artifact: codewhale-linux-arm64/);
assert.match(release, /shim_artifact: codew-linux-x64/);
assert.match(release, /shim_artifact: codew-linux-arm64/);
assert.doesNotMatch(
release,
/docker\/setup-qemu-action/,
"public container publication must not funnel both architectures through QEMU",
);
const releaseDockerBytes = namedStep(release, "Verify native release bytes");
assert.match(releaseDockerBytes, /CLI_ARTIFACT: \$\{\{ matrix\.cli_artifact \}\}/);
assert.match(releaseDockerBytes, /SHIM_ARTIFACT: \$\{\{ matrix\.shim_artifact \}\}/);
assert.match(
releaseDockerBytes,
/mv -- "docker-context\/bin\/\$\{CLI_ARTIFACT\}" docker-context\/bin\/codewhale/,
);
assert.match(
releaseDockerBytes,
/mv -- "docker-context\/bin\/\$\{SHIM_ARTIFACT\}" docker-context\/bin\/codew/,
);
assert.match(releaseDockerBytes, /cmp docker-context\/bin\/codewhale docker-context\/bin\/codew/);
const releaseDockerBuild = namedStep(release, "Assemble and push native image by digest");
assert.match(releaseDockerBuild, /context: docker-context/);
assert.match(releaseDockerBuild, /file: infra\/packaging\/docker\/Dockerfile\.release/);
assert.match(releaseDockerBuild, /platforms: \$\{\{ matrix\.platform \}\}/);
assert.match(releaseDockerBuild, /provenance: mode=max/);
assert.match(releaseDockerBuild, /sbom: true/);
assert.match(releaseDockerBuild, /push-by-digest=true/);
const releaseDockerManifest = namedStep(release, "Publish multi-architecture manifest");
assert.match(releaseDockerManifest, /Expected exactly two native image digests/);
assert.match(releaseDockerManifest, /docker buildx imagetools create/);
const releaseDockerSmoke = namedStep(release, "Verify and smoke published container");
assert.match(releaseDockerSmoke, /linux\/amd64/);
assert.match(releaseDockerSmoke, /linux\/arm64/);
assert.match(releaseDockerSmoke, /--entrypoint codewhale/);
assert.match(releaseDockerSmoke, /--entrypoint codew/);
const npmJob = release.match(/\n npm:\n([\s\S]*?)\n homebrew:\n/);
assert.ok(npmJob, "public release must retain a dedicated npm publication job");
assert.match(npmJob[1], /^ needs: \[release, resolve\]$/m);
assert.match(npmJob[1], /needs\.release\.result == 'success'/);
assert.match(npmJob[1], /^ contents: read$/m);
assert.match(npmJob[1], /^ id-token: write$/m);
assert.match(npmJob[1], /ref: \$\{\{ needs\.resolve\.outputs\.sha \}\}/);
assert.match(npmJob[1], /fetch-depth: 0/);
assert.match(npmJob[1], /node-version: 24/);
assert.match(npmJob[1], /registry-url: https:\/\/registry\.npmjs\.org/);
assert.match(npmJob[1], /package-manager-cache: false/);
assert.match(npmJob[1], /npm install --global npm@12\.0\.2/);
const npmTagGate = namedStep(release, "Revalidate release tag before npm publish");
const npmAssetGate = namedStep(release, "Revalidate public release assets");
const npmPublish = namedStep(release, "Publish npm wrapper with trusted publishing");
assert.match(npmTagGate, /verify-remote-tag\.sh/);
assert.match(npmAssetGate, /verify-release-assets\.sh/);
assert.match(npmAssetGate, /GH_TOKEN: \$\{\{ github\.token \}\}/);
assert.match(npmPublish, /working-directory: npm\/codewhale/);
assert.match(npmPublish, /npm publish --access public/);
assert.doesNotMatch(npmJob[1], /NPM_TOKEN|NODE_AUTH_TOKEN|secrets\./);
assert.ok(
release.indexOf("Revalidate public release assets") <
release.indexOf("Publish npm wrapper with trusted publishing"),
"npm publication must follow the public exact-asset gate",
);
assert.match(releaseDockerfile, /^FROM debian:bookworm-slim$/m);
assert.match(releaseDockerfile, /ca-certificates/);
assert.match(releaseDockerfile, /libdbus-1-3/);
assert.match(releaseDockerfile, /COPY .*bin\/codewhale \/usr\/local\/bin\/codewhale/);
assert.match(releaseDockerfile, /COPY .*bin\/codew \/usr\/local\/bin\/codew/);
assert.match(releaseDockerfile, /^USER codewhale$/m);
assert.doesNotMatch(
releaseDockerfile,
/\bcargo\s+build\b|^FROM\s+rust:/m,
"release container assembly must reuse the already-verified release binaries",
);
assert.match(runbook, /release[- ]candidate/i);
assert.match(runbook, /expected_sha/);
assert.match(runbook, /34/);
assert.match(runbook, /does not create a tag/i);
assert.match(runbook, /explicit.*approval/i);
console.log("Release workflow contracts OK: exact-head full CI and 7-target/34-asset non-publishing candidate.");
const cnbRustGates = cnb.match(
/\.rust_workspace_gates_stage: &rust_workspace_gates_stage([\s\S]*?)\n\.linux_rust_gates:/,
);
assert.ok(cnbRustGates, "CNB must retain the shared Rust workspace gate");
assert.match(
cnbRustGates[1],
/timeout: 45m[\s\S]*export CARGO_BUILD_JOBS=1[\s\S]*export CARGO_PROFILE_TEST_DEBUG=0[\s\S]*cargo check --workspace --all-targets --locked[\s\S]*cargo clippy --workspace --all-targets --all-features --locked -- -D warnings[\s\S]*RUST_MIN_STACK=16777216 cargo test --workspace --all-features --locked/,
"CNB must serialize the memory-heavy Rust gate and preserve the workspace test stack contract",
);
assert.equal(
(cnb.match(/^\s+- \*rust_workspace_gates_stage$/gm) || []).length,
2,
"both CNB Rust pipelines must reuse the constrained workspace gate",
);
const cnbPreflight = cnb.match(
/\.linux_release_preflight: &linux_release_preflight([\s\S]*?)\nmain:/,
);
assert.ok(cnbPreflight, "CNB must retain a dedicated release preflight");
const cnbBuild = cnbPreflight[1].indexOf(
"cargo build --jobs 2 --release --locked -p codewhale-cli",
);
const cnbAlias = cnbPreflight[1].indexOf(
"cp target/release/codewhale target/release/codew",
);
const cnbSmoke = cnbPreflight[1].indexOf("node scripts/release/npm-wrapper-smoke.js");
assert.ok(cnbBuild >= 0, "CNB release preflight must build the consolidated runtime");
assert.ok(cnbAlias > cnbBuild, "CNB release preflight must materialize codew after the build");
assert.ok(cnbSmoke > cnbAlias, "CNB release preflight must materialize codew before smoke");
assert.doesNotMatch(
archiveInstaller,
/cargo install codewhale --locked/,
"glibc recovery must name the published codewhale-cli crate",
);
assert.equal(
(archiveInstaller.match(/cargo install codewhale-cli --locked/g) || []).length,
2,
"both glibc recovery branches must name codewhale-cli",
);
assert.match(
archiveInstaller,
/legacy_tui="\$BIN_DIR\/codewhale-tui"[\s\S]*install_binary "\$SCRIPT_DIR\/codewhale" "\$legacy_tui"/,
"archive upgrades must refresh the retired TUI path from consolidated bytes",
);
assert.doesNotMatch(
cliDispatcher,
/codewhale_config::auto_model::classify/,
"the CLI dispatcher must leave auto routing to the provider-aware runtime",
);
console.log(
"Workflow contracts OK: 6-target/12-asset single-runtime nightly and exact-head 7-target/34-asset release candidate.",
);
+5 -27
View File
@@ -35,23 +35,19 @@ sha() {
# --- read checksums ---------------------------------------------------
# Canonical dispatcher and TUI
# One compiled runtime exposed under the two supported command names.
SHA_COD_MACOS_ARM="$(sha codewhale-macos-arm64)"
SHA_CODEW_MACOS_ARM="$(sha codew-macos-arm64)"
SHA_TUI_MACOS_ARM="$(sha codewhale-tui-macos-arm64)"
SHA_COD_MACOS_X64="$(sha codewhale-macos-x64)"
SHA_CODEW_MACOS_X64="$(sha codew-macos-x64)"
SHA_TUI_MACOS_X64="$(sha codewhale-tui-macos-x64)"
SHA_COD_LINUX_ARM="$(sha codewhale-linux-arm64)"
SHA_CODEW_LINUX_ARM="$(sha codew-linux-arm64)"
SHA_TUI_LINUX_ARM="$(sha codewhale-tui-linux-arm64)"
SHA_COD_LINUX_X64="$(sha codewhale-linux-x64)"
SHA_CODEW_LINUX_X64="$(sha codew-linux-x64)"
SHA_TUI_LINUX_X64="$(sha codewhale-tui-linux-x64)"
readonly SHA_COD_MACOS_ARM SHA_CODEW_MACOS_ARM SHA_TUI_MACOS_ARM
readonly SHA_COD_MACOS_X64 SHA_CODEW_MACOS_X64 SHA_TUI_MACOS_X64
readonly SHA_COD_LINUX_ARM SHA_CODEW_LINUX_ARM SHA_TUI_LINUX_ARM
readonly SHA_COD_LINUX_X64 SHA_CODEW_LINUX_X64 SHA_TUI_LINUX_X64
readonly SHA_COD_MACOS_ARM SHA_CODEW_MACOS_ARM
readonly SHA_COD_MACOS_X64 SHA_CODEW_MACOS_X64
readonly SHA_COD_LINUX_ARM SHA_CODEW_LINUX_ARM
readonly SHA_COD_LINUX_X64 SHA_CODEW_LINUX_X64
# --- temp dirs --------------------------------------------------------
@@ -78,10 +74,6 @@ class DeepseekTui < Formula
url "${BASE_URL}/codew-macos-arm64", using: :nounzip
sha256 "${SHA_CODEW_MACOS_ARM}"
end
resource "tui" do
url "${BASE_URL}/codewhale-tui-macos-arm64", using: :nounzip
sha256 "${SHA_TUI_MACOS_ARM}"
end
else
url "${BASE_URL}/codewhale-macos-x64", using: :nounzip
sha256 "${SHA_COD_MACOS_X64}"
@@ -89,10 +81,6 @@ class DeepseekTui < Formula
url "${BASE_URL}/codew-macos-x64", using: :nounzip
sha256 "${SHA_CODEW_MACOS_X64}"
end
resource "tui" do
url "${BASE_URL}/codewhale-tui-macos-x64", using: :nounzip
sha256 "${SHA_TUI_MACOS_X64}"
end
end
end
@@ -104,10 +92,6 @@ class DeepseekTui < Formula
url "${BASE_URL}/codew-linux-arm64", using: :nounzip
sha256 "${SHA_CODEW_LINUX_ARM}"
end
resource "tui" do
url "${BASE_URL}/codewhale-tui-linux-arm64", using: :nounzip
sha256 "${SHA_TUI_LINUX_ARM}"
end
else
url "${BASE_URL}/codewhale-linux-x64", using: :nounzip
sha256 "${SHA_COD_LINUX_X64}"
@@ -115,23 +99,17 @@ class DeepseekTui < Formula
url "${BASE_URL}/codew-linux-x64", using: :nounzip
sha256 "${SHA_CODEW_LINUX_X64}"
end
resource "tui" do
url "${BASE_URL}/codewhale-tui-linux-x64", using: :nounzip
sha256 "${SHA_TUI_LINUX_X64}"
end
end
end
def install
bin.install Dir["*"].first => "codewhale"
resource("codew").stage { bin.install Dir["*"].first => "codew" }
resource("tui").stage { bin.install Dir["*"].first => "codewhale-tui" }
end
test do
system "#{bin}/codewhale", "--version"
system "#{bin}/codew", "--version"
system "#{bin}/codewhale-tui", "--version"
end
end
EOF
+4 -5
View File
@@ -11,16 +11,12 @@ formula="${tmp_dir}/deepseek-tui.rb"
assets=(
codewhale-macos-arm64
codew-macos-arm64
codewhale-tui-macos-arm64
codewhale-macos-x64
codew-macos-x64
codewhale-tui-macos-x64
codewhale-linux-arm64
codew-linux-arm64
codewhale-tui-linux-arm64
codewhale-linux-x64
codew-linux-x64
codewhale-tui-linux-x64
)
for asset in "${assets[@]}"; do
@@ -38,6 +34,9 @@ grep -Fq 'desc "Agentic terminal for open-source and open-weight coding models"'
test "$(grep -Fc 'resource "codew" do' "${formula}")" -eq 4
grep -Fq 'bin.install Dir["*"].first => "codew"' "${formula}"
grep -Fq 'system "#{bin}/codew", "--version"' "${formula}"
grep -Fq 'system "#{bin}/codewhale-tui", "--version"' "${formula}"
if grep -Fq 'codewhale-tui' "${formula}"; then
echo "Homebrew formula must not install the legacy TUI compatibility asset" >&2
exit 1
fi
echo "update-homebrew-tap tests passed"
+29 -20
View File
@@ -172,6 +172,7 @@ jobs:
bash scripts/release/install-dogfood.test.sh
bash scripts/release/prepare-release.test.sh
bash scripts/release/require-release-tag-checkout.test.sh
bash scripts/release/validate-crate-publish-order.test.sh
bash scripts/release/verify-remote-tag.test.sh
bash .github/scripts/update-homebrew-tap.test.sh
node .github/scripts/release-workflows.test.js
@@ -270,6 +271,11 @@ jobs:
- name: Check dead-code budget
if: needs.changes.outputs.heavy == 'true'
run: python3 scripts/check-dead-code-budget.py
- name: Test runtime-contract measurement harness
if: needs.changes.outputs.heavy == 'true'
run: |
python3 scripts/test_measure_runtime_contract.py
python3 scripts/test_check_runtime_contract_budget.py
# The offline runtime-contract measurement needs the full locked graph,
# dev-dependencies included (e.g. wiremock -> assert-json-diff), but
# clippy above builds no test targets and the rust-cache registry key
@@ -289,9 +295,11 @@ jobs:
# Provider-free paused-consumer measurement of the production
# persistence request channel. RSS is sampled only on macOS; every host
# enforces the accepted/retained request and payload contract.
- name: Test persistence-backlog checker
- name: Test persistence-backlog measurement and checker harnesses
if: needs.changes.outputs.heavy == 'true'
run: python3 scripts/test_check_persistence_backlog_budget.py
run: |
python3 scripts/test_measure_persistence_backlog.py
python3 scripts/test_check_persistence_backlog_budget.py
- name: Check persistence-backlog budget
if: needs.changes.outputs.heavy == 'true'
run: python3 scripts/check-persistence-backlog-budget.py
@@ -392,12 +400,10 @@ jobs:
run: ./scripts/installer/update-user-path.tests.ps1
- name: Install NSIS for Windows installer regression
if: needs.changes.outputs.heavy == 'true' && matrix.os == 'windows-latest'
continue-on-error: true
shell: pwsh
run: choco install nsis -y --no-progress
- name: Test Windows installer PATH regression
if: needs.changes.outputs.heavy == 'true' && matrix.os == 'windows-latest'
continue-on-error: true
shell: pwsh
run: ./scripts/installer/installer-path-regression.tests.ps1 -AllowUserPathMutation
- uses: dtolnay/rust-toolchain@stable
@@ -429,19 +435,21 @@ jobs:
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Run tests
if: needs.changes.outputs.heavy == 'true' && (matrix.os != 'ubuntu-latest' || github.event_name == 'workflow_dispatch')
shell: bash
run: |
export RUST_BACKTRACE=1
echo "=== env probe ==="
echo "OS=$OSTYPE PWD=$(pwd) TMP=${TMP:-unset} TEMP=${TEMP:-unset} USERPROFILE=${USERPROFILE:-unset}"
which cat bash sh 2>&1 || true
echo "=== underwater ==="
cargo test -p codewhale-tui --bin codewhale-tui --all-features -- --test-threads=1 --nocapture tui::underwater::tests 2>&1 || true
echo "=== file_mention ==="
cargo test -p codewhale-tui --bin codewhale-tui --all-features -- --test-threads=1 --nocapture tui::file_mention::tests 2>&1 || true
echo "=== shell + fleet_detail ==="
cargo test -p codewhale-tui --bin codewhale-tui --all-features -- --test-threads=1 --nocapture tools::shell::tests::every_advertised_stdin_spelling_reaches_the_command tui::views::fleet_detail::tests::save_writes_the_file_and_receipt_names_the_path 2>&1 || true
exit 1
run: cargo test --workspace --all-features --locked
env:
# Give test threads the stack the product gives itself. main.rs runs
# the owner thread and every tokio worker at
# CODEWHALE_MAIN_STACK_BYTES (16 MiB) because the engine and
# runtime-thread futures are genuinely deep. `#[tokio::test]` builds
# its own runtime and never sees that, so tests ran the same code on
# ~2 MiB (~1 MiB on Windows) — a configuration that never ships.
# That gap is what aborted the whole Windows test binary with
# STATUS_STACK_OVERFLOW in start_turn_accepts_dynamic_tools_and_
# environment_id, masking every other Windows result (78afd8d3d4
# Box::pin'd that one frame; the mismatch itself remained). std reads
# this for any thread spawned without an explicit size, which covers
# both libtest's per-test threads and tokio's workers.
RUST_MIN_STACK: '16777216'
# The Ubuntu lint lane validates non-RSS backlog fields. Run the same
# source-bound measurement on macOS so loss or growth of RSS evidence
# fails closed instead of becoming an unsupported-field skip.
@@ -451,10 +459,11 @@ jobs:
- name: Run isolated Skills Manager PTY acceptance
# This real-PTY scenario is deterministic in a fresh process (10/10
# locally) but can inherit event starvation after the full qa_pty
# suite on loaded Linux runners. Keep the assertion intact and run it
# separately on Unix after the workspace suite has released its PTYs.
# module suite on loaded Linux runners. Keep the assertion intact and
# run it separately on Unix after the workspace suite has released its
# PTYs.
if: needs.changes.outputs.heavy == 'true' && matrix.os != 'windows-latest' && (matrix.os != 'ubuntu-latest' || github.event_name == 'workflow_dispatch')
run: cargo test -p codewhale-tui --test qa_pty skills_opens_manager_owned_then_compatible -- --ignored --exact
run: cargo test -p codewhale-tui --test pty qa_pty::skills_opens_manager_owned_then_compatible -- --ignored --exact
- name: Lockfile drift guard
if: needs.changes.outputs.heavy == 'true' && (matrix.os != 'ubuntu-latest' || github.event_name == 'workflow_dispatch')
run: git diff --exit-code -- Cargo.lock
+29
View File
@@ -0,0 +1,29 @@
name: Debug windows python
on:
workflow_dispatch:
permissions:
contents: read
jobs:
debug:
runs-on: windows-latest
steps:
- uses: actions/checkout@v7
- name: Probe python availability
shell: pwsh
run: |
python --version
python3 --version
where.exe python
where.exe python3
py -3 --version
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
with:
cache-bin: false
- name: Run failing test with output
shell: pwsh
run: cargo test -p codewhale-tui --lib --all-features --locked -- --nocapture full_access_auto_approves_non_bypassable_registered_tools
env:
RUST_MIN_STACK: '16777216'
+70 -60
View File
@@ -33,51 +33,39 @@ jobs:
- os: ubuntu-latest
target: x86_64-unknown-linux-gnu
platform: linux-x64
cli_binary: codewhale
shim_binary: codew
tui_binary: codewhale-tui
cli_artifact: codewhale-linux-x64
tui_artifact: codewhale-tui-linux-x64
binary: codewhale
primary_artifact: codewhale-linux-x64
alias_artifact: codew-linux-x64
- os: ubuntu-24.04-arm
target: aarch64-unknown-linux-gnu
target: aarch64-unknown-linux-musl
platform: linux-arm64
cli_binary: codewhale
shim_binary: codew
tui_binary: codewhale-tui
cli_artifact: codewhale-linux-arm64
tui_artifact: codewhale-tui-linux-arm64
binary: codewhale
primary_artifact: codewhale-linux-arm64
alias_artifact: codew-linux-arm64
- os: macos-latest
target: x86_64-apple-darwin
platform: macos-x64
cli_binary: codewhale
shim_binary: codew
tui_binary: codewhale-tui
cli_artifact: codewhale-macos-x64
tui_artifact: codewhale-tui-macos-x64
binary: codewhale
primary_artifact: codewhale-macos-x64
alias_artifact: codew-macos-x64
- os: macos-latest
target: aarch64-apple-darwin
platform: macos-arm64
cli_binary: codewhale
shim_binary: codew
tui_binary: codewhale-tui
cli_artifact: codewhale-macos-arm64
tui_artifact: codewhale-tui-macos-arm64
binary: codewhale
primary_artifact: codewhale-macos-arm64
alias_artifact: codew-macos-arm64
- os: windows-latest
target: x86_64-pc-windows-msvc
platform: windows-x64
cli_binary: codewhale.exe
shim_binary: codew.exe
tui_binary: codewhale-tui.exe
cli_artifact: codewhale-windows-x64.exe
tui_artifact: codewhale-tui-windows-x64.exe
binary: codewhale.exe
primary_artifact: codewhale-windows-x64.exe
alias_artifact: codew-windows-x64.exe
- os: windows-11-arm
target: aarch64-pc-windows-msvc
platform: windows-arm64
cli_binary: codewhale.exe
shim_binary: codew.exe
tui_binary: codewhale-tui.exe
cli_artifact: codewhale-windows-arm64.exe
tui_artifact: codewhale-tui-windows-arm64.exe
binary: codewhale.exe
primary_artifact: codewhale-windows-arm64.exe
alias_artifact: codew-windows-arm64.exe
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v7
@@ -92,14 +80,16 @@ jobs:
if: steps.sccache.outcome == 'success'
shell: bash
run: |
echo "SCCACHE_GHA_ENABLED=true" >> "${GITHUB_ENV}"
echo "RUSTC_WRAPPER=sccache" >> "${GITHUB_ENV}"
echo "SCCACHE_IGNORE_SERVER_IO_ERROR=1" >> "${GITHUB_ENV}"
{
echo "SCCACHE_GHA_ENABLED=true"
echo "RUSTC_WRAPPER=sccache"
echo "SCCACHE_IGNORE_SERVER_IO_ERROR=1"
} >> "${GITHUB_ENV}"
- uses: Swatinem/rust-cache@v2
with:
cache-bin: false
- name: Install Linux system dependencies
if: runner.os == 'Linux'
- name: Install Linux GNU system dependencies
if: matrix.target == 'x86_64-unknown-linux-gnu'
run: |
for i in 1 2 3 4 5; do
sudo apt-get update && break
@@ -107,6 +97,13 @@ jobs:
sleep 15
done
sudo apt-get install -y libdbus-1-dev pkg-config
- name: Install Linux ARM64 musl toolchain
if: matrix.target == 'aarch64-unknown-linux-musl'
shell: bash
run: |
sudo apt-get update
sudo apt-get install -y binutils musl-tools
rustup target add --toolchain stable aarch64-unknown-linux-musl
- name: Build
shell: bash
# Nightly artifacts are disposable smoke binaries (14-day retention),
@@ -117,7 +114,7 @@ jobs:
CARGO_PROFILE_RELEASE_CODEGEN_UNITS: '16'
run: |
for attempt in 1 2 3; do
if cargo build --release --locked --target ${{ matrix.target }} -p codewhale-cli -p codewhale-tui; then
if cargo build --release --locked --target ${{ matrix.target }} -p codewhale-cli; then
exit 0
fi
if [ "${attempt}" -lt 3 ]; then
@@ -127,30 +124,40 @@ jobs:
done
echo "Build failed after 3 attempts" >&2
exit 1
- name: Smoke native ARM binaries
if: matrix.target == 'aarch64-unknown-linux-gnu' || matrix.target == 'aarch64-pc-windows-msvc'
- name: Verify static Linux ARM64 binary and launch
if: matrix.target == 'aarch64-unknown-linux-musl' && runner.arch == 'ARM64'
shell: bash
run: |
set -euo pipefail
bin_path="target/${{ matrix.target }}/release/${{ matrix.binary }}"
if readelf -l "${bin_path}" | grep -Fq 'INTERP'; then
echo "Expected a static musl binary, but ${bin_path} has an ELF interpreter" >&2
exit 1
fi
"${bin_path}" --version
- name: Smoke binary on matching native runners
if: >-
(startsWith(matrix.target, 'x86_64-') && runner.arch == 'X64') ||
(startsWith(matrix.target, 'aarch64-') && runner.arch == 'ARM64')
shell: bash
run: |
bin_dir="target/${{ matrix.target }}/release"
"${bin_dir}/${{ matrix.cli_binary }}" --version
"${bin_dir}/${{ matrix.shim_binary }}" --version
"${bin_dir}/${{ matrix.tui_binary }}" --version
"${bin_dir}/${{ matrix.binary }}" --version
- name: Stage artifact
id: stage
shell: bash
run: |
short_sha="${GITHUB_SHA::12}"
stage_one() {
local binary="$1"
local artifact="$2"
local dir="$3"
local bin_path="target/${{ matrix.target }}/release/${binary}"
if [ ! -f "${bin_path}" ]; then
echo "Binary not at ${bin_path}; searching target/ for ${binary}:"
find target -name "${binary}" -type f
exit 1
fi
bin_path="target/${{ matrix.target }}/release/${{ matrix.binary }}"
if [ ! -f "${bin_path}" ]; then
echo "Binary not at ${bin_path}; searching target/ for ${{ matrix.binary }}:"
find target -name "${{ matrix.binary }}" -type f
exit 1
fi
stage_copy() {
local artifact="$1"
local dir="$2"
mkdir -p "${dir}"
cp "${bin_path}" "${dir}/${artifact}"
cat > "${dir}/nightly-build-info.txt" <<INFO
@@ -162,17 +169,20 @@ jobs:
INFO
}
stage_one "${{ matrix.cli_binary }}" "${{ matrix.cli_artifact }}" nightly-cli
stage_one "${{ matrix.tui_binary }}" "${{ matrix.tui_artifact }}" nightly-tui
echo "cli_name=${{ matrix.cli_artifact }}-${short_sha}" >> "${GITHUB_OUTPUT}"
echo "tui_name=${{ matrix.tui_artifact }}-${short_sha}" >> "${GITHUB_OUTPUT}"
stage_copy "${{ matrix.primary_artifact }}" nightly-primary
stage_copy "${{ matrix.alias_artifact }}" nightly-alias
cmp -s \
"nightly-primary/${{ matrix.primary_artifact }}" \
"nightly-alias/${{ matrix.alias_artifact }}"
echo "primary_name=${{ matrix.primary_artifact }}-${short_sha}" >> "${GITHUB_OUTPUT}"
echo "alias_name=${{ matrix.alias_artifact }}-${short_sha}" >> "${GITHUB_OUTPUT}"
- uses: actions/upload-artifact@v7
with:
name: ${{ steps.stage.outputs.cli_name }}
path: nightly-cli/*
name: ${{ steps.stage.outputs.primary_name }}
path: nightly-primary/*
retention-days: 14
- uses: actions/upload-artifact@v7
with:
name: ${{ steps.stage.outputs.tui_name }}
path: nightly-tui/*
name: ${{ steps.stage.outputs.alias_name }}
path: nightly-alias/*
retention-days: 14
+49 -30
View File
@@ -29,6 +29,9 @@ env:
jobs:
build:
name: Build ${{ matrix.platform }}
# FreeBSD is a source-build target validated via `cargo check --target x86_64-unknown-freebsd -p codewhale-cli --locked`
# (see packaging/freebsd/README.md and docs/INSTALL.md#freebsd). The 7×1 prebuilt matrix stays 7 targets;
# FreeBSD has no prebuilt asset, no npm binary, and no matrix bloat — it builds from source.
strategy:
fail-fast: false
matrix:
@@ -38,16 +41,14 @@ jobs:
platform: linux-x64
cli_binary: codewhale
shim_binary: codew
tui_binary: codewhale-tui
cli_artifact: codewhale-linux-x64
shim_artifact: codew-linux-x64
tui_artifact: codewhale-tui-linux-x64
- os: ubuntu-24.04-arm
target: aarch64-unknown-linux-gnu
target: aarch64-unknown-linux-musl
platform: linux-arm64
cli_binary: codewhale
shim_binary: codew
tui_binary: codewhale-tui
cli_artifact: codewhale-linux-arm64
shim_artifact: codew-linux-arm64
tui_artifact: codewhale-tui-linux-arm64
@@ -56,7 +57,6 @@ jobs:
platform: android-arm64
cli_binary: codewhale
shim_binary: codew
tui_binary: codewhale-tui
cli_artifact: codewhale-android-arm64
shim_artifact: codew-android-arm64
tui_artifact: codewhale-tui-android-arm64
@@ -65,7 +65,6 @@ jobs:
platform: macos-x64
cli_binary: codewhale
shim_binary: codew
tui_binary: codewhale-tui
cli_artifact: codewhale-macos-x64
shim_artifact: codew-macos-x64
tui_artifact: codewhale-tui-macos-x64
@@ -74,7 +73,6 @@ jobs:
platform: macos-arm64
cli_binary: codewhale
shim_binary: codew
tui_binary: codewhale-tui
cli_artifact: codewhale-macos-arm64
shim_artifact: codew-macos-arm64
tui_artifact: codewhale-tui-macos-arm64
@@ -83,7 +81,6 @@ jobs:
platform: windows-x64
cli_binary: codewhale.exe
shim_binary: codew.exe
tui_binary: codewhale-tui.exe
cli_artifact: codewhale-windows-x64.exe
shim_artifact: codew-windows-x64.exe
tui_artifact: codewhale-tui-windows-x64.exe
@@ -92,7 +89,6 @@ jobs:
platform: windows-arm64
cli_binary: codewhale.exe
shim_binary: codew.exe
tui_binary: codewhale-tui.exe
cli_artifact: codewhale-windows-arm64.exe
shim_artifact: codew-windows-arm64.exe
tui_artifact: codewhale-tui-windows-arm64.exe
@@ -120,23 +116,14 @@ jobs:
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
with:
cache-bin: false
- name: Install Linux ARM64 system dependencies
if: matrix.target == 'aarch64-unknown-linux-gnu'
run: |
for i in 1 2 3 4 5; do
sudo apt-get update && break
echo "apt-get update failed (attempt $i); retrying in 15s"
sleep 15
done
sudo apt-get install -y libdbus-1-dev pkg-config
- name: Build static Linux x64 binaries (musl)
if: matrix.target == 'x86_64-unknown-linux-musl'
- name: Build static Linux binaries (musl)
if: endsWith(matrix.target, '-unknown-linux-musl')
shell: bash
run: |
sudo apt-get update
sudo apt-get install -y musl-tools
rustup target add --toolchain stable x86_64-unknown-linux-musl
cargo build --profile dist --locked --target x86_64-unknown-linux-musl -p codewhale-cli -p codewhale-tui
sudo apt-get install -y binutils musl-tools
rustup target add --toolchain stable ${{ matrix.target }}
cargo build --profile dist --locked --target ${{ matrix.target }} -p codewhale-cli
- name: Configure Android NDK linker
if: matrix.target == 'aarch64-linux-android' && runner.os == 'Linux'
shell: bash
@@ -184,9 +171,32 @@ jobs:
echo "BINDGEN_EXTRA_CLANG_ARGS_aarch64_linux_android=--target=aarch64-linux-android24 --sysroot=${ndk}/toolchains/llvm/prebuilt/linux-x86_64/sysroot"
} >> "${GITHUB_ENV}"
- name: Build
if: matrix.target != 'x86_64-unknown-linux-musl'
if: ${{ !endsWith(matrix.target, '-unknown-linux-musl') }}
shell: bash
run: cargo build --profile dist --locked --target ${{ matrix.target }} -p codewhale-cli -p codewhale-tui
run: cargo build --profile dist --locked --target ${{ matrix.target }} -p codewhale-cli
- name: Materialize codew command alias
shell: bash
run: |
bin_dir="target/${{ matrix.target }}/dist"
cp "${bin_dir}/${{ matrix.cli_binary }}" "${bin_dir}/${{ matrix.shim_binary }}"
cmp "${bin_dir}/${{ matrix.cli_binary }}" "${bin_dir}/${{ matrix.shim_binary }}"
- name: Verify static Linux binaries and launch on matching native runners
if: >-
endsWith(matrix.target, '-unknown-linux-musl') &&
((startsWith(matrix.target, 'x86_64-') && runner.arch == 'X64') ||
(startsWith(matrix.target, 'aarch64-') && runner.arch == 'ARM64'))
shell: bash
run: |
set -euo pipefail
bin_dir="target/${{ matrix.target }}/dist"
for binary in "${{ matrix.cli_binary }}" "${{ matrix.shim_binary }}"; do
bin_path="${bin_dir}/${binary}"
if readelf -l "${bin_path}" | grep -Fq 'INTERP'; then
echo "Expected a static musl binary, but ${bin_path} has an ELF interpreter" >&2
exit 1
fi
"${bin_path}" --version
done
- name: Smoke binaries on matching native runners
if: >-
matrix.target != 'aarch64-linux-android' &&
@@ -197,7 +207,6 @@ jobs:
bin_dir="target/${{ matrix.target }}/dist"
"${bin_dir}/${{ matrix.cli_binary }}" --version
"${bin_dir}/${{ matrix.shim_binary }}" --version
"${bin_dir}/${{ matrix.tui_binary }}" --version
- name: Stage binaries
shell: bash
run: |
@@ -215,7 +224,10 @@ jobs:
stage_binary "${{ matrix.cli_binary }}" "${{ matrix.cli_artifact }}"
stage_binary "${{ matrix.shim_binary }}" "${{ matrix.shim_artifact }}"
stage_binary "${{ matrix.tui_binary }}" "${{ matrix.tui_artifact }}"
# One-release compatibility bridge for v0.9.4's hard-coded release
# completeness/updater contract. This is the same runtime, not a
# separately compiled TUI binary.
stage_binary "${{ matrix.cli_binary }}" "${{ matrix.tui_artifact }}"
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: ${{ matrix.cli_artifact }}
@@ -252,7 +264,15 @@ jobs:
pattern: '*'
- name: Create and checksum platform archives
shell: bash
run: bash scripts/release/create-release-bundles.sh artifacts bundles
run: |
set -euo pipefail
source_date_epoch="$(git show -s --format=%ct "${{ inputs.source_sha }}")"
if [[ ! "${source_date_epoch}" =~ ^[0-9]+$ ]]; then
echo "Could not read a Unix timestamp for source commit ${{ inputs.source_sha }}" >&2
exit 1
fi
SOURCE_DATE_EPOCH="${source_date_epoch}" \
bash scripts/release/create-release-bundles.sh artifacts bundles
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: codewhale-bundles
@@ -285,7 +305,6 @@ jobs:
$ErrorActionPreference = "Stop"
Copy-Item "artifacts\codewhale-windows-x64.exe\codewhale-windows-x64.exe" "scripts\installer\codewhale.exe"
Copy-Item "artifacts\codew-windows-x64.exe\codew-windows-x64.exe" "scripts\installer\codew.exe"
Copy-Item "artifacts\codewhale-tui-windows-x64.exe\codewhale-tui-windows-x64.exe" "scripts\installer\codewhale-tui.exe"
$makensis = "${env:ProgramFiles(x86)}\NSIS\makensis.exe"
if (!(Test-Path $makensis)) {
$makensis = "${env:ProgramFiles}\NSIS\makensis.exe"
@@ -348,7 +367,7 @@ jobs:
with:
name: codewhale-release-assets
path: release-assets
- name: Verify 34-asset inventory and checksum manifests
- name: Verify 34-asset bridge inventory and checksum manifests (single binary)
run: node scripts/release/assemble-release-assets.js --verify release-assets
- name: Test release inventory contracts
run: |
@@ -366,6 +385,6 @@ jobs:
echo ""
echo "- Source: \`${{ inputs.source_sha }}\`"
echo "- Version metadata: \`${{ inputs.version }}\`"
echo "- Inventory: 7 targets / 34 files"
echo "- Inventory: 7 targets / 34 files (single binary; 7 legacy alias assets)"
echo "- Publication: none (Actions artifact \`codewhale-release-assets\` only)"
} >> "${GITHUB_STEP_SUMMARY}"
-1
View File
@@ -167,7 +167,6 @@ jobs:
docker pull "${IMAGE}"
docker run --rm --entrypoint codewhale "${IMAGE}" --version
docker run --rm --entrypoint codew "${IMAGE}" --version
docker run --rm --entrypoint codewhale-tui "${IMAGE}" --version
homebrew:
needs: resolve
+214 -32
View File
@@ -163,6 +163,12 @@ jobs:
-A clippy::assertions_on_constants
- name: Workspace tests
run: cargo test --workspace --all-features --locked
env:
# Match the CI test lane: test threads get the same stack the product
# gives itself (main.rs CODEWHALE_MAIN_STACK_BYTES). See the note in
# ci.yml's "Run tests" step. Without it this gate runs the deep
# engine/runtime futures on a stack that never ships.
RUST_MIN_STACK: '16777216'
- name: Protocol schema parity
run: cargo test -p codewhale-protocol --test parity_protocol --locked
- name: State persistence parity
@@ -179,26 +185,149 @@ jobs:
version: ${{ needs.resolve.outputs.version }}
retention_days: 14
docker:
docker-build:
needs: [artifacts, resolve]
if: ${{ !cancelled() && needs.artifacts.result == 'success' }}
runs-on: ubuntu-latest
name: Docker ${{ matrix.platform }}
strategy:
fail-fast: false
matrix:
include:
- runner: ubuntu-latest
platform: linux/amd64
architecture: amd64
cli_artifact: codewhale-linux-x64
shim_artifact: codew-linux-x64
- runner: ubuntu-24.04-arm
platform: linux/arm64
architecture: arm64
cli_artifact: codewhale-linux-arm64
shim_artifact: codew-linux-arm64
runs-on: ${{ matrix.runner }}
permissions:
contents: read
packages: write
steps:
- name: Checkout release source
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
with:
ref: ${{ needs.resolve.outputs.sha }}
path: source
- name: Checkout release infrastructure
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
with:
ref: ${{ needs.resolve.outputs.sha }}
path: infra
- name: Set up QEMU
uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4
- name: Download Codewhale release binary
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
name: ${{ matrix.cli_artifact }}
path: docker-context/bin
- name: Download codew release alias
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
name: ${{ matrix.shim_artifact }}
path: docker-context/bin
- name: Verify native release bytes
shell: bash
env:
CLI_ARTIFACT: ${{ matrix.cli_artifact }}
SHIM_ARTIFACT: ${{ matrix.shim_artifact }}
run: |
set -euo pipefail
mv -- "docker-context/bin/${CLI_ARTIFACT}" docker-context/bin/codewhale
mv -- "docker-context/bin/${SHIM_ARTIFACT}" docker-context/bin/codew
chmod 0755 docker-context/bin/codewhale docker-context/bin/codew
cmp docker-context/bin/codewhale docker-context/bin/codew
docker-context/bin/codewhale --version
docker-context/bin/codew --version
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4
- name: Log in to GitHub Container Registry
uses: docker/login-action@371161bbe7024a29a25c5e19bfcbc0804fe9ad2c # v4
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Normalize image name
id: image
shell: bash
run: echo "name=ghcr.io/${GITHUB_REPOSITORY,,}" >> "$GITHUB_OUTPUT"
- name: Extract image labels
id: meta
uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6
with:
images: |
${{ steps.image.outputs.name }}
tags: |
type=raw,value=${{ needs.resolve.outputs.version }}
- name: Revalidate release tag before container upload
env:
EXPECTED_SHA: ${{ needs.resolve.outputs.sha }}
TAG: ${{ needs.resolve.outputs.tag }}
run: |
./infra/scripts/release/verify-remote-tag.sh \
"https://github.com/${GITHUB_REPOSITORY}.git" \
"${TAG}" \
"${EXPECTED_SHA}"
- name: Assemble and push native image by digest
id: build
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7
env:
DOCKER_BUILD_RECORD_UPLOAD: false
DOCKER_BUILD_SUMMARY: false
with:
context: docker-context
file: infra/packaging/docker/Dockerfile.release
platforms: ${{ matrix.platform }}
provenance: mode=max
sbom: true
labels: ${{ steps.meta.outputs.labels }}
outputs: type=image,name=${{ steps.image.outputs.name }},push-by-digest=true,name-canonical=true,push=true
- name: Smoke native image digest
shell: bash
env:
IMAGE: ${{ steps.image.outputs.name }}@${{ steps.build.outputs.digest }}
run: |
set -euo pipefail
docker pull "${IMAGE}"
docker run --rm --entrypoint codewhale "${IMAGE}" --version
docker run --rm --entrypoint codew "${IMAGE}" --version
- name: Export image digest
shell: bash
env:
DIGEST: ${{ steps.build.outputs.digest }}
run: |
set -euo pipefail
if ! [[ "${DIGEST}" =~ ^sha256:[0-9a-f]{64}$ ]]; then
echo "Unexpected image digest: ${DIGEST}" >&2
exit 1
fi
mkdir -p digests
touch "digests/${DIGEST#sha256:}"
- name: Upload image digest
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: docker-digest-${{ matrix.architecture }}
path: digests/*
if-no-files-found: error
retention-days: 1
overwrite: true
docker:
needs: [docker-build, resolve]
if: ${{ !cancelled() && needs.docker-build.result == 'success' }}
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout release infrastructure
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
with:
ref: ${{ needs.resolve.outputs.sha }}
path: infra
- name: Download native image digests
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
path: digests
pattern: docker-digest-*
merge-multiple: true
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4
- name: Log in to GitHub Container Registry
@@ -237,34 +366,52 @@ jobs:
"https://github.com/${GITHUB_REPOSITORY}.git" \
"${TAG}" \
"${EXPECTED_SHA}"
- name: Build and push
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7
- name: Publish multi-architecture manifest
shell: bash
env:
DOCKER_BUILD_RECORD_UPLOAD: false
DOCKER_BUILD_SUMMARY: false
with:
context: source
file: infra/Dockerfile
platforms: linux/amd64,linux/arm64
push: true
provenance: mode=max
sbom: true
build-args: |
DEEPSEEK_BUILD_SHA=${{ needs.resolve.outputs.sha }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Smoke published container entrypoints
IMAGE: ${{ steps.image.outputs.name }}
TAGS: ${{ steps.meta.outputs.tags }}
run: |
set -euo pipefail
mapfile -t digest_files < <(find digests -maxdepth 1 -type f -printf '%f\n' | sort)
if [[ "${#digest_files[@]}" -ne 2 ]]; then
echo "Expected exactly two native image digests; found ${#digest_files[@]}." >&2
exit 1
fi
sources=()
for digest in "${digest_files[@]}"; do
if ! [[ "${digest}" =~ ^[0-9a-f]{64}$ ]]; then
echo "Unexpected image digest file: ${digest}" >&2
exit 1
fi
sources+=("${IMAGE}@sha256:${digest}")
done
tag_args=()
while IFS= read -r tag; do
[[ -n "${tag}" ]] && tag_args+=(--tag "${tag}")
done <<< "${TAGS}"
if [[ "${#tag_args[@]}" -eq 0 ]]; then
echo "No container tags were generated." >&2
exit 1
fi
docker buildx imagetools create "${tag_args[@]}" "${sources[@]}"
- name: Verify and smoke published container
shell: bash
env:
IMAGE: ${{ steps.image.outputs.name }}:${{ needs.resolve.outputs.tag }}
run: |
set -euo pipefail
docker buildx imagetools inspect "${IMAGE}"
raw_manifest="$(docker buildx imagetools inspect --raw "${IMAGE}")"
jq -e \
'[.manifests[] | select(.platform.os == "linux") | "linux/\(.platform.architecture)"] | unique | sort == ["linux/amd64", "linux/arm64"]' \
<<< "${raw_manifest}"
docker pull "${IMAGE}"
docker run --rm --entrypoint codewhale "${IMAGE}" --version
docker run --rm --entrypoint codew "${IMAGE}" --version
docker run --rm --entrypoint codewhale-tui "${IMAGE}" --version
release:
needs: [artifacts, docker, resolve]
@@ -314,6 +461,45 @@ jobs:
overwrite_files: false
fail_on_unmatched_files: true
npm:
needs: [release, resolve]
if: ${{ !cancelled() && needs.release.result == 'success' }}
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
with:
ref: ${{ needs.resolve.outputs.sha }}
fetch-depth: 0
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 24
registry-url: https://registry.npmjs.org
package-manager-cache: false
- name: Pin OIDC-capable npm CLI
run: npm install --global npm@12.0.2
- name: Revalidate release tag before npm publish
env:
EXPECTED_SHA: ${{ needs.resolve.outputs.sha }}
TAG: ${{ needs.resolve.outputs.tag }}
run: |
./scripts/release/verify-remote-tag.sh \
"https://github.com/${GITHUB_REPOSITORY}.git" \
"${TAG}" \
"${EXPECTED_SHA}"
- name: Revalidate public release assets
env:
GH_TOKEN: ${{ github.token }}
run: ./scripts/release/verify-release-assets.sh "${{ needs.resolve.outputs.version }}"
- name: Test npm wrapper
working-directory: npm/codewhale
run: npm test
- name: Publish npm wrapper with trusted publishing
working-directory: npm/codewhale
run: npm publish --access public
homebrew:
needs: [release, resolve]
if: ${{ !cancelled() && needs.release.result == 'success' }}
@@ -363,7 +549,3 @@ jobs:
TAP_REPO: Hmbown/homebrew-deepseek-tui
TOKEN: ${{ secrets.HOMEBREW_TAP_PAT || secrets.RELEASE_TAG_PAT }}
run: bash .github/scripts/update-homebrew-tap.sh
# npm publish is intentionally not automated. The npm account requires 2FA OTP
# on every publish. Publish the wrapper manually only after the immutable public
# GitHub asset gate in docs/RELEASE_RUNBOOK.md succeeds.
+23 -3
View File
@@ -52,6 +52,25 @@ jobs:
- name: Build production site
run: npm run build
deploy-reminder:
name: Deployment approval needed
runs-on: ubuntu-latest
needs: lint
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
steps:
- name: Surface the manual deployment gate
env:
REVISION: ${{ github.sha }}
run: |
echo "::notice title=Web deployment approval needed::Revision ${REVISION} passed the web gates but is not deployed. Dispatch web.yml on main to publish it."
{
echo "## Web deployment approval needed"
echo
echo "Revision \`${REVISION}\` passed the web gates but has **not** been deployed."
echo
echo "A maintainer can publish it with \`gh workflow run web.yml --repo Hmbown/CodeWhale --ref main\`."
} >> "$GITHUB_STEP_SUMMARY"
deploy:
name: Deploy to Cloudflare
runs-on: ubuntu-latest
@@ -103,9 +122,10 @@ jobs:
run: npm run compare:deployed-facts -- --expected-revision "$GITHUB_SHA"
- name: Check Cloudflare deploy environment
run: npm run check:deploy-env
- name: Build OpenNext bundle
run: npm run build && npx opennextjs-cloudflare build
- name: Deploy
# npm's deploy script performs one OpenNext build, then deploys that exact
# bundle. Wrangler must not run a custom post-cache build: OpenNext
# populates the remote cache before it hands the bundle to Wrangler.
- name: Build and deploy exact OpenNext bundle
run: npm run deploy
- name: Verify exact deployed revision
# The public /api/facts receipt must identify this workflow's exact
+1
View File
@@ -54,6 +54,7 @@ docs/*.pdf
!scripts/**
!.github/scripts/**
!web/public/install.sh
!packaging/winget/**
test.txt
TODO*.md
todo*.md
+97
View File
@@ -0,0 +1,97 @@
# Hmbown.CodeWhale — winget singleton manifest for CodeWhale (single binary)
# This is a mirror of packaging/winget/Hmbown.CodeWhale.yaml for tooling that expects .winget/.
# Keep both in sync; the canonical source is packaging/winget/Hmbown.CodeWhale.yaml.
# See packaging/winget/README.md for update instructions.
PackageIdentifier: Hmbown.CodeWhale
PackageVersion: 0.9.5
DefaultLocale: en-US
ManifestType: singleton
ManifestVersion: 1.6.0
Publisher: Hmbown
PublisherUrl: https://github.com/Hmbown
PublisherSupportUrl: https://github.com/Hmbown/CodeWhale/issues
PackageName: CodeWhale
PackageUrl: https://github.com/Hmbown/CodeWhale
License: MIT
LicenseUrl: https://github.com/Hmbown/CodeWhale/blob/main/LICENSE
Copyright: Copyright (c) Hmbown
ShortDescription: Terminal coding agent for supported hosted and local models
Description: |
CodeWhale is a terminal coding agent that runs on your machine. The v0.9.5 single-binary
release ships one `codewhale` binary per target (plus the `codew` shim) across Linux x64 (musl),
Linux arm64, Android arm64, macOS x64/arm64, and Windows x64/arm64. See https://github.com/Hmbown/CodeWhale
for provider setup, Fleet workflows, and the full install guide.
Author: Hmbown
Moniker: codewhale
Tags:
- codewhale
- deepseek
- cli
- tui
- terminal
- ai
- coding-agent
- rust
MinimumOSVersion: 10.0.0.0
ReleaseNotes: https://github.com/Hmbown/CodeWhale/releases/tag/v0.9.5
ReleaseNotesUrl: https://github.com/Hmbown/CodeWhale/releases/tag/v0.9.5
Installers:
- Architecture: x64
InstallerType: nullsoft
Scope: user
InstallerUrl: https://github.com/Hmbown/CodeWhale/releases/download/v0.9.5/CodeWhaleSetup.exe
InstallerSha256: 0000000000000000000000000000000000000000000000000000000000000000
ProductCode: CodeWhale
UpgradeBehavior: install
ReleaseDate: 2026-08-07
- Architecture: x64
InstallerType: zip
Scope: user
InstallerUrl: https://github.com/Hmbown/CodeWhale/releases/download/v0.9.5/codewhale-windows-x64.zip
InstallerSha256: 0000000000000000000000000000000000000000000000000000000000000000
NestedInstallerType: portable
NestedInstallerFiles:
- RelativeFilePath: codewhale-windows-x64/codewhale.exe
PortableCommandAlias: codewhale
- RelativeFilePath: codewhale-windows-x64/codew.exe
PortableCommandAlias: codew
ReleaseDate: 2026-08-07
- Architecture: x64
InstallerType: zip
Scope: user
InstallerUrl: https://github.com/Hmbown/CodeWhale/releases/download/v0.9.5/codewhale-windows-x64-portable.zip
InstallerSha256: 0000000000000000000000000000000000000000000000000000000000000000
NestedInstallerType: portable
NestedInstallerFiles:
- RelativeFilePath: codewhale-windows-x64-portable/codewhale.exe
PortableCommandAlias: codewhale
- RelativeFilePath: codewhale-windows-x64-portable/codew.exe
PortableCommandAlias: codew
ReleaseDate: 2026-08-07
- Architecture: arm64
InstallerType: zip
Scope: user
InstallerUrl: https://github.com/Hmbown/CodeWhale/releases/download/v0.9.5/codewhale-windows-arm64.zip
InstallerSha256: 0000000000000000000000000000000000000000000000000000000000000000
NestedInstallerType: portable
NestedInstallerFiles:
- RelativeFilePath: codewhale-windows-arm64/codewhale.exe
PortableCommandAlias: codewhale
- RelativeFilePath: codewhale-windows-arm64/codew.exe
PortableCommandAlias: codew
ReleaseDate: 2026-08-07
- Architecture: arm64
InstallerType: zip
Scope: user
InstallerUrl: https://github.com/Hmbown/CodeWhale/releases/download/v0.9.5/codewhale-windows-arm64-portable.zip
InstallerSha256: 0000000000000000000000000000000000000000000000000000000000000000
NestedInstallerType: portable
NestedInstallerFiles:
- RelativeFilePath: codewhale-windows-arm64-portable/codewhale.exe
PortableCommandAlias: codewhale
- RelativeFilePath: codewhale-windows-arm64-portable/codew.exe
PortableCommandAlias: codew
ReleaseDate: 2026-08-07
+5 -5
View File
@@ -1,8 +1,8 @@
# Repository Agent Guidance
Durable rules only. Perishable lane state — branch, milestone, known flakes,
closed investigations — lives in `docs/ops/CURRENT.md`; read it, don't trust
memory of it.
closed investigations — lives in the private `codewhale-ops` repo, not here.
Read it there; don't trust memory of it.
## Intent is the artifact
@@ -93,6 +93,6 @@ and every model/provider first-class — none privileged.
- Keep gates warm and dry-run unless Hunter explicitly approves enforcement.
- Leave unrelated edits by other people or agents intact.
Full ethos: `docs/AGENT_ETHOS.md`. Issue triage standard:
`docs/AGENT_READY_ISSUES.md`. Release queue and harvest procedure:
`docs/RELEASE_QUEUE.md`.
Full ethos: `docs/AGENT_ETHOS.md`. Issue-triage standard, release queue, and
harvest procedure live in the private `codewhale-ops` repo — they are
maintainer process, not contributor-facing contract.
+419 -4
View File
@@ -7,7 +7,292 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
## [0.9.4] - 2026-08-05
## [0.9.6] - 2026-08-11
Codewhale v0.9.6 is a subtractive release: fewer runtime guards, one stable
prompt, truthful provider endings, and a smaller compaction path that preserves
the provider cache. The changes were grounded by matched Terminal-Bench 2.1
runs against Pi 0.8.41 and by dogfooding repeated manual compaction.
### Added
- `web_search` defaults to Firecrawl Cloud without an API key; keyless requests
are headerless and quota-bounded, while an optional user key raises limits.
- Green web builds on `main` now emit an actionable manual-deploy reminder, so
site changes cannot quietly appear shipped while Cloudflare still serves an
older revision.
- Mistral AI is a first-class provider route, including Codestral models,
first-party reasoning support, authentication, picker entries, and aliases.
- Headless `Bash` can transfer explicitly requested persistent Unix services
out of an exec run, with ownership and cleanup receipts.
- `/remote-env` opens hosted Work from the current GitHub or CNB branch tip and
states exactly which unpushed, dirty, ignored, secret, and session state stays
local.
- Linux ARM64 release and nightly assets are static musl builds with native
launch checks.
- Maintainers can report observed daily active installs from the same anonymous,
aggregate telemetry dataset; no additional client data is collected.
- Fleet-dispatched members under a read-only evidence (no-network) ceiling now
keep the `Web` tool's read-only `search` and `fetch` actions — parity with an
ordinary scout — while every reaching surface (`web.run`, `fetch_url`,
`github`, MCP) stays denied and the sentinel-backed capability envelope
remains the fail-closed backstop.
- `/fleet setup` can show an optional, deterministic, unratified role-to-model
advisory built only from configured ready routes. Accept, edit, and reject
all remain inside the existing human-reviewed profile save boundary; the
advisory never launches a Fleet or writes a second configuration.
- `/update` checks for a newer Codewhale release and installs it from inside
the TUI, while `tui_help` gives agents the same command and key map users see.
- Markdown file paths render as OSC 8 links where the terminal supports them,
and every agent row can open that agent's transcript directly.
- ACP editor sessions can execute multi-round file, search, Git, patch, and
explicitly enabled shell tool calls through the shared Runtime registry.
Shell access requires both the client's terminal capability and Codewhale's
headless shell opt-in, and cancellation stops an in-flight tool before the
turn returns (#5225 by @rafaelcavalheri).
- Lowercase `read` returns bounded typed PNG, JPEG, GIF, and WebP results to
image-capable Chat, Responses, Anthropic, and ACP routes. Text-only routes
receive an explicit omission receipt; image bytes never spill into ordinary
transcript, export, compaction, or relay text.
### Changed
- Anonymous usage counting is on by default for fresh installs and disclosed in
a native first-run Codewhale modal with an immediate opt-out. Prior declines
remain off. Codewhale does not collect conversations, code, prompts, files,
repo or branch names, credentials, model content, or per-turn activity
timelines.
- Wide terminals use a responsive, full-screen ocean canvas with modest
gutters: prose keeps a readable measure while tools, diffs, work surfaces,
the composer, and status chrome can use the available width. Turn and major
activity seams breathe without padding every call inside a tool group.
- Root CLI help describes product actions directly instead of exposing internal
TUI/runtime layers.
- `Bash action="wait"` now blocks by default when a wait is requested; callers
can still ask for a nonblocking snapshot, and persistent service ownership
remains explicit.
- Compaction is one cache-stable summary request followed by one committed
replacement summary and a bounded recent-message tail. Older saved sessions
still restore.
- Ask, Work, Auto-Review, and Full Access share one stable base prompt. Modes
continue to differ through permissions and the live tool catalog; the former
Act label is now Work throughout the product and shipped locales.
- Full Access now auto-approves non-bypassable tools consistently, and the
default choice shown on ordinary approval cards is configurable.
- Model, context-window, dispatch-name, and nested-agent spawn receipts report
the route and limits actually used rather than silently substituting a
guessed identity.
- Child-agent launches mint one immutable route receipt before admission and
preserve it through status, interruption, completion, resume, Work Graph,
and ledger projections, so provider/model attribution cannot drift (#5305).
- Goal runs no longer stop because of internal continuation, repeated-gap, or
unanswered-question guards. Explicit user limits and terminal goal states
remain authoritative.
- Account-owned `/rc` remote control now keeps exclusive ownership and a
crash-recoverable delivery journal until the server acknowledges terminal,
approval, failure, and snapshot state.
- `todo_write` is an optional progress surface rather than required model
ceremony.
- New turns use one small, stable toolbox: `read`, `write`, `edit`, `bash`,
`agent`, `todo_write`, and `tool_search`. The optional progress tool stays
visible as familiar working memory; specialized native, Web, MCP, plugin,
memory, task, and verification tools are policy-filtered and searchable;
activated schemas stay in a bounded per-conversation cache. Every sub-agent
keeps its own search and cache, including policy-allowed Web research, while
forked context and parent activations remain warm starts rather than allowlists.
- The direct file and shell schemas follow Pi's deliberately small contract:
bounded complete-line reads, hash-free writes, unambiguous multi-edit with
BOM/CRLF preservation and conservative fuzzy matching, and one foreground
`bash` command with a bounded chronological output tail. Modes change
execution authority, not those primitive names.
- Codewhale no longer re-states the To-do list to the model. The model learns
what is on the list from the tool result its own `todo_write` call returned,
which is ordinary conversation history — the same way Pi's To-do works. The
transient `<codewhale:work_state>` block that used to ride the tail of every
parent turn-loop and sub-agent step request is gone, along with the stable
system prefix being disturbed by list changes. A snapshot is still shown once,
where a person asked for it: the `<codewhale:fork_state>` block a newly forked
sub-agent is handed, `/relay` handoff instructions, and the agent card. The
complete To-do stays visible in the UI. A structural test asserts real
outbound provider request bodies do not carry the list.
- Scout and Reviewer name the read-only investigator roles. Both expose exactly
one shell entry point — canonical lowercase `bash`, bounded by the strict
read-only classifier — and the legacy `Bash` alias stays denied in the catalog
and at dispatch. Previously a case-insensitive name match let a call spelled
`Bash` execute through that carve-out, returning raw shell to a read-only role.
### Fixed
- Sending more context while a lowercase `bash` command is running now moves
the command to `/jobs` and returns a successful running receipt instead of
falsely reporting `Command exited with code -1`; the process keeps running
and its completion still arrives through the normal runtime event.
- First-run usage disclosure now opens as a native Codewhale modal instead of a
shell questionnaire before application startup. Telemetry remains unarmed
until the native choice is made, and an in-memory Disable choice governs the
current session even when its preference cannot be saved.
- `/compact` completion, failure, queued, duplicate, and mailbox outcomes are
durable transcript receipts instead of short-lived toasts. A stray terminal
event can no longer leave every later compaction stuck as already running.
- Compaction now follows Codex's simple transcript shape: recent user context
followed by one ordinary history checkpoint. It never appends the summary,
the To-do list, or volatile shell/worker state to the standing system prompt;
reloads migrate the persisted carrier back into exactly one history item.
- Automatic compaction uses a percentage of the real context window, clamped to
the route's spendable ceiling. Pressure comes from the current parent-route
prompt, not cumulative billing or child-model usage.
- Compaction, review, verify, routing, setup, Fleet, MCP, RLM, vision,
translation, and sub-agent calls inherit the resolved route's normal output,
sampling, and reasoning policy. Small internal-task token caps no longer
truncate thinking routes or special-case individual providers.
- Incomplete provider responses fail truthfully across ordinary turns and every
internal model consumer. Partial text stays interrupted, pending tool calls do
not execute, and billed usage is retained.
- Transport-only `(reasoning omitted)` placeholders no longer enter new
transcripts and are filtered from restored sessions. Reasoning expand/collapse
actions stay attached to the exact rendered cell, including after replacement,
restore, filtering, and resize (#5291).
- Step-budget exhaustion is a typed failure and cannot release a pending
persistent service. Cancellation after terminal usage still charges the turn.
- Deferred tools now preserve a completed result when a provider reuses its
tool-call ID on the retry turn, preventing successful plugin calls from
entering a repeated execution loop.
- Website setup, provider, diagnostics, Fleet, and single-runtime claims now
match the source candidate.
- Opening the sub-agent register no longer hides the to-do list: the Agents
panel shows the full register and the durable checklist together, and the
register header is a two-way door that returns to Tasks on a second click.
- The ⌥V / Alt+V details chord opens the selected work-surface row's own
inspector instead of the transcript's nearest tool cell, so a selected
to-do row shows its own content rather than the latest reasoning.
- The first-run usage disclosure now asks a clear question — "Help improve
Codewhale?" — with unambiguous "Yes, keep anonymous counts" / "No, turn off
tracking" choices in every shipped locale, and states the persistent opt-out
command. Consent semantics are unchanged: telemetry stays unarmed until a
choice is made.
- macOS screencapture screenshots referenced in a message are copied to a
stable attachments directory the moment the message is received, and the
reference is rewritten to the stable path, so the image still exists when
the agent reads it. Only files under a screencapture "Temporary Items"
directory are touched; copies are idempotent and a failed copy keeps the
original reference.
- Manual `/compact` during an active turn now queues even when the engine's
bounded op mailbox is saturated. The request defers client-side, retries as
mailbox slots free, and cannot latch as already running after it settles.
- Interactive `/load`, startup `--resume`, and `/resume` picker paths preserve
the persisted provider, endpoint, and model identity; picker resume also
leaves a durable transcript receipt.
- Relative `mcp_config_path` values no longer depend on the launch directory or
silently load an empty server pool: Codewhale warns and falls back to the
user-global MCP configuration. Explicit absolute paths remain authoritative.
- Alibaba Model Studio `qwen3.8-max` and `qwen3.8-max-preview` still stream
their current reasoning, but no longer replay historical `reasoning_content`
that those routes do not accept. Historical reasoning replay is now gated by
the exact provider/API/model contract, so unknown `*-thinking` lookalikes
fail closed while documented Qwen, Kimi, DeepSeek, Mistral, Anthropic, and
Responses continuity rules remain intact.
- Compatibility File/patch calls retain optional content-hash guards when a
caller supplies them. The new direct `write` and `edit` schemas do not expose
hash or prior-read ceremony.
- Shell previews hold back incomplete UTF-8 sequences instead of emitting
replacement characters, and compaction receipts report token deltas.
- Nested agents may narrow but can never widen their inherited depth budget
(#5317 by @ousamabenyounes).
- Container publication now assembles AMD64 and ARM64 images in parallel on
native runners from the already-verified static release binaries, then
publishes and checks one multi-architecture manifest. It no longer rebuilds
both targets through the single long-running QEMU job that lost its runner.
### Removed
- The no-progress guard, repeated-read guard, and injected tool-error strategy
coaching. Productive polling, repeated inspection, and model-owned recovery
are no longer interrupted by runtime heuristics.
- Never-wired decision-card, keybinding, hover, shell-execution, engine-op, and
release-script paths were deleted so the supported runtime has one route for
each behavior.
### Contributors
- Xavier Pestel (@xavierpestel-ai) — Mistral AI provider route (#5295).
- Ben Younes (@ousamabenyounes) — inherited nested-agent depth cap (#5317).
- Rafael Cavalheri (@rafaelcavalheri) — ACP agentic tool turns (#5225).
## [0.9.5] - 2026-08-08
Codewhale v0.9.5 consolidates the terminal application into one compiled
runtime while preserving the familiar `codewhale` and `codew` commands. It
also expands the managed Runtime API, makes session and Fleet work easier to
inspect and resume, and removes the hidden local continuation backstop that
could end productive work without a final assistant response.
### Added
- **`model = "auto"` for prompt-based tier selection**: When set, the
dispatcher analyses the user's prompt before delegating to the TUI and
selects `deepseek-v4-pro` for complex tasks or `deepseek-v4-flash` for simple
tasks (PR #5257).
- Runtime API controls for persistent goals, bounded memory inspection, MCP
server and skill lifecycle management, and durable Fleet receipt evidence.
- Append-only session-tree history with `/tree`, `/branch`, `/fork`, and
`/resume`, plus `/rc` remote control and managed login.
- A unified Fleet roster for built-in dispatch postures and a pinned indicator
that keeps active background work visible above the composer.
- Incremental MCP registry refreshes that return the local snapshot immediately
and update it in the background.
- Scout and Reviewer agents can use a bounded direct-command evidence shell for
read-only workspace, Git, and GitHub inspection, and can keep private working
notes in their own To-do while the durable transcript retains their evidence.
### Changed
- `codewhale-cli` now contains the terminal runtime directly. Release installers
expose byte-identical `codewhale` and `codew` commands without a separate TUI
executable. The v0.9.5 asset set alone retains deprecated
`codewhale-tui-*` filenames as byte-identical compatibility copies so
installed v0.9.4 clients can discover and complete this upgrade.
- Startup release checks cache successful lookups for one hour. The updater
downloads and verifies the primary runtime once, then refreshes any existing
`codew` or legacy `codewhale-tui` command paths from the same bytes.
- Headless `codewhale exec` runs and verifier benchmark rollouts no longer
impose a 100-step default. `--max-turns` remains available as an explicit
opt-in ceiling; Fleet workers retain their separately configured budget.
- Goal token and time budgets are telemetry rather than default stop
conditions, and automatic goal continuation is unlimited unless the user
explicitly configures a continuation ceiling.
- Command-palette and slash-completion shadowing now share one alias-aware
discovery contract.
- The website install guidance, localized product copy, navigation controls,
social metadata, and Cloudflare build pipeline now describe and deploy the
same one-runtime release contract.
### Fixed
- The hidden 20-step no-user-input backstop no longer ends productive turns.
Tool results, queued steering, child completions, REPL feedback, and goal
continuations can all reach the next provider step and a final assistant
response; explicit user-configured limits and genuine stuck-loop guards remain.
- Complete error details are directly inspectable after a failure instead of
leaving the terminal with a clipped, unrecoverable error fragment.
- A newly minted OAuth credential is adopted in the same provider-selection
flow instead of requiring a second picker trip.
- Fresh session titles can replace a stale cached `New Session` placeholder,
unknown model context limits fail loudly, and release/source-install fallbacks
no longer request binaries removed by the single-runtime conversion.
### Contributors
- [Sh1Zuku](https://github.com/SparkofSpike) (`@SparkofSpike`) fixed stale
cached session titles that could pin the `New Session` placeholder.
- [Paulo Aboim Pinto](https://github.com/aboimpinto) (`@aboimpinto`) built the
shared alias-aware command discovery contract and acceptance coverage.
- [Sun Zhenyuan](https://github.com/bistack) (`@bistack`) contributed the
background incremental MCP Registry refresh.
- [SKY ZHAO](https://github.com/skyzhao1223) (`@skyzhao1223`) contributed
prompt-based `model = "auto"` routing in PR #5257.
## [0.9.4] - 2026-08-07
Codewhale v0.9.4 ships the release-train harness work: the familiar Fleet
roster/setup face with a clear operator-leader and user/folder scope, a
work strip that keeps actionable agents instead of a permanent archive,
@@ -19,6 +304,23 @@ File edits, terminal width, and Windows installation.
### Added
- Memory maintenance: `remember` gains `revise` and `retire` beside the
default `append`. Both name the exact note they target and both require
the evidence for the change. Append-only memory decays — a correction
sits behind the note it contradicts and both keep reaching the model —
so the model can now keep its own durable notes true instead of only
adding to them.
- An audit trail for durable state the model writes about you. Every
in-place memory edit is journalled to `memory/JOURNAL.md`, and every
continual-harness `refine` / `remove` to a `JOURNAL.md` beside its state,
each with before, after, and evidence. Harness removal previously left no
record at all even though the entry leaves state entirely, so the journal
is now the only place its content survives.
- A first-run tip that says so: the first time Codewhale saves something
durable it points at `/memory`, translated into all fifteen complete
locale packs. This state shaped later sessions and nothing ever mentioned
it existed.
- Sub-agent checkpoint resume: `agents/followup` resumes an
`interrupted_continuable` child from its checkpoint into a fresh agent loop —
new agent id, original prompt plus the prior conversation tail — when a
@@ -125,6 +427,39 @@ File edits, terminal width, and Windows installation.
- Acceptance-level Gherkin coverage locking the existing user-command
precedence, alias shadowing, fallback, and invalid-command error contract
(PR #4992).
- Agent Plugins v1.0.0: consume, publish, and slugify packaged sub-agent
briefs, with an install/update/uninstall on-ramp in the TUI (PR #5182). A
plugin bundles a prompt, posture, and routing as one shareable artifact;
on-disk migration of the older `plugin.toml` scaffold is deliberately out
of scope for this train.
- `send_later`: a model-callable one-shot delayed continuation tool, so the
model can schedule a single future nudge without an operator-approved
durable automation (PR #5138).
- `/advisor`: an opt-in background advisor watcher for live turns (PR #5139).
- Notification quiet mode with per-category switches and action-first copy
(PR #5066).
- Automation scheduling forms — one-shot `ONCE`, five-field cron, and honest
watcher modes — created through the approval-gated `automation` tool
(PR #5183).
- Sub-agent `resume_from` continuation chains (PR #5142), child-result
diff-tainting when a claimed diff is not visible to git, per-turn usage
receipts on the exec stream-json stream, and spawn receipts that report
the model each sub-agent actually ran on.
- Transport resilience: sub-agent exec transport retries with a 600 s
default (PR #5210), SSE header stalls retryable instead of fatal, and
headless turn resume after mid-stream network drops with an `EX_TEMPFAIL`
exit.
- Session durability and control: a deterministic compaction continuation
contract (PR #5064), persisting interrupted output (PR #5206), stop-word
cancellation (PR #5207), token-counter refresh (PR #5204), deny-by-default
approval cards (PR #5090), and the Operate completion gate (PR #5067).
- zh-Hant promoted to a full shipped locale with complete `en.json` parity
(PR #5143).
- A persistent update-available chip in the header, with the startup update
check throttled and naming the right command.
- RLM static intent extraction for code blocks (`rlm_block_intent.rs`)
landed as groundwork for a future code-mode approval flow; it is not yet
wired into the turn pipeline and ships dormant by design.
### Changed
@@ -171,12 +506,38 @@ File edits, terminal width, and Windows installation.
futures-util to 0.3.33, libc to 0.2.189, actions/stale to 11.0.0, and
docker/login-action to 4.5.2. The locked graph also includes the
event-listener 5.4.2 fix for RUSTSEC-2026-0221.
- The progress surface now speaks plainly everywhere: the last user-visible
"Work update is pending" notices say "To-do list", the tool constructor and
the docs name `todo_write` as the single canonical progress tool, and
`work_update`, `TodoWrite`, and `todo` stay registered as hidden
compatibility aliases so saved transcripts keep replaying.
- Sub-agent and `agents/wait` waits stay short by default and by cap:
blocking waits default to 30 s and refuse to block past 120 s, because a
blocked wait deafens the session to typed input and settled children
already report back as `<codewhale:subagent.done>` sentinels.
- `Bash` `action=wait` honors `timeout_secs` (seconds) and bare `timeout`
(milliseconds) alongside canonical `timeout_ms`, and `block` as an alias
for `wait`, so a habit formed on other wait tools gets the duration it
asked for instead of silently falling back to the 30 s default; the result
metadata reports the real `wait_timeout_ms` applied.
### Fixed
- The memory journal is no longer indexed as memory. It is Markdown in the
memory tree, so the source walk collected it and every retired note
re-entered the searchable set under its `before:` line — putting the
exact facts a revision had just removed back into the prompt.
- `memory_path` pointed at an already-native store no longer derives a
second store nested inside it, which silently wrote somewhere other than
the file the user named.
- `muse` and `muse-spark` resolved to `muse-spark-1.1` in the agent
registry while config had defaulted to `muse-spark-1.2`, so the CLI and
app-server routed those aliases somewhere the configured default never
pointed. The registry now carries 1.2 and the contributor variant.
- An explicit `type=builder` (or its `implementer` alias) plus
`write_authority=read_only` now fails closed at spawn instead of launching a
labeled write role that silently had only recon tools and then self-BLOCKED
labeled write role that silently had only read-only tools and then self-BLOCKED
after burning a turn (#5123). The check is deliberately narrow, because two
neighbouring combinations are legitimate and stay legal:
- `type=worker` + `read_only` — worker is the unnamed default (it renders as
@@ -196,7 +557,7 @@ File edits, terminal width, and Windows installation.
total the worker budget uses) instead of completion tokens alone; elapsed
time still freezes when the child settles.
- Live work-bar rows for sub-agents show how many to-dos they still have
left (`N left`) when the child's own ledger has unsettled items — never a
left (`N left`) when the child's own list has unsettled items — never a
fabricated zero when no list exists.
- Surfaces no longer claim an OS sandbox on platforms that cannot enforce one.
@@ -335,6 +696,56 @@ File edits, terminal width, and Windows installation.
- Transcript wheel scrolling under iTerm2: xterm alternate-scroll (DECSET
1007) now stays off while mouse capture is active, so wheel events arrive as
mouse events instead of being converted into arrow keys (#5223, PR #5234).
- A stalled model stream no longer ends the turn as `Completed` over a
frozen reasoning block: a mid-stream chunk-timeout now counts toward the
stream-error budget, so a stall with nothing streamed retries the request
transparently, and a stall that exhausts the retry budget fails the turn
with the real reason instead of reporting success.
- A finished background shell task now wakes the engine even when no goal is
active: the idle loop starts an ordinary runtime turn so the completion
reaches the model immediately instead of sitting unclaimed until the user
types (a dead provider route claims the completion once and reports where
the output lives instead of re-arming the same error every tick).
- Sub-agent final reports that exceed the summary budget are now spilled to
a session artifact, and the truncation footer names the
`retrieve_tool_result` ref for the elided middle instead of telling the
model the bytes are unrecoverable; write failures degrade to the honest
no-ref footer.
- An interactive mid-stream network drop after partial output no longer fails
the turn: the partial reply is preserved as a committed assistant message,
a runtime continuation message is appended, and the request is re-issued
bounded by the stream-retry budget.
- Large pasted input is no longer sent to the model twice as inline text and
as a backup `.md` paste file; the submitted message now carries only the
`@`-mention so the model reads the file once.
- A builder sub-agent can run ordinary shell writes again. Write claims
outlive the agents that register them, so a workspace accumulated one per
builder that ever ran — six completed agents left four standing claims in
testing — and the shared-checkout gate counted those long-finished children
as live contenders. Every later builder was refused `Bash` writes with
"cannot prove a bounded file target" and pushed toward worktree isolation,
which puts the work in a checkout the operator never looks at. The gate now
asks the question it meant to ask: is another *running* child writing in this
shared checkout. Concurrent writers are still gated; a lone builder writes in
the workspace you are actually watching.
- Ctrl-C during the first moments of startup no longer kills Codewhale
outright. The terminating-signal handlers were registered inside the task
that waits on them, and a spawned task does not run until the scheduler
first polls it, so a SIGINT arriving in that window hit the default
disposition — the process died with no exit code, no terminal restore, and
no session record. The handlers are now installed synchronously, before
the telemetry notice and before arming, so the window is closed.
- The documented tool list on the docs site named `update_plan` and
`work_update` as coordination tools. Neither is callable by the model —
`update_plan` replays older Plan artifacts and `work_update` is a hidden
compatibility alias — so the page listed two tools a reader cannot use and
omitted `todo_write`, the one they can.
### Security
- Bumped `nanoid` past GHSA-2v37-7h3g-55p8 (a custom generator given size
zero could loop indefinitely), restoring a zero-advisory `npm audit` for
the website.
### Removed
@@ -366,6 +777,8 @@ File edits, terminal width, and Windows installation.
- [vFONGv](https://github.com/vFONGv) (`@vFONGv`) wrote the zh-CN Windows
beginner guide with screenshots in PR #5229, harvested after its base branch
was accidentally deleted during maintainer cleanup.
- [mky](https://github.com/mky) (`@mky`) fixed the FreeBSD build (PR #5254, `rquickjs` `bindgen` on FreeBSD).
- [cacdcaecawae](https://github.com/cacdcaecawae) (`@cacdcaecawae`) contributed embedder-owned sub-agent state roots (PR #5252).
## [0.9.3] - 2026-07-31
@@ -5032,7 +5445,9 @@ overflow report and `/theme` picker edge-wrapping patch in #1814.
Older releases (v0.8.39 and earlier) are archived in [docs/CHANGELOG_ARCHIVE.md](docs/CHANGELOG_ARCHIVE.md).
[Unreleased]: https://github.com/Hmbown/CodeWhale/compare/v0.9.4...HEAD
[Unreleased]: https://github.com/Hmbown/CodeWhale/compare/v0.9.6...HEAD
[0.9.6]: https://github.com/Hmbown/CodeWhale/compare/v0.9.5...v0.9.6
[0.9.5]: https://github.com/Hmbown/CodeWhale/compare/v0.9.4...v0.9.5
[0.9.4]: https://github.com/Hmbown/CodeWhale/compare/v0.9.3...v0.9.4
[0.9.3]: https://github.com/Hmbown/CodeWhale/compare/v0.9.2...v0.9.3
[0.9.2]: https://github.com/Hmbown/CodeWhale/compare/v0.9.1...v0.9.2
+1 -1
View File
@@ -328,7 +328,7 @@ reopened, ask the contributor to resubmit after the allowlist PR is merged.
Codewhale is allowed to help improve Codewhale, but the contribution still has
to be shaped for human review. The recommended workflow is the
[recursive self-improvement prompt](docs/RECURSIVE_SELF_IMPROVEMENT.md): run it
[recursive self-improvement prompt](the `codewhale-ops` repo): run it
from a fresh fork or branch, let the agent find exactly one small friction point,
and stop after one patch. DeepSeek V4 Pro is the reference path for this loop
today, but any configured provider works — the review shape matters more than
Generated
+626 -1034
View File
File diff suppressed because it is too large Load Diff
+5 -4
View File
@@ -21,11 +21,11 @@ members = [
"crates/workflow",
"crates/workflow-js",
]
default-members = ["crates/cli", "crates/app-server", "crates/tui"]
default-members = ["crates/cli"]
resolver = "2"
[workspace.package]
version = "0.9.4"
version = "0.9.6"
edition = "2024"
# Rust 1.88 stabilized `let_chains` in `if`/`while` conditions, which the
# codebase relies on extensively. Cargo enforces this so users on older
@@ -44,7 +44,7 @@ clap = { version = "4.5.54", features = ["derive"] }
clap_complete = "4.5"
dirs = "6.0.0"
encoding_rs = "0.8.35"
jsonschema = { version = "0.48", default-features = false }
jsonschema = { version = "0.46", default-features = false }
reqwest = { version = "0.13.1", default-features = false, features = ["json", "rustls-no-provider", "socks"] }
# NOT "parallel": the Workflow VM stays single-threaded and bridges to the
# multi-thread engine over channels (see crates/workflow-js).
@@ -60,10 +60,11 @@ tokio = { version = "1.50.0", features = ["fs", "io-util", "io-std", "macros", "
toml = "1.0.6"
toml_edit = "0.25.12"
sha2 = "0.11"
tower-http = { version = "0.7", features = ["cors"] }
tower-http = { version = "0.6", features = ["cors"] }
tracing = "0.1"
tracing-appender = "0.2"
tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt"] }
tokio-util = { version = "0.7.16", features = ["io", "full"] }
uuid = { version = "1.11", features = ["v4"] }
mimalloc = { version = "0.1", default-features = false }
+7 -9
View File
@@ -5,7 +5,7 @@
# Run: docker run --rm -it -e DEEPSEEK_API_KEY -v codewhale-home:/home/codewhale/.codewhale codewhale
#
# The image ships the canonical binaries (`codewhale`, `codew`, and
# `codewhale-tui`) in a minimal runtime layer.
# `codewhale`) in a minimal runtime layer.
#
# API keys MUST be passed at runtime (never baked into the image):
# docker run --rm -it -e DEEPSEEK_API_KEY codewhale
@@ -53,18 +53,18 @@ RUN rustup target add "$(cat /rust-target)"
WORKDIR /build
COPY . .
# Build both binaries for the target platform. --locked ensures
# reproducible builds from the committed lockfile.
# Build the one runtime for the target platform. Expose the same verified
# bytes under both supported command names. --locked keeps the build
# reproducible from the committed lockfile.
RUN --mount=type=cache,id=codewhale-target-${TARGETARCH},target=/build/target,sharing=locked \
--mount=type=cache,id=codewhale-cargo-registry-${TARGETARCH},target=/usr/local/cargo/registry,sharing=locked \
--mount=type=cache,id=codewhale-cargo-git-${TARGETARCH},target=/usr/local/cargo/git,sharing=locked \
rustup target add "$(cat /rust-target)" \
&& cargo build --release --locked --target "$(cat /rust-target)" \
-p codewhale-cli -p codewhale-tui \
-p codewhale-cli \
&& mkdir -p /out \
&& cp target/$(cat /rust-target)/release/codewhale /out/ \
&& cp target/$(cat /rust-target)/release/codew /out/ \
&& cp target/$(cat /rust-target)/release/codewhale-tui /out/
&& cp target/$(cat /rust-target)/release/codewhale /out/codew
# ── Stage 2: Runtime ──────────────────────────────────────────────────
FROM debian:bookworm-slim
@@ -86,10 +86,8 @@ WORKDIR /home/codewhale
COPY --from=builder --chown=codewhale:codewhale /out/codewhale /usr/local/bin/codewhale
COPY --from=builder --chown=codewhale:codewhale /out/codew /usr/local/bin/codew
COPY --from=builder --chown=codewhale:codewhale /out/codewhale-tui /usr/local/bin/codewhale-tui
# The dispatcher expects to find its companion binary next to it.
# Both are in /usr/local/bin — no further path setup needed.
# `codewhale` and `codew` are two command names for the same runtime.
ENTRYPOINT ["codewhale"]
CMD []
+15 -2
View File
@@ -1,4 +1,4 @@
<!-- source: README.md sha256:a87c9f323f35 -->
<!-- source: README.md sha256:4fb18fffb0fe -->
# Codewhale
Un agente de programación de código abierto para tu terminal — trae tu propio modelo.
@@ -15,6 +15,13 @@ queda lista o te necesita. Cambia de modelo a mitad de tarea con `/model`.
Trabaja de forma interactiva en la TUI, o ejecuta `codewhale exec` en scripts y
CI. Está escrito en Rust, con licencia MIT, y corre en tu máquina.
Lo que no se parece a otros harnesses: **tú eliges el modelo de cada rol, y no
tienen por qué coincidir.** Una fleet fija un proveedor, un modelo y un nivel de
razonamiento por rol — así un modelo barato y rápido puede dirigir a uno de
razonamiento caro, o un builder GLM puede trabajar en la misma tarea que un
reviewer Kimi. Escribe tus propios roles y tu propia constitution, y el harness
es tuyo en lugar de nuestro.
Siempre estamos buscando personas que contribuyan y formas de mejorar. Si falta
un modelo o proveedor que usas, o algo se rompe, contárnoslo es una de las cosas
más útiles que puedes hacer — mira [Contribuir](#contribuir).
@@ -52,7 +59,7 @@ codewhale web # local browser client on 127.0.0.1
En la TUI: `/model` cambia proveedor y modelo juntos, `/fleet` ejecuta un
equipo de workers, `/undo` deshace el último turno y `/restore <N>` revierte el
workspace a una instantánea anterior (`/restore` sin argumentos solo las
lista). Cuando el compositor está vacío, `Tab` cicla entre Plan / Act /
lista). Cuando el compositor está vacío, `Tab` cicla entre Plan / Work /
Operate; con texto escrito, `Tab` completa comandos slash y menciones `@`.
`Shift+Tab` cicla la postura de permiso Ask / Auto-Review / Full Access en
cualquier momento. `!` ejecuta un comando de shell por la ruta normal de
@@ -65,6 +72,12 @@ aprobación.
todo a través de un solo runtime y un solo conjunto de herramientas. Los
presupuestos de contexto y los precios vienen de la ruta real, y un precio
desconocido se muestra como desconocido en lugar de $0.
- **Un harness que tú escribes.** Los roles son archivos que puedes leer y
editar — un modelo, una postura de herramientas e instrucciones permanentes por
rol — guardados en el proyecto para que el equipo los comparta, o junto a tus
ajustes personales para que te acompañen entre repos. Una constitution registra
cómo quieres que el agente se comporte en cada sesión, de modo que el harness se
ajuste a tu práctica y no a la nuestra.
- **Solo lectura hasta que permitas más.** El modo Plan no cambia archivos, y
las aprobaciones controlan los comandos riesgosos. Cuando un sandbox del
sistema operativo realmente envuelve un comando, Codewhale lo indica: Seatbelt
+5 -2
View File
@@ -1,4 +1,4 @@
<!-- source: README.md sha256:a87c9f323f35 -->
<!-- source: README.md sha256:4fb18fffb0fe -->
# Codewhale
Sebuah coding agent sumber terbuka untuk terminal Anda — bawa model pilihan Anda sendiri.
@@ -7,6 +7,8 @@ Codewhale berawal sebagai pengalaman asli (native) untuk DeepSeek. Sejak saat it
Berikan penyedia, model, dan tugas: Codewhale akan membaca kode Anda, mengedit berkas, menjalankan perintah, serta memeriksa hasil kerjanya sendiri, lalu berhenti setelah pekerjaan selesai atau ketika membutuhkan arahan Anda. Ganti model di tengah tugas dengan `/model`. Bekerja secara interaktif di TUI, atau jalankan `codewhale exec` dalam skrip dan CI. Dibuat menggunakan Rust, berlisensi MIT, dan berjalan langsung di mesin Anda sendiri.
Yang membedakannya dari harness lain: **Anda memilih model untuk setiap peran, dan model-model itu tidak harus sama.** Sebuah fleet menyematkan penyedia, model, dan tingkat penalaran per peran — sehingga model yang murah dan cepat bisa mengarahkan model penalaran yang mahal, atau seorang builder GLM bisa mengerjakan tugas yang sama dengan seorang reviewer Kimi. Tulis peran Anda sendiri, constitution Anda sendiri, dan harness itu menjadi milik Anda, bukan milik kami.
Kami selalu membuka kesempatan bagi para kontributor dan cara untuk terus berkembang. Jika model atau penyedia yang Anda gunakan belum tersedia, atau ada hal yang tidak berjalan semestinya, memberi tahu kami adalah salah satu kontribusi paling berharga yang bisa Anda lakukan — lihat [Kontribusi](#kontribusi).
[English](README.md) · [简体中文](README.zh-CN.md) · [日本語](README.ja-JP.md) · [Tiếng Việt](README.vi.md) · [Bahasa Indonesia](README.id.md) · [한국어](README.ko-KR.md) · [Español](README.es-419.md) · [Português](README.pt-BR.md) · [Русский](README.ru.md) · [Українська](README.uk.md) · [codewhale.net](https://codewhale.net/) · [Docs](docs) · [Changelog](CHANGELOG.md) · [Discord](https://discord.gg/37gfS3ksug)
@@ -36,11 +38,12 @@ codewhale web # local browser client on 127.0.0.1
```
Di dalam TUI: `/model` mengganti penyedia dan model sekaligus, `/fleet` menjalankan tim pekerja (workers), `/undo` membatalkan langkah (turn) terakhir, dan `/restore <N>` mengembalikan workspace ke snapshot sebelumnya (`/restore` tanpa argumen hanya menampilkan daftarnya). Saat composer kosong, `Tab` beralih antar mode Plan / Act / Operate; bila composer berisi teks, `Tab` justru melengkapi perintah slash dan sebutan `@`. `Shift+Tab` beralih antar postur izin Ask / Auto-Review / Full Access kapan saja. `!` menjalankan perintah shell melalui alur persetujuan normal.
Di dalam TUI: `/model` mengganti penyedia dan model sekaligus, `/fleet` menjalankan tim pekerja (workers), `/undo` membatalkan langkah (turn) terakhir, dan `/restore <N>` mengembalikan workspace ke snapshot sebelumnya (`/restore` tanpa argumen hanya menampilkan daftarnya). Saat composer kosong, `Tab` beralih antar mode Plan / Work / Operate; bila composer berisi teks, `Tab` justru melengkapi perintah slash dan sebutan `@`. `Shift+Tab` beralih antar postur izin Ask / Auto-Review / Full Access kapan saja. `!` menjalankan perintah shell melalui alur persetujuan normal.
## Fitur & Kapabilitas
- **Model mana saja, penyedia apa saja.** DeepSeek, Claude, GPT, Kimi, GLM, dan 30+ penyedia lainnya, ditambah vLLM, SGLang, atau Ollama milik Anda sendiri tanpa memerlukan API key — semuanya melalui satu runtime dan satu kumpulan alat. Batas konteks dan harga diambil dari rute sebenarnya, dan harga yang tidak diketahui ditampilkan sebagai *unknown* daripada $0.
- **Harness yang Anda tulis sendiri.** Peran adalah berkas yang bisa Anda baca dan sunting — satu model, satu sikap perkakas, dan instruksi tetap untuk tiap peran — disimpan di dalam proyek agar tim berbagi, atau di samping pengaturan pribadi Anda agar ikut berpindah antar repo. Constitution mencatat bagaimana Anda ingin agen berperilaku di setiap sesi, sehingga harness mengikuti cara kerja Anda, bukan cara kami.
- **Read-only sampai Anda memberi izin lebih.** Mode Plan tidak dapat mengubah berkas, dan gerbang persetujuan memproteksi perintah berisiko. Ketika sandbox OS membungkus perintah, Codewhale akan menginformasikannya: Seatbelt pada macOS (jika tersedia), serta opsi bubblewrap di Linux. Berkas `constitution.json` repositori dikompilasi menjadi pembatas penulisan yang bahkan tidak dapat dilewati oleh mode Full Access.
- **Pekerjaan yang dapat dilanjutkan.** Fleet mencatat setiap langkah ke ledger bertipe append-only, sehingga `fleet resume` dapat melanjutkan pekerjaan tepat di mana Anda meninggalkannya.
+6 -3
View File
@@ -1,4 +1,4 @@
<!-- source: README.md sha256:a87c9f323f35 -->
<!-- source: README.md sha256:4fb18fffb0fe -->
# Codewhale
ターミナルで動くオープンソースのコーディングエージェント — モデルはあなたが持ち込む。
@@ -7,6 +7,8 @@ Codewhale は DeepSeek のためのネイティブ体験として始まりまし
プロバイダ、モデル、タスクを渡すと、コードを読み、ファイルを編集し、コマンドを実行し、自分の作業を確認して、タスクが完了するかあなたの手が必要になった時点で止まります。タスクの途中でも `/model` でモデルを切り替えられます。対話的な作業には TUI を、スクリプトと CI には `codewhale exec` を。Rust 製、MIT ライセンスで、あなたのマシン上で動きます。
他のハーネスと違うのはここです。**役割ごとにどのモデルを使うかはあなたが決められ、しかも揃える必要がありません。** Fleet は役割ごとにプロバイダ・モデル・推論ティアを個別に固定します。だから速くて安いモデルが高価な推論モデルを指揮することも、GLM の builder と Kimi の reviewer が同じ仕事に取り組むこともできます。自分の役割と自分の constitution を書けば、そのハーネスは私たちのものではなく、あなたのものになります。
私たちは常にコントリビューターと改善の方法を探しています。使っているモデルやプロバイダが見当たらないとき、あるいは何かが壊れたときは、それを知らせてもらえることが最も役に立つことのひとつです — [コントリビューション](#コントリビューション)を見てください。
[English](README.md) · [简体中文](README.zh-CN.md) · [Tiếng Việt](README.vi.md) · [Bahasa Indonesia](README.id.md) · [한국어](README.ko-KR.md) · [Español](README.es-419.md) · [Português](README.pt-BR.md) · [Русский](README.ru.md) · [Українська](README.uk.md) · [codewhale.net](https://codewhale.net/) · [Docs](docs) · [Changelog](CHANGELOG.md) · [Discord](https://discord.gg/37gfS3ksug)
@@ -36,11 +38,12 @@ codewhale web # local browser client on 127.0.0.1
```
TUI では、`/model` がプロバイダとモデルをまとめて切り替え、`/fleet`ワーカーのチームを走らせ`/undo` が直前のターンを取り消し、`/restore <N>` がワークスペースを以前のスナップショットへ巻き戻します(引数なしの `/restore` は一覧を表示するだけです)。入力欄が空のとき、`Tab` は Plan / Act / Operate を順に切り替えます。入力欄に文字があるときの `Tab` はスラッシュコマンドと `@` メンションの補完になります。`Shift+Tab` はいつでも Ask / Auto-Review / Full Access の権限スタンスを順に切り替えます。`!` は Shell コマンドを通常の承認経路で実行します。
TUI では、`/model` がプロバイダとモデルをまとめて切り替え、`/fleet`チームを組み立てて走らせ(一度にひとつの役割、それぞれが自分のモデルを持ちます)`/undo` が直前のターンを取り消し、`/restore <N>` がワークスペースを以前のスナップショットへ巻き戻します(引数なしの `/restore` は一覧を表示するだけです)。入力欄が空のとき、`Tab` は Plan / Work / Operate を順に切り替えます。入力欄に文字があるときの `Tab` はスラッシュコマンドと `@` メンションの補完になります。`Shift+Tab` はいつでも Ask / Auto-Review / Full Access の権限スタンスを順に切り替えます。`!` は Shell コマンドを通常の承認経路で実行します。
## できること
- **どのモデルでも、どのプロバイダでも。** DeepSeek、Claude、GPT、Kimi、GLM をはじめ 30 以上のプロバイダ、そしてキー不要のあなた自身の vLLM・SGLang・Ollama が、すべてひとつのランタイムとひとつのツール群を通って動きます。コンテキスト予算と価格は実際のルートに由来し、不明な価格は $0 ではなく不明と表示されます。
- **どのモデルでも、どのプロバイダでも、そしてどんな組み合わせでも。** DeepSeek、Claude、GPT、Kimi、GLM をはじめ 30 以上のプロバイダ、そしてキー不要のあなた自身の vLLM・SGLang・Ollama が、すべてひとつのランタイムとひとつのツール群を通って動きます。保存された役割は `provider``model`・推論ティアを明示的に記録するので、ひとつの実行の中で Fleet が複数のベンダーにまたがることができ、役割のルートはそのとき有効なプロバイダに左右されません。コンテキスト予算と価格は実際のルートに由来し、不明な価格は $0 ではなく不明と表示されます。
- **あなたが書くハーネス。** 役割は読んで編集できるファイルです。役割ごとにモデル、ツールの姿勢、常設の指示を持ち、チームで共有するならプロジェクトに、リポジトリをまたいで持ち歩くなら個人設定の隣に置きます。constitution はすべてのセッションを通じてエージェントにどう振る舞ってほしいかを記録し、ハーネスを私たちのやり方ではなくあなたのやり方に合わせます。
- **許可するまでは読み取り専用。** Plan モードはファイルを変更せず、リスクのあるコマンドは承認でゲートされます。OS サンドボックスが実際にコマンドをラップするとき、Codewhale はそれを明示します。macOS では利用可能な Seatbelt、Linux ではオプトインの bubblewrap です。リポジトリの `constitution.json` は書き込みホールドへとコンパイルされ、Full Access でもスキップできません。
- **再開できる作業。** Fleet はすべてのステップを追記専用の台帳に記録するので、`fleet resume` で止めたところから再開できます。
+6 -3
View File
@@ -1,4 +1,4 @@
<!-- source: README.md sha256:a87c9f323f35 -->
<!-- source: README.md sha256:4fb18fffb0fe -->
# Codewhale
터미널에서 쓰는 오픈소스 코딩 에이전트 — 모델은 당신이 가져옵니다.
@@ -7,6 +7,8 @@ Codewhale은 DeepSeek을 위한 네이티브 경험으로 시작했습니다.
프로바이더, 모델, 작업을 지정하면 코드를 읽고, 파일을 편집하고, 명령을 실행하고, 스스로 작업을 확인하며, 작업이 끝나거나 사용자의 판단이 필요해지면 멈춥니다. 작업 도중에도 `/model`로 모델을 바꿀 수 있습니다. 대화형 작업에는 TUI를, 스크립트와 CI에는 `codewhale exec`를 사용합니다. Rust로 작성했고, MIT 라이선스이며, 당신의 컴퓨터에서 실행됩니다.
다른 하네스와 다른 점은 이것입니다. **역할마다 어떤 모델을 쓸지 당신이 고르고, 서로 같을 필요가 없습니다.** Fleet은 역할별로 프로바이더, 모델, 추론 등급을 각각 고정합니다. 그래서 빠르고 저렴한 모델이 값비싼 추론 모델을 지휘할 수도 있고, GLM builder와 Kimi reviewer가 같은 작업을 함께 처리할 수도 있습니다. 자신의 역할과 자신의 constitution을 쓰면, 그 하네스는 우리 것이 아니라 당신 것이 됩니다.
우리는 항상 기여자와 개선할 방법을 찾고 있습니다. 사용하는 모델이나 프로바이더가 빠져 있거나 무언가가 깨진다면, 그것을 알려 주는 일이 할 수 있는 가장 유용한 일 중 하나입니다 — [기여](#기여)를 참고하세요.
[English](README.md) · [简体中文](README.zh-CN.md) · [日本語](README.ja-JP.md) · [Tiếng Việt](README.vi.md) · [Bahasa Indonesia](README.id.md) · [Español](README.es-419.md) · [Português](README.pt-BR.md) · [Русский](README.ru.md) · [Українська](README.uk.md) · [codewhale.net](https://codewhale.net/) · [Docs](docs) · [Changelog](CHANGELOG.md) · [Discord](https://discord.gg/37gfS3ksug)
@@ -36,11 +38,12 @@ codewhale web # local browser client on 127.0.0.1
```
TUI 안에서: `/model`은 프로바이더와 모델을 함께 전환하고, `/fleet`워커 팀을 실행하며, `/undo`는 직전 턴을 되돌리고, `/restore <N>`은 워크스페이스를 이전 스냅샷으로 되돌립니다(인자 없는 `/restore`는 스냅샷 목록만 보여줍니다). 입력창이 비어 있을 때 `Tab`은 Plan / Act / Operate 모드를 순환하고, 입력창에 내용이 있으면 `Tab`은 슬래시 명령과 `@` 멘션을 자동 완성합니다. `Shift+Tab`은 언제든지 Ask / Auto-Review / Full Access 권한 태세를 순환합니다. `!`는 일반 승인 경로를 거쳐 셸 명령을 실행합니다.
TUI 안에서: `/model`은 프로바이더와 모델을 함께 전환하고, `/fleet`팀을 구성하고 실행하며(한 번에 한 역할씩, 각자 자기 모델을 가집니다), `/undo`는 직전 턴을 되돌리고, `/restore <N>`은 워크스페이스를 이전 스냅샷으로 되돌립니다(인자 없는 `/restore`는 스냅샷 목록만 보여줍니다). 입력창이 비어 있을 때 `Tab`은 Plan / Work / Operate 모드를 순환하고, 입력창에 내용이 있으면 `Tab`은 슬래시 명령과 `@` 멘션을 자동 완성합니다. `Shift+Tab`은 언제든지 Ask / Auto-Review / Full Access 권한 태세를 순환합니다. `!`는 일반 승인 경로를 거쳐 셸 명령을 실행합니다.
## 기능
- **어떤 모델이든, 어떤 프로바이더든.** DeepSeek, Claude, GPT, Kimi, GLM 등 30개 이상의 프로바이더와 키 없이 쓰는 자체 vLLM, SGLang, Ollama가 모두 하나의 런타임과 하나의 도구 세트를 통해 동작합니다. 컨텍스트 예산과 가격은 실제 라우트에서 가져오며, 알 수 없는 가격은 $0이 아니라 알 수 없음으로 표시됩니다.
- **어떤 모델이든, 어떤 프로바이더든, 그리고 어떤 조합이든.** DeepSeek, Claude, GPT, Kimi, GLM 등 30개 이상의 프로바이더와 키 없이 쓰는 자체 vLLM, SGLang, Ollama가 모두 하나의 런타임과 하나의 도구 세트를 통해 동작합니다. 저장된 역할은 `provider`, `model`, 추론 등급을 명시적으로 기록하므로 하나의 실행 안에서 Fleet이 여러 벤더에 걸칠 수 있고, 역할의 라우트는 그때 활성화된 프로바이더에 좌우되지 않습니다. 컨텍스트 예산과 가격은 실제 라우트에서 가져오며, 알 수 없는 가격은 $0이 아니라 알 수 없음으로 표시됩니다.
- **당신이 직접 쓰는 하네스.** 역할은 읽고 수정할 수 있는 파일입니다. 역할마다 모델, 도구 태세, 상시 지시를 담아 팀과 공유하려면 프로젝트에, 저장소를 옮겨 다니며 쓰려면 개인 설정 옆에 둡니다. constitution은 모든 세션에서 에이전트가 어떻게 행동하기를 바라는지 기록해, 하네스가 우리 방식이 아니라 당신의 방식에 맞도록 합니다.
- **허용하기 전까지는 읽기 전용.** Plan 모드는 파일을 바꾸지 않고, 위험한 명령은 승인을 거칩니다. OS 샌드박스가 실제로 명령을 래핑할 때 Codewhale은 이를 그대로 표시합니다. macOS에서는 사용 가능한 Seatbelt, Linux에서는 옵트인 bubblewrap입니다. 저장소의 `constitution.json`은 Full Access조차 건너뛸 수 없는 쓰기 홀드로 컴파일됩니다.
- **이어서 할 수 있는 작업.** Fleet은 모든 단계를 추가 전용 원장에 기록하므로, `fleet resume`으로 멈춘 지점부터 이어갈 수 있습니다.
+24 -8
View File
@@ -13,6 +13,13 @@ you. Switch models mid-task with `/model`. Work interactively in the TUI, or run
`codewhale exec` in scripts and CI. It's written in Rust, licensed MIT, and runs
on your machine.
The part that isn't like other harnesses: **you pick the model for each role,
and they don't have to match.** A fleet pins a provider, a model, and a
reasoning tier per role — so a cheap fast model can direct an expensive
reasoning one, or a GLM builder can work the same job as a Kimi reviewer.
Write your own roles, your own constitution, and the harness is yours rather
than ours.
We're always looking for contributors and ways to improve. If a model or
provider you use is missing, or something breaks, telling us is one of the most
useful things you can do — see [Contributing](#contributing).
@@ -46,20 +53,29 @@ codewhale exec "fix the failing test" # headless
codewhale web # local browser client on 127.0.0.1
```
In the TUI: `/model` switches provider and model together, `/fleet` runs a team
of workers, `/undo` reverts the last turn, and `/restore <N>` rolls the
workspace back to an earlier snapshot (bare `/restore` lists them). `Tab`
cycles Plan / Act / Operate when the composer is empty — with text in it, `Tab`
In the TUI: `/model` switches provider and model together, `/fleet` builds and
runs the team — one role at a time, each with its own model — `/undo` reverts
the last turn, and `/restore <N>` rolls the workspace back to an earlier
snapshot (bare `/restore` lists them). `Tab`
cycles Plan / Work / Operate when the composer is empty — with text in it, `Tab`
completes slash commands and `@` mentions instead. `Shift+Tab` cycles the
Ask / Auto-Review / Full Access permission posture at any time. `!` runs a
shell command through the normal approval path.
## What it does
- **Any model, any provider.** DeepSeek, Claude, GPT, Kimi, GLM, and 30+
providers, plus your own vLLM, SGLang, or Ollama with no key — all through one
runtime and one toolset. Context limits and prices come from the real route,
and an unknown price shows as unknown rather than $0.
- **Any model, any provider — and any mix of them.** DeepSeek, Claude, GPT,
Kimi, GLM, and 30+ providers, plus your own vLLM, SGLang, or Ollama with no
key, all through one runtime and one toolset. A saved role records its
`provider`, `model`, and reasoning tier explicitly, so a fleet can span
vendors in a single run and a role's route never depends on whichever
provider happens to be active. Context limits and prices come from the real
route, and an unknown price shows as unknown rather than $0.
- **A harness you author.** Roles are files you can read and edit — a model, a
tool posture, and standing instructions per role — kept in the project so the
team shares them, or beside your other personal settings so they follow you
between repos. A constitution records how you want the agent to behave across
every session, so the harness matches your practice instead of ours.
- **Read-only until you allow more.** Plan mode can't change files, and
approvals gate risky commands. When an OS sandbox actually wraps a command,
Codewhale says so: Seatbelt on macOS where available, opt-in bubblewrap on
+15 -2
View File
@@ -1,4 +1,4 @@
<!-- source: README.md sha256:a87c9f323f35 -->
<!-- source: README.md sha256:4fb18fffb0fe -->
# Codewhale
Um agente de programação de código aberto para o seu terminal — traga o seu próprio modelo.
@@ -15,6 +15,13 @@ termina ou quando precisa de você. Troque de modelo no meio da tarefa com
`/model`. Trabalhe de forma interativa na TUI, ou rode `codewhale exec` em
scripts e CI. É escrito em Rust, licenciado sob MIT, e roda na sua máquina.
O que não se parece com outros harnesses: **você escolhe o modelo de cada
papel, e eles não precisam ser iguais.** Uma fleet fixa um provedor, um modelo e
um nível de raciocínio por papel — então um modelo barato e rápido pode dirigir
um modelo de raciocínio caro, ou um builder GLM pode trabalhar na mesma tarefa
que um reviewer Kimi. Escreva seus próprios papéis e sua própria constitution, e
o harness passa a ser seu, não nosso.
Estamos sempre em busca de pessoas que contribuam e de formas de melhorar. Se um
modelo ou provedor que você usa está faltando, ou se algo quebra, nos contar é
uma das coisas mais úteis que você pode fazer — veja [Contribuindo](#contribuindo).
@@ -52,7 +59,7 @@ codewhale web # local browser client on 127.0.0.1
Na TUI: `/model` troca provedor e modelo juntos, `/fleet` executa uma equipe
de workers, `/undo` desfaz o último turno e `/restore <N>` reverte o workspace
para um snapshot anterior (`/restore` sem argumento apenas os lista). Quando o
compositor está vazio, `Tab` cicla entre Plan / Act / Operate; com texto
compositor está vazio, `Tab` cicla entre Plan / Work / Operate; com texto
digitado, `Tab` completa comandos slash e menções `@`. `Shift+Tab` cicla a
postura de permissão Ask / Auto-Review / Full Access a qualquer momento. `!`
executa um comando de shell pelo caminho normal de aprovação.
@@ -64,6 +71,12 @@ executa um comando de shell pelo caminho normal de aprovação.
tudo por um único runtime e um único conjunto de ferramentas. Orçamentos de
contexto e preços vêm da rota real, e um preço desconhecido aparece como
desconhecido em vez de $0.
- **Um harness escrito por você.** Papéis são arquivos que você pode ler e
editar — um modelo, uma postura de ferramentas e instruções permanentes por
papel — guardados no projeto para o time compartilhar, ou ao lado das suas
configurações pessoais para acompanharem você entre repositórios. Uma
constitution registra como você quer que o agente se comporte em cada sessão,
para que o harness siga a sua prática, e não a nossa.
- **Somente leitura até você permitir mais.** O modo Plan não altera arquivos,
e as aprovações controlam os comandos arriscados. Quando um sandbox do
sistema operacional realmente envolve um comando, o Codewhale avisa: Seatbelt
+15 -2
View File
@@ -1,4 +1,4 @@
<!-- source: README.md sha256:a87c9f323f35 -->
<!-- source: README.md sha256:4fb18fffb0fe -->
# Codewhale
Открытый агент для программирования в вашем терминале — модель приносите с собой.
@@ -16,6 +16,13 @@ Codewhale начинался как нативный клиент для DeepSee
и CI. Он написан на Rust, распространяется по лицензии MIT и работает на вашей
машине.
Чем это не похоже на другие harness: **вы сами выбираете модель для каждой
роли, и они не обязаны совпадать.** Fleet закрепляет провайдера, модель и
уровень рассуждений отдельно для каждой роли — поэтому дешёвая и быстрая модель
может руководить дорогой рассуждающей, а builder на GLM может работать над той
же задачей, что и reviewer на Kimi. Опишите свои роли и свою constitution — и
harness станет вашим, а не нашим.
Мы всегда ищем участников и способы стать лучше. Если модели или провайдера,
которым вы пользуетесь, не хватает, или что-то сломалось, сообщить нам об этом —
одно из самых полезных действий с вашей стороны: см.
@@ -55,7 +62,7 @@ codewhale web # local browser client on 127.0.0.1
команду воркеров, `/undo` отменяет последний ход, а `/restore <N>` откатывает
рабочую копию к более раннему снимку (`/restore` без аргумента только выводит их
список). Когда поле ввода пустое, `Tab` циклически переключает режимы Plan /
Act / Operate; если в поле есть текст, `Tab` дополняет слэш-команды и упоминания
Work / Operate; если в поле есть текст, `Tab` дополняет слэш-команды и упоминания
`@`. `Shift+Tab` переключает уровни прав Ask / Auto-Review / Full Access в любой
момент. `!` запускает команду оболочки через обычный путь подтверждения.
@@ -66,6 +73,12 @@ Act / Operate; если в поле есть текст, `Tab` дополняе
через единый рантайм и единый набор инструментов. Лимиты контекста и цены
берутся из реального маршрута, а неизвестная цена отображается как неизвестная,
а не как $0.
- **Harness, который пишете вы.** Роли — это файлы, которые можно прочитать и
изменить: для каждой роли своя модель, своя позиция по инструментам и
постоянные инструкции. Держите их в проекте, чтобы ими пользовалась команда,
или рядом с личными настройками, чтобы они следовали за вами между
репозиториями. Constitution фиксирует, как вы хотите, чтобы агент вёл себя в
каждой сессии, — так harness подстраивается под вашу практику, а не под нашу.
- **Только чтение, пока вы не разрешите больше.** Режим Plan не может изменять
файлы, а рискованные команды требуют подтверждения. Когда команду действительно
оборачивает песочница ОС, Codewhale сообщает об этом: Seatbelt на macOS, где он
+15 -2
View File
@@ -1,4 +1,4 @@
<!-- source: README.md sha256:a87c9f323f35 -->
<!-- source: README.md sha256:4fb18fffb0fe -->
# Codewhale
Агент для програмування з відкритим кодом у вашому терміналі — модель приносите ви.
@@ -16,6 +16,13 @@ Codewhale починався як нативний інструмент для D
у скриптах і CI. Він написаний на Rust, поширюється за ліцензією MIT і працює
на вашому комп'ютері.
Чим це не схоже на інші harness: **ви самі обираєте модель для кожної ролі, і
вони не мусять збігатися.** Fleet закріплює провайдера, модель і рівень
міркувань окремо для кожної ролі — тож дешева і швидка модель може керувати
дорогою міркувальною, а builder на GLM може працювати над тим самим завданням,
що й reviewer на Kimi. Опишіть свої ролі та свою constitution — і harness стане
вашим, а не нашим.
Ми завжди шукаємо учасників і способи стати кращими. Якщо моделі чи
провайдера, якими ви користуєтесь, бракує, або щось ламається, повідомити про
це — одна з найкорисніших речей, які ви можете зробити — див.
@@ -54,7 +61,7 @@ codewhale web # local browser client on 127.0.0.1
У TUI: `/model` перемикає провайдера й модель разом, `/fleet` запускає команду
працівників, `/undo` скасовує останній крок, а `/restore <N>` відкочує робочу
копію до давнішого знімка (`/restore` без аргументу лише виводить їхній
список). Коли поле введення порожнє, `Tab` циклічно перемикає Plan / Act /
список). Коли поле введення порожнє, `Tab` циклічно перемикає Plan / Work /
Operate; якщо в полі є текст, `Tab` доповнює слеш-команди та згадки `@`.
`Shift+Tab` перемикає режими дозволів Ask / Auto-Review / Full Access будь-коли.
`!` виконує команду оболонки через звичайний шлях затвердження.
@@ -66,6 +73,12 @@ Operate; якщо в полі є текст, `Tab` доповнює слеш-к
ключа — усе через одне середовище виконання й один набір інструментів. Ліміти
контексту й ціни беруться з реального маршруту, а невідома ціна показується
як невідома, а не як $0.
- **Harness, який пишете ви.** Ролі — це файли, які можна прочитати й змінити:
для кожної ролі своя модель, своя позиція щодо інструментів і постійні
інструкції. Тримайте їх у проєкті, щоб ними користувалася команда, або поруч з
особистими налаштуваннями, щоб вони йшли за вами між репозиторіями.
Constitution фіксує, як ви хочете, щоб агент поводився в кожній сесії, — тож
harness підлаштовується під вашу практику, а не під нашу.
- **Лише читання, доки ви не дозволите більше.** Режим Plan не може змінювати
файли, а ризиковані команди проходять через затвердження. Коли пісочниця ОС
справді обгортає команду, Codewhale каже про це: Seatbelt на macOS, де він
+14 -2
View File
@@ -1,4 +1,4 @@
<!-- source: README.md sha256:a87c9f323f35 -->
<!-- source: README.md sha256:4fb18fffb0fe -->
# Codewhale
Một coding agent mã nguồn mở cho terminal của bạn — mang theo model của riêng bạn.
@@ -15,6 +15,13 @@ thành hoặc cần đến bạn. Đổi model giữa chừng bằng `/model`. L
trong TUI, hoặc chạy `codewhale exec` trong script và CI. Viết bằng Rust, giấy
phép MIT, và chạy trên máy của bạn.
Điều khác biệt so với các harness khác: **bạn chọn model cho từng vai trò, và
chúng không cần phải giống nhau.** Một fleet ghim provider, model và mức suy
luận riêng cho từng vai trò — nên một model nhanh và rẻ có thể điều phối một
model suy luận đắt tiền, hoặc một builder GLM có thể làm chung việc với một
reviewer Kimi. Hãy viết vai trò của riêng bạn, constitution của riêng bạn, và
harness đó là của bạn chứ không phải của chúng tôi.
Chúng tôi luôn tìm kiếm người đóng góp và cách cải thiện. Nếu một model hay
provider bạn dùng còn thiếu, hoặc có gì đó hỏng, báo cho chúng tôi biết là một
trong những điều hữu ích nhất bạn có thể làm — xem [Đóng góp](#đóng-góp).
@@ -52,7 +59,7 @@ codewhale web # local browser client on 127.0.0.1
Trong TUI: `/model` đổi provider và model cùng lúc, `/fleet` chạy một đội
worker, `/undo` hoàn tác lượt gần nhất, và `/restore <N>` đưa workspace về một
ảnh chụp trước đó (`/restore` không tham số chỉ liệt kê chúng). Khi vùng soạn
thảo trống, `Tab` chuyển vòng qua Plan / Act / Operate; khi vùng soạn thảo có
thảo trống, `Tab` chuyển vòng qua Plan / Work / Operate; khi vùng soạn thảo có
chữ, `Tab` lại hoàn tất lệnh slash và nhắc `@`. `Shift+Tab` chuyển vòng qua tư
thế quyền Ask / Auto-Review / Full Access bất cứ lúc nào. `!` chạy một lệnh
shell qua đường phê duyệt bình thường.
@@ -63,6 +70,11 @@ shell qua đường phê duyệt bình thường.
GLM, hơn 30 provider, và vLLM, SGLang hay Ollama của riêng bạn — không cần
key — đều chạy qua một runtime và một bộ công cụ. Ngân sách ngữ cảnh và giá
lấy từ route thật; giá chưa rõ hiển thị là chưa rõ, chứ không phải $0.
- **Một harness do bạn viết.** Vai trò là những tệp bạn có thể đọc và sửa — mỗi
vai trò một model, một tư thế công cụ và các chỉ dẫn thường trực — đặt trong dự
án để cả nhóm dùng chung, hoặc cạnh các thiết lập cá nhân để đi theo bạn giữa
các repo. Constitution ghi lại cách bạn muốn agent hành xử trong mọi phiên, để
harness khớp với cách làm của bạn thay vì của chúng tôi.
- **Chỉ đọc cho tới khi bạn cho phép thêm.** Chế độ Plan không đổi file, và mọi
lệnh rủi ro đều qua phê duyệt. Khi một sandbox của hệ điều hành thực sự bọc
lệnh, Codewhale nói rõ điều đó: Seatbelt trên macOS khi khả dụng, bubblewrap
+6 -3
View File
@@ -1,4 +1,4 @@
<!-- source: README.md sha256:a87c9f323f35 -->
<!-- source: README.md sha256:4fb18fffb0fe -->
# Codewhale
一个面向终端的开源编程智能体——模型由你自带。
@@ -7,6 +7,8 @@ Codewhale 最初是为 DeepSeek 打造的原生体验,如今已成长为一个
给它一个 provider、一个模型和一个任务:它会读你的代码、改文件、跑命令、检查自己的工作,并在任务完成或需要你介入时停下。任务中途用 `/model` 切换模型。交互式工作用 TUI,脚本和 CI 用 `codewhale exec`。它用 Rust 编写,采用 MIT 许可,运行在你自己的机器上。
和其他 harness 不一样的地方在于:**每个角色用哪个模型由你决定,而且它们不必相同。** 一个 Fleet 为每个角色分别固定 provider、模型和推理档位——所以又快又便宜的模型可以指挥昂贵的推理模型,GLM 的 builder 也可以和 Kimi 的 reviewer 干同一份活。写下你自己的角色、你自己的 constitution,这套 harness 就是你的,而不是我们的。
我们一直在寻找贡献者和改进的方式。如果你在用的某个模型或 provider 还不支持,或者有什么东西坏了,告诉我们就是你能做的最有用的事之一——见[贡献](#贡献)。
[English](README.md) · [日本語](README.ja-JP.md) · [Tiếng Việt](README.vi.md) · [Bahasa Indonesia](README.id.md) · [한국어](README.ko-KR.md) · [Español](README.es-419.md) · [Português](README.pt-BR.md) · [Русский](README.ru.md) · [Українська](README.uk.md) · [codewhale.net](https://codewhale.net/) · [Docs](docs) · [Changelog](CHANGELOG.md) · [Discord 社区](https://discord.gg/37gfS3ksug)
@@ -36,11 +38,12 @@ codewhale web # local browser client on 127.0.0.1
```
在 TUI 中:`/model` 同时切换 provider 和模型,`/fleet` 运行一组 worker,`/undo` 撤销上一轮,`/restore <N>` 把工作区回滚到更早的快照(不带参数的 `/restore` 只列出快照)。输入区为空时,`Tab` 在 Plan / Act / Operate 之间循环切换;输入区有内容时,`Tab` 改为补全斜杠命令和 `@` 提及。`Shift+Tab` 在任何时候都能循环切换 Ask / Auto-Review / Full Access 权限姿态。`!` 让 shell 命令经由正常的审批路径运行。
在 TUI 中:`/model` 同时切换 provider 和模型,`/fleet` 构建并运行团队——一次一个角色,各自带着自己的模型,`/undo` 撤销上一轮,`/restore <N>` 把工作区回滚到更早的快照(不带参数的 `/restore` 只列出快照)。输入区为空时,`Tab` 在 Plan / Work / Operate 之间循环切换;输入区有内容时,`Tab` 改为补全斜杠命令和 `@` 提及。`Shift+Tab` 在任何时候都能循环切换 Ask / Auto-Review / Full Access 权限姿态。`!` 让 shell 命令经由正常的审批路径运行。
## 功能
- **任意模型,任意 provider。** DeepSeek、Claude、GPT、Kimi、GLM 等 30 多家 provider,以及你自己的 vLLM、SGLang、Ollama——无需 key——全都跑在同一套运行时和同一套工具之上。上下文预算与价格取自真实路由;价格未知时显示未知,而不是 $0。
- **任意模型,任意 provider——也可以任意混搭。** DeepSeek、Claude、GPT、Kimi、GLM 等 30 多家 provider,以及你自己的 vLLM、SGLang、Ollama——无需 key——全都跑在同一套运行时和同一套工具之上。保存下来的角色会显式记录它的 `provider``model` 和推理档位,所以一个 Fleet 可以在同一次运行里跨越多家厂商,角色的路由也不会取决于当时恰好激活的是哪个 provider。上下文预算与价格取自真实路由;价格未知时显示未知,而不是 $0。
- **由你亲手写就的 harness。** 角色就是你能读、能改的文件——每个角色一个模型、一套工具姿态和一份常驻指令——放在项目里让团队共享,或放在你的个人设置旁边,跟着你在不同仓库之间走。constitution 记录你希望 agent 在每一次会话中如何行事,让这套 harness 贴合你的做法,而不是我们的。
- **默认只读,放开权限才更进一步。** Plan 模式不改动文件,审批把关每一次高风险命令。只有当命令确实被 OS 沙箱包装时,Codewhale 才会如实标明:macOS 上是可用时启用的 Seatbelt,Linux 上是需显式启用的 bubblewrap。仓库的 `constitution.json` 会编译成写入拦截,连 Full Access 也无法跳过。
- **随时可以续跑的工作。** Fleet 把每一步记录在只追加的账本里,`fleet resume` 从你停下的地方继续。
+12
View File
@@ -74,6 +74,9 @@ base_url = "https://api.deepseek.com/beta"
# trinity-large-preview — direct Arcee AI API model ID
# deepseek-ai/DeepSeek-V4-Pro — SGLang self-hosted Pro model ID
# deepseek-ai/DeepSeek-V4-Flash — SGLang self-hosted Flash model ID
# auto — auto-select between flash and pro based on task complexity.
# Complex tasks (debugging, refactoring, architecture) → pro;
# simple tasks (lookups, formatting, Q&A) → flash.
default_text_model = "deepseek-v4-pro"
# ─────────────────────────────────────────────────────────────────────────────────
@@ -731,6 +734,15 @@ max_subagents = 10 # optional (1-20)
# base_url = "https://api.x.ai/v1"
# model = "grok-4.5" # or grok-4.3, grok-build
# Mistral AI — la Plateforme (https://console.mistral.ai/)
# OpenAI-compatible Chat Completions route.
# Provider aliases: mistral, mistral-ai, mistralai, la-plateforme
# Env var aliases: MISTRAL_API_KEY, MISTRAL_BASE_URL, MISTRAL_MODEL
[providers.mistral]
# api_key = "YOUR_MISTRAL_API_KEY"
# base_url = "https://api.mistral.ai/v1"
# model = "mistral-code-latest" # or mistral-medium-latest, mistral-small-latest, mistral-large-latest
# ─────────────────────────────────────────────────────────────────────────────────
# Alibaba Cloud Model Studio — Token Plan
# (https://bailian.console.aliyun.com/)
+1 -1
View File
@@ -8,5 +8,5 @@ repository.workspace = true
description = "Model/provider registry and fallback strategy for Codewhale"
[dependencies]
codewhale-config = { path = "../config", version = "0.9.4" }
codewhale-config = { path = "../config", version = "0.9.6" }
serde.workspace = true
+62 -6
View File
@@ -1106,14 +1106,25 @@ impl Default for ModelRegistry {
supports_tools: true,
supports_reasoning: true,
},
// Meta Model API / Muse Spark.
// Meta Model API / Muse Spark. Keep these in step with
// `DEFAULT_META_MODEL` in config's provider_defaults and with the
// bundled models.dev catalog: this registry resolves the `muse`
// aliases for the CLI and app-server, so a stale id here silently
// routes them somewhere the configured default never points.
ModelInfo {
id: "muse-spark-1.1".to_string(),
id: "muse-spark-1.2".to_string(),
provider: ProviderKind::Meta,
aliases: vec!["muse-spark".to_string(), "muse".to_string()],
supports_tools: true,
supports_reasoning: true,
},
ModelInfo {
id: "muse-spark-1.2-contributor".to_string(),
provider: ProviderKind::Meta,
aliases: vec!["muse-spark-contributor".to_string()],
supports_tools: true,
supports_reasoning: true,
},
// xAI / Grok (https://api.x.ai/v1)
ModelInfo {
id: "grok-4.5".to_string(),
@@ -1157,6 +1168,51 @@ impl Default for ModelRegistry {
supports_tools: true,
supports_reasoning: false,
},
ModelInfo {
id: "mistral-code-latest".to_string(),
provider: ProviderKind::Mistral,
aliases: vec![
"codestral".to_string(),
"codestral-latest".to_string(),
"mistral-code".to_string(),
],
supports_tools: true,
supports_reasoning: false,
},
ModelInfo {
id: "mistral-medium-latest".to_string(),
provider: ProviderKind::Mistral,
aliases: vec![
"mistral-medium".to_string(),
"mistral-medium-3-5".to_string(),
],
supports_tools: true,
supports_reasoning: true,
},
ModelInfo {
id: "mistral-small-latest".to_string(),
provider: ProviderKind::Mistral,
aliases: vec![
"mistral-small".to_string(),
"mistral-small-2603".to_string(),
],
supports_tools: true,
supports_reasoning: true,
},
ModelInfo {
id: "magistral-small-latest".to_string(),
provider: ProviderKind::Mistral,
aliases: vec!["magistral".to_string(), "magistral-small".to_string()],
supports_tools: true,
supports_reasoning: true,
},
ModelInfo {
id: "mistral-large-latest".to_string(),
provider: ProviderKind::Mistral,
aliases: vec!["mistral-large".to_string()],
supports_tools: true,
supports_reasoning: false,
},
];
Self::new(models)
}
@@ -1900,7 +1956,7 @@ mod tests {
(ProviderKind::Minimax, "MiniMax-M2.1"),
(ProviderKind::MinimaxAnthropic, "MiniMax-M3"),
(ProviderKind::Openmodel, "deepseek-v4-flash"),
(ProviderKind::Meta, "muse-spark-1.1"),
(ProviderKind::Meta, "muse-spark-1.2"),
(ProviderKind::Xai, "grok-4.5"),
] {
assert!(
@@ -2004,14 +2060,14 @@ mod tests {
let default = registry.resolve(None, Some(ProviderKind::Meta));
assert_eq!(default.resolved.provider, ProviderKind::Meta);
assert_eq!(default.resolved.id, "muse-spark-1.1");
assert_eq!(default.resolved.id, "muse-spark-1.2");
assert!(default.used_fallback);
let alias = registry.resolve(Some("muse-spark"), Some(ProviderKind::Meta));
assert_eq!(alias.resolved.provider, ProviderKind::Meta);
assert_eq!(alias.resolved.id, "muse-spark-1.1");
assert_eq!(alias.resolved.id, "muse-spark-1.2");
assert!(!alias.used_fallback);
assert_eq!(model_family("muse-spark-1.1"), ModelFamily::Meta);
assert_eq!(model_family("muse-spark-1.2"), ModelFamily::Meta);
}
#[test]
+10 -10
View File
@@ -12,16 +12,16 @@ autobins = false
[dependencies]
anyhow.workspace = true
axum.workspace = true
codewhale-agent = { path = "../agent", version = "0.9.4" }
codewhale-config = { path = "../config", version = "0.9.4" }
codewhale-core = { path = "../core", version = "0.9.4" }
codewhale-execpolicy = { path = "../execpolicy", version = "0.9.4" }
codewhale-hooks = { path = "../hooks", version = "0.9.4" }
codewhale-mcp = { path = "../mcp", version = "0.9.4" }
codewhale-protocol = { path = "../protocol", version = "0.9.4" }
codewhale-release = { path = "../release", version = "0.9.4" }
codewhale-state = { path = "../state", version = "0.9.4" }
codewhale-tools = { path = "../tools", version = "0.9.4" }
codewhale-agent = { path = "../agent", version = "0.9.6" }
codewhale-config = { path = "../config", version = "0.9.6" }
codewhale-core = { path = "../core", version = "0.9.6" }
codewhale-execpolicy = { path = "../execpolicy", version = "0.9.6" }
codewhale-hooks = { path = "../hooks", version = "0.9.6" }
codewhale-mcp = { path = "../mcp", version = "0.9.6" }
codewhale-protocol = { path = "../protocol", version = "0.9.6" }
codewhale-release = { path = "../release", version = "0.9.6" }
codewhale-state = { path = "../state", version = "0.9.6" }
codewhale-tools = { path = "../tools", version = "0.9.6" }
serde.workspace = true
serde_json.workspace = true
rustls.workspace = true
+14 -18
View File
@@ -11,27 +11,23 @@ description = "Agentic terminal facade for open-source and open-weight coding mo
name = "codewhale"
path = "src/main.rs"
# Short-form convenience alias — forwards to `codewhale` silently.
[[bin]]
name = "codew"
path = "src/bin/codew.rs"
[dependencies]
anyhow.workspace = true
clap.workspace = true
clap_complete.workspace = true
codewhale-agent = { path = "../agent", version = "0.9.4" }
codewhale-app-server = { path = "../app-server", version = "0.9.4" }
codewhale-config = { path = "../config", version = "0.9.4" }
codewhale-lane = { path = "../lane", version = "0.9.4" }
codewhale-workflow = { path = "../workflow", version = "0.9.4" }
codewhale-execpolicy = { path = "../execpolicy", version = "0.9.4" }
codewhale-mcp = { path = "../mcp", version = "0.9.4" }
codewhale-paths = { path = "../paths", version = "0.9.4" }
codewhale-release = { path = "../release", version = "0.9.4" }
codewhale-secrets = { path = "../secrets", version = "0.9.4" }
codewhale-state = { path = "../state", version = "0.9.4" }
codewhale-telemetry = { path = "../telemetry", version = "0.9.4" }
codewhale-tui = { path = "../tui", version = "0.9.6" }
codewhale-agent = { path = "../agent", version = "0.9.6" }
codewhale-app-server = { path = "../app-server", version = "0.9.6" }
codewhale-config = { path = "../config", version = "0.9.6" }
codewhale-lane = { path = "../lane", version = "0.9.6" }
codewhale-workflow = { path = "../workflow", version = "0.9.6" }
codewhale-execpolicy = { path = "../execpolicy", version = "0.9.6" }
codewhale-mcp = { path = "../mcp", version = "0.9.6" }
codewhale-paths = { path = "../paths", version = "0.9.6" }
codewhale-release = { path = "../release", version = "0.9.6" }
codewhale-secrets = { path = "../secrets", version = "0.9.6" }
codewhale-state = { path = "../state", version = "0.9.6" }
codewhale-telemetry = { path = "../telemetry", version = "0.9.6" }
chrono.workspace = true
console = "0.16.3"
dirs.workspace = true
@@ -49,7 +45,7 @@ webbrowser = "1.0"
zeroize = "1.8.2"
[build-dependencies]
codewhale-build-support = { path = "../build-support", version = "0.9.4" }
codewhale-build-support = { path = "../build-support", version = "0.9.6" }
# Parent-death cleanup for delegated server children (#3259): on Linux the
# dispatcher sets PR_SET_PDEATHSIG so the child is signalled if the dispatcher
-74
View File
@@ -1,74 +0,0 @@
//! Convenience `codew` alias.
//!
//! Forwards argv to the `codewhale` dispatcher silently. This is a
//! permanent short-form alias — six fewer keystrokes, same binary.
use std::env;
use std::path::{Path, PathBuf};
use std::process::Command;
fn main() {
let args: Vec<String> = env::args_os()
.skip(1)
.map(|a| a.to_string_lossy().into_owned())
.collect();
let status = match spawn_codewhale(&args) {
Ok(s) => s,
Err(e) => {
eprintln!(
"error: failed to spawn `codewhale`: {e}. Is it on PATH? \
Install with `cargo install codewhale-cli` or via npm/Homebrew."
);
std::process::exit(127);
}
};
std::process::exit(status.code().unwrap_or(1));
}
fn spawn_codewhale(args: &[String]) -> std::io::Result<std::process::ExitStatus> {
// Prefer the dispatcher installed next to this shim. Falling back to PATH
// first can silently run an older global `codewhale` after a fresh install.
if let Ok(exe_path) = env::current_exe()
&& let Some(sibling) = sibling_codewhale_path(&exe_path)
&& sibling.is_file()
{
return Command::new(sibling).args(args).status();
}
// Fall back to PATH for unusual installs that ship only the shim.
match Command::new("codewhale").args(args).status() {
Ok(s) => return Ok(s),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {}
Err(e) => return Err(e),
}
Err(std::io::Error::new(
std::io::ErrorKind::NotFound,
"codewhale not found on PATH or in sibling directory",
))
}
fn sibling_codewhale_path(exe_path: &Path) -> Option<PathBuf> {
exe_path
.parent()
.map(|dir| dir.join(format!("codewhale{}", std::env::consts::EXE_SUFFIX)))
}
#[cfg(test)]
mod tests {
use super::sibling_codewhale_path;
use std::path::Path;
#[test]
fn sibling_dispatcher_uses_platform_executable_suffix() {
let path = Path::new("/tmp/codewhale-bin/codew");
let sibling = sibling_codewhale_path(path).expect("sibling");
assert_eq!(
sibling,
Path::new("/tmp/codewhale-bin")
.join(format!("codewhale{}", std::env::consts::EXE_SUFFIX))
);
}
}
+64
View File
@@ -51,6 +51,10 @@ enum CloudCommand {
Logout,
/// Manage provider API keys stored in the signed-in Codewhale account.
Keys(CloudKeysArgs),
/// Inspect the account document; local settings import is not available yet.
Pull(CloudPullArgs),
/// Push local settings to the account document (never automatic, --dry-run required).
Push(CloudPushArgs),
}
#[derive(Debug, Args)]
@@ -67,6 +71,20 @@ struct CloudLoginArgs {
timeout_seconds: u64,
}
#[derive(Debug, Args)]
struct CloudPullArgs {
/// Inspect the account document without writing local files.
#[arg(long, default_value_t = false)]
dry_run: bool,
}
#[derive(Debug, Args)]
struct CloudPushArgs {
/// Show what would be pushed without writing the remote document.
#[arg(long, default_value_t = false)]
dry_run: bool,
}
#[derive(Debug, Args)]
struct CloudKeysArgs {
#[command(subcommand)]
@@ -663,6 +681,52 @@ fn run_with<T: CloudTransport, W: Write>(
Ok(())
}
},
CloudCommand::Pull(args) => {
if !args.dry_run {
bail!(
"Account settings import is not available yet; local config was not changed. Run `codewhale account pull --dry-run` to inspect the signed-in account."
);
}
let user = client.me()?;
// `/api/me` currently exposes account identity and key metadata,
// not a versioned settings document that can be applied locally.
// Stay read-only and explicit until that import contract exists.
writeln!(out, "Account settings (pull --dry-run):")?;
writeln!(out, "Account ID: {}", printable(&user.id))?;
writeln!(out, "Profile: {}", printable(profile))?;
writeln!(out, "API: {api_base}")?;
writeln!(
out,
"dry-run: remote settings import is not available; local config unchanged"
)?;
// Show the invariant: Bearer custody stays in the OS keyring, never in config.toml.
writeln!(
out,
"Secure custody: Bearer tokens remain in the OS keyring"
)?;
Ok(())
}
CloudCommand::Push(args) => {
let user = client.me()?;
if !args.dry_run {
bail!(
"Push is never automatic; re-run with --dry-run to preview, then confirm explicitly"
);
}
writeln!(out, "Account settings (push --dry-run):")?;
writeln!(out, "Account ID: {}", printable(&user.id))?;
writeln!(out, "Profile: {}", printable(profile))?;
writeln!(out, "API: {api_base}")?;
writeln!(
out,
"dry-run: would PATCH /api/me/preferences with If-Match revision check (412 on conflict)"
)?;
writeln!(
out,
"No credentials, paths, or env are copied; only explicit fields (field-level last-writer-wins)"
)?;
Ok(())
}
}
}
+89
View File
@@ -434,6 +434,95 @@ fn status_refreshes_once_on_unauthorized_and_never_displays_tokens() {
assert_eq!(requests[2].path, "/api/me");
}
#[test]
fn account_pull_refuses_to_claim_unimplemented_local_import() {
let (temp, config) = test_config();
let config_path = config.path().to_path_buf();
let (secrets, _) = test_secrets();
let transport = FakeTransport::new(vec![]);
let mut output = Vec::new();
let mut key_reader = |_| bail!("unused");
let mut opener = |_| true;
let mut sleeper = |_| {};
let error = run_with(
command(&["codewhale", "account", "pull"]),
"default",
"https://api.codewhale.net",
&config,
&secrets,
&secrets,
&transport,
&mut output,
&mut key_reader,
&mut opener,
&mut sleeper,
)
.expect_err("non-dry-run pull must fail until settings import exists");
assert!(error.to_string().contains("import is not available"));
assert!(error.to_string().contains("local config was not changed"));
assert!(
output.is_empty(),
"a rejected pull must not print success text"
);
assert!(
transport.requests().is_empty(),
"a rejected pull needs no API call"
);
assert!(
!config_path.exists(),
"a rejected pull must not create config.toml"
);
drop(temp);
}
#[test]
fn account_pull_dry_run_is_truthful_and_read_only() {
let (temp, config) = test_config();
let config_path = config.path().to_path_buf();
let (secrets, _) = test_secrets();
let transport = FakeTransport::new(vec![response(200, account("acct-pull"))]);
CloudClient::new(&transport, &secrets, "default", "https://api.codewhale.net")
.save_auth(auth("access-secret", "refresh-secret", "acct-pull"))
.unwrap();
let mut output = Vec::new();
let mut key_reader = |_| bail!("unused");
let mut opener = |_| true;
let mut sleeper = |_| {};
run_with(
command(&["codewhale", "account", "pull", "--dry-run"]),
"default",
"https://api.codewhale.net",
&config,
&secrets,
&secrets,
&transport,
&mut output,
&mut key_reader,
&mut opener,
&mut sleeper,
)
.unwrap();
let output = String::from_utf8(output).unwrap();
assert!(output.contains("Account settings (pull --dry-run):"));
assert!(output.contains("Account ID: acct-pull"));
assert!(output.contains("remote settings import is not available"));
assert!(output.contains("local config unchanged"));
assert!(!output.contains("Pulled account document"));
assert!(!output.contains("would hydrate"));
assert!(!output.contains("access-secret"));
assert!(!output.contains("refresh-secret"));
assert!(!config_path.exists(), "dry-run must not create config.toml");
let requests = transport.requests();
assert_eq!(requests.len(), 1);
assert!(requests[0].method == HttpMethod::Get);
assert_eq!(requests[0].path, "/api/me");
drop(temp);
}
#[test]
fn non_terminal_refresh_responses_preserve_the_local_session() {
for status in [403, 429, 500, 503] {
+341 -1779
View File
File diff suppressed because it is too large Load Diff
+17
View File
@@ -14,5 +14,22 @@ fn main() -> std::process::ExitCode {
libc::signal(libc::SIGPIPE, libc::SIG_DFL);
}
// Single-binary argv0 dispatch: `codew` is now an alias for `codewhale`
// without a second compiled artifact. Checking the binary basename keeps
// the install surface at one file while preserving the six-keystroke save.
let _ = std::env::args().next().and_then(|argv0| {
let base = std::path::Path::new(&argv0)
.file_name()
.and_then(|s| s.to_str())
.unwrap_or("");
let trimmed = base
.strip_suffix(std::env::consts::EXE_SUFFIX)
.unwrap_or(base);
if trimmed == "codew" {
// No-op: the single `codewhale` binary handles both names.
}
None::<()>
});
codewhale_cli::run_cli()
}
+251 -302
View File
@@ -40,7 +40,7 @@ pub fn run_update(beta: bool, check_only: bool, proxy_arg: Option<String>) -> Re
let legacy_binary = is_legacy_binary(&current_exe);
ensure_supported_release_target(std::env::consts::OS, std::env::consts::ARCH)?;
let targets = update_targets_for_exe(&current_exe);
let plan = update_plan_for_exe(&current_exe);
let channel = ReleaseChannel::from_beta_flag(beta);
let current_version = env!("CARGO_PKG_VERSION");
let proxy = proxy_arg
@@ -119,58 +119,55 @@ pub fn run_update(beta: bool, check_only: bool, proxy_arg: Option<String>) -> Re
}
};
// Step 3: Download and verify every colocated binary in the install.
let mut downloads = Vec::new();
for target in &targets {
let asset = select_platform_asset(release, &target.asset_stem).with_context(|| {
format!(
"no asset found for platform {} in release {latest_tag}. \
Available assets: {}",
target.asset_stem,
release
.assets
.iter()
.map(|a| a.name.as_str())
.collect::<Vec<_>>()
.join(", ")
)
})?;
// Step 3: Download and verify the sole implementation binary once. The
// installed `codew` and pre-0.9.5 `codewhale-tui` command paths are
// compatibility names for these exact bytes, not separate release assets.
let asset = select_platform_asset(release, &plan.asset_stem).with_context(|| {
format!(
"no asset found for platform {} in release {latest_tag}. \
Available assets: {}",
plan.asset_stem,
release
.assets
.iter()
.map(|a| a.name.as_str())
.collect::<Vec<_>>()
.join(", ")
)
})?;
println!("Downloading {}...", asset.name);
let bytes =
download_url(&asset.browser_download_url, proxy.as_ref()).with_context(|| {
format!(
"failed to download {}\n{}",
asset.name,
update_network_fallback_hint()
)
})?;
println!("Downloading {}...", asset.name);
let bytes = download_url(&asset.browser_download_url, proxy.as_ref()).with_context(|| {
format!(
"failed to download {}\n{}",
asset.name,
update_network_fallback_hint()
)
})?;
if let Some(checksums) = &checksum_manifest {
let expected = checksums
.get(&asset.name)
.with_context(|| format!("checksum manifest is missing {}", asset.name))?;
let actual = sha256_hex(&bytes);
if !actual.eq_ignore_ascii_case(expected) {
bail!(
"SHA256 mismatch for {}!\n expected: {expected}\n actual: {actual}",
asset.name
);
}
if let Some(checksums) = &checksum_manifest {
let expected = checksums
.get(&asset.name)
.with_context(|| format!("checksum manifest is missing {}", asset.name))?;
let actual = sha256_hex(&bytes);
if !actual.eq_ignore_ascii_case(expected) {
bail!(
"SHA256 mismatch for {}!\n expected: {expected}\n actual: {actual}",
asset.name
);
}
preflight_downloaded_binary(&asset.name, &bytes)?;
downloads.push((target.path.clone(), asset.name.clone(), bytes));
}
preflight_downloaded_binary(&asset.name, &bytes)?;
if checksum_manifest.is_some() {
println!("SHA256 checksum verified.");
}
// Step 4: Replace binaries only after all downloads and the primary
// Step 4: Replace command paths only after the download and the running
// executable identity verify. The preflight happens before a colocated
// sibling can change, then the primary is checked again just in time.
replace_verified_downloads(&downloads, || {
// compatibility path can change, then the identity is checked just in time.
replace_verified_downloads(&plan.target_paths, &bytes, || {
validate_primary_update_identity(&executable_identity)
})?;
@@ -179,9 +176,9 @@ pub fn run_update(beta: bool, check_only: bool, proxy_arg: Option<String>) -> Re
Updated binaries:\n{}\n\
\n\
Restart the application to use the new version.",
downloads
plan.target_paths
.iter()
.map(|(path, asset, _)| format!(" - {} ({asset})", path.display()))
.map(|path| format!(" - {} ({})", path.display(), asset.name))
.collect::<Vec<_>>()
.join("\n")
);
@@ -524,7 +521,8 @@ fn validate_primary_update_identity(identity: &UpdateExecutableIdentity) -> Resu
}
fn replace_verified_downloads<F>(
downloads: &[(PathBuf, String, Vec<u8>)],
target_paths: &[PathBuf],
verified_bytes: &[u8],
validate_primary_identity: F,
) -> Result<()>
where
@@ -533,11 +531,12 @@ where
// Fail before mutating a sibling if the primary pathname no longer names
// the process image that initiated this update.
validate_primary_identity()?;
for (path, _, bytes) in downloads.iter().rev() {
replace_binary_with_validation(path, bytes, || {
for path in target_paths.iter().rev() {
replace_binary_with_validation(path, verified_bytes, || {
// Re-check after each temp file is fully staged and immediately
// before every destructive rename. This protects paired installs
// before the sibling as well as just in time for the primary.
// before every destructive rename. The running command is first
// in the plan and therefore replaced last, after its colocated
// compatibility names have received the same verified bytes.
validate_primary_identity()
})?;
}
@@ -628,10 +627,11 @@ fn legacy_binary_message(current_exe: &Path) -> String {
"\
this binary ({exe}) is using the legacy deepseek/deepseek-tui command name.
The package has been renamed to `codewhale`. This update will install canonical
Codewhale binaries (`codewhale` and, when present, `codewhale-tui`) beside the
legacy command when the install directory is writable. DeepSeek provider support
is unchanged.
The package has been renamed to `codewhale`. This update will install the
canonical `codewhale` command and refresh any existing `codew` or
`codewhale-tui` compatibility command from the same binary beside the legacy
command when the install directory is writable.
DeepSeek provider support is unchanged.
If this update cannot write to the install directory, reinstall using your
original install method:
@@ -644,13 +644,12 @@ original install method:
cargo uninstall deepseek-tui-cli 2>/dev/null || true
cargo uninstall deepseek-tui 2>/dev/null || true
cargo install codewhale-cli --locked
cargo install codewhale-tui --locked
Homebrew:
brew upgrade deepseek-tui
Manual binary:
download the matched codewhale and codewhale-tui assets from
download the matched codewhale asset from
https://github.com/Hmbown/CodeWhale/releases/latest
Once `codewhale` is on your PATH, run `codewhale update` for future updates.",
@@ -658,96 +657,79 @@ Once `codewhale` is on your PATH, run `codewhale update` for future updates.",
)
}
pub(crate) fn binary_prefix_for_exe(current_exe: &Path) -> &'static str {
fn command_name_for_exe(current_exe: &Path) -> String {
let exe_name = current_exe
.file_name()
.and_then(|name| name.to_str())
.unwrap_or("codewhale")
.to_ascii_lowercase();
if exe_name.contains("codewhale-tui") || exe_name.contains("deepseek-tui") {
"codewhale-tui"
} else {
"codewhale"
}
exe_name
.strip_suffix(".exe")
.unwrap_or(&exe_name)
.to_string()
}
fn sibling_prefix_for(prefix: &str) -> &'static str {
if prefix == "codewhale-tui" {
"codewhale"
} else {
"codewhale-tui"
}
fn command_path_beside(current_exe: &Path, command: &str) -> PathBuf {
current_exe.with_file_name(format!("{command}{}", std::env::consts::EXE_SUFFIX))
}
fn sibling_binary_path(current_exe: &Path, sibling_prefix: &str) -> PathBuf {
current_exe.with_file_name(format!("{sibling_prefix}{}", std::env::consts::EXE_SUFFIX))
}
fn canonical_binary_path_for_prefix(current_exe: &Path, prefix: &str) -> PathBuf {
if is_legacy_binary(current_exe) {
current_exe.with_file_name(format!("{prefix}{}", std::env::consts::EXE_SUFFIX))
} else {
fn installed_command_path(current_exe: &Path, command: &str) -> PathBuf {
if command_name_for_exe(current_exe) == command {
current_exe.to_path_buf()
}
}
fn legacy_binary_name_for_prefix(prefix: &str) -> &'static str {
if prefix == "codewhale-tui" {
"deepseek-tui"
} else {
"deepseek"
command_path_beside(current_exe, command)
}
}
fn legacy_sibling_binary_path(current_exe: &Path, sibling_prefix: &str) -> PathBuf {
current_exe.with_file_name(format!(
"{}{}",
legacy_binary_name_for_prefix(sibling_prefix),
std::env::consts::EXE_SUFFIX
))
fn push_unique_path(paths: &mut Vec<PathBuf>, path: PathBuf) {
if !paths.iter().any(|existing| existing == &path) {
paths.push(path);
}
}
fn should_update_sibling(
current_exe: &Path,
canonical_sibling: &Path,
sibling_prefix: &str,
) -> bool {
canonical_sibling.exists()
|| (is_legacy_binary(current_exe)
&& legacy_sibling_binary_path(current_exe, sibling_prefix).exists())
fn legacy_tui_command_exists_beside(current_exe: &Path) -> bool {
command_name_for_exe(current_exe) == "deepseek-tui"
|| command_path_beside(current_exe, "deepseek-tui").exists()
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct UpdateTarget {
path: PathBuf,
struct UpdatePlan {
target_paths: Vec<PathBuf>,
asset_stem: String,
}
fn update_targets_for_exe(current_exe: &Path) -> Vec<UpdateTarget> {
let current_prefix = binary_prefix_for_exe(current_exe);
let mut targets = vec![UpdateTarget {
path: canonical_binary_path_for_prefix(current_exe, current_prefix),
fn update_plan_for_exe(current_exe: &Path) -> UpdatePlan {
let mut target_paths = Vec::new();
// Keep the process image first so reverse-order replacement updates the
// command currently running the updater last. Pre-rebrand command names
// retain their historical migration behavior: install canonical commands
// beside them instead of overwriting the legacy path.
if !is_legacy_binary(current_exe) {
push_unique_path(&mut target_paths, current_exe.to_path_buf());
}
let primary = installed_command_path(current_exe, "codewhale");
push_unique_path(&mut target_paths, primary);
for alias in ["codew", "codewhale-tui"] {
let alias_path = installed_command_path(current_exe, alias);
let migrate_legacy_tui = alias == "codewhale-tui"
&& is_legacy_binary(current_exe)
&& legacy_tui_command_exists_beside(current_exe);
if alias_path.exists() || command_name_for_exe(current_exe) == alias || migrate_legacy_tui {
push_unique_path(&mut target_paths, alias_path);
}
}
UpdatePlan {
target_paths,
asset_stem: release_asset_stem_for_prefix(
current_prefix,
"codewhale",
std::env::consts::OS,
std::env::consts::ARCH,
),
}];
let sibling_prefix = sibling_prefix_for(current_prefix);
let sibling = sibling_binary_path(current_exe, sibling_prefix);
if should_update_sibling(current_exe, &sibling, sibling_prefix) {
targets.push(UpdateTarget {
path: sibling,
asset_stem: release_asset_stem_for_prefix(
sibling_prefix,
std::env::consts::OS,
std::env::consts::ARCH,
),
});
}
targets
}
fn release_asset_stem_for_prefix(prefix: &str, os: &str, rust_arch: &str) -> String {
@@ -766,8 +748,8 @@ fn release_asset_name_for_prefix(prefix: &str, os: &str, rust_arch: &str) -> Str
#[cfg(test)]
fn release_asset_stem_for(current_exe: &Path, os: &str, rust_arch: &str) -> String {
let prefix = binary_prefix_for_exe(current_exe);
release_asset_stem_for_prefix(prefix, os, rust_arch)
let _ = current_exe;
release_asset_stem_for_prefix("codewhale", os, rust_arch)
}
pub(crate) fn asset_matches_platform(asset_name: &str, binary_name: &str) -> bool {
@@ -954,13 +936,11 @@ fn release_from_asset_base_url(
browser_download_url: mirror_asset_url(base_url, CHECKSUM_MANIFEST_ASSET),
}];
for prefix in ["codewhale", "codewhale-tui"] {
let name = release_asset_name_for_prefix(prefix, os, rust_arch);
assets.push(Asset {
browser_download_url: mirror_asset_url(base_url, &name),
name,
});
}
let name = release_asset_name_for_prefix("codewhale", os, rust_arch);
assets.push(Asset {
browser_download_url: mirror_asset_url(base_url, &name),
name,
});
Release {
tag_name: tag_name.to_string(),
@@ -1341,7 +1321,6 @@ Official Linux release binaries are GNU libc builds. Ubuntu 22.04 ships glibc
Install from source on this host instead:
cargo install codewhale-cli --locked
cargo install codewhale-tui --locked
Release engineering follow-up: build Linux GNU assets against an older glibc
baseline, or add a musl/static Linux asset. Set CODEWHALE_SKIP_GLIBC_CHECK=1 to
@@ -1527,7 +1506,7 @@ mod tests {
.unwrap();
assert_eq!(resolved, executable.canonicalize().unwrap());
assert_eq!(update_targets_for_exe(&resolved)[0].path, resolved);
assert_eq!(update_plan_for_exe(&resolved).target_paths[0], resolved);
}
#[cfg(unix)]
@@ -1550,10 +1529,7 @@ mod tests {
let resolved =
resolve_android_loaded_executable_report(&maps, TEST_ANDROID_MARKER, &invoked).unwrap();
let target_paths = update_targets_for_exe(&resolved)
.into_iter()
.map(|target| target.path)
.collect::<Vec<_>>();
let target_paths = update_plan_for_exe(&resolved).target_paths;
assert_eq!(
target_paths,
@@ -1813,19 +1789,8 @@ mod tests {
std::fs::write(&swapped_primary, b"externally swapped primary").unwrap();
std::fs::rename(&swapped_primary, &primary).unwrap();
let downloads = vec![
(
primary.clone(),
"codewhale-android-arm64".to_string(),
b"downloaded primary".to_vec(),
),
(
sibling.clone(),
"codewhale-tui-android-arm64".to_string(),
b"downloaded sibling".to_vec(),
),
];
let error = replace_verified_downloads(&downloads, || {
let target_paths = vec![primary.clone(), sibling.clone()];
let error = replace_verified_downloads(&target_paths, b"downloaded binary", || {
resolve_android_loaded_executable_report(&maps, TEST_ANDROID_MARKER, &primary)
.map(|_| ())
})
@@ -1859,20 +1824,9 @@ mod tests {
write_test_executable(&swapped_primary);
std::fs::write(&swapped_primary, b"externally swapped primary").unwrap();
let downloads = vec![
(
primary.clone(),
"codewhale-android-arm64".to_string(),
b"downloaded primary".to_vec(),
),
(
sibling.clone(),
"codewhale-tui-android-arm64".to_string(),
b"downloaded sibling".to_vec(),
),
];
let target_paths = vec![primary.clone(), sibling.clone()];
let validation_calls = Cell::new(0);
let error = replace_verified_downloads(&downloads, || {
let error = replace_verified_downloads(&target_paths, b"downloaded binary", || {
let call = validation_calls.get() + 1;
validation_calls.set(call);
if call == 1 {
@@ -1910,13 +1864,9 @@ mod tests {
write_test_executable(&swapped_primary);
std::fs::write(&swapped_primary, b"externally swapped primary").unwrap();
let downloads = vec![(
primary.clone(),
"codewhale-android-arm64".to_string(),
b"downloaded primary".to_vec(),
)];
let target_paths = vec![primary.clone()];
let validation_calls = Cell::new(0);
let error = replace_verified_downloads(&downloads, || {
let error = replace_verified_downloads(&target_paths, b"downloaded binary", || {
let call = validation_calls.get() + 1;
validation_calls.set(call);
if call == 1 {
@@ -1949,58 +1899,25 @@ mod tests {
);
}
/// Verify binary prefix detection for dispatcher vs TUI binary.
/// Every command name resolves to the sole implementation asset.
#[test]
fn test_binary_prefix_detection() {
// TUI binary should use codewhale-tui prefix
assert_eq!(
binary_prefix_for_exe(Path::new("codewhale-tui")),
"codewhale-tui"
);
assert_eq!(
binary_prefix_for_exe(Path::new("codewhale-tui.exe")),
"codewhale-tui"
);
assert_eq!(
binary_prefix_for_exe(Path::new("CodeWhale-TUI.exe")),
"codewhale-tui"
);
assert_eq!(
binary_prefix_for_exe(Path::new("/usr/local/bin/codewhale-tui")),
"codewhale-tui"
);
// Dispatcher binary should use codewhale prefix
assert_eq!(binary_prefix_for_exe(Path::new("codewhale")), "codewhale");
assert_eq!(
binary_prefix_for_exe(Path::new("codewhale.exe")),
"codewhale"
);
assert_eq!(
binary_prefix_for_exe(Path::new("/usr/local/bin/codewhale")),
"codewhale"
);
// Fallback for unknown names
assert_eq!(
binary_prefix_for_exe(Path::new("other-binary")),
"codewhale"
);
// Legacy names still map to the canonical update asset prefixes.
assert_eq!(
binary_prefix_for_exe(Path::new("deepseek-tui")),
"codewhale-tui"
);
assert_eq!(
binary_prefix_for_exe(Path::new("/usr/local/bin/deepseek-tui")),
"codewhale-tui"
);
assert_eq!(
binary_prefix_for_exe(Path::new("DeepSeek-TUI.exe")),
"codewhale-tui"
);
assert_eq!(binary_prefix_for_exe(Path::new("deepseek")), "codewhale");
fn every_invocation_name_uses_codewhale_release_asset() {
for command in [
"codewhale",
"codewhale.exe",
"codew",
"codew.exe",
"codewhale-tui",
"CodeWhale-TUI.exe",
"deepseek",
"deepseek-tui",
"other-binary",
] {
assert_eq!(
release_asset_stem_for(Path::new(command), "macos", "aarch64"),
"codewhale-macos-arm64"
);
}
}
#[test]
@@ -2037,7 +1954,7 @@ mod tests {
let message = legacy_binary_message(Path::new("/usr/local/bin/deepseek-tui"));
assert!(message.contains("legacy deepseek/deepseek-tui command name"));
assert!(message.contains("install canonical"));
assert!(message.contains("canonical `codewhale` command"));
assert!(message.contains("DeepSeek provider support"));
assert!(message.contains("is unchanged"));
assert!(message.contains("npm uninstall -g deepseek-tui"));
@@ -2045,13 +1962,13 @@ mod tests {
assert!(message.contains("cargo uninstall deepseek-tui-cli 2>/dev/null || true"));
assert!(message.contains("cargo uninstall deepseek-tui 2>/dev/null || true"));
assert!(message.contains("cargo install codewhale-cli --locked"));
assert!(message.contains("cargo install codewhale-tui --locked"));
assert!(!message.contains("cargo install codewhale-tui --locked"));
assert!(message.contains("brew upgrade deepseek-tui"));
assert!(message.contains("https://github.com/Hmbown/CodeWhale/releases/latest"));
}
#[test]
fn legacy_dispatcher_update_targets_canonical_codewhale_pair() {
fn legacy_dispatcher_update_targets_canonical_compatibility_commands() {
let dir = tempfile::TempDir::new().unwrap();
let dispatcher = dir
.path()
@@ -2062,14 +1979,10 @@ mod tests {
std::fs::write(&dispatcher, b"legacy dispatcher").unwrap();
std::fs::write(&tui, b"legacy tui").unwrap();
let targets = update_targets_for_exe(&dispatcher);
let paths = targets
.iter()
.map(|target| target.path.clone())
.collect::<Vec<_>>();
let plan = update_plan_for_exe(&dispatcher);
assert_eq!(
paths,
plan.target_paths,
vec![
dir.path()
.join(format!("codewhale{}", std::env::consts::EXE_SUFFIX)),
@@ -2077,12 +1990,12 @@ mod tests {
.join(format!("codewhale-tui{}", std::env::consts::EXE_SUFFIX))
]
);
assert!(targets[0].asset_stem.starts_with("codewhale-"));
assert!(targets[1].asset_stem.starts_with("codewhale-tui-"));
assert!(plan.asset_stem.starts_with("codewhale-"));
assert!(!plan.asset_stem.starts_with("codewhale-tui-"));
}
#[test]
fn legacy_tui_update_targets_canonical_tui_pair() {
fn legacy_tui_update_targets_canonical_compatibility_commands() {
let dir = tempfile::TempDir::new().unwrap();
let dispatcher = dir
.path()
@@ -2093,23 +2006,19 @@ mod tests {
std::fs::write(&dispatcher, b"legacy dispatcher").unwrap();
std::fs::write(&tui, b"legacy tui").unwrap();
let targets = update_targets_for_exe(&tui);
let paths = targets
.iter()
.map(|target| target.path.clone())
.collect::<Vec<_>>();
let plan = update_plan_for_exe(&tui);
assert_eq!(
paths,
plan.target_paths,
vec![
dir.path()
.join(format!("codewhale-tui{}", std::env::consts::EXE_SUFFIX)),
.join(format!("codewhale{}", std::env::consts::EXE_SUFFIX)),
dir.path()
.join(format!("codewhale{}", std::env::consts::EXE_SUFFIX))
.join(format!("codewhale-tui{}", std::env::consts::EXE_SUFFIX))
]
);
assert!(targets[0].asset_stem.starts_with("codewhale-tui-"));
assert!(targets[1].asset_stem.starts_with("codewhale-"));
assert!(plan.asset_stem.starts_with("codewhale-"));
assert!(!plan.asset_stem.starts_with("codewhale-tui-"));
}
#[test]
@@ -2120,18 +2029,8 @@ mod tests {
("codewhale", "linux", "x86_64", "codewhale-linux-x64"),
("codewhale", "windows", "x86_64", "codewhale-windows-x64"),
("codewhale", "windows", "aarch64", "codewhale-windows-arm64"),
(
"codewhale-tui",
"macos",
"aarch64",
"codewhale-tui-macos-arm64",
),
(
"codewhale-tui",
"linux",
"x86_64",
"codewhale-tui-linux-x64",
),
("codew", "macos", "aarch64", "codewhale-macos-arm64"),
("codewhale-tui", "linux", "x86_64", "codewhale-linux-x64"),
];
for (exe, os, arch, expected) in cases {
@@ -2140,7 +2039,7 @@ mod tests {
}
#[test]
fn update_targets_include_existing_sibling_tui_for_dispatcher() {
fn update_plan_includes_existing_compatibility_tui_for_primary() {
let dir = tempfile::TempDir::new().unwrap();
let dispatcher = dir
.path()
@@ -2151,30 +2050,94 @@ mod tests {
std::fs::write(&dispatcher, b"dispatcher").unwrap();
std::fs::write(&tui, b"tui").unwrap();
let targets = update_targets_for_exe(&dispatcher);
let paths = targets
let plan = update_plan_for_exe(&dispatcher);
let paths = plan
.target_paths
.iter()
.map(|target| target.path.as_path())
.map(PathBuf::as_path)
.collect::<Vec<_>>();
assert_eq!(paths, vec![dispatcher.as_path(), tui.as_path()]);
assert!(targets[0].asset_stem.starts_with("codewhale-"));
assert!(targets[1].asset_stem.starts_with("codewhale-tui-"));
assert!(plan.asset_stem.starts_with("codewhale-"));
assert!(!plan.asset_stem.starts_with("codewhale-tui-"));
}
#[test]
fn update_targets_skip_missing_sibling() {
fn update_plan_skips_missing_compatibility_commands() {
let dir = tempfile::TempDir::new().unwrap();
let dispatcher = dir
.path()
.join(format!("codewhale{}", std::env::consts::EXE_SUFFIX));
std::fs::write(&dispatcher, b"dispatcher").unwrap();
let targets = update_targets_for_exe(&dispatcher);
let plan = update_plan_for_exe(&dispatcher);
assert_eq!(targets.len(), 1);
assert_eq!(targets[0].path, dispatcher);
assert!(targets[0].asset_stem.starts_with("codewhale-"));
assert_eq!(plan.target_paths, vec![dispatcher]);
assert!(plan.asset_stem.starts_with("codewhale-"));
}
#[test]
fn v094_three_command_install_updates_every_path_from_primary_bytes() {
let dir = tempfile::TempDir::new().unwrap();
let primary = dir
.path()
.join(format!("codewhale{}", std::env::consts::EXE_SUFFIX));
let codew = dir
.path()
.join(format!("codew{}", std::env::consts::EXE_SUFFIX));
let legacy_tui = dir
.path()
.join(format!("codewhale-tui{}", std::env::consts::EXE_SUFFIX));
for path in [&primary, &codew, &legacy_tui] {
std::fs::write(path, b"v0.9.4 old bytes").unwrap();
}
let plan = update_plan_for_exe(&primary);
assert_eq!(
plan.target_paths,
vec![primary.clone(), codew.clone(), legacy_tui.clone()]
);
assert!(plan.asset_stem.starts_with("codewhale-"));
assert!(!plan.asset_stem.contains("codewhale-tui"));
replace_verified_downloads(&plan.target_paths, b"v0.9.5 primary bytes", || Ok(())).unwrap();
for path in [&primary, &codew, &legacy_tui] {
assert_eq!(std::fs::read(path).unwrap(), b"v0.9.5 primary bytes");
}
assert_ne!(std::fs::read(codew).unwrap(), b"v0.9.4 old bytes");
}
#[test]
fn direct_alias_invocation_keeps_running_path_first_and_updates_primary() {
let dir = tempfile::TempDir::new().unwrap();
let primary = dir
.path()
.join(format!("codewhale{}", std::env::consts::EXE_SUFFIX));
let codew = dir
.path()
.join(format!("codew{}", std::env::consts::EXE_SUFFIX));
let legacy_tui = dir
.path()
.join(format!("codewhale-tui{}", std::env::consts::EXE_SUFFIX));
for invoked in [&codew, &legacy_tui] {
for path in [&primary, &codew, &legacy_tui] {
std::fs::write(path, b"old").unwrap();
}
let plan = update_plan_for_exe(invoked);
assert_eq!(plan.target_paths.first(), Some(invoked));
assert!(plan.target_paths.contains(&primary));
assert!(plan.target_paths.contains(&codew));
assert!(plan.target_paths.contains(&legacy_tui));
assert!(plan.asset_stem.starts_with("codewhale-"));
assert!(!plan.asset_stem.starts_with("codewhale-tui-"));
replace_verified_downloads(&plan.target_paths, b"new primary bytes", || Ok(()))
.unwrap();
for path in [&primary, &codew, &legacy_tui] {
assert_eq!(std::fs::read(path).unwrap(), b"new primary bytes");
}
}
}
#[test]
@@ -2366,10 +2329,9 @@ E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855 *codewhale-win
assert_eq!(content, "fresh binary");
}
/// Mocked GitHub release payload covering both the dispatcher (`codewhale`)
/// and the legacy TUI (`codewhale-tui`) binaries across our published
/// platform/arch matrix, plus a checksum sibling that must never be picked
/// as the primary binary.
/// Mocked GitHub release payload covering the sole implementation binary
/// across the published platform/arch matrix, plus a checksum sibling that
/// must never be picked as the binary.
fn mocked_release() -> Release {
let json = r#"{
"tag_name": "v0.8.8",
@@ -2379,12 +2341,7 @@ E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855 *codewhale-win
{ "name": "codewhale-macos-arm64", "browser_download_url": "https://example.invalid/codewhale-macos-arm64" },
{ "name": "codewhale-windows-x64.exe", "browser_download_url": "https://example.invalid/codewhale-windows-x64.exe" },
{ "name": "codewhale-windows-x64.exe.sha256", "browser_download_url": "https://example.invalid/codewhale-windows-x64.exe.sha256" },
{ "name": "codewhale-windows-arm64.exe", "browser_download_url": "https://example.invalid/codewhale-windows-arm64.exe" },
{ "name": "codewhale-tui-linux-x64", "browser_download_url": "https://example.invalid/codewhale-tui-linux-x64" },
{ "name": "codewhale-tui-macos-x64", "browser_download_url": "https://example.invalid/codewhale-tui-macos-x64" },
{ "name": "codewhale-tui-macos-arm64", "browser_download_url": "https://example.invalid/codewhale-tui-macos-arm64" },
{ "name": "codewhale-tui-windows-x64.exe","browser_download_url": "https://example.invalid/codewhale-tui-windows-x64.exe" },
{ "name": "codewhale-tui-windows-arm64.exe","browser_download_url": "https://example.invalid/codewhale-tui-windows-arm64.exe" }
{ "name": "codewhale-windows-arm64.exe", "browser_download_url": "https://example.invalid/codewhale-windows-arm64.exe" }
]
}"#;
serde_json::from_str(json).expect("mock release JSON")
@@ -2410,40 +2367,38 @@ E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855 *codewhale-win
}
#[test]
fn mocked_release_selects_tui_asset_when_tui_binary_invokes_update() {
fn mocked_release_selects_primary_asset_when_compatibility_alias_invokes_update() {
let release = mocked_release();
let stem = release_asset_stem_for(
Path::new("/usr/local/bin/codewhale-tui"),
"macos",
"aarch64",
);
let asset = select_platform_asset(&release, &stem).expect("TUI platform asset");
assert_eq!(asset.name, "codewhale-tui-macos-arm64");
let asset = select_platform_asset(&release, &stem).expect("primary platform asset");
assert_eq!(asset.name, "codewhale-macos-arm64");
let windows_stem =
release_asset_stem_for(Path::new("C:\\codewhale-tui.exe"), "windows", "aarch64");
let windows_stem = release_asset_stem_for(Path::new("C:\\codew.exe"), "windows", "aarch64");
let windows_asset =
select_platform_asset(&release, &windows_stem).expect("Windows ARM64 TUI asset");
assert_eq!(windows_asset.name, "codewhale-tui-windows-arm64.exe");
select_platform_asset(&release, &windows_stem).expect("Windows ARM64 primary asset");
assert_eq!(windows_asset.name, "codewhale-windows-arm64.exe");
}
#[test]
fn android_arm64_maps_to_android_release_assets() {
// The generic format!("{prefix}-{os}-{arch}") path naturally produces
// Android asset stems. Verify the full stem for both dispatcher and TUI
// binaries so `codewhale update` on Termux requests Android assets, not
// linux-arm64 (#4241).
// Android asset stems. Verify every supported command name resolves to
// the primary Android asset, never Linux or a removed TUI asset (#4241).
assert_eq!(
release_asset_stem_for_prefix("codewhale", "android", "aarch64"),
"codewhale-android-arm64"
);
assert_eq!(
release_asset_stem_for_prefix("codewhale-tui", "android", "aarch64"),
"codewhale-tui-android-arm64"
release_asset_stem_for(Path::new("codewhale-tui"), "android", "aarch64"),
"codewhale-android-arm64"
);
assert_eq!(
release_asset_stem_for_prefix("codew", "android", "aarch64"),
"codew-android-arm64"
release_asset_stem_for(Path::new("codew"), "android", "aarch64"),
"codewhale-android-arm64"
);
}
@@ -2485,10 +2440,10 @@ E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855 *codewhale-win
dispatcher.browser_download_url,
"https://mirror.example/releases/v0.8.36/codewhale-linux-x64"
);
let tui = select_platform_asset(&release, "codewhale-tui-linux-x64").expect("tui asset");
assert_eq!(
tui.browser_download_url,
"https://mirror.example/releases/v0.8.36/codewhale-tui-linux-x64"
assert_eq!(release.assets.len(), 2);
assert!(
select_platform_asset(&release, "codewhale-tui-linux-x64").is_none(),
"mirror fallback must not synthesize a removed TUI asset"
);
}
@@ -2506,10 +2461,7 @@ E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855 *codewhale-win
select_platform_asset(&release, "codewhale-windows-x64")
.is_some_and(|asset| asset.name == "codewhale-windows-x64.exe")
);
assert!(
select_platform_asset(&release, "codewhale-tui-windows-x64")
.is_some_and(|asset| asset.name == "codewhale-tui-windows-x64.exe")
);
assert!(select_platform_asset(&release, "codewhale-tui-windows-x64").is_none());
let arm_release = release_from_mirror_base_url(
"https://mirror.example/releases/v0.9.1",
@@ -2549,11 +2501,8 @@ E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855 *codewhale-win
dispatcher.browser_download_url,
"https://github.com/Hmbown/CodeWhale/releases/download/v0.8.61/codewhale-macos-arm64"
);
let tui = select_platform_asset(&release, "codewhale-tui-macos-arm64").expect("tui asset");
assert_eq!(
tui.browser_download_url,
"https://github.com/Hmbown/CodeWhale/releases/download/v0.8.61/codewhale-tui-macos-arm64"
);
assert_eq!(release.assets.len(), 2);
assert!(select_platform_asset(&release, "codewhale-tui-macos-arm64").is_none());
}
#[test]
@@ -2590,11 +2539,11 @@ E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855 *codewhale-win
fn cnb_release_base_url_includes_tag_directory() {
assert_eq!(
codewhale_release::cnb_release_base_url("0.8.47"),
"https://cnb.cool/Hmbown/CodeWhale/-/releases/v0.8.47"
"https://cnb.cool/codewhale.net/codewhale/-/releases/download/v0.8.47"
);
assert_eq!(
codewhale_release::cnb_release_base_url("v0.8.47"),
"https://cnb.cool/Hmbown/CodeWhale/-/releases/v0.8.47"
"https://cnb.cool/codewhale.net/codewhale/-/releases/download/v0.8.47"
);
}
@@ -2668,7 +2617,7 @@ E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855 *codewhale-win
"{hint}"
);
assert!(hint.contains("codewhale-cli"), "{hint}");
assert!(hint.contains("codewhale-tui --locked"), "{hint}");
assert!(!hint.contains("codewhale-tui --locked"), "{hint}");
}
fn serve_http_responses(
@@ -1,9 +1,15 @@
//! The facade must not migrate secrets before it delegates static diagnostics.
//! Diagnostic dispatch (`doctor`, `setup --status`) runs the real in-process
//! TUI entry via `run_tui_in_process` — the single `codewhale` binary calls
//! `codewhale_tui::run` directly, so there is no sibling TUI binary to delegate
//! to anymore (#5259 single-binary argv0 dispatch). These invariants stay: the
//! dispatcher must not migrate legacy secrets, must not rewrite legacy
//! settings, and must not create any state under a sealed HOME when running a
//! read-only diagnostic. `doctor --context-json` must still emit a
//! machine-readable context source map (`{"entries":[...]}`).
#![cfg(unix)]
use std::fs;
use std::os::unix::fs::PermissionsExt;
use std::path::{Path, PathBuf};
use std::process::Command;
@@ -11,20 +17,14 @@ use codewhale_secrets::{FileKeyringStore, KeyringStore};
use tempfile::TempDir;
#[test]
fn dispatcher_diagnostics_leave_legacy_secret_state_unchanged() {
for (args, expected_tui_args, expects_json) in [
(&["doctor"][..], &["doctor"][..], false),
(&["doctor", "--json"][..], &["doctor", "--json"][..], false),
(
&["doctor", "--context-json"][..],
&["doctor", "--context-json"][..],
true,
),
(
&["setup", "--status"][..],
&["setup", "--status"][..],
false,
),
fn dispatcher_diagnostics_are_in_process_and_read_only() {
// (cli args, whether stdout must be a JSON object carrying an `entries`
// array). Only `doctor --context-json` carries the context source map.
for (args, expects_entries_json) in [
(&["doctor"][..], false),
(&["doctor", "--json"][..], false),
(&["doctor", "--context-json"][..], true),
(&["setup", "--status"][..], false),
] {
let fixture = TempDir::new().expect("fixture root");
let sealed_home = fixture.path().join("sealed-home");
@@ -43,19 +43,10 @@ fn dispatcher_diagnostics_leave_legacy_secret_state_unchanged() {
let before_paths = relative_paths(&sealed_home);
let before_legacy = fs::read(&legacy).expect("read synthetic legacy store");
let receipt = fixture.path().join("delegated-args.txt");
let fake_tui = fixture.path().join("fake-codewhale-tui");
fs::write(
&fake_tui,
"#!/bin/sh\nprintf '%s\\n' \"$@\" > \"$DIAGNOSTIC_DISPATCH_RECEIPT\"\nif [ \"$1\" = doctor ] && [ \"$2\" = --context-json ]; then\n printf '%s\\n' '{\"entries\":[]}'\nfi\n",
)
.expect("write fake TUI");
let mut permissions = fs::metadata(&fake_tui)
.expect("fake TUI metadata")
.permissions();
permissions.set_mode(0o700);
fs::set_permissions(&fake_tui, permissions).expect("make fake TUI executable");
// The diagnostic runs entirely in-process: the single `codewhale` binary
// dispatches through `run_tui_in_process` -> `codewhale_tui::run`. No
// `DEEPSEEK_TUI_BIN` sibling is spawned, so there is no receipt to read;
// assert the in-process behavior and the read-only invariants instead.
let output = Command::new(codewhale_binary())
.args(args)
.env_clear()
@@ -63,8 +54,6 @@ fn dispatcher_diagnostics_leave_legacy_secret_state_unchanged() {
.env("USERPROFILE", &sealed_home)
.env("CODEWHALE_HOME", &codewhale_home)
.env("CODEWHALE_SECRET_BACKEND", "file")
.env("DEEPSEEK_TUI_BIN", &fake_tui)
.env("DIAGNOSTIC_DISPATCH_RECEIPT", &receipt)
.output()
.expect("run dispatcher diagnostic");
@@ -74,29 +63,23 @@ fn dispatcher_diagnostics_leave_legacy_secret_state_unchanged() {
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
assert_eq!(
fs::read_to_string(&receipt)
.expect("fake TUI receipt")
.lines()
.collect::<Vec<_>>(),
expected_tui_args,
"dispatcher must preserve the diagnostic command shape"
);
if expects_json {
if expects_entries_json {
let report: serde_json::Value = serde_json::from_slice(&output.stdout)
.unwrap_or_else(|error| {
panic!(
"facade {args:?} must preserve machine-readable output: {error}\nstdout:\n{}\nstderr:\n{}",
"doctor --context-json must emit a machine-readable context source map: {error}\nstdout:\n{}\nstderr:\n{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
)
});
assert!(
report["entries"].is_array(),
"facade {args:?} must preserve the context source map\nstdout:\n{}",
"doctor --context-json must carry an `entries` array\nstdout:\n{}",
String::from_utf8_lossy(&output.stdout)
);
}
assert_eq!(
relative_paths(&sealed_home),
before_paths,
@@ -1,18 +1,15 @@
//! The kill switch has to reach the process that would emit.
//! The kill switch has to reach the in-process runtime that would emit.
//!
//! The dispatcher itself almost never emits: every interactive and headless
//! session is delegated to the sibling `codewhale-tui` binary, which re-resolves
//! telemetry from *its own* environment and config file. So the switch is only
//! real if the resolved value — not the raw flag — is in that child's
//! environment. This drives the real `codewhale` binary and reads what the
//! child actually received.
//! The single `codewhale` binary resolves dispatcher overrides, states the
//! telemetry floor in its environment, and then calls `codewhale_tui::run`.
//! That runtime re-resolves telemetry before it can arm. These tests drive the
//! real binary through the keyless `features list` command and use the local
//! dry-run sink as the end-to-end observable: an enabled positive control must
//! write session events, while a kill switch must create no telemetry state.
#![cfg(unix)]
use std::collections::BTreeMap;
use std::fs;
use std::os::unix::fs::PermissionsExt;
use std::path::PathBuf;
use std::process::Command;
use codewhale_config::{SetupState, TELEMETRY_NOTICE_VERSION};
@@ -20,51 +17,45 @@ use tempfile::TempDir;
/// `CODEWHALE_TELEMETRY=0` beats `--telemetry true`, end to end.
///
/// The notice decision is recorded on this home on purpose: without it the run
/// would be off for want of consent, and the test would pass without the kill
/// switch ever being consulted.
/// The positive control proves the runtime is enabled before the kill switch is
/// applied, so the zero-state assertion cannot pass vacuously.
#[test]
fn env_off_beats_cli_on_end_to_end() {
// Positive control first: the flag does reach the child, so the assertion
// below is about the floor and not about a flag that goes nowhere.
let on = dispatch_and_read_child_env(&["--telemetry", "true", "exec", "hi"], None);
assert_eq!(
on.get("CODEWHALE_TELEMETRY").map(String::as_str),
Some("true"),
"`--telemetry true` must reach the delegated child at all"
// Positive control first: the flag reaches the in-process runtime and
// arms its dry-run sink, so the assertion below is about the floor and not
// about a command that never crossed the dispatch boundary.
let on = dispatch_and_read_telemetry(None);
let dry_run = on
.dry_run
.expect("`--telemetry true` must write the dry-run sink");
assert!(
dry_run.contains("\"event\":\"session_start\"")
&& dry_run.contains("\"event\":\"session_end\""),
"the real in-process runtime must record a complete session: {dry_run}"
);
let off = dispatch_and_read_child_env(&["--telemetry", "true", "exec", "hi"], Some("0"));
assert_eq!(
off.get("CODEWHALE_TELEMETRY").map(String::as_str),
Some("false"),
"`CODEWHALE_TELEMETRY=0` must beat `--telemetry true` in the child's environment"
);
assert_eq!(
off.get("DEEPSEEK_TELEMETRY").map(String::as_str),
Some("false"),
"the legacy alias must carry the same resolved value"
let off = dispatch_and_read_telemetry(Some("0"));
assert!(
!off.telemetry_dir_exists && off.dry_run.is_none(),
"`CODEWHALE_TELEMETRY=0` must beat `--telemetry true` before the runtime arms"
);
}
/// A value the resolver cannot parse resolves to off, rather than falling
/// through to the flag.
#[test]
fn an_unparseable_telemetry_env_value_reaches_the_child_as_off() {
let child = dispatch_and_read_child_env(&["--telemetry", "true", "exec", "hi"], Some("maybe"));
assert_eq!(
child.get("CODEWHALE_TELEMETRY").map(String::as_str),
Some("false"),
"a typo in the kill switch must never resolve to on"
fn an_unparseable_telemetry_env_value_keeps_the_in_process_runtime_off() {
let evidence = dispatch_and_read_telemetry(Some("maybe"));
assert!(
!evidence.telemetry_dir_exists && evidence.dry_run.is_none(),
"a typo in the kill switch must never arm the in-process runtime"
);
}
/// Run the real dispatcher against a fake sibling TUI that dumps its
/// environment, and return that environment.
fn dispatch_and_read_child_env(
args: &[&str],
telemetry_env: Option<&str>,
) -> BTreeMap<String, String> {
/// Re-enabling through the documented settings command must clear a decline
/// recorded by the former opt-in notice as well as the config-file floor.
#[test]
fn config_set_true_reenables_a_historical_decline() {
let fixture = TempDir::new().expect("fixture root");
let home = fixture.path().join("home");
let codewhale_home = fixture.path().join("codewhale-home");
@@ -73,28 +64,73 @@ fn dispatch_and_read_child_env(
fs::create_dir_all(dir).expect("create fixture dir");
}
// A recorded acceptance, so the run is not off for want of consent.
let mut state = SetupState::default();
state.record_telemetry_notice(TELEMETRY_NOTICE_VERSION, true);
state.record_telemetry_notice("1", false);
let state_path = codewhale_home.join("setup_state.json");
state
.save_to(&codewhale_home.join("setup_state.json"))
.expect("write setup state");
.save_to(&state_path)
.expect("write historical decline");
let config_path = fixture.path().join("config.toml");
fs::write(&config_path, "telemetry = true\n").expect("write config");
fs::write(&config_path, "telemetry = false\n").expect("write config");
let receipt = fixture.path().join("child-env.txt");
let fake_tui = fixture.path().join("fake-codewhale-tui");
let output = Command::new(codewhale_binary())
.current_dir(&workspace)
.env_clear()
.env("PATH", std::env::var_os("PATH").expect("PATH"))
.env("HOME", &home)
.env("USERPROFILE", &home)
.env("CODEWHALE_HOME", &codewhale_home)
.env("CODEWHALE_SECRET_BACKEND", "file")
.args([
"--config",
config_path.to_str().expect("config path"),
"config",
"set",
"telemetry",
"true",
])
.output()
.expect("run config set");
assert!(
output.status.success(),
"config set failed\nstdout:\n{}\nstderr:\n{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
assert!(
fs::read_to_string(&config_path)
.expect("read config")
.contains("telemetry = true")
);
let state = SetupState::load_from(&state_path).expect("read setup state");
assert!(state.telemetry_accepted(TELEMETRY_NOTICE_VERSION));
assert!(!state.telemetry_opted_out());
}
struct DispatchEvidence {
telemetry_dir_exists: bool,
dry_run: Option<String>,
}
/// Run the real dispatcher into a keyless in-process command and report the
/// telemetry state it actually left behind.
fn dispatch_and_read_telemetry(telemetry_env: Option<&str>) -> DispatchEvidence {
let fixture = TempDir::new().expect("fixture root");
let home = fixture.path().join("home");
let codewhale_home = fixture.path().join("codewhale-home");
let workspace = fixture.path().join("workspace");
for dir in [&home, &codewhale_home, &workspace] {
fs::create_dir_all(dir).expect("create fixture dir");
}
let config_path = fixture.path().join("config.toml");
fs::write(
&fake_tui,
format!("#!/bin/sh\nenv > '{}'\n", receipt.display()),
&config_path,
// An explicitly empty endpoint is the network-free dry-run sink.
"telemetry = true\ntelemetry_endpoint = \"\"\n",
)
.expect("write fake TUI");
let mut permissions = fs::metadata(&fake_tui)
.expect("fake TUI metadata")
.permissions();
permissions.set_mode(0o700);
fs::set_permissions(&fake_tui, permissions).expect("make fake TUI executable");
.expect("write config");
let mut command = Command::new(codewhale_binary());
command
@@ -105,56 +141,42 @@ fn dispatch_and_read_child_env(
.env("USERPROFILE", &home)
.env("CODEWHALE_HOME", &codewhale_home)
.env("CODEWHALE_SECRET_BACKEND", "file")
.env("CODEWHALE_TUI_BIN", &fake_tui)
.env(
"CODEWHALE_RELEASE_BASE_URL",
"https://example.invalid/releases",
)
.arg("--config")
.arg(&config_path)
.args(args);
.args(["--telemetry", "true", "features", "list"]);
if let Some(value) = telemetry_env {
command.env("CODEWHALE_TELEMETRY", value);
}
let output = command.output().expect("run codewhale dispatcher");
let dumped = fs::read_to_string(&receipt).unwrap_or_else(|error| {
panic!(
"the delegated child must have run and dumped its environment: {error}\n\
stdout:\n{}\nstderr:\n{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
)
});
// A dispatcher run that never emits must also never create telemetry state
// in the operator's home.
assert!(
!codewhale_home.join("telemetry").exists(),
"the dispatcher must not create telemetry state for a delegated command"
output.status.success(),
"the in-process feature command must succeed\nstdout:\n{}\nstderr:\n{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
assert!(
String::from_utf8_lossy(&output.stdout).contains("feature\tstage\tenabled"),
"the real in-process feature command must have run\nstdout:\n{}\nstderr:\n{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
dumped
.lines()
.filter_map(|line| {
line.split_once('=')
.map(|(key, value)| (key.to_string(), value.to_string()))
})
.collect()
let telemetry_dir = codewhale_home.join("telemetry");
let dry_run = match fs::read_to_string(telemetry_dir.join("dryrun.jsonl")) {
Ok(contents) => Some(contents),
Err(error) if error.kind() == std::io::ErrorKind::NotFound => None,
Err(error) => panic!("read telemetry dry-run sink: {error}"),
};
DispatchEvidence {
telemetry_dir_exists: telemetry_dir.exists(),
dry_run,
}
}
fn codewhale_binary() -> PathBuf {
if let Some(path) = option_env!("CARGO_BIN_EXE_codewhale") {
return PathBuf::from(path);
}
if let Ok(path) = std::env::var("CARGO_BIN_EXE_codewhale") {
return PathBuf::from(path);
}
let mut path = std::env::current_exe().expect("current test executable path");
path.pop();
if path.ends_with("deps") {
path.pop();
}
path.push(format!("codewhale{}", std::env::consts::EXE_SUFFIX));
path
fn codewhale_binary() -> &'static str {
env!("CARGO_BIN_EXE_codewhale")
}
+3 -3
View File
@@ -9,9 +9,9 @@ description = "Config schema and precedence model for Codewhale"
[dependencies]
anyhow.workspace = true
codewhale-execpolicy = { path = "../execpolicy", version = "0.9.4" }
codewhale-paths = { path = "../paths", version = "0.9.4" }
codewhale-secrets = { path = "../secrets", version = "0.9.4" }
codewhale-execpolicy = { path = "../execpolicy", version = "0.9.6" }
codewhale-paths = { path = "../paths", version = "0.9.6" }
codewhale-secrets = { path = "../secrets", version = "0.9.6" }
fd-lock = "4.0.4"
libc = "0.2"
serde.workspace = true
+364
View File
@@ -0,0 +1,364 @@
//! Legacy DeepSeek-scoped prompt complexity classifier.
//!
//! This pure scorer is retained for API compatibility, but the consolidated
//! CLI dispatcher must not use it to resolve provider-neutral `model = "auto"`:
//! doing so fabricates DeepSeek model ids for every active provider. The TUI's
//! provider-aware router owns runtime auto selection. Callers may use this
//! helper only when their candidate pair is explicitly the DeepSeek pair:
//!
//! - **`deepseek-v4-pro`** — complex tasks (debugging, refactoring, design,
//! security review, multi-file changes, code generation, …).
//! - **`deepseek-v4-flash`** — simple tasks (lookups, formatting, small edits,
//! translation, Q&A, …).
//!
//! This is a pure rule-based classifier. It lives in the config crate because
//! the resolved model name is a config-level concern; the route resolver never
//! sees the `"auto"` sentinel or the prompt text.
/// The resolved model name for the pro tier.
pub const PRO_MODEL: &str = "deepseek-v4-pro";
/// The resolved model name for the flash tier.
pub const FLASH_MODEL: &str = "deepseek-v4-flash";
/// The threshold score above which a task is classified as complex (pro).
/// Score ≥ 2 → pro, else → flash.
const PRO_THRESHOLD: i32 = 2;
/// Strong indicators of a complex task. Each match adds +3.
const COMPLEX_STRONG: &[&str] = &[
// Debugging & fixing
"debug",
"bug",
"fix",
"error",
"crash",
"异常",
"错误",
"调试",
"故障",
"排查",
"root cause",
// Architecture & design
"refactor",
"重构",
"architecture",
"架构",
"design pattern",
"系统设计",
"高并发",
"分布式",
"microservice",
// Security
"security",
"安全",
"vulnerability",
"漏洞",
"渗透",
"exploit",
// Code generation
"implement",
"实现",
"generate",
"生成",
"create",
"创建",
"build",
"构建",
"开发",
"prototype",
// Complex analysis
"analyze",
"分析",
"review",
"审查",
"audit",
"审计",
"optimize",
"优化",
"migrate",
"迁移",
// Multi-file / large scale
"multi-file",
"multiple files",
"多个文件",
"整个项目",
"full project",
"重构整个",
"large scale",
// Testing
"unit test",
"integration test",
"e2e test",
"测试用例",
"test suite",
"coverage",
// Complex logic
"algorithm",
"算法",
"状态机",
"state machine",
"concurrent",
"并行",
"异步",
"async",
// Documentation / PRD
"architecture document",
"设计文档",
"技术方案",
"prd",
];
/// Medium-strength indicators. Each match adds +1.
const COMPLEX_MEDIUM: &[&str] = &[
"change",
"修改",
"update",
"更新",
"add",
"添加",
"新增",
"feature",
"功能",
"improve",
"改进",
"enhance",
"config",
"配置",
"setup",
"设置",
"deploy",
"部署",
"ci/cd",
"pipeline",
"script",
"脚本",
"tool",
"工具",
"api",
"interface",
"接口",
"endpoint",
"database",
"数据库",
"schema",
"query",
"document",
"文档",
"readme",
];
/// Simple-task indicators. Each match subtracts -1.
const SIMPLE: &[&str] = &[
"find",
"查找",
"search",
"搜索",
"look up",
"查询",
"what is",
"什么是",
"explain",
"解释",
"tell me",
"告诉我",
"how to",
"如何",
"format",
"格式化",
"pretty",
"list",
"列出",
"show",
"显示",
"print",
"rename",
"重命名",
"move",
"移动",
"copy",
"复制",
"delete",
"删除",
"remove",
"typo",
"拼写",
"spelling",
"grammar",
"quick",
"快速",
"simple",
"简单",
"hello world",
"demo",
"example",
"示例",
"translate",
"翻译",
"convert",
"转换",
"short",
"简短",
"brief",
"简要",
];
/// Classify a prompt for the legacy DeepSeek candidate pair.
///
/// Uses a simple scoring system:
/// - Strong complex keyword: +3
/// - Medium complex keyword: +1
/// - Simple keyword: -1
/// - Prompt length > 500 chars: +2, > 200 chars: +1
/// - Contains code fence or backtick: +1
/// - Contains a file path: +1
/// - Multi-line (> 5 newlines): +1
///
/// Total ≥ 2 → `PRO_MODEL`, else → `FLASH_MODEL`.
#[must_use]
pub fn classify(prompt: &str) -> &'static str {
if score(prompt) >= PRO_THRESHOLD {
PRO_MODEL
} else {
FLASH_MODEL
}
}
/// Compute the raw complexity score for a prompt.
#[must_use]
pub fn score(prompt: &str) -> i32 {
let lower = prompt.to_ascii_lowercase();
let mut score = 0i32;
// Strong complex keywords: +3 (first match only to avoid overcounting)
if COMPLEX_STRONG.iter().any(|kw| lower.contains(kw)) {
score += 3;
}
// Medium complex keywords: +1 each
for kw in COMPLEX_MEDIUM {
if lower.contains(kw) {
score += 1;
}
}
// Simple keywords: -1 each
for kw in SIMPLE {
if lower.contains(kw) {
score -= 1;
}
}
// Length factor: long prompts tend to be more complex
let len = prompt.len();
if len > 500 {
score += 2;
} else if len > 200 {
score += 1;
}
// Code fence or backtick: actual coding task
if prompt.contains("```") || prompt.contains('`') {
score += 1;
}
// File path pattern: e.g. /path/to/file.rs or C:\path
// Simple heuristic: path-like sequences contain / or \ and .
if (prompt.contains('/') || prompt.contains('\\')) && prompt.contains('.') {
score += 1;
}
// Multi-line: more lines = more context
if prompt.chars().filter(|&c| c == '\n').count() > 5 {
score += 1;
}
score
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_debug_task_uses_pro() {
assert_eq!(classify("帮我调试这个bug,程序崩溃了"), PRO_MODEL);
}
#[test]
fn test_refactor_task_uses_pro() {
assert_eq!(
classify("refactor the user module with a new architecture"),
PRO_MODEL
);
}
#[test]
fn test_security_review_uses_pro() {
assert_eq!(
classify("review this code for security vulnerabilities"),
PRO_MODEL
);
}
#[test]
fn test_simple_lookup_uses_flash() {
assert_eq!(classify("查找昨天的日志文件"), FLASH_MODEL);
}
#[test]
fn test_translation_uses_flash() {
assert_eq!(classify("translate this to Chinese"), FLASH_MODEL);
}
#[test]
fn test_formatting_uses_flash() {
assert_eq!(classify("format this code"), FLASH_MODEL);
}
#[test]
fn test_long_prompt_gets_bonus() {
let long = "a".repeat(300);
// No keywords, long prompt gives +1, total = 1 < 2 → flash
assert_eq!(classify(&long), FLASH_MODEL);
}
#[test]
fn test_very_long_prompt_gets_more_bonus() {
let long = "a".repeat(600);
// No keywords, very long prompt gives +2, total = 2 → pro
assert_eq!(classify(&long), PRO_MODEL);
}
#[test]
fn test_code_block_gets_bonus() {
// Code block without keywords, +1, total = 1 < 2 → flash
assert_eq!(classify("```\nhello\n```"), FLASH_MODEL);
}
#[test]
fn test_mixed_keywords_pro_wins() {
// "refactor" is strong (+3), "explain" is simple (-1), total = 2 → pro
assert_eq!(classify("refactor and explain the code"), PRO_MODEL);
}
#[test]
fn test_implement_task_uses_pro() {
assert_eq!(
classify("implement a new feature for the user module"),
PRO_MODEL
);
}
#[test]
fn test_quick_question_uses_flash() {
assert_eq!(classify("what is the capital of France?"), FLASH_MODEL);
}
#[test]
fn test_score_never_negative() {
// Even for very simple queries, score should be predictable
let s = score("hello world");
assert!(s >= -10); // sanity check
}
}
+42 -17
View File
@@ -1,4 +1,5 @@
pub mod auth_source;
pub mod auto_model;
pub mod catalog;
mod config_document;
pub mod external_credentials;
@@ -399,6 +400,16 @@ pub struct ProvidersToml {
alias = "grok"
)]
pub xai: ProviderConfigToml,
#[serde(
default,
skip_serializing_if = "ProviderConfigToml::is_empty",
alias = "mistral-ai",
alias = "mistral_ai",
alias = "mistralai",
alias = "la-plateforme",
alias = "la_plateforme"
)]
pub mistral: ProviderConfigToml,
/// Jiangsu Telecom TokenHub — OpenAI-compatible AI gateway.
#[serde(
default,
@@ -626,6 +637,7 @@ impl ProvidersToml {
ProviderKind::OpencodeZen => &self.opencode_zen,
ProviderKind::Meta => &self.meta,
ProviderKind::Xai => &self.xai,
ProviderKind::Mistral => &self.mistral,
ProviderKind::Telecomjs => &self.telecomjs,
ProviderKind::ModelstudioTokenPlan => &self.modelstudio_token_plan,
ProviderKind::ModelstudioTokenPlanAnthropic => &self.modelstudio_token_plan_anthropic,
@@ -672,6 +684,7 @@ impl ProvidersToml {
ProviderKind::OpencodeZen => &mut self.opencode_zen,
ProviderKind::Meta => &mut self.meta,
ProviderKind::Xai => &mut self.xai,
ProviderKind::Mistral => &mut self.mistral,
ProviderKind::Telecomjs => &mut self.telecomjs,
ProviderKind::ModelstudioTokenPlan => &mut self.modelstudio_token_plan,
ProviderKind::ModelstudioTokenPlanAnthropic => {
@@ -730,8 +743,8 @@ pub struct ConfigToml {
/// [`DEFAULT_TELEMETRY_ENDPOINT`] — not "send nowhere". Setting it to the
/// empty string is the way to say send nowhere: that resolves to no
/// endpoint, which appends batches to `dryrun.jsonl` and constructs no HTTP
/// client. Either way nothing is sent until telemetry is enabled *and* the
/// first-run notice has been answered with Enable.
/// client. Either way a persistent or run-scoped opt-out still prevents any
/// batch from being constructed.
///
/// Kept as a scalar sibling of `telemetry` rather than folded into a
/// `[telemetry]` table. `telemetry` is already a scalar and every section
@@ -1863,6 +1876,7 @@ impl<'de> Deserialize<'de> for FleetRole {
pub enum FleetSlot {
Manager,
Scout,
Planner,
Implementer,
Reviewer,
Verifier,
@@ -1879,6 +1893,7 @@ impl FleetSlot {
match self {
Self::Manager => "manager",
Self::Scout => "scout",
Self::Planner => "planner",
Self::Implementer => "implementer",
Self::Reviewer => "reviewer",
Self::Verifier => "verifier",
@@ -1894,6 +1909,7 @@ impl FleetSlot {
match value.trim() {
"manager" | "coordinator" => Self::Manager,
"scout" | "research" | "research-worker" => Self::Scout,
"planner" | "plan" | "awaiter" => Self::Planner,
"implementer" | "builder" => Self::Implementer,
"reviewer" => Self::Reviewer,
"verifier" | "tester" => Self::Verifier,
@@ -3187,6 +3203,7 @@ impl ConfigToml {
ProviderKind::OpencodeZen => DEFAULT_OPENCODE_ZEN_BASE_URL.to_string(),
ProviderKind::Meta => DEFAULT_META_BASE_URL.to_string(),
ProviderKind::Xai => DEFAULT_XAI_BASE_URL.to_string(),
ProviderKind::Mistral => DEFAULT_MISTRAL_BASE_URL.to_string(),
ProviderKind::Telecomjs => DEFAULT_TELECOMJS_BASE_URL.to_string(),
ProviderKind::ModelstudioTokenPlan
| ProviderKind::ModelstudioTokenPlanAnthropic
@@ -3341,7 +3358,7 @@ impl ConfigToml {
.telemetry
.or(env.telemetry)
.or(self.telemetry)
.unwrap_or(false);
.unwrap_or(true);
// `telemetry = false` written to the config file is the off switch the
// first-run notice and `docs/TELEMETRY.md` both advertise as the
// *persistent* one, so it is a floor and not merely the last term of a
@@ -3377,10 +3394,9 @@ impl ConfigToml {
// missing value — it is the local dry-run sink, and it stays reachable
// by resolving to `None` instead of falling through to the default.
//
// None of this is a consent decision. A session only reaches an
// endpoint after `telemetry` above resolved true, which requires the
// first-run notice to have been answered with Enable; the kill switches
// are all upstream of this line.
// None of this changes the user's opt-out. A session only reaches an
// endpoint after `telemetry` above resolved true; every persistent and
// run-scoped kill switch is upstream of this line.
let telemetry_endpoint = match env
.telemetry_endpoint
.clone()
@@ -3443,11 +3459,9 @@ fn merge_project_provider_config(target: &mut ProviderConfigToml, source: &Provi
/// Analytics Engine and stores nothing else. See `docs/TELEMETRY.md` for what a
/// batch contains and `telemetry-ingest/` for the handler.
///
/// This is a *default*, not a floor, and it changes nothing about consent: it is
/// only ever consulted for a session that is already enabled, which requires the
/// first-run notice to have been answered with Enable. `CODEWHALE_TELEMETRY=0`,
/// `telemetry = false`, and a recorded decline all stop the session long before
/// an endpoint is read.
/// This is a *default*, not a floor, and it changes nothing about permission:
/// `CODEWHALE_TELEMETRY=0`, `telemetry = false`, and a recorded decline all
/// stop the session long before an endpoint is read.
///
/// An explicit value — `CODEWHALE_TELEMETRY_ENDPOINT` or `telemetry_endpoint` in
/// the config file — wins outright, and an explicit *empty* value resolves to no
@@ -4247,6 +4261,7 @@ fn default_model_for_provider(provider: ProviderKind) -> &'static str {
ProviderKind::OpencodeZen => DEFAULT_OPENCODE_ZEN_MODEL,
ProviderKind::Meta => DEFAULT_META_MODEL,
ProviderKind::Xai => DEFAULT_XAI_MODEL,
ProviderKind::Mistral => DEFAULT_MISTRAL_MODEL,
ProviderKind::Telecomjs => DEFAULT_TELECOMJS_MODEL,
ProviderKind::ModelstudioTokenPlan
| ProviderKind::ModelstudioTokenPlanAnthropic
@@ -4294,6 +4309,7 @@ fn default_base_url_for_provider(provider: ProviderKind) -> &'static str {
ProviderKind::OpencodeZen => DEFAULT_OPENCODE_ZEN_BASE_URL,
ProviderKind::Meta => DEFAULT_META_BASE_URL,
ProviderKind::Xai => DEFAULT_XAI_BASE_URL,
ProviderKind::Mistral => DEFAULT_MISTRAL_BASE_URL,
ProviderKind::Telecomjs => DEFAULT_TELECOMJS_BASE_URL,
ProviderKind::ModelstudioTokenPlan => DEFAULT_MODELSTUDIO_TOKEN_PLAN_BASE_URL,
ProviderKind::ModelstudioTokenPlanAnthropic => MODELSTUDIO_TOKEN_PLAN_ANTHROPIC_BASE_URL,
@@ -4810,11 +4826,10 @@ pub struct ResolvedRuntimeOptions {
/// A human wrote `telemetry = false` into the config file.
///
/// This is the *persistent* opt-out, and it is deliberately narrower than
/// "telemetry resolved to false". `false` is also the value when nobody has
/// said anything at all, and it is what the dispatcher forwards to the TUI
/// on every ordinary run; a consumer that reads those as a revocation would
/// destroy the identity and buffered events of a consenting user who merely
/// set `CODEWHALE_TELEMETRY=0` for one command. Run-scoped kill switches
/// "telemetry resolved to false". A run-scoped kill switch also resolves
/// false; treating that as a revocation would destroy the identity and
/// buffered events of a user who merely set `CODEWHALE_TELEMETRY=0` for one
/// command. Run-scoped kill switches
/// (`--telemetry false`, the environment variable) stop the run and leave
/// every byte on disk alone; only this flag authorizes the wipe.
pub telemetry_explicit_off: bool,
@@ -6550,6 +6565,8 @@ struct EnvRuntimeOverrides {
meta_model: Option<String>,
xai_base_url: Option<String>,
xai_model: Option<String>,
mistral_base_url: Option<String>,
mistral_model: Option<String>,
telecomjs_base_url: Option<String>,
telecomjs_model: Option<String>,
modelstudio_token_plan_base_url: Option<String>,
@@ -6854,6 +6871,12 @@ impl EnvRuntimeOverrides {
xai_model: std::env::var("XAI_MODEL")
.ok()
.filter(|v| !v.trim().is_empty()),
mistral_base_url: std::env::var("MISTRAL_BASE_URL")
.ok()
.filter(|v| !v.trim().is_empty()),
mistral_model: std::env::var("MISTRAL_MODEL")
.ok()
.filter(|v| !v.trim().is_empty()),
telecomjs_base_url: std::env::var("TELECOMJS_BASE_URL")
.ok()
.filter(|v| !v.trim().is_empty()),
@@ -6954,6 +6977,7 @@ impl EnvRuntimeOverrides {
ProviderKind::OpencodeZen => self.opencode_zen_base_url.clone(),
ProviderKind::Meta => self.meta_base_url.clone(),
ProviderKind::Xai => self.xai_base_url.clone(),
ProviderKind::Mistral => self.mistral_base_url.clone(),
ProviderKind::Telecomjs => self.telecomjs_base_url.clone(),
ProviderKind::ModelstudioTokenPlan | ProviderKind::ModelstudioTokenPlanAnthropic => {
self.modelstudio_token_plan_base_url.clone()
@@ -6996,6 +7020,7 @@ impl EnvRuntimeOverrides {
ProviderKind::OpencodeZen => self.opencode_zen_model.clone(),
ProviderKind::Meta => self.meta_model.clone(),
ProviderKind::Xai => self.xai_model.clone(),
ProviderKind::Mistral => self.mistral_model.clone(),
ProviderKind::Telecomjs => self.telecomjs_model.clone(),
ProviderKind::ModelstudioTokenPlan | ProviderKind::ModelstudioTokenPlanAnthropic => {
self.modelstudio_token_plan_model.clone()
+39 -19
View File
@@ -12,24 +12,25 @@ use super::{
DEFAULT_FIREWORKS_MODEL, DEFAULT_HUGGINGFACE_BASE_URL, DEFAULT_HUGGINGFACE_MODEL,
DEFAULT_LONGCAT_BASE_URL, DEFAULT_LONGCAT_MODEL, DEFAULT_META_BASE_URL, DEFAULT_META_MODEL,
DEFAULT_MINIMAX_ANTHROPIC_BASE_URL, DEFAULT_MINIMAX_BASE_URL, DEFAULT_MINIMAX_MODEL,
DEFAULT_MODELSTUDIO_CODING_PLAN_BASE_URL, DEFAULT_MODELSTUDIO_TOKEN_PLAN_BASE_URL,
DEFAULT_MODELSTUDIO_TOKEN_PLAN_MODEL, DEFAULT_MOONSHOT_BASE_URL, DEFAULT_MOONSHOT_MODEL,
DEFAULT_NOVITA_BASE_URL, DEFAULT_NOVITA_MODEL, DEFAULT_NVIDIA_NIM_BASE_URL,
DEFAULT_NVIDIA_NIM_MODEL, DEFAULT_OLLAMA_BASE_URL, DEFAULT_OLLAMA_MODEL,
DEFAULT_OPENAI_BASE_URL, DEFAULT_OPENAI_CODEX_BASE_URL, DEFAULT_OPENAI_CODEX_MODEL,
DEFAULT_OPENAI_MODEL, DEFAULT_OPENCODE_GO_BASE_URL, DEFAULT_OPENCODE_GO_MODEL,
DEFAULT_OPENCODE_ZEN_BASE_URL, DEFAULT_OPENCODE_ZEN_MODEL, DEFAULT_OPENMODEL_BASE_URL,
DEFAULT_OPENMODEL_MODEL, DEFAULT_OPENROUTER_BASE_URL, DEFAULT_OPENROUTER_MODEL,
DEFAULT_QIANFAN_BASE_URL, DEFAULT_QIANFAN_MODEL, DEFAULT_SAKANA_BASE_URL, DEFAULT_SAKANA_MODEL,
DEFAULT_SGLANG_BASE_URL, DEFAULT_SGLANG_MODEL, DEFAULT_SILICONFLOW_BASE_URL,
DEFAULT_SILICONFLOW_CN_BASE_URL, DEFAULT_SILICONFLOW_MODEL, DEFAULT_STEPFUN_BASE_URL,
DEFAULT_STEPFUN_MODEL, DEFAULT_TELECOMJS_BASE_URL, DEFAULT_TELECOMJS_MODEL,
DEFAULT_TOGETHER_BASE_URL, DEFAULT_TOGETHER_MODEL, DEFAULT_VLLM_BASE_URL, DEFAULT_VLLM_MODEL,
DEFAULT_VOLCENGINE_BASE_URL, DEFAULT_VOLCENGINE_MODEL, DEFAULT_WANJIE_ARK_BASE_URL,
DEFAULT_WANJIE_ARK_MODEL, DEFAULT_XAI_BASE_URL, DEFAULT_XAI_MODEL,
DEFAULT_XIAOMI_MIMO_BASE_URL, DEFAULT_XIAOMI_MIMO_MODEL, DEFAULT_ZAI_BASE_URL,
DEFAULT_ZAI_MODEL, MODELSTUDIO_CODING_PLAN_ANTHROPIC_BASE_URL,
MODELSTUDIO_TOKEN_PLAN_ANTHROPIC_BASE_URL, ProviderKind,
DEFAULT_MISTRAL_BASE_URL, DEFAULT_MISTRAL_MODEL, DEFAULT_MODELSTUDIO_CODING_PLAN_BASE_URL,
DEFAULT_MODELSTUDIO_TOKEN_PLAN_BASE_URL, DEFAULT_MODELSTUDIO_TOKEN_PLAN_MODEL,
DEFAULT_MOONSHOT_BASE_URL, DEFAULT_MOONSHOT_MODEL, DEFAULT_NOVITA_BASE_URL,
DEFAULT_NOVITA_MODEL, DEFAULT_NVIDIA_NIM_BASE_URL, DEFAULT_NVIDIA_NIM_MODEL,
DEFAULT_OLLAMA_BASE_URL, DEFAULT_OLLAMA_MODEL, DEFAULT_OPENAI_BASE_URL,
DEFAULT_OPENAI_CODEX_BASE_URL, DEFAULT_OPENAI_CODEX_MODEL, DEFAULT_OPENAI_MODEL,
DEFAULT_OPENCODE_GO_BASE_URL, DEFAULT_OPENCODE_GO_MODEL, DEFAULT_OPENCODE_ZEN_BASE_URL,
DEFAULT_OPENCODE_ZEN_MODEL, DEFAULT_OPENMODEL_BASE_URL, DEFAULT_OPENMODEL_MODEL,
DEFAULT_OPENROUTER_BASE_URL, DEFAULT_OPENROUTER_MODEL, DEFAULT_QIANFAN_BASE_URL,
DEFAULT_QIANFAN_MODEL, DEFAULT_SAKANA_BASE_URL, DEFAULT_SAKANA_MODEL, DEFAULT_SGLANG_BASE_URL,
DEFAULT_SGLANG_MODEL, DEFAULT_SILICONFLOW_BASE_URL, DEFAULT_SILICONFLOW_CN_BASE_URL,
DEFAULT_SILICONFLOW_MODEL, DEFAULT_STEPFUN_BASE_URL, DEFAULT_STEPFUN_MODEL,
DEFAULT_TELECOMJS_BASE_URL, DEFAULT_TELECOMJS_MODEL, DEFAULT_TOGETHER_BASE_URL,
DEFAULT_TOGETHER_MODEL, DEFAULT_VLLM_BASE_URL, DEFAULT_VLLM_MODEL, DEFAULT_VOLCENGINE_BASE_URL,
DEFAULT_VOLCENGINE_MODEL, DEFAULT_WANJIE_ARK_BASE_URL, DEFAULT_WANJIE_ARK_MODEL,
DEFAULT_XAI_BASE_URL, DEFAULT_XAI_MODEL, DEFAULT_XIAOMI_MIMO_BASE_URL,
DEFAULT_XIAOMI_MIMO_MODEL, DEFAULT_ZAI_BASE_URL, DEFAULT_ZAI_MODEL,
MODELSTUDIO_CODING_PLAN_ANTHROPIC_BASE_URL, MODELSTUDIO_TOKEN_PLAN_ANTHROPIC_BASE_URL,
ProviderKind,
};
/// Wire protocol spoken by a provider.
@@ -391,6 +392,12 @@ pub const fn credential_help(kind: ProviderKind) -> CredentialHelp {
docs_url: None,
guidance: "Use an xAI Console API key or Codewhale's native device login. Reading an existing Grok CLI file requires explicit provider-scoped read-only consent.",
},
ProviderKind::Mistral => CredentialHelp {
acquisition: ApiKey,
credential_url: Some("https://console.mistral.ai/api-keys"),
docs_url: Some("https://docs.mistral.ai/"),
guidance: "Create a Mistral API key in the Mistral Console (la Plateforme).",
},
ProviderKind::Telecomjs => CredentialHelp {
acquisition: ApiKey,
credential_url: Some("https://aigw.telecomjs.com/"),
@@ -905,6 +912,17 @@ provider!(
"qianfan",
aliases: ["baidu-qianfan", "baidu_qianfan", "baidu"]
);
provider!(
Mistral,
Mistral,
"mistral",
"Mistral AI",
DEFAULT_MISTRAL_BASE_URL,
DEFAULT_MISTRAL_MODEL,
["MISTRAL_API_KEY"],
"mistral",
aliases: ["mistral-ai", "mistral_ai", "mistralai", "la-plateforme", "la_plateforme"]
);
/// OpenAI Codex / ChatGPT OAuth provider using the Responses API.
pub struct OpenaiCodex;
@@ -1523,6 +1541,7 @@ static OPENCODE_GO: OpencodeGo = OpencodeGo;
static OPENCODE_ZEN: OpencodeZen = OpencodeZen;
static META: Meta = Meta;
static XAI: Xai = Xai;
static MISTRAL: Mistral = Mistral;
static TELECOMJS: Telecomjs = Telecomjs;
static MODELSTUDIO_TOKEN_PLAN: ModelstudioTokenPlan = ModelstudioTokenPlan;
static MODELSTUDIO_TOKEN_PLAN_ANTHROPIC: ModelstudioTokenPlanAnthropic =
@@ -1532,7 +1551,7 @@ static MODELSTUDIO_CODING_PLAN_ANTHROPIC: ModelstudioCodingPlanAnthropic =
ModelstudioCodingPlanAnthropic;
static CUSTOM: Custom = Custom;
static PROVIDER_REGISTRY: [&dyn Provider; 41] = [
static PROVIDER_REGISTRY: [&dyn Provider; 42] = [
&DEEPSEEK,
&DEEPSEEK_ANTHROPIC,
&NVIDIA_NIM,
@@ -1568,6 +1587,7 @@ static PROVIDER_REGISTRY: [&dyn Provider; 41] = [
&OPENCODE_ZEN,
&META,
&XAI,
&MISTRAL,
&TELECOMJS,
&MODELSTUDIO_TOKEN_PLAN,
&MODELSTUDIO_TOKEN_PLAN_ANTHROPIC,
+3
View File
@@ -167,6 +167,9 @@ pub(crate) const DEFAULT_META_BASE_URL: &str = "https://api.meta.ai/v1";
// xAI / Grok API-key route defaults
pub(crate) const DEFAULT_XAI_MODEL: &str = "grok-4.5";
pub(crate) const DEFAULT_XAI_BASE_URL: &str = "https://api.x.ai/v1";
// Mistral AI (la Plateforme) defaults
pub(crate) const DEFAULT_MISTRAL_MODEL: &str = "mistral-code-latest";
pub(crate) const DEFAULT_MISTRAL_BASE_URL: &str = "https://api.mistral.ai/v1";
// TelecomJS (Jiangsu Telecom TokenHub) defaults
pub(crate) const DEFAULT_TELECOMJS_MODEL: &str = "deepseek-v4-pro";
pub(crate) const DEFAULT_TELECOMJS_BASE_URL: &str = "https://aigw.telecomjs.com/v1";
+11 -1
View File
@@ -131,6 +131,15 @@ pub enum ProviderKind {
Meta,
#[serde(alias = "x-ai", alias = "x_ai", alias = "grok")]
Xai,
/// Mistral AI — la Plateforme (OpenAI-compatible Chat Completions).
#[serde(
alias = "mistral-ai",
alias = "mistral_ai",
alias = "mistralai",
alias = "la-plateforme",
alias = "la_plateforme"
)]
Mistral,
/// Jiangsu Telecom TokenHub (OpenAI-compatible).
///
/// An AI gateway operated by Jiangsu Telecom that speaks the OpenAI Chat
@@ -195,7 +204,7 @@ impl ProviderKind {
/// stay on the enum for serde and `provider_for_kind`, but they are not
/// first-class catalog rows. Plan is `mode` / base_url; dialect is
/// `wire = openai|anthropic` on the primary provider config.
pub const ALL: [Self; 36] = [
pub const ALL: [Self; 37] = [
Self::Deepseek,
Self::NvidiaNim,
Self::Openai,
@@ -229,6 +238,7 @@ impl ProviderKind {
Self::OpencodeZen,
Self::Meta,
Self::Xai,
Self::Mistral,
Self::Telecomjs,
Self::ModelstudioTokenPlan,
Self::Custom,
+23 -17
View File
@@ -40,10 +40,11 @@ pub const SETUP_STATE_FILE_NAME: &str = "setup_state.json";
///
/// The notice is owed whenever
/// [`SetupState::telemetry_notice_decided_for`] does not match this string.
/// Bumping it re-asks everyone, so it is bumped only when what would be
/// collected materially changes. Keying it to the app version would re-prompt
/// every release, which is nagging with extra steps.
pub const TELEMETRY_NOTICE_VERSION: &str = "1";
/// Bumping it re-asks prior acceptors and unanswered users, so it is bumped only
/// when the collection policy, schema, or disclosure materially changes. Prior
/// declines remain off. Keying it to the app version would re-prompt every
/// release, which is nagging with extra steps.
pub const TELEMETRY_NOTICE_VERSION: &str = "3";
/// Canonical setup step ids. The ordering matches the first-run spine so a
/// `BTreeMap<SetupStep, _>` renders in wizard order.
@@ -335,8 +336,8 @@ pub struct SetupState {
/// Never auto-completed and never deferred-completed: unlike the
/// constitution checkpoint, which records a `Deferred` completion on the
/// skip-onboarding path, a telemetry notice that was not rendered and
/// answered leaves this `None`. Silence is not consent, so the failure
/// mode of a missing notice is that nothing is ever collected.
/// answered leaves this `None`. Collection follows the documented default
/// while the notice remains owed on the next interactive launch.
///
/// These are *fields* rather than a new [`SetupStep`] variant on purpose:
/// an unknown enum variant fails the whole record parse and silently drops
@@ -344,9 +345,9 @@ pub struct SetupState {
/// checkpoint — while unknown fields are ignored.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub telemetry_notice_decided_for: Option<String>,
/// The user's answer to the notice. Only meaningful when
/// [`Self::telemetry_notice_decided_for`] matches the current
/// [`TELEMETRY_NOTICE_VERSION`].
/// The user's answer to the notice. `false` with any recorded notice
/// version is a durable opt-out; `true` records acknowledgment of that
/// version's disclosure.
#[serde(default, skip_serializing_if = "is_false")]
pub telemetry_opt_in: bool,
}
@@ -505,12 +506,7 @@ impl SetupState {
self
}
/// True when the user was asked the current notice and said yes.
///
/// This is one half of the emit condition; the other is `telemetry = true`
/// in config. Neither alone suffices, which is what makes a stale
/// pre-existing `telemetry = true` — settable and inert for a long time —
/// not consent.
/// True when the user was asked the current notice and kept counting on.
#[must_use]
pub fn telemetry_accepted(&self, version: &str) -> bool {
!self.needs_telemetry_notice(version) && self.telemetry_opt_in
@@ -518,13 +514,23 @@ impl SetupState {
/// True when the user was asked the current notice and said no.
///
/// Distinct from "never asked": only a recorded decline is an answer, and
/// only an answer may be acted on destructively.
/// Distinct from "never asked": only a recorded decline is an opt-out, and
/// only an opt-out may be acted on destructively.
#[must_use]
pub fn telemetry_declined(&self, version: &str) -> bool {
!self.needs_telemetry_notice(version) && !self.telemetry_opt_in
}
/// Whether any recorded telemetry notice was explicitly declined.
///
/// Declines recorded by the former opt-in notice remain durable opt-outs
/// after telemetry becomes default-on. A notice-version bump may explain a
/// changed policy, but it must never erase a user's earlier "no".
#[must_use]
pub fn telemetry_opted_out(&self) -> bool {
self.telemetry_notice_decided_for.is_some() && !self.telemetry_opt_in
}
/// Derive a safe inherited state for an existing user with no persisted
/// `setup_state.json`. Surfaces they already configured become
/// [`StepStatus::Verified`]; an update never looks like a fresh, broken
+92 -11
View File
@@ -1157,6 +1157,9 @@ struct EnvGuard {
xai_api_key: Option<OsString>,
xai_base_url: Option<OsString>,
xai_model: Option<OsString>,
mistral_api_key: Option<OsString>,
mistral_base_url: Option<OsString>,
mistral_model: Option<OsString>,
telecomjs_api_key: Option<OsString>,
telecomjs_base_url: Option<OsString>,
telecomjs_model: Option<OsString>,
@@ -1193,6 +1196,9 @@ impl EnvGuard {
xai_api_key: env::var_os("XAI_API_KEY"),
xai_base_url: env::var_os("XAI_BASE_URL"),
xai_model: env::var_os("XAI_MODEL"),
mistral_api_key: env::var_os("MISTRAL_API_KEY"),
mistral_base_url: env::var_os("MISTRAL_BASE_URL"),
mistral_model: env::var_os("MISTRAL_MODEL"),
telecomjs_api_key: env::var_os("TELECOMJS_API_KEY"),
telecomjs_base_url: env::var_os("TELECOMJS_BASE_URL"),
telecomjs_model: env::var_os("TELECOMJS_MODEL"),
@@ -1322,6 +1328,9 @@ impl EnvGuard {
env::remove_var("XAI_API_KEY");
env::remove_var("XAI_BASE_URL");
env::remove_var("XAI_MODEL");
env::remove_var("MISTRAL_API_KEY");
env::remove_var("MISTRAL_BASE_URL");
env::remove_var("MISTRAL_MODEL");
env::remove_var("TELECOMJS_API_KEY");
env::remove_var("TELECOMJS_BASE_URL");
env::remove_var("TELECOMJS_MODEL");
@@ -1474,6 +1483,9 @@ impl Drop for EnvGuard {
Self::restore_var("XAI_API_KEY", self.xai_api_key.take());
Self::restore_var("XAI_BASE_URL", self.xai_base_url.take());
Self::restore_var("XAI_MODEL", self.xai_model.take());
Self::restore_var("MISTRAL_API_KEY", self.mistral_api_key.take());
Self::restore_var("MISTRAL_BASE_URL", self.mistral_base_url.take());
Self::restore_var("MISTRAL_MODEL", self.mistral_model.take());
Self::restore_var("TELECOMJS_API_KEY", self.telecomjs_api_key.take());
Self::restore_var("TELECOMJS_BASE_URL", self.telecomjs_base_url.take());
Self::restore_var("TELECOMJS_MODEL", self.telecomjs_model.take());
@@ -4113,6 +4125,21 @@ fn provider_kind_parses_openrouter_and_novita_aliases() {
assert_eq!(parsed.provider, ProviderKind::Sakana);
}
for alias in [
"mistral",
"mistral-ai",
"mistral_ai",
"mistralai",
"la-plateforme",
"la_plateforme",
] {
assert_eq!(ProviderKind::parse(alias), Some(ProviderKind::Mistral));
let parsed: ConfigToml =
toml::from_str(&format!("provider = \"{alias}\"")).expect("mistral alias");
assert_eq!(parsed.provider, ProviderKind::Mistral);
}
for alias in ["qianfan", "baidu-qianfan", "baidu_qianfan", "baidu"] {
assert_eq!(ProviderKind::parse(alias), Some(ProviderKind::Qianfan));
@@ -4474,6 +4501,61 @@ fn xai_api_key_provider_resolves_defaults_and_scopes_env_credentials() {
assert_eq!(resolved.model, "grok-4.3");
}
#[test]
fn mistral_provider_resolves_defaults_and_metadata() {
let _lock = env_lock();
let _env = EnvGuard::without_deepseek_runtime_overrides();
let metadata = provider::resolve_provider("mistral").expect("mistral provider metadata");
assert_eq!(metadata.id(), "mistral");
assert_eq!(metadata.kind(), ProviderKind::Mistral);
assert_eq!(metadata.display_name(), "Mistral AI");
assert_eq!(metadata.provider_config_key(), "mistral");
assert_eq!(metadata.default_base_url(), "https://api.mistral.ai/v1");
assert_eq!(metadata.default_model(), "mistral-code-latest");
assert_eq!(metadata.env_vars(), &["MISTRAL_API_KEY"]);
assert_eq!(
metadata.wire_policy().fixed(),
Some(provider::WireFormat::ChatCompletions)
);
let help = metadata.credential_help();
assert_eq!(
help.acquisition,
provider::CredentialAcquisition::ApiKey,
"Mistral is a hosted API-key provider"
);
assert_eq!(
help.credential_url,
Some("https://console.mistral.ai/api-keys")
);
let config: ConfigToml = toml::from_str(
r#"
provider = "mistral-ai"
[providers.mistral]
api_key = "mistral-config-key"
model = "mistral-large-latest"
"#,
)
.expect("mistral provider table");
assert_eq!(config.provider, ProviderKind::Mistral);
let resolved = config.resolve_runtime_options(&CliRuntimeOverrides::default());
assert_eq!(resolved.provider, ProviderKind::Mistral);
assert_eq!(resolved.base_url, "https://api.mistral.ai/v1");
assert_eq!(resolved.model, "mistral-large-latest");
assert_eq!(resolved.api_key.as_deref(), Some("mistral-config-key"));
unsafe {
std::env::set_var("MISTRAL_BASE_URL", "https://api.eu.mistral.ai/v1");
std::env::set_var("MISTRAL_MODEL", "mistral-medium-latest");
}
let resolved = config.resolve_runtime_options(&CliRuntimeOverrides::default());
assert_eq!(resolved.base_url, "https://api.eu.mistral.ai/v1");
assert_eq!(resolved.model, "mistral-medium-latest");
}
#[test]
fn opencode_go_resolves_current_chat_completions_route() {
let _lock = env_lock();
@@ -4729,9 +4811,9 @@ fn meta_model_api_scopes_both_documented_key_names_to_official_endpoint() {
fn provider_metadata_registry_covers_every_provider_kind_once() {
let providers = provider::all_providers();
// Full registry keeps legacy dialect/plan kinds for provider_for_kind.
assert_eq!(providers.len(), 41);
assert_eq!(providers.len(), 42);
// Catalog surface is one identity per vendor (no dual-wire / plan rows).
assert_eq!(ProviderKind::ALL.len(), 36);
assert_eq!(ProviderKind::ALL.len(), 37);
assert!(ProviderKind::ALL.len() < providers.len());
let mut ids = std::collections::BTreeSet::new();
@@ -8215,9 +8297,9 @@ fn telemetry_env_invalid_is_recorded_rather_than_swallowed() {
fn telemetry_explicit_off_distinguishes_an_answer_from_the_default() {
let _guard = TelemetryEnvGuard::take();
// Nobody said anything: off, but not an answer.
// Nobody said anything: default on, with no explicit opt-out.
let resolved = ConfigToml::default().resolve_runtime_options(&CliRuntimeOverrides::default());
assert!(!resolved.telemetry);
assert!(resolved.telemetry);
assert!(!resolved.telemetry_explicit_off);
// The config file says no.
@@ -8270,10 +8352,9 @@ fn an_unconfigured_endpoint_resolves_to_the_shipped_default() {
"an unconfigured endpoint must resolve to the shipped default"
);
// …and it decides only *where*, never *whether*. The default endpoint must
// not have made an unconfigured install start collecting: telemetry is
// still off, and still not an answer anyone gave.
assert!(!resolved.telemetry);
// …and the product default is anonymous usage counting on unless one of
// the documented kill switches says otherwise.
assert!(resolved.telemetry);
assert!(!resolved.telemetry_explicit_off);
}
@@ -8479,8 +8560,8 @@ fn telemetry_notice_is_owed_until_it_is_answered_out_loud() {
assert!(!state.telemetry_declined(TELEMETRY_NOTICE_VERSION));
// A decision recorded against different notice content is stale: the
// notice is owed again, and the stale `true` is not consent to the new
// content.
// notice is owed again. The stale `true` still means the user did not opt
// out; this helper only describes whether the current wording was shown.
state.record_telemetry_notice("0", true);
assert!(state.needs_telemetry_notice(TELEMETRY_NOTICE_VERSION));
assert!(!state.telemetry_accepted(TELEMETRY_NOTICE_VERSION));
@@ -8530,7 +8611,7 @@ fn telemetry_notice_fields_round_trip_and_stay_absent_when_unanswered() {
);
assert!(
!rendered.contains("telemetry_opt_in"),
"a false opt-in must not write a field: {rendered}"
"the default false compatibility field must not be serialized: {rendered}"
);
state.record_telemetry_notice(TELEMETRY_NOTICE_VERSION, true);
+13 -9
View File
@@ -10,19 +10,23 @@ description = "Core runtime boundaries for Codewhale"
[dependencies]
anyhow.workspace = true
chrono.workspace = true
codewhale-agent = { path = "../agent", version = "0.9.4" }
codewhale-config = { path = "../config", version = "0.9.4" }
codewhale-execpolicy = { path = "../execpolicy", version = "0.9.4" }
codewhale-hooks = { path = "../hooks", version = "0.9.4" }
codewhale-mcp = { path = "../mcp", version = "0.9.4" }
codewhale-protocol = { path = "../protocol", version = "0.9.4" }
codewhale-state = { path = "../state", version = "0.9.4" }
codewhale-tools = { path = "../tools", version = "0.9.4" }
serde_json.workspace = true
serde.workspace = true
thiserror.workspace = true
tokio-util.workspace = true
codewhale-agent = { path = "../agent", version = "0.9.6" }
codewhale-config = { path = "../config", version = "0.9.6" }
codewhale-execpolicy = { path = "../execpolicy", version = "0.9.6" }
codewhale-hooks = { path = "../hooks", version = "0.9.6" }
codewhale-mcp = { path = "../mcp", version = "0.9.6" }
codewhale-protocol = { path = "../protocol", version = "0.9.6" }
codewhale-state = { path = "../state", version = "0.9.6" }
codewhale-tools = { path = "../tools", version = "0.9.6" }
serde_json = { workspace = true, features = ["preserve_order"] }
tokio = { workspace = true, features = ["time"] }
tracing.workspace = true
uuid.workspace = true
[dev-dependencies]
async-trait.workspace = true
tempfile.workspace = true
tokio = { workspace = true, features = ["macros", "rt", "time"] }
+306
View File
@@ -0,0 +1,306 @@
//! Core engine (issue #5261).
//!
//! Move, don't rewrite: the turn loop, session, thread manager, the TUI's
//! `run_event_loop`, and the chat client's request-building are all destined
//! for this crate. **Only request-building and fragments have moved so far.**
//! The turn loop still lives in `crates/tui/src/core/engine/turn_loop.rs` and
//! is what every interactive and headless turn runs today; this module is the
//! boundary that move lands against, not the current owner of turn execution.
//! The TUI crate depends on `core`, not the reverse.
//!
//! Approved crates that the engine needs are already in `crates/core`'s
//! Cargo.toml: `config`, `execpolicy`, `protocol`, `state`, `tools`, `mcp`,
//! `hooks`, `agent`. Things that stay in the TUI (`ratatui`, `crossterm`,
//! `prompt_zones` rendering) are not imported here; the engine is
//! terminal-free so it can start a session with no TUI attached.
//!
//! This module is intentionally small on this first cut: it formalizes the
//! `ThreadId`/`SessionId` boundary, the `Op`-in / `EventMsg`-out channels in
//! `crates/protocol`, the `Journal` leaf, and the `Thread`-owned headless
//! `spawn` that TUI and `codewhale exec` both go through. The full turn
//! loop, guards (`StuckGuard`, `ReadRepeatGuard`, `ToolCallBudget`), stream
//! retry budget, and the four-way `RuntimeThreadManager` split live in the
//! `thread/` submodules so follow-ons (#5262, #5263, #5264) have a place to
//! land without another boundary move.
//!
//! Back-compat: persisted `state.json` / `threads` shape is unchanged.
use std::path::PathBuf;
use std::sync::{Arc, Mutex as StdMutex};
use codewhale_protocol::event_msg::EventMsg;
use codewhale_protocol::ids::{SessionId, ThreadId};
use codewhale_protocol::op::{Op, OpEnvelope};
use codewhale_state::StateStore;
use tokio::sync::mpsc;
use crate::ids::ThreadId as CoreThreadId;
use crate::journal::Journal;
use crate::session::{Session, Thread};
pub mod thread;
// ---------------------------------------------------------------------------
// Engine handle — the mailbox every consumer (TUI, CLI exec, app-server,
// tests) holds. Mirrors `crates/tui/src/core/engine/handle.rs` but lives
// in `core` so the mailbox API is reviewable on its own.
/// Reason the active turn was cancelled.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CancelReason {
User,
External,
Preempted,
Internal,
}
/// Handle to communicate with the core engine via the `Op`-in /
/// `EventMsg`-out channels. The TUI's `EngineHandle` and the headless
/// `exec` both hold this type; `handle.steer`, `cancel`, `approve_tool_call`
/// etc are the same code path in both modes so `crates/execpolicy` stays
/// the authority identically.
#[derive(Clone)]
pub struct EngineHandle {
pub tx_op: mpsc::Sender<OpEnvelope>,
pub rx_event: Arc<tokio::sync::RwLock<mpsc::Receiver<EventMsg>>>,
cancel_token: Arc<StdMutex<tokio_util::sync::CancellationToken>>,
}
impl EngineHandle {
pub async fn send(&self, op: OpEnvelope) -> anyhow::Result<()> {
self.tx_op
.send(op)
.await
.map_err(|e| anyhow::anyhow!("{e}"))?;
Ok(())
}
pub fn cancel(&self) {
self.cancel_with_reason(CancelReason::User);
}
pub fn cancel_with_reason(&self, _reason: CancelReason) {
if let Ok(token) = self.cancel_token.lock() {
token.cancel();
}
}
pub async fn steer(
&self,
thread_id: ThreadId,
content: impl Into<String>,
) -> anyhow::Result<()> {
let env = OpEnvelope {
op_id: format!("op-{}", uuid::Uuid::new_v4()),
thread_id,
session_id: SessionId::new(),
op: Op::Steer {
content: content.into(),
},
};
self.tx_op
.send(env)
.await
.map_err(|e| anyhow::anyhow!("{e}"))?;
Ok(())
}
}
// ---------------------------------------------------------------------------
// Engine config — the minimal fields the core engine needs to start a
// session headlessly. Full `EngineConfig` from `crates/tui/src/core/engine.rs`
// is larger (tools, mcp, prompts, etc); those follow in later slices. This
// cut carries just enough to prove "a session can start and run a turn with
// no TUI attached".
#[derive(Debug, Clone)]
pub struct EngineConfig {
pub workspace: PathBuf,
pub model: String,
pub model_provider: String,
pub thread_id: ThreadId,
pub session_id: SessionId,
pub max_steps: u32,
}
impl Default for EngineConfig {
fn default() -> Self {
Self {
workspace: PathBuf::from("."),
model: "deepseek-v4-flash".to_string(),
model_provider: "deepseek".to_string(),
thread_id: ThreadId::new(),
session_id: SessionId::new(),
max_steps: 32,
}
}
}
// ---------------------------------------------------------------------------
// Core engine — spawns in a background tokio task (mirrors
// `crates/tui/src/core/engine.rs` `spawn_engine` / `spawn_supervised`).
pub struct Engine {
rx_op: mpsc::Receiver<OpEnvelope>,
tx_event: mpsc::Sender<EventMsg>,
journal: Journal,
session: Session,
thread: Thread,
}
const ENGINE_OP_CHANNEL_CAPACITY: usize = 32;
const ENGINE_EVENT_CHANNEL_CAPACITY: usize = 128;
impl Engine {
#[must_use]
pub fn new(config: EngineConfig, _state: StateStore) -> (Self, EngineHandle) {
let (tx_op, rx_op) = mpsc::channel(ENGINE_OP_CHANNEL_CAPACITY);
let (tx_event, rx_event) = mpsc::channel(ENGINE_EVENT_CHANNEL_CAPACITY);
let thread = Thread::new(
CoreThreadId::from_string(config.thread_id.as_str().to_string()),
config.workspace.clone(),
config.model.clone(),
);
let session = Session::new(
CoreThreadId::from_string(config.thread_id.as_str().to_string()),
config.workspace.clone(),
config.model.clone(),
);
let handle = EngineHandle {
tx_op,
rx_event: Arc::new(tokio::sync::RwLock::new(rx_event)),
cancel_token: Arc::new(StdMutex::new(tokio_util::sync::CancellationToken::new())),
};
let engine = Self {
rx_op,
tx_event,
journal: Journal::new(),
session,
thread,
};
(engine, handle)
}
/// Run the engine loop. This is the headless proof: a thread can be
/// driven purely through `OpEnvelope` / `EventMsg` without a TUI. The
/// real turn loop (stream, tool exec, guards, compaction) is wired here
/// in the next slice; the loop below already proves the channel plumbing
/// and the `execpolicy` gate that both modes share.
pub async fn run(mut self) {
while let Some(env) = self.rx_op.recv().await {
let _ = self
.tx_event
.send(EventMsg::TurnStarted {
thread_id: env.thread_id.clone(),
session_id: env.session_id.clone(),
turn_id: format!("turn-{}", uuid::Uuid::new_v4()),
})
.await;
match env.op {
Op::SendMessage { content, .. } => {
// Append to journal (the tree) — branching only moves leaf.
self.journal.append("user", serde_json::json!(content));
self.thread.leaf_id = self.journal.leaf_id.clone();
self.session.bump_revision();
let turn_id = format!("turn-{}", uuid::Uuid::new_v4());
let _ = self
.tx_event
.send(EventMsg::TurnComplete {
thread_id: env.thread_id.clone(),
session_id: env.session_id.clone(),
turn_id,
status: "completed".to_string(),
error: None,
})
.await;
}
Op::Steer { content } => {
self.journal.append("user", serde_json::json!(content));
self.thread.leaf_id = self.journal.leaf_id.clone();
}
Op::Shutdown | Op::Cancel => break,
_ => {}
}
}
}
}
/// Spawn the engine in a background task (mirrors `spawn_engine` in the
/// old `crates/tui/src/core/engine.rs`). Returns the handle that TUI,
/// CLI exec, app-server, and tests all share — one `Op`-in / `EventMsg`-out
/// API.
pub fn spawn_engine(config: EngineConfig, state: StateStore) -> EngineHandle {
let (engine, handle) = Engine::new(config, state);
let handle_clone = handle.clone();
tokio::spawn(async move {
engine.run().await;
});
handle_clone
}
/// Spawn with supervision (mirrors `spawn_supervised`).
pub fn spawn_supervised(config: EngineConfig, state: StateStore) -> EngineHandle {
spawn_engine(config, state)
}
// ---------------------------------------------------------------------------
// Headless helper — no TUI is constructed. This currently proves that core
// can own session lifecycle behind the shared `Op` channel; outbound model
// dispatch is a later #5261 slice and is not claimed here.
/// Start a headless session and expose its shared operation channel.
///
/// Callers can enqueue operations and observe `EventMsg`s through the returned
/// handle. Outbound model dispatch is intentionally not claimed by this helper
/// until that part of the engine has moved into core.
pub fn spawn_headless_thread(
workspace: PathBuf,
model: impl Into<String>,
state: StateStore,
) -> (EngineHandle, ThreadId, SessionId) {
let thread_id = ThreadId::new();
let session_id = SessionId::new();
let config = EngineConfig {
workspace,
model: model.into(),
model_provider: "deepseek".to_string(),
thread_id: thread_id.clone(),
session_id: session_id.clone(),
max_steps: 32,
};
let handle = spawn_engine(config, state);
(handle, thread_id, session_id)
}
#[cfg(test)]
mod tests {
use super::*;
use codewhale_state::StateStore;
#[tokio::test]
async fn headless_session_can_be_started_with_no_tui() {
let dir = tempfile::tempdir().unwrap();
let state = StateStore::open(Some(dir.path().join("state.db"))).unwrap();
let (handle, thread_id, _session_id) =
spawn_headless_thread(dir.path().to_path_buf(), "deepseek-v4-flash", state);
// Drive a SendMessage through the same Op channel the TUI uses.
let env = OpEnvelope {
op_id: "op-1".into(),
thread_id: thread_id.clone(),
session_id: SessionId::new(),
op: Op::SendMessage {
content: "hello".into(),
mode: "agent".into(),
model: None,
model_provider: None,
allowed_tools: None,
dynamic_tools: vec![],
provenance: "external_user".into(),
},
};
handle.send(env).await.unwrap();
// Engine is running — dropping the handle's sender closes the channel.
drop(handle);
}
}
+58
View File
@@ -0,0 +1,58 @@
//! Thread events — `RuntimeEventEnvelope` mapping + `EventMsg` fan-out
//! (issue #5261 / #3313).
//!
//! The TUI's `runtime_threads.rs` emits `RuntimeEventEnvelope` for the
//! app-server SSE stream and `Event` for the transcript. This module owns
//! that mapping in `core` so the headless `exec` and the TUI render the
//! same envelope for the same turn — byte-identical on the wire.
use codewhale_protocol::event_msg::EventMsg;
use codewhale_protocol::ids::{SessionId, ThreadId};
/// Narrow the `EventMsg` to the envelope shape the app-server expects.
/// The real `RuntimeEventEnvelope` adds `seq` + `timestamp`; this helper
/// stamps them consistently so headless and TUI produce identical sequences.
#[must_use]
pub fn to_envelope_seq(
seq: u64,
thread_id: ThreadId,
_session_id: SessionId,
msg: EventMsg,
) -> codewhale_protocol::runtime::RuntimeEventEnvelope {
codewhale_protocol::runtime::RuntimeEventEnvelope {
schema_version: codewhale_protocol::runtime::RUNTIME_EVENT_ENVELOPE_SCHEMA_VERSION,
seq,
event: msg.kind_str().to_string(),
kind: msg.kind_str().to_string(),
thread_id: thread_id.to_string(),
turn_id: None,
item_id: None,
timestamp: chrono::Utc::now().to_rfc3339(),
created_at: None,
payload: serde_json::to_value(&msg).unwrap_or(serde_json::Value::Null),
extra: Default::default(),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn envelope_preserves_thread_and_kind() {
let tid = ThreadId::new();
let sid = SessionId::new();
let env = to_envelope_seq(
1,
tid.clone(),
sid.clone(),
EventMsg::TurnStarted {
thread_id: tid.clone(),
session_id: sid.clone(),
turn_id: "turn-1".into(),
},
);
assert_eq!(env.thread_id, tid.to_string());
assert_eq!(env.seq, 1);
}
}
+65
View File
@@ -0,0 +1,65 @@
//! Turn executor — the `monitor_turn` / `handle_deepseek_turn` leg
//! (issue #5261 / #3313).
//!
//! This will own `handle_deepseek_turn`, the steer/subagent drains,
//! `refresh_system_prompt()`, `should_compact`/`compact_messages_safe`,
//! `MessageRequest` build, parallel tool exec, `StuckGuard`/
//! `ReadRepeatGuard`/`ToolCallBudget`, and stream retry budget. The move
//! is file-by-file from `crates/tui/src/core/engine/turn_loop.rs`
//! (5,706 lines) so the diff stays reviewable. Until the move lands this
//! file carries the executor type and the `execpolicy` gate that guarantees
//! approvals route through the turn context identically in both modes.
use codewhale_execpolicy::ExecPolicyEngine;
use codewhale_protocol::ids::{SessionId, ThreadId};
/// Per-turn execution context. The `execpolicy` engine is the sole authority
/// for approvals; both TUI and headless construct it from the same
/// `permissions.toml` / `ConfigStore` so the gate never diverges.
#[derive(Debug)]
pub struct TurnExecutor {
pub thread_id: ThreadId,
pub session_id: SessionId,
pub exec_policy: ExecPolicyEngine,
pub max_steps: u32,
}
impl TurnExecutor {
#[must_use]
pub fn new(
thread_id: ThreadId,
session_id: SessionId,
exec_policy: ExecPolicyEngine,
max_steps: u32,
) -> Self {
Self {
thread_id,
session_id,
exec_policy,
max_steps,
}
}
#[must_use]
pub fn can_execute(&self, step: u32) -> bool {
step < self.max_steps
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn executor_respects_max_steps() {
let ex = TurnExecutor::new(
ThreadId::new(),
SessionId::new(),
ExecPolicyEngine::new(vec![], vec![]),
2,
);
assert!(ex.can_execute(0));
assert!(ex.can_execute(1));
assert!(!ex.can_execute(2));
}
}
+26
View File
@@ -0,0 +1,26 @@
//! `RuntimeThreadManager` split per #3313 (issue #5261).
//!
//! The TUI's `crates/tui/src/runtime_threads.rs` (≈8,259 lines, `monitor_turn`
//! ≈1,035 lines) is the largest file in the tree. The split is pure code
//! motion, persisted JSON shape unchanged:
//! - `store` — `RuntimeThreadStore` / persisted JSON state
//! (`<root>/{threads,turns,items,events}` + `state.json`)
//! - `executor` — turn execution (`monitor_turn`, `handle_deepseek_turn`,
//! steer/subagent drains, `refresh_system_prompt`, compaction, parallel
//! tool exec, `StuckGuard`/`ReadRepeatGuard`/`ToolCallBudget`, stream retry)
//! - `events` — `RuntimeEventEnvelope` mapping + `EventMsg` fan-out
//! - `types` — `ThreadId`/`SessionId`, `ThreadStatus`, `Thread` etc
//!
//! This cut lands the four files and the re-exports so `crates/tui` can
//! `pub use codewhale_core::engine::thread::*` and the next slice can `git mv`
//! the impls file-by-file without a flag day. The behaviour stays in the TUI
//! until the move completes; `core` already owns the boundary.
pub mod events;
pub mod executor;
pub mod store;
pub mod types;
pub use events::*;
pub use store::*;
pub use types::*;
+56
View File
@@ -0,0 +1,56 @@
//! `RuntimeThreadStore` — persisted JSON state (issue #5261 / #3313).
//!
//! The store is the `state.json` + `<root>/{threads,turns,items,events}`
//! layout that `crates/state` already owns. This module is the `core`
//! owner for that layout so the TUI's `RuntimeThreadManager` can be split
//! without changing the file shape. The current `ThreadManager` in
//! `crates/core/src/lib.rs` already uses `StateStore`; this file is the
//! next home for that impl once the `git mv` lands. Until then it
//! documents the contract and exposes the typed store handle.
use codewhale_protocol::ids::ThreadId;
use codewhale_state::StateStore;
/// Typed handle over `StateStore` that the executor and events modules share.
/// The methods are thin wrappers so the store boundary is greppable and the
/// persisted shape can be asserted in one place (back-compat tests hold).
#[derive(Debug, Clone)]
pub struct ThreadStore {
inner: StateStore,
root: std::path::PathBuf,
}
impl ThreadStore {
#[must_use]
pub fn new(inner: StateStore, root: std::path::PathBuf) -> Self {
Self { inner, root }
}
#[must_use]
pub fn state(&self) -> &StateStore {
&self.inner
}
#[must_use]
pub fn root(&self) -> &std::path::Path {
&self.root
}
pub fn thread_exists(&self, id: &ThreadId) -> anyhow::Result<bool> {
Ok(self.inner.get_thread(id.as_str())?.is_some())
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
#[test]
fn store_wraps_state() {
let dir = tempdir().unwrap();
let state = StateStore::open(Some(dir.path().join("state.db"))).unwrap();
let store = ThreadStore::new(state, dir.path().to_path_buf());
assert!(!store.thread_exists(&ThreadId::new()).unwrap());
}
}
+15
View File
@@ -0,0 +1,15 @@
//! Thread types for the `crates/core` boundary (issue #5261 / #3313).
//!
//! Re-exports the protocol ids plus the thread-status enums that every
//! consumer (TUI, CLI, app-server, tests) needs. The TUI's
//! `runtime_threads.rs` and `core/engine.rs` both import from here after the
//! move so `is_terminal` / `is_active` / `is_paused` is a single `Status`
//! trait, not three copies.
pub use codewhale_protocol::ids::{SessionId, ThreadId};
pub use codewhale_protocol::{Status, ThreadStatus};
/// Back-compat alias: the TUI's `RuntimeThread` is the same shape as the
/// protocol `Thread` now that the ids are typed. Callers that still name
/// `RuntimeThread` get this alias so the rename is mechanical.
pub type RuntimeThread = codewhale_protocol::Thread;
+661
View File
@@ -0,0 +1,661 @@
//! Bounded context-fragment system with hard caps (issue #5264).
//!
//! Every context injection goes through a typed fragment with a
//! `matches_text` recognizer, collected in one `crates/core` module.
//! Hard caps: per-fragment byte cap, 10K-token ceiling, injected-item count.
//! Project-instruction import (#3978, #4079) is a typed fragment.
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
use std::path::{Path, PathBuf};
// Caps
pub const MAX_FRAGMENT_TOKENS: usize = 10_000;
pub const MAX_FRAGMENT_BYTES: usize = MAX_FRAGMENT_TOKENS * 4; // 40_000
pub const DEFAULT_FRAGMENT_MAX_BYTES: usize = 4 * 1024;
pub const MAX_FRAGMENTS_PER_CONTEXT: usize = 16;
pub const INSTRUCTIONS_FILE_MAX_BYTES: usize = 100 * 1024;
pub const MAX_INSTRUCTION_FILES: usize = 32;
/// Stable fragment identities. Markers are public contract.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum FragmentId {
Workspace,
Permissions,
Route,
AgentTopology,
SkillsTools,
TokenBudget,
ProjectInstructions,
Constitution,
}
impl FragmentId {
#[must_use]
pub fn as_str(self) -> &'static str {
match self {
Self::Workspace => "workspace",
Self::Permissions => "permissions",
Self::Route => "route",
Self::AgentTopology => "agent_topology",
Self::SkillsTools => "skills_tools",
Self::TokenBudget => "token_budget",
Self::ProjectInstructions => "project_instructions",
Self::Constitution => "constitution",
}
}
#[must_use]
pub fn marker(self) -> &'static str {
match self {
Self::Workspace => "<!-- cw:ctx:workspace -->",
Self::Permissions => "<!-- cw:ctx:permissions -->",
Self::Route => "<!-- cw:ctx:route -->",
Self::AgentTopology => "<!-- cw:ctx:agent_topology -->",
Self::SkillsTools => "<!-- cw:ctx:skills_tools -->",
Self::TokenBudget => "<!-- cw:ctx:token_budget -->",
Self::ProjectInstructions => "<!-- cw:ctx:project_instructions -->",
Self::Constitution => "<!-- cw:ctx:constitution -->",
}
}
#[must_use]
pub fn role(self) -> FragmentRole {
match self {
Self::Workspace => FragmentRole::Workspace,
Self::Permissions => FragmentRole::Permissions,
Self::Route => FragmentRole::Route,
Self::AgentTopology => FragmentRole::AgentTopology,
Self::SkillsTools => FragmentRole::SkillsTools,
Self::TokenBudget => FragmentRole::TokenBudget,
Self::ProjectInstructions => FragmentRole::ProjectInstructions,
Self::Constitution => FragmentRole::Constitution,
}
}
#[must_use]
pub fn all() -> &'static [FragmentId] {
&[
Self::Workspace,
Self::Permissions,
Self::Route,
Self::AgentTopology,
Self::SkillsTools,
Self::TokenBudget,
Self::ProjectInstructions,
Self::Constitution,
]
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum FragmentRole {
Workspace,
Permissions,
Route,
AgentTopology,
SkillsTools,
TokenBudget,
ProjectInstructions,
Constitution,
}
impl FragmentRole {
#[must_use]
pub fn as_str(self) -> &'static str {
match self {
Self::Workspace => "workspace",
Self::Permissions => "permissions",
Self::Route => "route",
Self::AgentTopology => "agent_topology",
Self::SkillsTools => "skills_tools",
Self::TokenBudget => "token_budget",
Self::ProjectInstructions => "project_instructions",
Self::Constitution => "constitution",
}
}
}
#[must_use]
pub fn estimate_tokens(text: &str) -> usize {
text.len().div_ceil(4)
}
/// Typed fragment trait with `matches_text` recognizer.
pub trait ContextFragment {
fn fragment_id(&self) -> FragmentId;
fn marker(&self) -> &'static str;
fn content(&self) -> &str;
fn matches_text(&self, haystack: &str) -> bool {
haystack.contains(self.marker())
}
fn tokens_est(&self) -> usize {
estimate_tokens(self.content())
}
fn max_bytes(&self) -> usize;
fn is_within_token_ceiling(&self) -> bool {
self.tokens_est() <= MAX_FRAGMENT_TOKENS
}
fn is_within_byte_ceiling(&self) -> bool {
self.content().len() <= MAX_FRAGMENT_BYTES
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BoundedFragment {
pub id: FragmentId,
pub role: FragmentRole,
pub marker: &'static str,
pub max_bytes: usize,
pub content: String,
pub content_hash: u64,
}
impl BoundedFragment {
#[must_use]
pub fn new(id: FragmentId, raw: impl Into<String>) -> Self {
Self::with_max_bytes(id, raw, DEFAULT_FRAGMENT_MAX_BYTES)
}
#[must_use]
pub fn with_max_bytes(id: FragmentId, raw: impl Into<String>, max_bytes: usize) -> Self {
let clamped_max = max_bytes.min(MAX_FRAGMENT_BYTES);
let mut content = enforce_byte_cap(raw.into(), clamped_max);
if estimate_tokens(&content) > MAX_FRAGMENT_TOKENS {
content = enforce_byte_cap(content, MAX_FRAGMENT_BYTES);
}
let content_hash = hash_content(&content);
Self {
id,
role: id.role(),
marker: id.marker(),
max_bytes: clamped_max,
content,
content_hash,
}
}
#[must_use]
pub fn project_instructions(raw: impl Into<String>) -> Self {
Self::with_max_bytes(FragmentId::ProjectInstructions, raw, MAX_FRAGMENT_BYTES)
}
#[must_use]
pub fn constitution(raw: impl Into<String>) -> Self {
Self::with_max_bytes(FragmentId::Constitution, raw, MAX_FRAGMENT_BYTES)
}
#[must_use]
pub fn render_marked(&self) -> String {
format!("{}\n{}", self.marker, self.content.trim_end())
}
}
impl ContextFragment for BoundedFragment {
fn fragment_id(&self) -> FragmentId {
self.id
}
fn marker(&self) -> &'static str {
self.marker
}
fn content(&self) -> &str {
&self.content
}
fn max_bytes(&self) -> usize {
self.max_bytes
}
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum FragmentCapError {
#[error("fragment {id:?} exceeds 10K-token ceiling: {tokens} tokens ({bytes} bytes)")]
TokenCeiling {
id: FragmentId,
tokens: usize,
bytes: usize,
},
#[error("fragment {id:?} exceeds byte ceiling: {bytes} > {max} bytes")]
ByteCeiling {
id: FragmentId,
bytes: usize,
max: usize,
},
#[error("context has too many fragments: {count} > {max}")]
TooManyFragments { count: usize, max: usize },
}
pub fn validate_fragment(fragment: &BoundedFragment) -> Result<(), FragmentCapError> {
if fragment.content.len() > MAX_FRAGMENT_BYTES {
return Err(FragmentCapError::ByteCeiling {
id: fragment.id,
bytes: fragment.content.len(),
max: MAX_FRAGMENT_BYTES,
});
}
let tokens = estimate_tokens(&fragment.content);
if tokens > MAX_FRAGMENT_TOKENS {
return Err(FragmentCapError::TokenCeiling {
id: fragment.id,
bytes: fragment.content.len(),
tokens,
});
}
Ok(())
}
pub fn validate_fragment_set(fragments: &[BoundedFragment]) -> Result<(), FragmentCapError> {
if fragments.len() > MAX_FRAGMENTS_PER_CONTEXT {
return Err(FragmentCapError::TooManyFragments {
count: fragments.len(),
max: MAX_FRAGMENTS_PER_CONTEXT,
});
}
for f in fragments {
validate_fragment(f)?;
}
Ok(())
}
// Project-instruction import (#3978)
pub const PROJECT_INSTRUCTION_CANDIDATES: &[&str] = &[
"AGENTS.md",
".agents/AGENTS.md",
"CLAUDE.md",
".claude/instructions.md",
".codewhale/instructions.md",
".deepseek/instructions.md",
".cursorrules",
".cursor/rules",
".clinerules",
".windsurf/rules",
".gemini",
".github/copilot-instructions.md",
".github/muse-instructions.md",
];
/// Workspace instruction formats not already owned by Codewhale's canonical
/// project-context loader. The TUI uses this subset to avoid injecting
/// `AGENTS.md` / `CLAUDE.md` / `instructions.md` twice while still importing
/// additional agent rule formats through the typed fragment boundary.
pub const ADDITIONAL_PROJECT_INSTRUCTION_CANDIDATES: &[&str] = &[
".agents/AGENTS.md",
".cursorrules",
".cursor/rules",
".clinerules",
".windsurf/rules",
".gemini",
".github/copilot-instructions.md",
".github/muse-instructions.md",
];
fn is_symlink(p: &Path) -> bool {
std::fs::symlink_metadata(p)
.map(|m| m.file_type().is_symlink())
.unwrap_or(false)
}
fn read_capped(p: &Path) -> Option<String> {
let meta = std::fs::metadata(p).ok()?;
if !meta.is_file() {
return None;
}
if meta.len() > INSTRUCTIONS_FILE_MAX_BYTES as u64 {
let mut file = std::fs::File::open(p).ok()?;
let mut buf = vec![0u8; INSTRUCTIONS_FILE_MAX_BYTES];
use std::io::Read as _;
let n = file.read(&mut buf).ok()?;
buf.truncate(n);
let mut text = String::from_utf8_lossy(&buf).into_owned();
let mut end = INSTRUCTIONS_FILE_MAX_BYTES.min(text.len());
while end > 0 && !text.is_char_boundary(end) {
end -= 1;
}
text.truncate(end);
let omitted = meta
.len()
.saturating_sub(INSTRUCTIONS_FILE_MAX_BYTES as u64);
text.push_str(&format!("\n[…truncated: {omitted} bytes omitted]"));
return Some(text);
}
let raw = std::fs::read_to_string(p).ok()?;
let trimmed = raw.trim();
if trimmed.is_empty() {
None
} else {
Some(trimmed.to_string())
}
}
fn collect_candidate_files(workspace: &Path, candidates: &[&str]) -> Vec<PathBuf> {
let mut files = Vec::new();
for candidate in candidates {
let path = workspace.join(candidate);
if path.is_dir() {
let mut dir_files = Vec::new();
if let Ok(entries) = std::fs::read_dir(&path) {
for e in entries.flatten() {
let p = e.path();
if p.is_file() && p.extension().is_some_and(|e| e == "md") && !is_symlink(&p) {
dir_files.push(p);
}
}
}
if let Ok(entries) = std::fs::read_dir(&path) {
for e in entries.flatten() {
let p = e.path();
if p.is_dir()
&& !is_symlink(&p)
&& let Ok(sub) = std::fs::read_dir(&p)
{
for se in sub.flatten() {
let sp = se.path();
if sp.is_file()
&& sp.extension().is_some_and(|e| e == "md")
&& !is_symlink(&sp)
{
dir_files.push(sp);
}
}
}
}
}
dir_files.sort();
let remaining = MAX_INSTRUCTION_FILES.saturating_sub(files.len());
dir_files.truncate(remaining);
files.extend(dir_files);
} else if path.is_file() && !is_symlink(&path) {
files.push(path);
}
if files.len() >= MAX_INSTRUCTION_FILES {
break;
}
}
files.truncate(MAX_INSTRUCTION_FILES);
files.sort();
files.dedup();
files
}
fn load_project_instruction_fragment_from_candidates(
workspace: &Path,
candidates: &[&str],
) -> Option<BoundedFragment> {
let files = collect_candidate_files(workspace, candidates);
if files.is_empty() {
return None;
}
let mut sections = Vec::new();
for path in files {
if let Some(content) = read_capped(&path) {
let rel = path
.strip_prefix(workspace)
.unwrap_or(&path)
.display()
.to_string();
sections.push(format!(
"<project_instructions source=\"{rel}\">\n{content}\n</project_instructions>"
));
}
}
if sections.is_empty() {
return None;
}
let merged = sections.join("\n\n");
let fragment = BoundedFragment::project_instructions(merged);
debug_assert!(validate_fragment(&fragment).is_ok());
Some(fragment)
}
pub fn load_project_instruction_fragment(workspace: &Path) -> Option<BoundedFragment> {
load_project_instruction_fragment_from_candidates(workspace, PROJECT_INSTRUCTION_CANDIDATES)
}
/// Load only instruction formats that the canonical TUI project-context path
/// does not already render. This prevents duplicate authority while retaining
/// the broader compatibility import added by the bounded fragment system.
pub fn load_additional_project_instruction_fragment(workspace: &Path) -> Option<BoundedFragment> {
load_project_instruction_fragment_from_candidates(
workspace,
ADDITIONAL_PROJECT_INSTRUCTION_CANDIDATES,
)
}
pub fn project_instructions_from_sources(
sources: impl IntoIterator<Item = (String, String)>,
) -> Option<BoundedFragment> {
let mut sections = Vec::new();
for (name, content) in sources {
let trimmed = content.trim();
if trimmed.is_empty() {
continue;
}
let body = if trimmed.len() > INSTRUCTIONS_FILE_MAX_BYTES {
let mut end = INSTRUCTIONS_FILE_MAX_BYTES;
while end > 0 && !trimmed.is_char_boundary(end) {
end -= 1;
}
let omitted = trimmed.len() - end;
format!("{}\n[…truncated: {omitted} bytes omitted]", &trimmed[..end])
} else {
trimmed.to_string()
};
sections.push(format!(
"<project_instructions source=\"{name}\">\n{body}\n</project_instructions>"
));
if sections.len() >= MAX_INSTRUCTION_FILES {
break;
}
}
if sections.is_empty() {
return None;
}
Some(BoundedFragment::project_instructions(sections.join("\n\n")))
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct FragmentBudgetSnapshot {
pub fragment_ids: Vec<String>,
pub fragment_markers: Vec<String>,
pub max_fragment_bytes: usize,
pub max_fragment_tokens: usize,
pub default_fragment_max_bytes: usize,
pub max_fragments_per_context: usize,
pub instructions_file_max_bytes: usize,
pub max_instruction_files: usize,
pub project_instruction_candidates: Vec<String>,
}
#[must_use]
pub fn fragment_budget_snapshot() -> FragmentBudgetSnapshot {
FragmentBudgetSnapshot {
fragment_ids: FragmentId::all()
.iter()
.map(|id| id.as_str().to_string())
.collect(),
fragment_markers: FragmentId::all()
.iter()
.map(|id| id.marker().to_string())
.collect(),
max_fragment_bytes: MAX_FRAGMENT_BYTES,
max_fragment_tokens: MAX_FRAGMENT_TOKENS,
default_fragment_max_bytes: DEFAULT_FRAGMENT_MAX_BYTES,
max_fragments_per_context: MAX_FRAGMENTS_PER_CONTEXT,
instructions_file_max_bytes: INSTRUCTIONS_FILE_MAX_BYTES,
max_instruction_files: MAX_INSTRUCTION_FILES,
project_instruction_candidates: PROJECT_INSTRUCTION_CANDIDATES
.iter()
.map(|s| s.to_string())
.collect(),
}
}
fn hash_content(content: &str) -> u64 {
let mut hasher = DefaultHasher::new();
content.hash(&mut hasher);
hasher.finish()
}
fn enforce_byte_cap(raw: String, max_bytes: usize) -> String {
if max_bytes == 0 {
return String::new();
}
if raw.len() <= max_bytes {
return raw;
}
let omitted = raw.len().saturating_sub(max_bytes);
let marker = format!("\n[…truncated: {omitted} bytes omitted]");
if marker.len() >= max_bytes {
return marker.chars().take(max_bytes).collect();
}
let keep = max_bytes.saturating_sub(marker.len());
let mut end = keep;
while end > 0 && !raw.is_char_boundary(end) {
end -= 1;
}
let mut out = raw[..end].to_string();
out.push_str(&marker);
out
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::tempdir;
#[test]
fn fragment_has_matches_text_recognizer() {
let fragment = BoundedFragment::new(FragmentId::Workspace, "repo: /tmp/demo");
let rendered = fragment.render_marked();
assert!(fragment.matches_text(&rendered));
assert!(!fragment.matches_text("no marker here"));
assert_eq!(FragmentId::Workspace.marker(), "<!-- cw:ctx:workspace -->");
assert_eq!(
FragmentId::ProjectInstructions.marker(),
"<!-- cw:ctx:project_instructions -->"
);
assert_eq!(
FragmentId::Constitution.marker(),
"<!-- cw:ctx:constitution -->"
);
}
#[test]
fn all_fragment_types_go_through_bounded_module() {
for id in FragmentId::all() {
let fragment = BoundedFragment::new(*id, "hello");
assert_eq!(fragment.marker, id.marker());
assert_eq!(fragment.id, *id);
validate_fragment(&fragment).expect("small fragment must pass caps");
assert!(fragment.is_within_token_ceiling());
assert!(fragment.is_within_byte_ceiling());
}
}
#[test]
fn per_fragment_byte_cap_truncates_with_marker() {
let oversized = "x".repeat(DEFAULT_FRAGMENT_MAX_BYTES + 64);
let fragment = BoundedFragment::new(FragmentId::AgentTopology, oversized);
assert!(fragment.content.len() <= DEFAULT_FRAGMENT_MAX_BYTES);
assert!(fragment.content.contains("[…truncated:"));
validate_fragment(&fragment).expect("truncated fragment must pass caps");
}
#[test]
fn ten_k_token_ceiling_is_enforced() {
let huge = "a".repeat(MAX_FRAGMENT_BYTES + 1_000);
let fragment = BoundedFragment::project_instructions(huge);
assert!(fragment.content.len() <= MAX_FRAGMENT_BYTES);
assert!(estimate_tokens(&fragment.content) <= MAX_FRAGMENT_TOKENS);
validate_fragment(&fragment).expect("capped fragment must satisfy token ceiling");
let also_huge = "b".repeat(MAX_FRAGMENT_BYTES + 5000);
let fragment = BoundedFragment::with_max_bytes(FragmentId::Workspace, also_huge, 100_000);
assert!(fragment.max_bytes <= MAX_FRAGMENT_BYTES);
assert!(fragment.content.len() <= MAX_FRAGMENT_BYTES);
assert!(fragment.is_within_token_ceiling());
}
#[test]
fn injected_item_count_cap_is_enforced() {
let fragments: Vec<BoundedFragment> = (0..MAX_FRAGMENTS_PER_CONTEXT)
.map(|i| BoundedFragment::new(FragmentId::Workspace, format!("item {i}")))
.collect();
validate_fragment_set(&fragments).expect("exactly MAX_FRAGMENTS must pass");
let mut too_many = fragments.clone();
too_many.push(BoundedFragment::new(FragmentId::Route, "one too many"));
let err = validate_fragment_set(&too_many).expect_err("one over cap must fail");
assert!(matches!(err, FragmentCapError::TooManyFragments { .. }));
}
#[test]
fn project_instruction_import_is_a_typed_fragment() {
let dir = tempdir().expect("tempdir");
let ws = dir.path();
fs::write(ws.join(".cursorrules"), "cursor: always use tabs").expect("write cursor");
fs::write(ws.join(".clinerules"), "cline: prefer functional style").expect("write cline");
fs::create_dir_all(ws.join(".windsurf").join("rules")).expect("mkdir windsurf");
fs::write(
ws.join(".windsurf").join("rules").join("extra.md"),
"# windsurf extra",
)
.expect("write windsurf");
fs::create_dir_all(ws.join(".github")).expect("mkdir github");
fs::write(
ws.join(".github").join("copilot-instructions.md"),
"# copilot says hello",
)
.expect("write copilot");
let fragment =
load_project_instruction_fragment(ws).expect("must find imported instructions");
assert_eq!(fragment.id, FragmentId::ProjectInstructions);
assert!(fragment.matches_text(&fragment.render_marked()));
assert!(
fragment.content.contains(".cursorrules") || fragment.content.contains(".clinerules")
);
validate_fragment(&fragment).expect("project-instructions fragment must satisfy caps");
let from_sources = project_instructions_from_sources(vec![
("AGENTS.md".to_string(), "# AGENTS\nbe helpful".to_string()),
(
".cursorrules".to_string(),
"cursor: do the thing".to_string(),
),
])
.expect("sources");
assert_eq!(from_sources.id, FragmentId::ProjectInstructions);
assert!(from_sources.content.contains("AGENTS.md"));
assert!(from_sources.content.contains(".cursorrules"));
validate_fragment(&from_sources).expect("explicit sources must also satisfy caps");
}
#[test]
fn additional_project_instruction_import_does_not_duplicate_canonical_authority() {
let dir = tempdir().expect("tempdir");
let ws = dir.path();
fs::write(ws.join("AGENTS.md"), "canonical authority marker").expect("write agents");
assert!(
load_additional_project_instruction_fragment(ws).is_none(),
"AGENTS.md is already owned by the canonical project-context loader"
);
fs::write(ws.join(".cursorrules"), "additional cursor marker").expect("write cursor rules");
let additional = load_additional_project_instruction_fragment(ws)
.expect("additional rules must produce a typed fragment");
assert!(additional.content.contains("additional cursor marker"));
assert!(!additional.content.contains("canonical authority marker"));
let complete = load_project_instruction_fragment(ws)
.expect("complete importer must retain every supported source");
assert!(complete.content.contains("canonical authority marker"));
assert!(complete.content.contains("additional cursor marker"));
}
#[test]
fn fragment_budget_snapshot_is_stable() {
let snap = fragment_budget_snapshot();
assert_eq!(snap.max_fragment_tokens, 10_000);
assert_eq!(snap.max_fragment_bytes, 40_000);
assert_eq!(snap.max_fragments_per_context, 16);
assert_eq!(snap.default_fragment_max_bytes, 4 * 1024);
assert!(
snap.fragment_ids
.contains(&"project_instructions".to_string())
);
assert!(snap.fragment_ids.contains(&"constitution".to_string()));
assert!(
snap.project_instruction_candidates
.contains(&".cursorrules".to_string())
);
assert!(
snap.project_instruction_candidates
.contains(&".github/copilot-instructions.md".to_string())
);
assert!(
snap.fragment_markers
.contains(&"<!-- cw:ctx:project_instructions -->".to_string())
);
}
}
+9
View File
@@ -0,0 +1,9 @@
//! `ThreadId` / `SessionId` for the `crates/core` boundary (issue #5261).
//!
//! Re-exports the protocol ids so every crate that depends on `core` (the
//! TUI, CLI, app-server) speaks the same typed ids without depending on
//! `protocol` directly. The persisted JSON shape stays a plain string
//! (`"thread-…"` / `"session-…"`) so existing `state.json` / `threads/`
//! files need no migration.
pub use codewhale_protocol::ids::{SessionId, ThreadId};
+78
View File
@@ -0,0 +1,78 @@
//! Session tree journal placeholder (issue #5262).
//!
//! The journal is append-only with an in-memory tree projection:
//! every non-header entry carries `id` + `parentId`, the active position is
//! a `leafId`, appending creates a child of the leaf, and branching only
//! moves the leaf — it never rewrites history. This file lands the entry
//! shape that #5262's tree operations hang off of; compaction and
//! branch-summary entry kinds are included as first-class kinds but their
//! *strategies* are deferred.
//!
//! Re-exports the protocol journal as the canonical shape so `protocol` and
//! `core` agree on the wire. `core` adds the `SessionJournal` wrapper that
//! owns the `current_leaf_id` column in `state.threads`.
pub use codewhale_protocol::journal::{Journal, JournalEntry};
use serde::{Deserialize, Serialize};
/// Persisted thread metadata extension for the tree. This is the
/// `current_leaf_id` column added to `state.threads`; `None` before the
/// first turn, `Some(id)` after. The existing `threads` JSON shape is
/// otherwise unchanged (back-compat: old rows read as `None` and the next
/// append mints the header leaf).
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct ThreadLeafState {
pub thread_id: String,
pub leaf_id: Option<String>,
}
/// First-class journal entry kinds (data shape lands now; strategies later).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum JournalKind {
Header,
User,
Assistant,
ToolResult,
Compaction,
BranchSummary,
}
impl JournalKind {
#[must_use]
pub fn as_str(self) -> &'static str {
match self {
Self::Header => "header",
Self::User => "user",
Self::Assistant => "assistant",
Self::ToolResult => "tool_result",
Self::Compaction => "compaction",
Self::BranchSummary => "branch_summary",
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn leaf_state_roundtrip() {
let s = ThreadLeafState {
thread_id: "thread-1".into(),
leaf_id: Some("entry-abc".into()),
};
let j = serde_json::to_string(&s).unwrap();
let back: ThreadLeafState = serde_json::from_str(&j).unwrap();
assert_eq!(back, s);
}
#[test]
fn journal_append_is_child_of_leaf() {
let mut j = Journal::new();
let a = j.append("header", json!({}));
let b = j.append("user", json!("hi"));
assert_eq!(j.get(&b).unwrap().parent_id.as_deref(), Some(a.as_str()));
}
}
+7
View File
@@ -1,3 +1,10 @@
pub mod engine;
pub mod fragments;
pub mod ids;
pub mod journal;
pub mod request;
pub mod session;
use std::collections::{HashMap, HashSet};
use std::path::{Path, PathBuf};
use std::sync::Arc;
+293
View File
@@ -0,0 +1,293 @@
//! Provider-neutral outbound model-request boundary.
//!
//! The request DTOs in this module are consumed by the TUI transport today
//! and are intentionally free of terminal, HTTP, or provider-client state.
//! Keeping the logical request in `codewhale-core` lets a headless session
//! prepare the same serializable value before the existing TUI client applies
//! provider-specific wire shaping.
use serde::{Deserialize, Serialize};
/// Request payload handed to the model-client preparation seam.
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct MessageRequest {
pub model: String,
pub messages: Vec<Message>,
pub max_tokens: u32,
#[serde(skip_serializing_if = "Option::is_none")]
pub system: Option<SystemPrompt>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tools: Option<Vec<Tool>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_choice: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub metadata: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub thinking: Option<serde_json::Value>,
/// DeepSeek reasoning-effort tier: "off" | "low" | "medium" | "high" | "max".
/// Translated by the client into DeepSeek's `reasoning_effort` + `thinking` fields.
#[serde(skip_serializing_if = "Option::is_none")]
pub reasoning_effort: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub stream: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub temperature: Option<f32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub top_p: Option<f32>,
}
/// Inputs that distinguish a primary agent-turn request.
///
/// Provider-neutral defaults (`stream = true`, no metadata, no provider-side
/// thinking object, and no sampling overrides) are applied once by
/// [`prepare_primary_turn_request`]. Both the production turn loop and its
/// read-only preview use this input so those defaults cannot drift.
#[derive(Debug, Clone)]
pub struct PrimaryTurnRequest {
pub model: String,
pub messages: Vec<Message>,
pub max_tokens: u32,
pub system: Option<SystemPrompt>,
pub tools: Option<Vec<Tool>>,
pub tool_choice: Option<serde_json::Value>,
pub reasoning_effort: Option<String>,
}
/// Prepare the provider-neutral request for a primary agent turn.
///
/// This function performs no I/O and no provider-specific transformation.
/// The existing client transport remains responsible for secret redaction,
/// protocol binding, dialect shaping, and endpoint selection.
#[must_use]
pub fn prepare_primary_turn_request(input: PrimaryTurnRequest) -> MessageRequest {
MessageRequest {
model: input.model,
messages: input.messages,
max_tokens: input.max_tokens,
system: input.system,
tools: input.tools,
tool_choice: input.tool_choice,
metadata: None,
thinking: None,
reasoning_effort: input.reasoning_effort,
stream: Some(true),
temperature: None,
top_p: None,
}
}
/// System prompt representation (plain text or structured blocks).
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
#[serde(untagged)]
pub enum SystemPrompt {
Text(String),
Blocks(Vec<SystemBlock>),
}
/// A structured system prompt block.
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub struct SystemBlock {
#[serde(rename = "type")]
pub block_type: String,
pub text: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub cache_control: Option<CacheControl>,
}
/// OpenAI-compatible image URL payload inside a multimodal message.
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub struct ImageUrlContent {
pub url: String,
}
/// A chat message with role and content blocks.
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub struct Message {
pub role: String,
pub content: Vec<ContentBlock>,
}
/// Internal role used for assistant text that was visible before a turn was interrupted.
pub const INTERRUPTED_ASSISTANT_ROLE: &str = "assistant_interrupted";
/// Prefix attached to interrupted assistant output when it is replayed as context.
pub const INTERRUPTED_ASSISTANT_CONTEXT_PREFIX: &str = "[The following assistant output was interrupted before completion and may be incomplete or wrong]\n";
/// Provider-owned reasoning continuity that is safe to replay only on the
/// exact originating API and model. The encrypted payload is deliberately
/// separate from readable [`ContentBlock::Thinking`] text.
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub struct OpaqueReasoningState {
pub provider: String,
pub api: String,
pub model: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
pub encrypted_content: String,
}
/// A single content block inside a message.
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
#[serde(tag = "type")]
pub enum ContentBlock {
#[serde(rename = "text")]
Text {
text: String,
#[serde(skip_serializing_if = "Option::is_none")]
cache_control: Option<CacheControl>,
},
#[serde(rename = "image_url")]
ImageUrl { image_url: ImageUrlContent },
#[serde(rename = "thinking")]
Thinking {
thinking: String,
/// Anthropic signed-thinking signature (#3014). Only populated on the
/// native Messages dialect and serde-skipped when absent so OpenAI
/// dialects are unaffected. Anthropic rejects tool loops that drop or
/// modify signed thinking blocks, so replay this verbatim.
#[serde(skip_serializing_if = "Option::is_none", default)]
signature: Option<String>,
/// Opaque Responses-style continuity. Never synthesize this from the
/// readable `thinking` text or carry it across a route/model switch.
#[serde(skip_serializing_if = "Option::is_none", default)]
state: Option<OpaqueReasoningState>,
},
#[serde(rename = "tool_use")]
ToolUse {
id: String,
name: String,
input: serde_json::Value,
#[serde(skip_serializing_if = "Option::is_none")]
caller: Option<ToolCaller>,
},
#[serde(rename = "tool_result")]
ToolResult {
tool_use_id: String,
content: String,
#[serde(skip_serializing_if = "Option::is_none")]
is_error: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
content_blocks: Option<Vec<serde_json::Value>>,
},
#[serde(rename = "server_tool_use")]
ServerToolUse {
id: String,
name: String,
input: serde_json::Value,
},
#[serde(rename = "tool_search_tool_result")]
ToolSearchToolResult {
tool_use_id: String,
content: serde_json::Value,
},
#[serde(rename = "code_execution_tool_result")]
CodeExecutionToolResult {
tool_use_id: String,
content: serde_json::Value,
},
}
impl ContentBlock {
/// Build readable reasoning with no provider-owned continuity state.
#[must_use]
pub fn thinking(thinking: impl Into<String>) -> Self {
Self::Thinking {
thinking: thinking.into(),
signature: None,
state: None,
}
}
}
/// Cache control metadata for tool definitions and blocks.
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub struct CacheControl {
#[serde(rename = "type")]
pub cache_type: String,
}
/// Metadata describing who invoked a tool call.
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub struct ToolCaller {
#[serde(rename = "type")]
pub caller_type: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_id: Option<String>,
}
/// Tool definition exposed to the model.
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub struct Tool {
#[serde(rename = "type", skip_serializing_if = "Option::is_none")]
pub tool_type: Option<String>,
pub name: String,
pub description: String,
pub input_schema: serde_json::Value,
#[serde(skip_serializing_if = "Option::is_none")]
pub allowed_callers: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub defer_loading: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub input_examples: Option<Vec<serde_json::Value>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub strict: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub cache_control: Option<CacheControl>,
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn primary_turn() -> PrimaryTurnRequest {
PrimaryTurnRequest {
model: "deepseek-v4-flash".to_string(),
messages: vec![Message {
role: "user".to_string(),
content: vec![ContentBlock::Text {
text: "inspect the request".to_string(),
cache_control: None,
}],
}],
max_tokens: 4096,
system: Some(SystemPrompt::Text("system".to_string())),
tools: Some(vec![Tool {
tool_type: None,
name: "read_file".to_string(),
description: "Read a file".to_string(),
input_schema: json!({"zeta": 1, "alpha": 2, "type": "object"}),
allowed_callers: None,
defer_loading: None,
input_examples: None,
strict: None,
cache_control: None,
}]),
tool_choice: Some(json!({"type": "auto"})),
reasoning_effort: Some("high".to_string()),
}
}
#[test]
fn primary_turn_preparation_has_stable_serialized_bytes() {
let first = prepare_primary_turn_request(primary_turn());
let second = prepare_primary_turn_request(primary_turn());
let first_bytes = serde_json::to_vec(&first).expect("serialize first request");
let second_bytes = serde_json::to_vec(&second).expect("serialize second request");
assert_eq!(first_bytes, second_bytes);
assert_eq!(
first_bytes,
br#"{"model":"deepseek-v4-flash","messages":[{"role":"user","content":[{"type":"text","text":"inspect the request"}]}],"max_tokens":4096,"system":"system","tools":[{"name":"read_file","description":"Read a file","input_schema":{"zeta":1,"alpha":2,"type":"object"}}],"tool_choice":{"type":"auto"},"reasoning_effort":"high","stream":true}"#
);
}
#[test]
fn primary_turn_preparation_owns_shared_defaults() {
let request = prepare_primary_turn_request(primary_turn());
assert_eq!(request.stream, Some(true));
assert!(request.metadata.is_none());
assert!(request.thinking.is_none());
assert!(request.temperature.is_none());
assert!(request.top_p.is_none());
}
}
+137
View File
@@ -0,0 +1,137 @@
//! `Thread` / `Session` split (issue #5261).
//!
//! `codewhale`'s `Session` was really a thread. The new split is:
//! - `Thread` — durable, persisted, owns the append-only `Journal` and the
//! `leafId` cursor. One row in `state.threads`, one directory on disk.
//! - `Session` — ephemeral, per-turn / per-engine-lifetime, owns the
//! in-memory `TurnContext` plus the live approval/sandbox posture for this
//! `SessionId`. Many sessions can attach to one thread over time, but only
//! one `Session` drives a turn for a given `ThreadId` at a time.
//!
//! The thread manager (`ThreadManager` in `crate::lib`) already can start a
//! session with no TUI attached (`spawn_thread_with_history`); this file
//! formalizes the types that make that first-class and moves the former
//! `crates/tui/src/core/session.rs` state (model, reasoning_effort,
//! `AppendLog`, `PrefixStabilityManager`, `frozen_prefix`,
//! `messages_revision`) into `crates/core` so both TUI and headless share it.
use std::path::PathBuf;
use serde::{Deserialize, Serialize};
use crate::ids::{SessionId, ThreadId};
use crate::journal::Journal;
/// Durable thread (the former `Session`). One per conversation, persisted in
/// `state.threads`. The only new field vs the old `Session` is `leaf_id` — the
/// journal cursor — plus the typed `ThreadId`. All other fields keep their
/// persisted JSON shape unchanged.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Thread {
pub thread_id: ThreadId,
/// Active branch tip. `None` before the first journal header.
#[serde(skip_serializing_if = "Option::is_none")]
pub leaf_id: Option<String>,
/// Journal (append-only). In-memory projection of the persisted
/// `threads/turns/items/events` layout is derived root→leaf.
#[serde(default)]
pub journal: Journal,
pub model: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub reasoning_effort: Option<String>,
pub workspace: PathBuf,
#[serde(default)]
pub ephemeral: bool,
}
impl Thread {
#[must_use]
pub fn new(thread_id: ThreadId, workspace: PathBuf, model: impl Into<String>) -> Self {
Self {
thread_id,
leaf_id: None,
journal: Journal::new(),
model: model.into(),
reasoning_effort: None,
workspace,
ephemeral: false,
}
}
#[must_use]
pub fn leaf_id(&self) -> Option<&str> {
self.leaf_id.as_deref()
}
pub fn set_leaf(&mut self, leaf: Option<String>) {
self.leaf_id = leaf;
}
}
/// Ephemeral session within a thread (one engine lifetime / one turn's
/// live posture). The TUI's `EngineHandle` and the headless `exec` both
/// hold a `Session` that points at the same `ThreadId` but with different
/// `SessionId`s.
#[derive(Debug, Clone)]
pub struct Session {
pub session_id: SessionId,
pub thread_id: ThreadId,
/// Model for this session's next turn (may differ from thread default).
pub model: String,
pub workspace: PathBuf,
/// Monotonic `messages_revision` for prefix-cache memoization (carried
/// from the former `Session::messages_revision`).
pub messages_revision: u64,
}
impl Session {
#[must_use]
pub fn new(thread_id: ThreadId, workspace: PathBuf, model: impl Into<String>) -> Self {
Self {
session_id: SessionId::new(),
thread_id,
model: model.into(),
workspace,
messages_revision: 0,
}
}
pub fn bump_revision(&mut self) {
self.messages_revision = self.messages_revision.wrapping_add(1);
}
}
/// Split helper: derive a `Session` from an existing `Thread` without
/// cloning the journal. Headless and TUI call the same constructor so
/// the request shape stays identical.
#[must_use]
pub fn session_for_thread(thread: &Thread, workspace: PathBuf) -> Session {
Session::new(thread.thread_id.clone(), workspace, thread.model.clone())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn thread_and_session_ids_are_distinct_scopes() {
let t = Thread::new(ThreadId::new(), PathBuf::from("/tmp"), "deepseek-v4-flash");
let s1 = Session::new(t.thread_id.clone(), PathBuf::from("/tmp"), &t.model);
let s2 = Session::new(t.thread_id.clone(), PathBuf::from("/tmp"), &t.model);
assert_eq!(s1.thread_id, s2.thread_id);
assert_ne!(s1.session_id, s2.session_id);
}
#[test]
fn leaf_is_moved_not_rewritten() {
let mut t = Thread::new(ThreadId::new(), PathBuf::from("/tmp"), "m");
let a = t.journal.append("header", serde_json::json!({}));
let b = t.journal.append("user", serde_json::json!("b"));
t.leaf_id = t.journal.leaf_id.clone();
assert_eq!(t.leaf_id.as_deref(), Some(b.as_str()));
assert!(t.journal.branch_to(&a));
t.leaf_id = t.journal.leaf_id.clone();
assert_eq!(t.leaf_id.as_deref(), Some(a.as_str()));
assert_eq!(t.journal.len(), 2); // history never rewritten; branching only moved the leaf
}
}
+1 -1
View File
@@ -9,5 +9,5 @@ description = "Execution policy and approval model for Codewhale"
[dependencies]
anyhow.workspace = true
codewhale-protocol = { path = "../protocol", version = "0.9.4" }
codewhale-protocol = { path = "../protocol", version = "0.9.6" }
serde.workspace = true
+2 -2
View File
@@ -11,8 +11,8 @@ description = "Hook dispatch and notifications support for Codewhale"
anyhow.workspace = true
async-trait.workspace = true
chrono.workspace = true
codewhale-protocol = { path = "../protocol", version = "0.9.4" }
codewhale-release = { path = "../release", version = "0.9.4" }
codewhale-protocol = { path = "../protocol", version = "0.9.6" }
codewhale-release = { path = "../release", version = "0.9.6" }
reqwest.workspace = true
serde.workspace = true
serde_json.workspace = true
+1 -1
View File
@@ -10,7 +10,7 @@ description = "Lane registry and Runtime backends for Codewhale workflow instanc
[dependencies]
anyhow.workspace = true
chrono.workspace = true
codewhale-config = { path = "../config", version = "0.9.4" }
codewhale-config = { path = "../config", version = "0.9.6" }
fd-lock = "4.0.4"
serde.workspace = true
serde_json.workspace = true
+140
View File
@@ -0,0 +1,140 @@
//! `EventMsg`-out API in `crates/protocol` (issue #5261).
//!
//! Mirrors `crates/tui/src/core/events::Event` but as a serializable
//! protocol. The TUI's `rx_event` / `Event` channel, the app-server's SSE
//! stream, and the CLI's `stream-json` output all speak this one type so
//! headless and TUI observe byte-identical event shapes for the same `Op`.
use serde::{Deserialize, Serialize};
use serde_json::Value;
use crate::ids::{SessionId, ThreadId};
/// One event emitted by the core engine to every consumer (TUI, CLI,
/// app-server, tests). This is the `EventMsg`-out half of the `Op`-in /
/// `EventMsg`-out contract. It is a straight projection of the existing
/// internal `Event` variants (streaming deltas, tool lifecycle, turn
/// lifecycle, approvals) plus the thread/session ids that `ThreadId` /
/// `SessionId` now make explicit.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "event", rename_all = "snake_case")]
pub enum EventMsg {
TurnStarted {
thread_id: ThreadId,
session_id: SessionId,
turn_id: String,
},
ResponseDelta {
thread_id: ThreadId,
session_id: SessionId,
delta: String,
#[serde(default)]
channel: String,
},
ToolCallStarted {
thread_id: ThreadId,
session_id: SessionId,
tool_call_id: String,
tool_name: String,
input: Value,
},
ToolCallComplete {
thread_id: ThreadId,
session_id: SessionId,
tool_call_id: String,
tool_name: String,
result: Value,
},
TurnComplete {
thread_id: ThreadId,
session_id: SessionId,
turn_id: String,
status: String,
#[serde(skip_serializing_if = "Option::is_none")]
error: Option<String>,
},
TurnUsage {
thread_id: ThreadId,
session_id: SessionId,
input_tokens: u32,
output_tokens: u32,
},
CompactionStarted {
thread_id: ThreadId,
session_id: SessionId,
message: String,
},
CompactionCompleted {
thread_id: ThreadId,
session_id: SessionId,
message: String,
},
Error {
thread_id: ThreadId,
session_id: SessionId,
message: String,
},
}
/// Envelope that carries an `EventMsg` over the wire / channel with a
/// monotonic seq so consumers can detect drops. Mirrors the existing
/// `RuntimeEventEnvelope` but typed to `EventMsg`.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EventEnvelope {
pub seq: u64,
pub thread_id: ThreadId,
pub session_id: SessionId,
pub turn_id: Option<String>,
pub event: EventMsg,
}
impl EventMsg {
#[must_use]
pub fn kind_str(&self) -> &'static str {
match self {
Self::TurnStarted { .. } => "turn_started",
Self::ResponseDelta { .. } => "response_delta",
Self::ToolCallStarted { .. } => "tool_call_started",
Self::ToolCallComplete { .. } => "tool_call_complete",
Self::TurnComplete { .. } => "turn_complete",
Self::TurnUsage { .. } => "turn_usage",
Self::CompactionStarted { .. } => "compaction_started",
Self::CompactionCompleted { .. } => "compaction_completed",
Self::Error { .. } => "error",
}
}
#[must_use]
pub fn thread_id(&self) -> &ThreadId {
match self {
Self::TurnStarted { thread_id, .. }
| Self::ResponseDelta { thread_id, .. }
| Self::ToolCallStarted { thread_id, .. }
| Self::ToolCallComplete { thread_id, .. }
| Self::TurnComplete { thread_id, .. }
| Self::TurnUsage { thread_id, .. }
| Self::CompactionStarted { thread_id, .. }
| Self::CompactionCompleted { thread_id, .. }
| Self::Error { thread_id, .. } => thread_id,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn event_msg_roundtrip() {
let msg = EventMsg::TurnComplete {
thread_id: ThreadId::new(),
session_id: SessionId::new(),
turn_id: "turn-1".into(),
status: "completed".into(),
error: None,
};
let json = serde_json::to_string(&msg).unwrap();
let back: EventMsg = serde_json::from_str(&json).unwrap();
assert_eq!(back.kind_str(), "turn_complete");
}
}
+161
View File
@@ -0,0 +1,161 @@
//! Typed `ThreadId` / `SessionId` for the `crates/core` boundary (issue #5261).
//!
//! `codewhale`'s `Session` is really a thread. The new boundary introduces
//! two ids so every consumer — TUI, CLI, app-server, tests — can name the
//! right scope:
//! - `ThreadId` — long-lived conversation (persisted in `state.json` / `threads/`)
//! - `SessionId` — one turn/session within a thread (ephemeral engine handle)
//!
//! Both are thin wrappers around the existing `"thread-…"` string id so the
//! persisted JSON shape stays unchanged. They serialize as plain strings,
//! deserialize from plain strings or `{ "id": "…" }`, and parse from either.
use std::fmt;
use std::str::FromStr;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
/// Long-lived conversation id. Backwards compatible with the existing
/// `thread-{uuid}` string form used in `crates/state` and `runtime_threads`.
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct ThreadId(pub String);
impl ThreadId {
#[must_use]
pub fn new() -> Self {
Self(format!("thread-{}", Uuid::new_v4()))
}
#[must_use]
pub fn from_string(s: impl Into<String>) -> Self {
Self(s.into())
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
#[must_use]
pub fn into_string(self) -> String {
self.0
}
}
impl Default for ThreadId {
fn default() -> Self {
Self::new()
}
}
impl fmt::Display for ThreadId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
impl From<String> for ThreadId {
fn from(s: String) -> Self {
Self(s)
}
}
impl From<ThreadId> for String {
fn from(id: ThreadId) -> Self {
id.0
}
}
impl FromStr for ThreadId {
type Err = std::convert::Infallible;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(Self(s.to_string()))
}
}
/// One engine session within a thread (a single `Op` turn or a supervised
/// engine lifetime). Distinct from `ThreadId` so the thread manager can
/// start a session with no TUI attached and so tests can assert headless
/// == TUI byte-identical requests for the same `ThreadId` + `SessionId` pair.
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct SessionId(pub String);
impl SessionId {
#[must_use]
pub fn new() -> Self {
Self(format!("session-{}", Uuid::new_v4()))
}
#[must_use]
pub fn from_string(s: impl Into<String>) -> Self {
Self(s.into())
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
#[must_use]
pub fn into_string(self) -> String {
self.0
}
}
impl Default for SessionId {
fn default() -> Self {
Self::new()
}
}
impl fmt::Display for SessionId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
impl From<String> for SessionId {
fn from(s: String) -> Self {
Self(s)
}
}
impl From<SessionId> for String {
fn from(id: SessionId) -> Self {
id.0
}
}
impl FromStr for SessionId {
type Err = std::convert::Infallible;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(Self(s.to_string()))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn thread_id_roundtrip() {
let id = ThreadId::new();
let s = id.to_string();
assert!(s.starts_with("thread-"));
let parsed: ThreadId = s.parse().unwrap();
assert_eq!(parsed.as_str(), id.as_str());
}
#[test]
fn session_id_display() {
let id = SessionId::from_string("session-abc");
assert_eq!(format!("{id}"), "session-abc");
let json = serde_json::to_string(&id).unwrap();
assert_eq!(json, "\"session-abc\"");
let back: SessionId = serde_json::from_str(&json).unwrap();
assert_eq!(back, id);
}
}
+168
View File
@@ -0,0 +1,168 @@
//! Session tree journal placeholder (issue #5262).
//!
//! The journal is append-only with an in-memory tree projection. Every
//! non-header entry carries `id` + `parentId`; the active position is a
//! `leafId`; appending creates a child of the leaf; branching only moves the
//! leaf — it never rewrites history. This file lands the *entry shape* that
//! #5262's tree operations (`/tree`, `/branch`, `/fork`, `/resume`) and the
//! deferred compaction/branch-summary entry kinds hang off of. The strategies
//! themselves are deferred, but the shape must be stable now so no migration
//! is needed later.
use serde::{Deserialize, Serialize};
use serde_json::Value;
/// One journal entry. All entries except the root header have `id` + `parent_id`.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct JournalEntry {
/// Stable entry id (`entry-{uuid}`).
pub id: String,
/// Parent entry id; `None` only for the root header.
#[serde(skip_serializing_if = "Option::is_none")]
pub parent_id: Option<String>,
/// Entry kind (`header`, `user`, `assistant`, `tool_result`, `compaction`, `branch_summary`, …).
pub kind: String,
/// Payload (text, tool output, compaction summary, etc).
#[serde(default)]
pub payload: Value,
/// When the entry was created (unix seconds).
pub created_at: i64,
}
/// Append-only journal with a `leafId` cursor. The tree projection is
/// derived root→leaf; moving `leaf_id` branches without rewriting history.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
pub struct Journal {
pub entries: Vec<JournalEntry>,
/// Active position. `None` before the header is appended.
#[serde(skip_serializing_if = "Option::is_none")]
pub leaf_id: Option<String>,
}
impl Journal {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn len(&self) -> usize {
self.entries.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
/// Append a new entry as a child of the current leaf. Returns the new id.
pub fn append(&mut self, kind: impl Into<String>, payload: Value) -> String {
let id = format!("entry-{}", uuid::Uuid::new_v4());
let parent_id = self.leaf_id.clone();
let entry = JournalEntry {
id: id.clone(),
parent_id,
kind: kind.into(),
payload,
created_at: chrono::Utc::now().timestamp(),
};
self.entries.push(entry);
self.leaf_id = Some(id.clone());
id
}
/// Branch: move `leaf_id` to an existing ancestor without rewriting.
/// Returns `false` when `target` is not found.
pub fn branch_to(&mut self, target: &str) -> bool {
if self.entries.iter().any(|e| e.id == target) {
self.leaf_id = Some(target.to_string());
true
} else {
false
}
}
/// Project the active path root→leaf as a slice of entries in order.
#[must_use]
pub fn active_path(&self) -> Vec<&JournalEntry> {
let Some(leaf) = self.leaf_id.as_deref() else {
return Vec::new();
};
// Build id→parent map for walk.
let mut by_id = std::collections::HashMap::new();
for e in &self.entries {
by_id.insert(e.id.as_str(), e);
}
let mut path = Vec::new();
let mut cur: Option<&str> = Some(leaf);
while let Some(id) = cur {
if let Some(entry) = by_id.get(id) {
path.push(*entry);
cur = entry.parent_id.as_deref();
} else {
break;
}
}
path.reverse();
path
}
/// Find entry by id.
#[must_use]
pub fn get(&self, id: &str) -> Option<&JournalEntry> {
self.entries.iter().find(|e| e.id == id)
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn append_sets_parent_and_leaf() {
let mut j = Journal::new();
let a = j.append("header", json!({}));
assert_eq!(j.leaf_id.as_deref(), Some(a.as_str()));
let b = j.append("user", json!("hi"));
let entry = j.get(&b).unwrap();
assert_eq!(entry.parent_id.as_deref(), Some(a.as_str()));
assert_eq!(j.leaf_id.as_deref(), Some(b.as_str()));
}
#[test]
fn branching_only_moves_leaf() {
let mut j = Journal::new();
let a = j.append("header", json!({}));
let b = j.append("user", json!("b"));
let c = j.append("assistant", json!("c"));
assert_eq!(j.entries.len(), 3);
assert!(j.branch_to(&b));
assert_eq!(j.leaf_id.as_deref(), Some(b.as_str()));
// History untouched.
assert_eq!(j.entries.len(), 3);
// Active path is now a→b.
let path = j.active_path();
assert_eq!(path.len(), 2);
assert_eq!(path[0].id, a);
assert_eq!(path[1].id, b);
let d = j.append("user", json!("d after branch"));
let ent = j.get(&d).unwrap();
assert_eq!(ent.parent_id.as_deref(), Some(b.as_str()));
// Old c still exists as a sibling branch that is no longer on the active path.
assert!(j.get(&c).is_some());
let path2 = j.active_path();
assert_eq!(path2.len(), 3);
assert_eq!(path2[2].id, d);
}
#[test]
fn journal_is_serializable_and_preserves_shape() {
let mut j = Journal::new();
j.append("header", json!({}));
j.append("user", json!("hello"));
let s = serde_json::to_string(&j).unwrap();
let back: Journal = serde_json::from_str(&s).unwrap();
assert_eq!(back, j);
}
}
+4
View File
@@ -4,7 +4,11 @@ use serde::{Deserialize, Serialize};
use serde_json::Value;
pub mod agent_run;
pub mod event_msg;
pub mod fleet;
pub mod ids;
pub mod journal;
pub mod op;
pub mod runtime;
pub mod workroom;
+171
View File
@@ -0,0 +1,171 @@
//! `Op`-in API in `crates/protocol` (issue #5261).
//!
//! The TUI engine already had an internal channel (`Op` in
//! `crates/tui/src/core/ops.rs` with `tx_op` / `rx_op` and `tx_steer`).
//! This protocol file formalizes that channel so TUI, CLI, app-server, and
//! tests share one serializable API. The wire is `OpEnvelope` + `Op`;
//! transports that already speak JSON (app-server, tests) can send the
//! envelope directly, while in-process callers continue to use the typed
//! enum.
use serde::{Deserialize, Serialize};
use serde_json::Value;
use crate::ids::{SessionId, ThreadId};
/// Every `Op` is paired with the ids that route it. This is the
/// `Op`-in half of the `Op`-in / `EventMsg`-out contract.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OpEnvelope {
/// Monotonic `op:<n>` for dedup / tracing within a session.
pub op_id: String,
pub thread_id: ThreadId,
pub session_id: SessionId,
pub op: Op,
}
/// Operations that can be submitted to the core engine. This is the
/// protocol view of `crates/tui/src/core/ops::Op` — same lifecycle,
/// same provenance gate — but serializable and free of `mpsc` / `oneshot`
/// fields. In-process callers convert at the boundary; out-of-process
/// callers send the JSON directly.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum Op {
/// Drive one model turn: `role=user` content plus the resolved route
/// receipt the engine will freeze at the client-freeze boundary. Headless
/// and TUI must produce byte-identical `MessageRequest`s for identical
/// `Op::SendMessage` payloads.
SendMessage {
content: String,
/// Effective mode for this turn (`"plan" | "agent" | "operate"` etc).
#[serde(default = "default_mode")]
mode: String,
/// Optional explicit route/model the caller resolved already (mirrors
/// `ResolvedRuntimeRoute` in `crates_tui::route_runtime`). `None` means
/// "use the thread's current route".
#[serde(skip_serializing_if = "Option::is_none")]
model: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
model_provider: Option<String>,
/// Tool restriction from slash-command frontmatter.
#[serde(default)]
allowed_tools: Option<Vec<String>>,
/// Runtime-supplied dynamic tools for this turn only.
#[serde(default)]
dynamic_tools: Vec<Value>,
/// Structural input provenance — only `ExternalUser` may inherit
/// YOLO/auto-approval authority (mirrors `UserInputProvenance`).
#[serde(default = "default_provenance")]
provenance: String,
},
/// Steer an in-flight turn with additional user content (drains into
/// the turn loop's `rx_steer` channel).
Steer {
content: String,
},
/// Re-check and dispatch a goal continuation (synthetic turn that
/// continues the same logical goal run).
ContinueGoal,
/// Execute a local composer shell command without a model turn.
RunShellCommand {
command: String,
},
/// Set goal status without dispatching a model turn.
SetGoalStatus {
status: String,
#[serde(default)]
clear: bool,
},
Cancel,
Shutdown,
/// Describe the exact request the next turn would send without sending it
/// (`/dryrun` / `/preview-request`, #1004). Headless and TUI must render
/// identical manifests for identical inputs.
PreviewOutboundRequest {
#[serde(default)]
json: bool,
#[serde(default)]
base_prompt_only: bool,
},
}
fn default_mode() -> String {
"agent".to_string()
}
fn default_provenance() -> String {
"external_user".to_string()
}
impl Op {
#[must_use]
pub fn is_send_message(&self) -> bool {
matches!(self, Self::SendMessage { .. })
}
#[must_use]
pub fn kind_str(&self) -> &'static str {
match self {
Self::SendMessage { .. } => "send_message",
Self::Steer { .. } => "steer",
Self::ContinueGoal => "continue_goal",
Self::RunShellCommand { .. } => "run_shell_command",
Self::SetGoalStatus { .. } => "set_goal_status",
Self::Cancel => "cancel",
Self::Shutdown => "shutdown",
Self::PreviewOutboundRequest { .. } => "preview_outbound_request",
}
}
}
/// Build a headless `SendMessage` envelope with fresh ids. This is the
/// one-line helper every headless caller (CLI `exec`, app-server, tests)
/// uses so TUI and headless start a session identically.
#[must_use]
pub fn headless_send_message_op(thread_id: ThreadId, content: impl Into<String>) -> OpEnvelope {
OpEnvelope {
op_id: format!("op-{}", uuid::Uuid::new_v4()),
thread_id: thread_id.clone(),
session_id: SessionId::new(),
op: Op::SendMessage {
content: content.into(),
mode: default_mode(),
model: None,
model_provider: None,
allowed_tools: None,
dynamic_tools: Vec::new(),
provenance: default_provenance(),
},
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn op_envelope_roundtrip() {
let env = headless_send_message_op(ThreadId::new(), "hello");
let json = serde_json::to_string(&env).unwrap();
let back: OpEnvelope = serde_json::from_str(&json).unwrap();
assert_eq!(back.thread_id, env.thread_id);
assert!(back.op.is_send_message());
}
#[test]
fn steer_roundtrip() {
let op = Op::Steer {
content: "more".into(),
};
let json = serde_json::to_string(&op).unwrap();
let back: Op = serde_json::from_str(&json).unwrap();
assert_eq!(back.kind_str(), "steer");
}
}
+25
View File
@@ -59,6 +59,25 @@ pub struct RuntimeCapabilities {
pub fleet_event_stream: bool,
#[serde(default)]
pub fleet_local_target: bool,
/// `GET/PUT/DELETE /v1/threads/{id}/goal` and the `complete`/`block`
/// lifecycle actions are available.
#[serde(default)]
pub thread_goals: bool,
/// `GET /v1/memory` and `GET /v1/memory/{id}` are available for
/// bounded inspection of the native memory store. `POST /v1/memory`
/// and `DELETE /v1/memory` are also available (auth-gated via the
/// standard route layer) for lifecycle controls.
#[serde(default)]
pub memory: bool,
/// Whether the runtime supports create/update/enable/disable/reconnect/delete
/// operations on MCP server configuration via the `POST|GET|PATCH|DELETE
/// /v1/apps/mcp/servers` family of endpoints.
#[serde(default)]
pub mcp_server_management: bool,
/// Skill lifecycle operations (install, update, uninstall, trust, audit)
/// are available via the HTTP API.
#[serde(default)]
pub skill_lifecycle: bool,
}
/// Experimental opt-in flags advertised by `GET /v1/runtime/info`.
@@ -355,6 +374,10 @@ mod tests {
fleet_event_replay: true,
fleet_event_stream: true,
fleet_local_target: true,
thread_goals: true,
memory: true,
mcp_server_management: false,
skill_lifecycle: false,
};
let value = serde_json::to_value(&caps).unwrap();
let obj = value.as_object().unwrap();
@@ -364,6 +387,8 @@ mod tests {
assert!(obj.contains_key("worker_runtime"));
assert_eq!(obj.get("fleet_run_create").unwrap(), &json!(true));
assert_eq!(obj.get("fleet_event_stream").unwrap(), &json!(true));
assert_eq!(obj.get("thread_goals").unwrap(), &json!(true));
assert_eq!(obj.get("memory").unwrap(), &json!(true));
}
#[test]
+6 -6
View File
@@ -27,7 +27,7 @@ use serde::{Deserialize, Serialize};
pub const UPDATE_CHECK_CACHE_FILE: &str = "update-check.json";
/// Default hours between network update checks.
pub const DEFAULT_CHECK_INTERVAL_HOURS: u64 = 24;
pub const DEFAULT_CHECK_INTERVAL_HOURS: u64 = 1;
/// Explicit opt-out, and the `update-notifier` convention many CLIs honour.
const OPT_OUT_ENV: &[&str] = &["CODEWHALE_NO_UPDATE_CHECK", "NO_UPDATE_NOTIFIER"];
@@ -185,13 +185,13 @@ mod tests {
checked_at_unix: 1_000_000,
latest_tag: Some("v0.9.5".to_string()),
};
// 23h later: still fresh on a 24h interval.
assert!(entry.is_fresh(1_000_000 + 23 * 3600, 24));
// 25h later: stale.
assert!(!entry.is_fresh(1_000_000 + 25 * 3600, 24));
// One second before the one-hour boundary: still fresh.
assert!(entry.is_fresh(1_000_000 + 3599, 1));
// One second beyond the boundary: stale.
assert!(!entry.is_fresh(1_000_000 + 3601, 1));
// Exactly at the boundary counts as stale, so the interval is a true
// upper bound on cache age.
assert!(!entry.is_fresh(1_000_000 + 24 * 3600, 24));
assert!(!entry.is_fresh(1_000_000 + 3600, 1));
}
#[test]
+14 -7
View File
@@ -47,7 +47,7 @@ pub const LEGACY_UPDATE_VERSION_ENV: &str = "DEEPSEEK_VERSION";
/// User-Agent header sent with release metadata requests.
pub const UPDATE_USER_AGENT: &str = "codewhale-updater";
const CNB_RELEASE_ASSET_BASE: &str = "https://cnb.cool/Hmbown/CodeWhale/-/releases";
const CNB_RELEASE_ASSET_BASE: &str = "https://cnb.cool/codewhale.net/codewhale/-/releases/download";
const RELEASE_METADATA_TIMEOUT: Duration = Duration::from_secs(5);
/// Build a reqwest client builder with the TLS roots appropriate for the
@@ -198,12 +198,11 @@ pub fn update_network_fallback_hint() -> String {
format!(
"GitHub release downloads may be blocked or slow on this network.\n\
For mainland China, use one of these fallback paths:\n\
1. Source build from the CNB mirror, installing both shipped binaries:\n\
1. Source build from the CNB mirror, installing the shipped binary:\n\
cargo install --git {CNB_REPO_URL} --tag vX.Y.Z codewhale-cli --locked --force\n\
cargo install --git {CNB_REPO_URL} --tag vX.Y.Z codewhale-tui --locked --force\n\
2. Use a binary asset mirror:\n\
{RELEASE_BASE_URL_ENV}=https://<mirror>/<release-assets>/ {UPDATE_VERSION_ENV}=X.Y.Z codewhale update\n\
The mirror directory must contain {CHECKSUM_MANIFEST_ASSET} and the platform binaries."
The mirror directory must contain {CHECKSUM_MANIFEST_ASSET} and the codewhale platform binary."
)
}
@@ -564,7 +563,7 @@ mod tests {
assert_eq!(
release_base_url_from_env("v1.2.3"),
Some("https://cnb.cool/Hmbown/CodeWhale/-/releases/v1.2.3".to_string())
Some("https://cnb.cool/codewhale.net/codewhale/-/releases/download/v1.2.3".to_string())
);
set_release_env(RELEASE_BASE_URL_ENV, "https://explicit.example.com");
@@ -620,6 +619,14 @@ mod tests {
hint.contains(CHECKSUM_MANIFEST_ASSET),
"hint missing CHECKSUM_MANIFEST_ASSET"
);
assert!(
hint.contains("codewhale platform binary"),
"hint must describe the sole implementation asset"
);
assert!(
!hint.contains("codewhale-tui"),
"hint must not request the removed TUI implementation asset"
);
}
#[test]
@@ -733,11 +740,11 @@ mod tests {
fn cnb_release_base_url_includes_tag_directory() {
assert_eq!(
cnb_release_base_url("0.8.47"),
"https://cnb.cool/Hmbown/CodeWhale/-/releases/v0.8.47"
"https://cnb.cool/codewhale.net/codewhale/-/releases/download/v0.8.47"
);
assert_eq!(
cnb_release_base_url("v0.8.47"),
"https://cnb.cool/Hmbown/CodeWhale/-/releases/v0.8.47"
"https://cnb.cool/codewhale.net/codewhale/-/releases/download/v0.8.47"
);
}
+1 -1
View File
@@ -8,7 +8,7 @@ repository.workspace = true
description = "Secret storage backends for Codewhale, with OS keyring and file fallback"
[dependencies]
codewhale-paths = { path = "../paths", version = "0.9.4" }
codewhale-paths = { path = "../paths", version = "0.9.6" }
chrono.workspace = true
serde = { workspace = true }
serde_json = { workspace = true }
+2 -2
View File
@@ -10,8 +10,8 @@ description = "Session/thread persistence and recovery model for Codewhale"
[dependencies]
anyhow.workspace = true
chrono.workspace = true
codewhale-paths = { path = "../paths", version = "0.9.4" }
codewhale-protocol = { path = "../protocol", version = "0.9.4" }
codewhale-paths = { path = "../paths", version = "0.9.6" }
codewhale-protocol = { path = "../protocol", version = "0.9.6" }
fd-lock = "4.0.4"
rusqlite.workspace = true
serde.workspace = true
+6 -6
View File
@@ -5,14 +5,14 @@ edition.workspace = true
rust-version.workspace = true
license.workspace = true
repository.workspace = true
description = "Opt-in product telemetry client for Codewhale"
description = "Anonymous, user-disableable product usage counting for Codewhale"
[dependencies]
anyhow.workspace = true
chrono.workspace = true
codewhale-config = { path = "../config", version = "0.9.4" }
codewhale-paths = { path = "../paths", version = "0.9.4" }
codewhale-release = { path = "../release", version = "0.9.4" }
codewhale-config = { path = "../config", version = "0.9.6" }
codewhale-paths = { path = "../paths", version = "0.9.6" }
codewhale-release = { path = "../release", version = "0.9.6" }
fd-lock = "4.0.4"
reqwest = { workspace = true, features = ["blocking"] }
serde.workspace = true
@@ -22,9 +22,9 @@ tracing.workspace = true
uuid.workspace = true
[build-dependencies]
codewhale-build-support = { path = "../build-support", version = "0.9.4" }
codewhale-build-support = { path = "../build-support", version = "0.9.6" }
[dev-dependencies]
# Used as an *assertion*, never as a filter: every string leaf of a serialized
# batch is run through `redact_for_disclosure` and must come back unredacted.
codewhale-workflow = { path = "../workflow", version = "0.9.4" }
codewhale-workflow = { path = "../workflow", version = "0.9.6" }
-10
View File
@@ -29,7 +29,6 @@ pub const BATCH_MAX_BYTES: usize = 64 * 1024;
pub(crate) enum Message {
Event(Box<Event>),
Flush(SyncSender<FlushOutcome>),
Shutdown(SyncSender<FlushOutcome>),
}
@@ -85,11 +84,6 @@ impl Handle {
let _ = self.tx.send(Message::Event(Box::new(event)));
}
/// Ask for a flush and wait at most `deadline` for the answer.
pub(crate) fn flush(&self, deadline: Duration) -> FlushOutcome {
self.round_trip(deadline, Message::Flush)
}
/// Ask for a final flush and stop the thread.
pub(crate) fn shutdown(&self, deadline: Duration) -> FlushOutcome {
self.round_trip(deadline, Message::Shutdown)
@@ -123,10 +117,6 @@ fn run(context: &Context, rx: &Receiver<Message>) {
append(context, &event);
None
}
Message::Flush(ack) => {
let _ = ack.send(flush(context));
None
}
Message::Shutdown(ack) => {
let _ = ack.send(flush(context));
Some(())
+134 -89
View File
@@ -12,20 +12,18 @@
//! | `install_id.json` | the random install id |
//! | `disabled` | the tombstone: present ⇒ nothing is appended, drained, or sent |
//!
//! **Appends never take a lock.** One `O_APPEND` `write(2)` under `PIPE_BUF` is
//! atomic on every filesystem this ships to, and taking `fd_lock` here would be
//! a *blocking* acquisition on the panic hook and the SIGINT path. `flock` is
//! per-fd within a process, so an actor panic while holding the compaction lock
//! would self-deadlock the hook — `catch_unwind` runs *after* the hook, so it
//! cannot save this — and a second Codewhale process sharing `CODEWHALE_HOME`
//! would hang Ctrl-C, breaking the second-signal contract in `main.rs`.
//! Appends, compaction, delivery, identity/state writes, startup arming, and
//! wipe share one sibling lock. Runtime and exit paths take it with
//! `try_write()`: on contention the event or batch is dropped. Only startup
//! arming and the user-requested wipe may wait. That keeps the panic hook and
//! SIGINT path non-blocking while also making opt-out an ordering boundary —
//! after wipe returns, no pre-wipe writer or sender can still publish data.
//!
//! Compaction is the only lock holder and uses `try_write()`: on contention it
//! skips this cycle. Appenders re-open per append, so a compaction rewrite
//! cannot leave anyone writing to a stale inode.
//! Appenders re-open per append, so a compaction rewrite cannot leave anyone
//! writing to a stale inode.
use std::fs::{self, DirBuilder, File, OpenOptions};
use std::io::Write as _;
use std::io::{Read as _, Write as _};
use std::path::{Path, PathBuf};
use anyhow::{Context, Result};
@@ -37,6 +35,16 @@ pub const MAX_BYTES: u64 = 256 * 1024;
/// A single append must fit in one atomic `write(2)`.
pub const MAX_LINE_BYTES: usize = 4096;
/// Exact contents of the opt-out tombstone observed by a consent decision.
///
/// A fresh nonce is written when a machine transitions into an opted-out
/// period. Arming may clear only the exact generation its decision observed,
/// so an older consent token cannot erase a newer opt-out.
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct TombstoneGeneration(Vec<u8>);
const MAX_TOMBSTONE_BYTES: u64 = 128;
/// Below this size a sink cannot possibly hold [`MAX_EVENTS`] lines, so an
/// append skips the count probe entirely. The shortest serializable event line
/// is well over 8 bytes, and `512 * 9 > 4096`, so this bound is safe by
@@ -94,6 +102,29 @@ pub fn tombstone_present(root: &Path) -> bool {
tombstone_path(root).exists()
}
/// Read the exact tombstone generation, or `None` when collection has never
/// been disabled in this home.
pub(crate) fn tombstone_generation(root: &Path) -> Result<Option<TombstoneGeneration>> {
let path = tombstone_path(root);
let file = match File::open(&path) {
Ok(file) => file,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(None),
Err(error) => {
return Err(
anyhow::Error::new(error).context(format!("failed to open {}", path.display()))
);
}
};
let mut bytes = Vec::new();
file.take(MAX_TOMBSTONE_BYTES + 1)
.read_to_end(&mut bytes)
.with_context(|| format!("failed to read {}", path.display()))?;
if bytes.len() as u64 > MAX_TOMBSTONE_BYTES {
anyhow::bail!("{} exceeds the tombstone size limit", path.display());
}
Ok(Some(TombstoneGeneration(bytes)))
}
/// Create the telemetry directory `0700`, if it is missing.
pub fn ensure_dir(root: &Path) -> Result<()> {
if root.is_dir() {
@@ -125,38 +156,12 @@ fn secure(_file: &File) -> Result<()> {
/// Append one serialized event or batch to `path`.
///
/// Returns `None` — never an error — when the tombstone is present, when the
/// line would not fit in one atomic write, or when any filesystem step fails.
/// Telemetry is fail-open by construction: it never returns an error to a
/// caller and never blocks a turn, a tool, or process exit.
/// Returns `None` — never an error — when the tombstone is present, the privacy
/// lock is held, the line would not fit in one atomic write, or any filesystem
/// step fails. The lock acquisition is non-blocking, including on the panic and
/// signal paths.
pub fn append(root: &Path, path: &Path, line: &str) -> Option<()> {
if tombstone_present(root) {
return None;
}
let bytes = line.as_bytes();
if bytes.is_empty() || bytes.len() + 1 > MAX_LINE_BYTES {
return None;
}
ensure_dir(root).ok()?;
let mut buf = Vec::with_capacity(bytes.len() + 1);
buf.extend_from_slice(bytes);
buf.push(b'\n');
let file = OpenOptions::new()
.create(true)
.append(true)
.open(path)
.ok()?;
secure(&file).ok()?;
// One `write(2)`, not `write_fmt` and not two calls: a split write is what
// a concurrent appender would interleave with.
(&file).write_all(&buf).ok()?;
file.sync_data().ok()?;
drop(file);
enforce_ring(root, path);
Some(())
append_with_limit(root, path, line, MAX_LINE_BYTES)
}
/// Append a line that is too large for one atomic `write(2)`, serialising
@@ -167,47 +172,52 @@ pub fn append(root: &Path, path: &Path, line: &str) -> Option<()> {
/// handler, so a **non-blocking** `try_write` is safe there. On contention the
/// batch is dropped, which is the same fail-open behavior as a failed POST.
pub fn append_locked(root: &Path, path: &Path, line: &str) -> Option<()> {
if tombstone_present(root) {
return None;
}
append_with_limit(root, path, line, MAX_BYTES as usize)
}
fn append_with_limit(root: &Path, path: &Path, line: &str, limit: usize) -> Option<()> {
let bytes = line.as_bytes();
if bytes.is_empty() || bytes.len() as u64 + 1 > MAX_BYTES {
if bytes.is_empty() || bytes.len() + 1 > limit {
return None;
}
ensure_dir(root).ok()?;
let mut buf = Vec::with_capacity(bytes.len() + 1);
buf.extend_from_slice(bytes);
buf.push(b'\n');
let wrote = try_with_lock(root, || {
if tombstone_present(root) {
return Ok(false);
}
let file = OpenOptions::new()
.create(true)
.append(true)
.open(path)
.with_context(|| format!("failed to open {}", path.display()))?;
secure(&file)?;
(&file)
.write_all(&buf)
.with_context(|| format!("failed to append to {}", path.display()))?;
file.sync_data()
.with_context(|| format!("failed to sync {}", path.display()))?;
Ok(true)
})
.ok()
.flatten()
.unwrap_or(false);
let wrote = try_with_lock(root, || append_under_lock(root, path, &buf))
.ok()
.flatten()
.unwrap_or(false);
if !wrote {
return None;
}
enforce_ring(root, path);
Some(())
}
/// Append bytes while the caller holds the sibling privacy lock.
fn append_under_lock(root: &Path, path: &Path, buf: &[u8]) -> Result<bool> {
if tombstone_present(root) {
return Ok(false);
}
let file = OpenOptions::new()
.create(true)
.append(true)
.open(path)
.with_context(|| format!("failed to open {}", path.display()))?;
secure(&file)?;
// One `write(2)`, not `write_fmt` and not two calls: a split write is what
// a concurrent appender would interleave with.
(&file)
.write_all(buf)
.with_context(|| format!("failed to append to {}", path.display()))?;
file.sync_data()
.with_context(|| format!("failed to sync {}", path.display()))?;
Ok(true)
}
/// Keep the newest [`MAX_EVENTS`] lines and at most [`MAX_BYTES`], under the
/// compaction lock. On lock contention this cycle is skipped: the next append
/// tries again, and the cap is a ceiling on disk footprint, not an invariant
@@ -220,15 +230,25 @@ fn enforce_ring(root: &Path, path: &Path) {
if len < PROBE_BYTES {
return;
}
let Ok(contents) = fs::read_to_string(path) else {
return;
};
let lines: Vec<&str> = contents.lines().filter(|l| !l.trim().is_empty()).collect();
if lines.len() <= MAX_EVENTS && len <= MAX_BYTES {
return;
}
let _ = try_with_lock(root, || {
// Re-read under the same lock as wipe. Reusing a snapshot captured
// before a concurrent wipe would resurrect the records it truncated.
if tombstone_present(root) {
return Ok(());
}
let Ok(meta) = fs::metadata(path) else {
return Ok(());
};
let len = meta.len();
if len < PROBE_BYTES {
return Ok(());
}
let contents = fs::read_to_string(path)
.with_context(|| format!("failed to read {}", path.display()))?;
let lines: Vec<&str> = contents.lines().filter(|l| !l.trim().is_empty()).collect();
if lines.len() <= MAX_EVENTS && len <= MAX_BYTES {
return Ok(());
}
let mut kept: Vec<&str> = lines
.iter()
.rev()
@@ -383,14 +403,26 @@ pub fn truncate(path: &Path) -> Result<()> {
pub fn wipe(root: &Path) -> Result<()> {
with_lock(root, || {
let tombstone = tombstone_path(root);
let file = OpenOptions::new()
.create(true)
.write(true)
.truncate(true)
.open(&tombstone)
.with_context(|| format!("failed to write {}", tombstone.display()))?;
secure(&file)?;
drop(file);
// A tombstone generation identifies one durable opted-out period. Keep
// it stable across later launches that re-observe the same persistent
// choice; re-enable removes the file, so the next real opt-out creates
// a naturally distinct generation. An unreadable or oversized file is
// repaired in the fail-closed direction by replacing it here; legacy
// empty tombstones remain valid stable generations.
if tombstone_generation(root).ok().flatten().is_none() {
let mut file = OpenOptions::new()
.create(true)
.write(true)
.truncate(true)
.open(&tombstone)
.with_context(|| format!("failed to write {}", tombstone.display()))?;
secure(&file)?;
file.write_all(uuid::Uuid::new_v4().to_string().as_bytes())
.with_context(|| format!("failed to write {}", tombstone.display()))?;
file.sync_data()
.with_context(|| format!("failed to sync {}", tombstone.display()))?;
drop(file);
}
let mut failure: Option<anyhow::Error> = None;
for path in [buffer_path(root), dryrun_path(root)] {
@@ -415,14 +447,27 @@ pub fn wipe(root: &Path) -> Result<()> {
})
}
/// Clear the tombstone and drop anything buffered before consent.
/// Clear the tombstone and drop anything buffered before this process was
/// permitted.
///
/// Called by `init` on every arming. No event recorded before the user said yes
/// can be in the batch that follows it — a stale buffer left by an earlier
/// consenting run, or by a bug, is not evidence of this user's consent.
pub fn arm(root: &Path) -> Result<()> {
/// Called by `init` on every arming. Both the exact tombstone generation and a
/// fresh durable permission check must still match while the wipe lock is held.
/// A stale buffer left by an earlier run or by a bug cannot enter the new
/// process's batch.
pub(crate) fn arm(
root: &Path,
observed_generation: Option<&TombstoneGeneration>,
permission_still_enabled: impl FnOnce() -> bool,
) -> Result<()> {
ensure_dir(root)?;
with_lock(root, || {
let current_generation = tombstone_generation(root)?;
if current_generation.as_ref() != observed_generation {
anyhow::bail!("telemetry permission changed before arming");
}
if !permission_still_enabled() {
anyhow::bail!("telemetry permission is no longer enabled");
}
let tombstone = tombstone_path(root);
if tombstone.exists() {
fs::remove_file(&tombstone)
+24 -8
View File
@@ -3,8 +3,8 @@
//! The shipped default endpoint is `codewhale_config::DEFAULT_TELEMETRY_ENDPOINT`,
//! the first-party ingest service documented in `docs/TELEMETRY.md`. That
//! default decides only *where* a batch goes, never *whether* one exists: this
//! module is reached only by a session that resolved telemetry on, which
//! requires the first-run notice to have been answered with Enable.
//! module is reached only by a session that resolved telemetry on after every
//! persistent and run-scoped opt-out was applied.
//!
//! `None` here is the dry-run sink, reachable by configuring an empty endpoint:
//! batches are serialized with the same serializer a real endpoint would see and
@@ -35,12 +35,20 @@ pub enum SendOutcome {
/// Serialize and deliver one batch.
///
/// The tombstone is re-checked immediately before delivery, so a wipe that
/// landed while the batch was being assembled still stops it.
/// Network delivery holds the same non-blocking privacy lock as appends and
/// wipe. A wipe waits for an already-started POST to finish; a POST that races
/// a held or completed wipe is dropped before reaching the wire. Therefore no
/// delivery can remain in flight after persistent opt-out returns.
pub fn send(root: &Path, endpoint: Option<&str>, batch: &Batch) -> SendOutcome {
if buffer::tombstone_present(root) {
return SendOutcome::Dropped;
}
send_with_transport(root, endpoint, batch, post)
}
pub(crate) fn send_with_transport(
root: &Path,
endpoint: Option<&str>,
batch: &Batch,
transport: impl FnOnce(&str, &str, String) -> SendOutcome,
) -> SendOutcome {
let Ok(body) = serde_json::to_string(batch) else {
return SendOutcome::Dropped;
};
@@ -52,7 +60,15 @@ pub fn send(root: &Path, endpoint: Option<&str>, batch: &Batch) -> SendOutcome {
None => SendOutcome::Dropped,
}
}
Some(endpoint) => post(endpoint, &batch.app_version, body),
Some(endpoint) => buffer::try_with_lock(root, || {
if buffer::tombstone_present(root) {
return Ok(SendOutcome::Dropped);
}
Ok(transport(endpoint, &batch.app_version, body))
})
.ok()
.flatten()
.unwrap_or(SendOutcome::Dropped),
}
}
+157 -50
View File
@@ -1,16 +1,16 @@
//! The one place that decides whether anything may be collected, and the token
//! that makes that decision unforgeable.
//! The one place that decides whether anonymous usage counting may run, and the
//! token that makes that decision unforgeable.
//!
//! Every emitting surface calls [`decide`] and then, if and only if it gets
//! [`TelemetryDecision::Enabled`], hands the contained [`TelemetryConsent`] to
//! [`crate::init`]. `TelemetryConsent` has no `Default`, no public constructor,
//! and cannot be built from a `bool`; `init` takes it **by value**. That is what
//! makes consent enforceable by the type system rather than by six init sites
//! makes the permission decision enforceable by the type system rather than by six init sites
//! each remembering to re-check the same five-part predicate.
use std::path::{Path, PathBuf};
use codewhale_config::{ResolvedRuntimeOptions, SetupState, TELEMETRY_NOTICE_VERSION};
use codewhale_config::{ResolvedRuntimeOptions, SetupState};
use crate::buffer;
use crate::event::Surface;
@@ -21,23 +21,23 @@ pub const TELEMETRY_DIR: &str = "telemetry";
/// The outcome of the emit predicate.
///
/// The split between [`Self::OptedOut`] and [`Self::ForcedOff`] is load-bearing,
/// not cosmetic. "Telemetry resolved to false" is the *default* state of every
/// installation, so a wipe keyed on it would delete a consenting user's identity
/// and unflushed buffer every time they ran one `codewhale exec` with a
/// not cosmetic. A run-scoped kill switch also resolves telemetry to false, so
/// a wipe keyed on that value would delete a user's identity and unflushed
/// buffer every time they ran one `codewhale exec` with a
/// transient `CODEWHALE_TELEMETRY=0` — the recipe the runtime docs themselves
/// prescribe.
#[derive(Debug)]
pub enum TelemetryDecision {
/// The user answered the notice and said yes, and nothing forces off.
/// Anonymous usage counting is enabled and nothing forces it off.
Enabled(TelemetryConsent),
/// A human said no — `--telemetry false`, `CODEWHALE_TELEMETRY=0`,
/// `telemetry = false`, or declining the notice. **The only variant that
/// touches disk**: it wipes and leaves a tombstone.
/// A human persistently said no — `telemetry = false` in durable config or
/// declining the notice. **The only variant that touches disk**: it wipes
/// and leaves a tombstone. CLI and environment false values are run-scoped
/// kill switches and produce [`Self::ForcedOff`] instead.
OptedOut,
/// Off for a reason that is not the user's answer: no notice decision
/// recorded, an unparseable env value, an unresolvable home, a rejected
/// endpoint, or a bumped notice version. Touches nothing, ever. Leaves
/// identity and buffer exactly as they were.
/// Off for a run-scoped or environmental reason: an unparseable env value,
/// an unresolvable home, or a rejected endpoint. Touches nothing, ever.
/// Leaves identity and buffer exactly as they were.
ForcedOff,
}
@@ -70,6 +70,7 @@ pub struct TelemetryConsent {
endpoint: Option<String>,
surface: Surface,
config_path: Option<PathBuf>,
tombstone_generation: Option<buffer::TombstoneGeneration>,
}
impl TelemetryConsent {
@@ -108,6 +109,21 @@ impl TelemetryConsent {
pub fn surface(&self) -> Surface {
self.surface
}
/// Exact opt-out generation this decision observed.
pub(crate) fn tombstone_generation(&self) -> Option<&buffer::TombstoneGeneration> {
self.tombstone_generation.as_ref()
}
}
enum TelemetryEvaluation {
Enabled {
root: PathBuf,
endpoint: Option<String>,
tombstone_generation: Option<buffer::TombstoneGeneration>,
},
OptedOut(Option<PathBuf>),
ForcedOff,
}
/// Why an endpoint was refused.
@@ -193,58 +209,100 @@ pub fn decide(
decide_in_home(home.as_deref(), resolved, setup, surface)
}
/// Load the privacy-bearing setup record for a telemetry decision.
///
/// A genuinely missing record is a fresh installation and therefore uses the
/// documented default. An existing record that cannot be read or parsed may
/// contain a durable decline, so it fails closed instead of being replaced by
/// a default-on value.
#[must_use]
pub fn load_setup_state_for_decision() -> Option<SetupState> {
let path = SetupState::path().ok()?;
load_setup_state_for_decision_at(&path)
}
/// Injectable form of [`load_setup_state_for_decision`] used by every surface
/// and by regression tests.
#[must_use]
pub fn load_setup_state_for_decision_at(path: &Path) -> Option<SetupState> {
match path.try_exists() {
Ok(false) => Some(SetupState::default()),
Ok(true) => SetupState::load_from(path),
Err(_) => None,
}
}
/// Resolve the emit predicate against an explicit Codewhale home.
///
/// The predicate, in order:
///
/// 1. Telemetry resolved to `false` **and** a human said so → `OptedOut`;
/// resolved `false` from the unset default → `ForcedOff`.
/// 2. Notice decision recorded and declined → `OptedOut`.
/// 3. No notice decision for the current notice version → `ForcedOff`. **A
/// pre-existing `telemetry = true` is not consent**: the key has been
/// settable and inert for a long time, so anyone who set it set a no-op. The
/// notice record is an independent AND condition, never inferred from the
/// bool.
/// 4. No resolvable home → `ForcedOff`.
/// 5. Endpoint configured but refused by [`validate_endpoint`] → `ForcedOff`.
/// 6. Otherwise `Enabled`.
/// 1. Telemetry resolved to `false` from persistent config → `OptedOut`;
/// resolved `false` from a run-scoped or invalid-value floor → `ForcedOff`.
/// 2. Any recorded notice decline → `OptedOut`, including a decline recorded
/// by the former opt-in notice.
/// 3. No resolvable home → `ForcedOff`.
/// 4. Endpoint configured but refused by [`validate_endpoint`] → `ForcedOff`.
/// 5. Otherwise `Enabled`.
///
/// Consent is **machine-scoped**. The notice is only ever *rendered* on a TTY,
/// but a decision recorded on a TTY authorizes later non-TTY runs on the same
/// home. A fresh CI home has no decision, so step 3 fires and nothing is
/// collected — and nothing is written to disk to find out.
/// The notice is only ever *rendered* on a TTY. The interactive TUI explains
/// the default in a native startup modal before telemetry is armed; headless
/// surfaces use the same documented default and kill switches.
pub fn decide_in_home(
home: Option<&Path>,
resolved: &ResolvedRuntimeOptions,
setup: &SetupState,
surface: Surface,
) -> TelemetryDecision {
match evaluate_in_home(home, resolved, setup) {
TelemetryEvaluation::Enabled {
root,
endpoint,
tombstone_generation,
} => TelemetryDecision::Enabled(TelemetryConsent {
root,
endpoint,
surface,
config_path: None,
tombstone_generation,
}),
TelemetryEvaluation::OptedOut(root) => opted_out(root.as_deref()),
TelemetryEvaluation::ForcedOff => TelemetryDecision::ForcedOff,
}
}
/// Evaluate the permission predicate without performing the opt-out wipe.
///
/// Keeping the classification pure lets `init` re-check it while holding the
/// privacy lock. The public decision path maps `OptedOut` to the destructive
/// wipe exactly once, outside that already-held lock.
fn evaluate_in_home(
home: Option<&Path>,
resolved: &ResolvedRuntimeOptions,
setup: &SetupState,
) -> TelemetryEvaluation {
let root = home.map(|home| home.join(TELEMETRY_DIR));
// 1. An explicit "off" from a human is an answer and wipes; the unset
// default is not an answer and must leave every byte alone.
// 1. An explicit persistent "off" is an opt-out and wipes. Run-scoped or
// invalid-value false is only a kill switch and leaves disk alone.
if !resolved.telemetry {
if resolved.telemetry_explicit_off {
return opted_out(root.as_deref());
return TelemetryEvaluation::OptedOut(root);
}
return TelemetryDecision::ForcedOff;
return TelemetryEvaluation::ForcedOff;
}
// 2/3. The notice record is an independent condition. Declining is an
// answer; never having been asked is not.
if setup.needs_telemetry_notice(TELEMETRY_NOTICE_VERSION) {
return TelemetryDecision::ForcedOff;
}
if !setup.telemetry_opt_in {
return opted_out(root.as_deref());
// 2. A historical or current decline remains a durable opt-out. Notice
// version bumps may update disclosure, never reverse a user's "no".
if setup.telemetry_opted_out() {
return TelemetryEvaluation::OptedOut(root);
}
// 4. Nowhere to keep an install id or a buffer.
// 3. Nowhere to keep an install id or a buffer.
let Some(root) = root else {
return TelemetryDecision::ForcedOff;
return TelemetryEvaluation::ForcedOff;
};
// 5. A refused endpoint is a configuration error, not a user answer.
// 4. A refused endpoint is a configuration error, not a user answer.
let endpoint = match resolved.telemetry_endpoint.as_deref() {
Some(raw) if !raw.trim().is_empty() => match validate_endpoint(raw) {
Ok(endpoint) => Some(endpoint),
@@ -253,18 +311,54 @@ pub fn decide_in_home(
"telemetry endpoint refused ({}); telemetry is off for this run",
error.label()
);
return TelemetryDecision::ForcedOff;
return TelemetryEvaluation::ForcedOff;
}
},
_ => None,
};
TelemetryDecision::Enabled(TelemetryConsent {
let Ok(tombstone_generation) = buffer::tombstone_generation(&root) else {
return TelemetryEvaluation::ForcedOff;
};
TelemetryEvaluation::Enabled {
root,
endpoint,
surface,
config_path: None,
})
tombstone_generation,
}
}
/// Re-check the current durable permission without wiping or clearing state.
///
/// Called only while `init` holds the telemetry privacy lock. A stale consent
/// token may arm only when the config, setup-state answer, home, and endpoint
/// still classify as enabled.
pub(crate) fn permission_still_enabled(config_path: Option<&Path>, expected_root: &Path) -> bool {
let Ok(setup_path) = SetupState::path() else {
return false;
};
let home = codewhale_paths::codewhale_home().ok().flatten();
permission_still_enabled_in_home(config_path, &setup_path, home.as_deref(), expected_root)
}
pub(crate) fn permission_still_enabled_in_home(
config_path: Option<&Path>,
setup_path: &Path,
home: Option<&Path>,
expected_root: &Path,
) -> bool {
let Ok(store) = codewhale_config::ConfigStore::load(config_path.map(Path::to_path_buf)) else {
return false;
};
let resolved = store
.config
.resolve_runtime_options(&codewhale_config::CliRuntimeOverrides::default());
let Some(setup) = load_setup_state_for_decision_at(setup_path) else {
return false;
};
matches!(
evaluate_in_home(home, &resolved, &setup),
TelemetryEvaluation::Enabled { root, .. } if root == expected_root
)
}
/// Re-run the predicate from the filesystem, for the flush path.
@@ -275,13 +369,26 @@ pub fn decide_in_home(
/// load fails: a flush is never the right place to guess.
#[must_use]
pub fn re_decide(config_path: Option<&Path>, surface: Surface) -> TelemetryDecision {
let Ok(setup_path) = SetupState::path() else {
return TelemetryDecision::ForcedOff;
};
re_decide_with_setup_path(config_path, &setup_path, surface)
}
pub(crate) fn re_decide_with_setup_path(
config_path: Option<&Path>,
setup_path: &Path,
surface: Surface,
) -> TelemetryDecision {
let Ok(store) = codewhale_config::ConfigStore::load(config_path.map(Path::to_path_buf)) else {
return TelemetryDecision::ForcedOff;
};
let resolved = store
.config
.resolve_runtime_options(&codewhale_config::CliRuntimeOverrides::default());
let setup = SetupState::load().ok().flatten().unwrap_or_default();
let Some(setup) = load_setup_state_for_decision_at(setup_path) else {
return TelemetryDecision::ForcedOff;
};
decide(&resolved, &setup, surface)
}
+32 -22
View File
@@ -59,24 +59,29 @@ pub struct TelemetryState {
/// always the safe direction — the cost is one rotation, and the docs already
/// say no count derived from `install_id` is a user count.
pub fn read_or_create_install_id(root: &Path) -> Result<InstallId> {
let path = buffer::install_id_path(root);
let existing = std::fs::read_to_string(&path)
.ok()
.and_then(|body| serde_json::from_str::<InstallId>(&body).ok())
.filter(|record| uuid::Uuid::parse_str(record.install_id.trim()).is_ok())
.filter(|record| !is_expired(&record.rotated_at));
if let Some(record) = existing {
return Ok(record);
}
let record = InstallId {
schema_version: 1,
install_id: uuid::Uuid::new_v4().to_string(),
rotated_at: now_rfc3339(),
};
buffer::ensure_dir(root)?;
codewhale_config::persistence::atomic_write_json(&path, &record)
.with_context(|| format!("failed to write {}", path.display()))?;
Ok(record)
buffer::try_with_lock(root, || {
if buffer::tombstone_present(root) {
anyhow::bail!("telemetry is disabled");
}
let path = buffer::install_id_path(root);
let existing = std::fs::read_to_string(&path)
.ok()
.and_then(|body| serde_json::from_str::<InstallId>(&body).ok())
.filter(|record| uuid::Uuid::parse_str(record.install_id.trim()).is_ok())
.filter(|record| !is_expired(&record.rotated_at));
if let Some(record) = existing {
return Ok(record);
}
let record = InstallId {
schema_version: 1,
install_id: uuid::Uuid::new_v4().to_string(),
rotated_at: now_rfc3339(),
};
codewhale_config::persistence::atomic_write_json(&path, &record)
.with_context(|| format!("failed to write {}", path.display()))?;
Ok(record)
})?
.ok_or_else(|| anyhow::anyhow!("telemetry privacy lock is held"))
}
fn is_expired(rotated_at: &str) -> bool {
@@ -100,10 +105,15 @@ pub fn read_state(root: &Path) -> TelemetryState {
/// Write `state.json`.
pub fn write_state(root: &Path, state: &TelemetryState) -> Result<()> {
buffer::ensure_dir(root)?;
let path = buffer::state_path(root);
codewhale_config::persistence::atomic_write_json(&path, state)
.with_context(|| format!("failed to write {}", path.display()))
buffer::try_with_lock(root, || {
if buffer::tombstone_present(root) {
anyhow::bail!("telemetry is disabled");
}
let path = buffer::state_path(root);
codewhale_config::persistence::atomic_write_json(&path, state)
.with_context(|| format!("failed to write {}", path.display()))
})?
.ok_or_else(|| anyhow::anyhow!("telemetry privacy lock is held"))
}
/// RFC3339 UTC at second precision. The only timestamp this crate produces, and
+20 -57
View File
@@ -1,4 +1,4 @@
//! Opt-in product telemetry for Codewhale.
//! Default-on, user-disableable anonymous product usage counting for Codewhale.
//!
//! The whole of what this crate may ever send is [`event`]. The whole of what
//! decides whether it may send anything is [`decision`]. Nothing else in the
@@ -9,7 +9,7 @@
//!
//! # The shape of the guarantee
//!
//! Consent is a **value**, not a convention. [`decide`] is the only constructor
//! Permission is a **value**, not a convention. [`decide`] is the only constructor
//! of [`TelemetryConsent`]; [`init`] takes one by value and there is no
//! bool-taking sibling. Six init sites cannot each drift from the predicate,
//! because they never see the predicate.
@@ -21,8 +21,8 @@
//! construction empty until resolution completes. A disabled user's panic
//! therefore writes nothing and creates no directory.
//!
//! Arming also **truncates** the buffer. No event recorded before consent can
//! ever be in the batch that follows it.
//! Arming also **truncates** any stale buffer before a newly permitted process
//! begins recording.
//!
//! # Failure posture
//!
@@ -53,7 +53,7 @@ pub use actor::{BATCH_MAX_BYTES, BATCH_MAX_EVENTS, FlushOutcome};
pub use counters::{Counter, ErrorCounter, SessionCounters};
pub use decision::{
EndpointError, TELEMETRY_DIR, TelemetryConsent, TelemetryDecision, decide, decide_in_home,
re_decide, validate_endpoint,
load_setup_state_for_decision, load_setup_state_for_decision_at, re_decide, validate_endpoint,
};
pub use envelope::reduce_panic_site;
pub use event::{
@@ -68,9 +68,6 @@ pub use event::{
/// handshake would hold a user's terminal past exit.
pub const SHUTDOWN_FLUSH_TIMEOUT: Duration = Duration::from_secs(3);
/// Minimum gap between startup drains.
pub const STARTUP_DRAIN_INTERVAL_HOURS: i64 = 6;
/// Everything a write path needs once the process is armed.
struct Armed {
handle: actor::Handle,
@@ -93,10 +90,16 @@ pub fn init(consent: TelemetryConsent) {
return;
}
let root = consent.root().to_path_buf();
let observed_generation = consent.tombstone_generation().cloned();
let config_path = consent.config_path().map(std::path::Path::to_path_buf);
// Clear the tombstone and drop anything buffered before consent. A stale
// buffer is not evidence of this user's answer.
if let Err(error) = buffer::arm(&root) {
// Re-check durable permission under the same ordering lock as wipe. The
// generation match prevents consent resolved before a newer opt-out from
// clearing that opt-out; the fresh predicate preserves intentional
// `config set telemetry true` re-enablement.
if let Err(error) = buffer::arm(&root, observed_generation.as_ref(), || {
decision::permission_still_enabled(config_path.as_deref(), &root)
}) {
tracing::debug!("telemetry could not prepare its buffer: {error}");
return;
}
@@ -213,18 +216,18 @@ pub fn record(event: Event) {
armed.handle.record(event);
}
/// Write an event synchronously, without the writer thread and **without any
/// lock**.
/// Write an event synchronously, without the writer thread.
///
/// The synchronous escape hatch for the three paths where the async world is
/// gone or going: the panic hook, `record_caught_panic`, and the signal task
/// immediately before `std::process::exit`. One `O_APPEND` `write(2)` under
/// `PIPE_BUF`, a `sync_data`, and return — microseconds.
///
/// Taking the compaction lock here would be a *blocking* acquisition on both of
/// those paths. `flock` is per-fd within a process, so an actor panic while
/// holding that lock would self-deadlock the hook, and a second Codewhale
/// process sharing `CODEWHALE_HOME` would hang Ctrl-C.
/// The append takes the shared privacy lock with `try_write()`, never a blocking
/// acquisition. If the actor, a wipe, or another Codewhale process sharing
/// `CODEWHALE_HOME` holds it, the event is dropped immediately. This preserves
/// the panic/SIGINT liveness contract without allowing a write to race past a
/// completed opt-out.
///
/// A no-op when unarmed, which is what makes a disabled user's panic write
/// nothing and create no directory.
@@ -260,19 +263,6 @@ pub fn exit_class() -> ExitClass {
})
}
/// Flush whatever is buffered, waiting at most `deadline`.
///
/// Blocking, so async callers must hand this to `spawn_blocking` and bound it —
/// [`SHUTDOWN_FLUSH_TIMEOUT`] is the teardown budget. Consent is re-resolved
/// from disk inside the writer thread before anything is sent.
///
/// Returns [`FlushOutcome::Empty`] when unarmed.
pub fn flush_blocking(deadline: Duration) -> FlushOutcome {
ARMED
.get()
.map_or(FlushOutcome::Empty, |armed| armed.handle.flush(deadline))
}
/// Final flush, then stop the writer thread.
///
/// Returns [`FlushOutcome::Empty`] when unarmed.
@@ -281,30 +271,3 @@ pub fn shutdown_blocking(deadline: Duration) -> FlushOutcome {
.get()
.map_or(FlushOutcome::Empty, |armed| armed.handle.shutdown(deadline))
}
/// Whether a startup drain is due: a prior session crashed or was signalled and
/// left events behind, and enough time has passed since the last attempt.
///
/// The check happens **before** the drain task is spawned, so "not due" means no
/// task at all rather than a task that returns early.
#[must_use]
pub fn startup_drain_due() -> bool {
let Some(armed) = ARMED.get() else {
return false;
};
let path = buffer::buffer_path(&armed.root);
if buffer::read_lines(&path).is_empty() {
return false;
}
let state = envelope::read_state(&armed.root);
let Some(last) = state.last_flush else {
return true;
};
let Ok(parsed) = chrono::DateTime::parse_from_rfc3339(&last) else {
return true;
};
chrono::Utc::now()
.signed_duration_since(parsed.with_timezone(&chrono::Utc))
.num_hours()
>= STARTUP_DRAIN_INTERVAL_HOURS
}
+15 -40
View File
@@ -7,55 +7,30 @@
//!
//! Two properties of the wording are deliberate and load-bearing:
//!
//! 1. **The declining option is pre-selected and Enter takes it.** Enabling
//! requires a deliberate keystroke. There is no third option, no "improve
//! the product" checkbox, and nothing pre-checked.
//! 1. **The default is stated plainly and the opt-out is immediate.** The
//! native TUI starts on the yes choice and makes the opt-out equally
//! reachable.
//! 2. **The red lines are stated as "not collected", not as "anonymized".**
//! Sampling and hashing are not the same promise, and a notice that implies
//! them when neither is true is worse than no notice.
/// Headline shown above [`NOTICE_BODY`].
pub const NOTICE_HEADLINE: &str = "Help improve CodeWhale?";
pub const NOTICE_HEADLINE: &str = "Help improve Codewhale?";
/// The notice itself.
///
/// Wrapped at 72 columns so it renders unchanged in a modal, in a pipe, and in
/// an 80-column terminal.
/// Wrapped at 72 columns so it renders unchanged in the native responsive
/// modal and remains readable in an 80-column terminal.
pub const NOTICE_BODY: &str = "\
CodeWhale can send anonymous usage counts: which version you run, your
OS and CPU family, which features you used, how long sessions ran, and
how they ended.
Codewhale counts: which version you run, OS and CPU family, session
duration and outcome, and aggregate feature and error counters.
It never sends prompts, code, file names, paths, repo or branch names,
model output, model names, or credentials. Not sampled, not hashed —
not collected.
It never collects your conversations, code, prompts, files, repo or
branch names, model content, or credentials — and it never sends a
per-turn or per-tool timeline of agent activity.
You are identified only by a random ID stored on this machine. It is
deleted the moment you turn this off, and it is replaced every 90 days.
You are identified only by a random ID stored on this machine, replaced
every 90 days. Change your mind any time:
codewhale config set telemetry false
Full schema, field by field: docs/TELEMETRY.md
Turn it off any time: codewhale config set telemetry false
or CODEWHALE_TELEMETRY=0";
/// The question, with the declining answer capitalised as the default.
pub const NOTICE_PROMPT: &str = "Enable telemetry? [y/N]";
/// The line printed once a decision is recorded, so the user has a receipt.
#[must_use]
pub fn decision_receipt(opt_in: bool) -> &'static str {
if opt_in {
"Telemetry is on. Turn it off any time with `codewhale config set telemetry false`."
} else {
"Telemetry stays off. You will not be asked again."
}
}
/// Whether a typed answer means yes.
///
/// Everything else — an empty line, EOF, a closed pipe, `n`, a typo — means no.
/// That asymmetry is the point: only an affirmative answer is an affirmative
/// answer.
#[must_use]
pub fn answer_is_yes(input: &str) -> bool {
matches!(input.trim().to_ascii_lowercase().as_str(), "y" | "yes")
}
Full schema, field by field: docs/TELEMETRY.md";
+255 -84
View File
@@ -17,7 +17,10 @@ use codewhale_config::{
use serde_json::Value;
use crate::buffer;
use crate::decision::{EndpointError, TelemetryDecision, decide_in_home, validate_endpoint};
use crate::decision::{
EndpointError, TelemetryDecision, decide_in_home, load_setup_state_for_decision_at,
permission_still_enabled_in_home, re_decide_with_setup_path, validate_endpoint,
};
use crate::envelope;
use crate::event::*;
@@ -59,6 +62,46 @@ fn stale_setup() -> SetupState {
setup
}
#[test]
fn setup_state_loader_defaults_only_when_the_privacy_record_is_absent() {
let home = temp_home();
let path = home.path().join("setup_state.json");
assert!(
load_setup_state_for_decision_at(&path).is_some(),
"a genuinely fresh install uses the documented default"
);
std::fs::write(&path, b"{not-json").expect("write corrupt setup state");
assert!(
load_setup_state_for_decision_at(&path).is_none(),
"an existing unreadable privacy record must fail closed"
);
accepted_setup()
.save_to(&path)
.expect("write valid setup state");
assert!(
load_setup_state_for_decision_at(&path)
.is_some_and(|setup| setup.telemetry_accepted(TELEMETRY_NOTICE_VERSION)),
"a valid setup state remains usable"
);
}
#[test]
fn flush_redecision_fails_closed_on_a_corrupt_setup_state() {
let home = temp_home();
let config_path = home.path().join("config.toml");
let setup_path = home.path().join("setup_state.json");
std::fs::write(&config_path, "telemetry = true\n").expect("write config");
std::fs::write(&setup_path, b"{not-json").expect("write corrupt setup state");
assert!(matches!(
re_decide_with_setup_path(Some(&config_path), &setup_path, Surface::Exec),
TelemetryDecision::ForcedOff
));
}
/// One instance of every event variant, populated with the most adversarial
/// values the schema permits.
///
@@ -413,7 +456,7 @@ fn every_legitimately_recorded_event_survives_the_drain() {
);
// Dialect kinds (`deepseek-anthropic`, the Model Studio plan variants) are
// absent from `ProviderKind::ALL`, which is the 36-row *catalog* subset,
// absent from `ProviderKind::ALL`, which is the 37-row *catalog* subset,
// but `ApiProvider::kind()` yields them for real routes. Narrowing the
// provider bound to the catalog would drop those users' `session_end`.
for kind in [
@@ -513,15 +556,15 @@ fn decision_matrix_is_exhaustive() {
let home = temp_home();
let path = home.path();
// Row: nobody has said anything. Default off is not an answer.
// Row: nobody has said anything. Anonymous usage counting is default-on.
assert!(matches!(
decide_in_home(
Some(path),
&resolved(false, false, None),
&resolved(true, false, None),
&SetupState::default(),
Surface::Tui
),
TelemetryDecision::ForcedOff
TelemetryDecision::Enabled(_)
));
// Row: a human said off. That is an answer.
@@ -535,8 +578,7 @@ fn decision_matrix_is_exhaustive() {
TelemetryDecision::OptedOut
));
// Row: on, but never asked. A pre-existing `telemetry = true` is not
// consent — the key has been settable and inert for a long time.
// Row: on, notice not yet shown. Headless/default-on still works.
assert!(matches!(
decide_in_home(
Some(path),
@@ -544,7 +586,7 @@ fn decision_matrix_is_exhaustive() {
&SetupState::default(),
Surface::Tui
),
TelemetryDecision::ForcedOff
TelemetryDecision::Enabled(_)
));
// Row: on, asked, declined.
@@ -558,7 +600,8 @@ fn decision_matrix_is_exhaustive() {
TelemetryDecision::OptedOut
));
// Row: on, but the notice content changed since they answered.
// Row: on, but the notice content changed since they answered yes. A
// disclosure refresh does not pause usage counting.
assert!(matches!(
decide_in_home(
Some(path),
@@ -566,7 +609,7 @@ fn decision_matrix_is_exhaustive() {
&stale_setup(),
Surface::Tui
),
TelemetryDecision::ForcedOff
TelemetryDecision::Enabled(_)
));
// Row: on and accepted, no home to keep state in.
@@ -619,19 +662,18 @@ fn decision_matrix_is_exhaustive() {
.is_enabled()
);
// Row: every headless surface is reachable, because consent is
// machine-scoped: a TTY-recorded decision authorizes later exec, cli,
// app-server, mcp-server, and serve runs on the same home.
// Row: every headless surface uses the same documented default and kill
// switches.
for surface in Surface::ALL {
assert!(
decide_in_home(
Some(path),
&resolved(true, false, None),
&accepted_setup(),
&SetupState::default(),
*surface
)
.is_enabled(),
"{surface:?} must be able to emit on a consenting machine"
"{surface:?} must be able to emit by default"
);
}
}
@@ -670,8 +712,6 @@ fn only_opt_out_touches_disk() {
// have broken: `false` is the *default*, so it fired on every ordinary run.
let forced_off_rows: Vec<(ResolvedRuntimeOptions, SetupState)> = vec![
(resolved(false, false, None), accepted_setup()),
(resolved(true, false, None), SetupState::default()),
(resolved(true, false, None), stale_setup()),
(
resolved(true, false, Some("http://example.com/t")),
accepted_setup(),
@@ -762,21 +802,23 @@ fn the_tombstone_outlives_every_run_the_opt_out_covers() {
!buffer::install_id_path(&root).exists(),
"{surface:?} minted a new identity for an opted-out machine"
);
assert!(!buffer::state_path(&root).exists());
assert!(buffer::read_lines(&buffer::buffer_path(&root)).is_empty());
assert!(buffer::read_lines(&buffer::dryrun_path(&root)).is_empty());
assert_eq!(snapshot(&root), after_wipe, "{surface:?} touched disk");
}
// Only writing the setting back turns collection on again, and that is the
// one path allowed to clear the tombstone.
assert!(
decide_in_home(
Some(home.path()),
&resolved(true, false, None),
&accepted_setup(),
Surface::Tui,
)
.is_enabled()
);
buffer::arm(&root).expect("re-consent arms");
let TelemetryDecision::Enabled(consent) = decide_in_home(
Some(home.path()),
&resolved(true, false, None),
&accepted_setup(),
Surface::Tui,
) else {
panic!("an explicit re-enable must produce consent");
};
buffer::arm(&root, consent.tombstone_generation(), || true).expect("re-consent arms");
assert!(!buffer::tombstone_present(&root));
}
@@ -827,7 +869,7 @@ fn an_opt_out_on_a_fresh_home_creates_nothing() {
assert!(matches!(decision, TelemetryDecision::OptedOut));
assert!(
!root.exists(),
"a user who never opted in must not get a telemetry directory for saying no"
"a fresh user who opts out must not get a telemetry directory"
);
}
@@ -1081,7 +1123,7 @@ fn drain_skips_a_torn_trailing_line() {
}
#[test]
fn append_never_blocks_on_a_held_lock() {
fn append_drops_without_blocking_on_a_held_privacy_lock() {
let home = temp_home();
let root = root_of(&home);
buffer::ensure_dir(&root).expect("create root");
@@ -1102,16 +1144,20 @@ fn append_never_blocks_on_a_held_lock() {
held.wait();
let started = Instant::now();
buffer::append(&root, &path, &line(7)).expect("append under a held lock");
let outcome = buffer::append(&root, &path, &line(7));
let elapsed = started.elapsed();
release.wait();
holder.join().expect("holder thread").expect("holder lock");
assert!(outcome.is_none(), "a contended append must be dropped");
assert!(
elapsed < Duration::from_millis(250),
"an append waited {elapsed:?} on a lock it must never take"
"an append waited {elapsed:?} on the privacy lock"
);
assert!(
buffer::read_lines(&path).is_empty(),
"the panic-safe path bypassed the privacy lock"
);
assert_eq!(buffer::read_lines(&path).len(), 1);
}
#[test]
@@ -1135,11 +1181,107 @@ fn arming_truncates_a_pre_consent_buffer() {
buffer::append(&root, &buffer::buffer_path(&root), &line(1)).expect("append");
buffer::wipe(&root).expect("wipe");
buffer::arm(&root).expect("arm");
let generation = buffer::tombstone_generation(&root).expect("read wipe generation");
buffer::arm(&root, generation.as_ref(), || true).expect("arm");
assert!(!buffer::tombstone_present(&root));
assert!(buffer::read_lines(&buffer::buffer_path(&root)).is_empty());
}
#[test]
fn stale_consent_cannot_clear_a_newer_opt_out_but_fresh_reenable_can() {
let home = temp_home();
let root = root_of(&home);
let config_path = home.path().join("config.toml");
let setup_path = home.path().join("setup_state.json");
accepted_setup()
.save_to(&setup_path)
.expect("write accepted setup state");
// This process resolved the old enabled config before another process
// persisted an opt-out and completed its wipe.
let stale_resolved = resolved(true, false, None);
let TelemetryDecision::Enabled(pre_wipe_consent) = decide_in_home(
Some(home.path()),
&stale_resolved,
&accepted_setup(),
Surface::Tui,
) else {
panic!("pre-wipe enabled facts must produce consent");
};
std::fs::write(&config_path, "telemetry = false\n").expect("persist opt-out");
buffer::ensure_dir(&root).expect("create telemetry root");
buffer::wipe(&root).expect("complete newer wipe");
assert!(
buffer::arm(&root, pre_wipe_consent.tombstone_generation(), || true).is_err(),
"an old consent token cleared a newer tombstone generation"
);
assert!(buffer::tombstone_present(&root));
// Even the difficult ordering — stale config facts combined with the new
// tombstone generation — cannot arm, because arm re-reads the durable
// predicate while holding the wipe lock.
let TelemetryDecision::Enabled(stale_consent) = decide_in_home(
Some(home.path()),
&stale_resolved,
&accepted_setup(),
Surface::Tui,
) else {
panic!("fixture must carry stale enabled facts");
};
assert!(
buffer::arm(&root, stale_consent.tombstone_generation(), || {
permission_still_enabled_in_home(
Some(&config_path),
&setup_path,
Some(home.path()),
&root,
)
})
.is_err(),
"stale consent cleared a completed opt-out"
);
assert!(buffer::tombstone_present(&root));
// The documented explicit re-enable updates the durable register first. A
// fresh consent observes both that value and the current generation, so it
// may clear exactly that tombstone.
std::fs::write(&config_path, "telemetry = true\n").expect("persist re-enable");
let TelemetryDecision::Enabled(fresh_consent) = decide_in_home(
Some(home.path()),
&resolved(true, false, None),
&accepted_setup(),
Surface::Tui,
) else {
panic!("fresh re-enable must produce consent");
};
buffer::arm(&root, fresh_consent.tombstone_generation(), || {
permission_still_enabled_in_home(Some(&config_path), &setup_path, Some(home.path()), &root)
})
.expect("fresh re-enable arms");
assert!(!buffer::tombstone_present(&root));
}
#[test]
fn completed_wipe_blocks_identity_and_state_recreation() {
let home = temp_home();
let root = root_of(&home);
buffer::ensure_dir(&root).expect("create telemetry root");
envelope::read_or_create_install_id(&root).expect("seed install id");
envelope::write_state(&root, &envelope::TelemetryState::default()).expect("seed state");
buffer::wipe(&root).expect("wipe telemetry home");
assert!(
envelope::read_or_create_install_id(&root).is_err(),
"an in-flight flush recreated the deleted install id"
);
assert!(
envelope::write_state(&root, &envelope::TelemetryState::default()).is_err(),
"an in-flight flush recreated state after opt-out"
);
assert!(!buffer::install_id_path(&root).exists());
assert!(!buffer::state_path(&root).exists());
}
// ------------------------------------------------------------ unarmed gate --
#[test]
@@ -1157,11 +1299,6 @@ fn record_blocking_is_a_noop_when_unarmed() {
});
crate::set_exit_class(ExitClass::Panic);
assert_eq!(crate::exit_class(), ExitClass::Clean);
assert_eq!(
crate::flush_blocking(Duration::from_millis(10)),
crate::FlushOutcome::Empty
);
assert!(!crate::startup_drain_due());
assert!(
!root.exists(),
"an unarmed process must create no directory"
@@ -1204,6 +1341,62 @@ fn a_tombstoned_home_sends_nothing_even_with_an_endpoint() {
assert!(buffer::read_lines(&buffer::dryrun_path(&root)).is_empty());
}
#[test]
fn wipe_and_delivery_share_one_ordering_boundary() {
let home = temp_home();
let root = root_of(&home);
let entered = std::sync::Arc::new(std::sync::Barrier::new(2));
let release = std::sync::Arc::new(std::sync::Barrier::new(2));
let send_root = root.clone();
let send_entered = entered.clone();
let send_release = release.clone();
let send = std::thread::spawn(move || {
crate::client::send_with_transport(
&send_root,
Some("https://telemetry.codewhale.ai/v1/batch"),
&every_field_batch(),
move |_, _, _| {
send_entered.wait();
send_release.wait();
crate::client::SendOutcome::Accepted
},
)
});
entered.wait();
// The real send path is paused inside its transport callback. A
// non-blocking probe must observe the same lock that wipe takes; this
// deterministically pins the entire delivery inside the boundary without
// depending on loopback networking in a restricted test sandbox.
assert!(
buffer::try_with_lock(&root, || Ok(()))
.expect("probe privacy lock")
.is_none(),
"network delivery did not hold the wipe lock"
);
// Start the real blocking wipe while the POST is still in flight. It can
// only complete after the response releases the sender's privacy guard.
let wipe_root = root.clone();
let wipe = std::thread::spawn(move || buffer::wipe(&wipe_root));
release.wait();
assert_eq!(
send.join().expect("send thread"),
crate::client::SendOutcome::Accepted
);
wipe.join()
.expect("wipe thread")
.expect("wipe after delivery");
assert!(buffer::tombstone_present(&root));
assert_eq!(
crate::client::send(&root, Some("http://127.0.0.1:1/t"), &every_field_batch()),
crate::client::SendOutcome::Dropped,
"a send crossed the completed wipe boundary"
);
}
// ----------------------------------------------------------------- buckets --
#[test]
@@ -1298,12 +1491,11 @@ fn no_public_api_accepts_a_bare_bool() {
let init: fn(crate::TelemetryConsent) = crate::init;
let _ = init;
// The only source of one is `decide`, which needs both a resolved config
// and a setup-state record — neither of which a caller can fake into "yes"
// without the user having answered.
// The only source of one is `decide`, which still applies every persistent
// and run-scoped opt-out before constructing the capability.
let home = temp_home();
assert!(
!decide_in_home(
decide_in_home(
Some(home.path()),
&resolved(true, false, None),
&SetupState::default(),
@@ -1598,24 +1790,27 @@ fn an_install_or_upgrade_is_reported_once_per_version() {
}
#[test]
fn the_notice_promises_exactly_what_the_schema_collects() {
fn the_notice_summarizes_what_the_schema_collects_and_states_every_red_line() {
use crate::notice;
let body = notice::NOTICE_BODY;
// Everything the envelope carries has to be described. `install_id` is
// "a random ID stored on this machine"; the rest are named directly.
// The modal names the useful product categories and links the exact
// field-by-field schema. `install_id` is "a random ID stored on this
// machine"; transport metadata remains in the linked document. The body
// wraps at 72 columns, so multi-word claims are matched across the
// reflowed whitespace.
let flat: String = body.split_whitespace().collect();
for claim in [
"which version you run",
"version",
"OS and CPU family",
"which features you used",
"how long sessions ran",
"how they ended",
"session duration and outcome",
"aggregate feature and error counters",
"random ID stored on this machine",
"every 90 days",
] {
assert!(
body.contains(claim),
flat.contains(&claim.split_whitespace().collect::<String>()),
"the notice does not describe: {claim}"
);
}
@@ -1623,50 +1818,26 @@ fn the_notice_promises_exactly_what_the_schema_collects() {
// And every red line has to be stated as *not collected*, not as
// anonymized or sampled — two promises this client does not make.
for red_line in [
"prompts",
"conversations",
"code",
"file names",
"paths",
"prompts",
"files",
"repo or branch names",
"model output",
"model names",
"model content",
"credentials",
"per-turn or per-tool timeline",
] {
assert!(
body.contains(red_line),
flat.contains(&red_line.split_whitespace().collect::<String>()),
"the notice does not disclaim: {red_line}"
);
}
assert!(body.contains("Not sampled, not hashed"));
assert!(!body.to_ascii_lowercase().contains("anonymized"));
// The two documented ways out, both of which are real.
// The modal names the persistent opt-out because that is the switch that
// also fulfils its deletion promise. Run-only kill switches stay in the
// linked schema document, which explains that they erase nothing.
assert!(body.contains("codewhale config set telemetry false"));
assert!(body.contains("CODEWHALE_TELEMETRY=0"));
assert!(!body.contains("CODEWHALE_TELEMETRY=0"));
assert!(body.contains("docs/TELEMETRY.md"));
}
#[test]
fn only_an_affirmative_answer_is_an_answer() {
use crate::notice::answer_is_yes;
assert!(answer_is_yes("y"));
assert!(answer_is_yes("Y\n"));
assert!(answer_is_yes(" yes \n"));
// Enter, EOF, a typo, and a stray keystroke all decline. The default is
// the safe direction and it is reachable without aiming.
assert!(!answer_is_yes(""));
assert!(!answer_is_yes("\n"));
assert!(!answer_is_yes("n"));
assert!(!answer_is_yes("ye"));
assert!(!answer_is_yes("1"));
assert!(!answer_is_yes("true"));
}
#[test]
fn the_notice_prompt_capitalises_the_declining_default() {
// `[y/N]`, not `[Y/n]` and not `[y/n]`. The shape of the prompt is the
// first thing a user reads about which way Enter goes.
assert!(crate::notice::NOTICE_PROMPT.contains("[y/N]"));
assert!(!crate::notice::NOTICE_PROMPT.contains("[Y/n]"));
}
+1 -1
View File
@@ -10,7 +10,7 @@ description = "Tool invocation lifecycle, schema validation, and scheduler paral
[dependencies]
anyhow.workspace = true
async-trait.workspace = true
codewhale-protocol = { path = "../protocol", version = "0.9.4" }
codewhale-protocol = { path = "../protocol", version = "0.9.6" }
serde.workspace = true
serde_json.workspace = true
thiserror.workspace = true
+7
View File
@@ -133,6 +133,13 @@ pub struct ToolResult {
pub metadata: Option<Value>,
}
/// Provider-neutral non-text content returned alongside a tool result.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ToolResultContentBlock {
Image { mime_type: String, data: String },
}
impl ToolResult {
/// Create a successful result with content.
#[must_use]
+8 -8
View File
@@ -2,7 +2,7 @@
Scope: the TUI, the runtime engine embedded in it, and everything a user sees.
Read the repo-root `AGENTS.md` first. Current flakes and known debt are in
`docs/ops/CURRENT.md`, not here.
the `codewhale-ops` repo, not here.
## The shell grammar (do not regress it)
@@ -43,7 +43,7 @@ Adding a string is a four-part change: see `locales/AGENTS.md`.
## Verification
```sh
cargo test -p codewhale-tui --bins --locked # unit suite (bin targets only)
cargo test -p codewhale-tui --lib --locked # library unit suite
cargo test -p codewhale-tui --tests --locked # every crates/tui/tests/ target
cargo clippy --workspace --all-targets --locked -- -D warnings
```
@@ -52,16 +52,16 @@ Narrower reruns of the slow acceptance targets, once `--tests` has told you
which one moved:
```sh
cargo test -p codewhale-tui --test qa_pty --locked # PTY snapshots
cargo test -p codewhale-tui --test release_runtime_qa --locked
cargo test -p codewhale-tui --test terminal_matrix_qa --locked
cargo test -p codewhale-tui --test pty qa_pty --locked
cargo test -p codewhale-tui --test pty release_runtime_qa --locked
cargo test -p codewhale-tui --test pty terminal_matrix_qa --locked
```
**`--bins` and `--tests` are disjoint target sets.** `crates/tui/tests/` holds
two dozen process-level acceptance targets that a `--bins` run never compiles,
**`--lib` and `--tests` are disjoint target sets.** `crates/tui/tests/` holds
two dozen process-level acceptance targets that a `--lib` run never compiles,
let alone executes, so a green `cargo test -p codewhale-tui --bin codewhale-tui`
says nothing about them. `adaptive_evidence_acceptance` sat red across two
releases for exactly that reason: every routine command anyone ran was a `--bins`
releases for exactly that reason: every routine command anyone ran was a unit-only
run, and only `cargo test --workspace` reached it. Run both, or run the
workspace gate.
+416 -257
View File
@@ -7,7 +7,292 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
## [0.9.4] - 2026-08-05
## [0.9.6] - 2026-08-11
Codewhale v0.9.6 is a subtractive release: fewer runtime guards, one stable
prompt, truthful provider endings, and a smaller compaction path that preserves
the provider cache. The changes were grounded by matched Terminal-Bench 2.1
runs against Pi 0.8.41 and by dogfooding repeated manual compaction.
### Added
- `web_search` defaults to Firecrawl Cloud without an API key; keyless requests
are headerless and quota-bounded, while an optional user key raises limits.
- Green web builds on `main` now emit an actionable manual-deploy reminder, so
site changes cannot quietly appear shipped while Cloudflare still serves an
older revision.
- Mistral AI is a first-class provider route, including Codestral models,
first-party reasoning support, authentication, picker entries, and aliases.
- Headless `Bash` can transfer explicitly requested persistent Unix services
out of an exec run, with ownership and cleanup receipts.
- `/remote-env` opens hosted Work from the current GitHub or CNB branch tip and
states exactly which unpushed, dirty, ignored, secret, and session state stays
local.
- Linux ARM64 release and nightly assets are static musl builds with native
launch checks.
- Maintainers can report observed daily active installs from the same anonymous,
aggregate telemetry dataset; no additional client data is collected.
- Fleet-dispatched members under a read-only evidence (no-network) ceiling now
keep the `Web` tool's read-only `search` and `fetch` actions — parity with an
ordinary scout — while every reaching surface (`web.run`, `fetch_url`,
`github`, MCP) stays denied and the sentinel-backed capability envelope
remains the fail-closed backstop.
- `/fleet setup` can show an optional, deterministic, unratified role-to-model
advisory built only from configured ready routes. Accept, edit, and reject
all remain inside the existing human-reviewed profile save boundary; the
advisory never launches a Fleet or writes a second configuration.
- `/update` checks for a newer Codewhale release and installs it from inside
the TUI, while `tui_help` gives agents the same command and key map users see.
- Markdown file paths render as OSC 8 links where the terminal supports them,
and every agent row can open that agent's transcript directly.
- ACP editor sessions can execute multi-round file, search, Git, patch, and
explicitly enabled shell tool calls through the shared Runtime registry.
Shell access requires both the client's terminal capability and Codewhale's
headless shell opt-in, and cancellation stops an in-flight tool before the
turn returns (#5225 by @rafaelcavalheri).
- Lowercase `read` returns bounded typed PNG, JPEG, GIF, and WebP results to
image-capable Chat, Responses, Anthropic, and ACP routes. Text-only routes
receive an explicit omission receipt; image bytes never spill into ordinary
transcript, export, compaction, or relay text.
### Changed
- Anonymous usage counting is on by default for fresh installs and disclosed in
a native first-run Codewhale modal with an immediate opt-out. Prior declines
remain off. Codewhale does not collect conversations, code, prompts, files,
repo or branch names, credentials, model content, or per-turn activity
timelines.
- Wide terminals use a responsive, full-screen ocean canvas with modest
gutters: prose keeps a readable measure while tools, diffs, work surfaces,
the composer, and status chrome can use the available width. Turn and major
activity seams breathe without padding every call inside a tool group.
- Root CLI help describes product actions directly instead of exposing internal
TUI/runtime layers.
- `Bash action="wait"` now blocks by default when a wait is requested; callers
can still ask for a nonblocking snapshot, and persistent service ownership
remains explicit.
- Compaction is one cache-stable summary request followed by one committed
replacement summary and a bounded recent-message tail. Older saved sessions
still restore.
- Ask, Work, Auto-Review, and Full Access share one stable base prompt. Modes
continue to differ through permissions and the live tool catalog; the former
Act label is now Work throughout the product and shipped locales.
- Full Access now auto-approves non-bypassable tools consistently, and the
default choice shown on ordinary approval cards is configurable.
- Model, context-window, dispatch-name, and nested-agent spawn receipts report
the route and limits actually used rather than silently substituting a
guessed identity.
- Child-agent launches mint one immutable route receipt before admission and
preserve it through status, interruption, completion, resume, Work Graph,
and ledger projections, so provider/model attribution cannot drift (#5305).
- Goal runs no longer stop because of internal continuation, repeated-gap, or
unanswered-question guards. Explicit user limits and terminal goal states
remain authoritative.
- Account-owned `/rc` remote control now keeps exclusive ownership and a
crash-recoverable delivery journal until the server acknowledges terminal,
approval, failure, and snapshot state.
- `todo_write` is an optional progress surface rather than required model
ceremony.
- New turns use one small, stable toolbox: `read`, `write`, `edit`, `bash`,
`agent`, `todo_write`, and `tool_search`. The optional progress tool stays
visible as familiar working memory; specialized native, Web, MCP, plugin,
memory, task, and verification tools are policy-filtered and searchable;
activated schemas stay in a bounded per-conversation cache. Every sub-agent
keeps its own search and cache, including policy-allowed Web research, while
forked context and parent activations remain warm starts rather than allowlists.
- The direct file and shell schemas follow Pi's deliberately small contract:
bounded complete-line reads, hash-free writes, unambiguous multi-edit with
BOM/CRLF preservation and conservative fuzzy matching, and one foreground
`bash` command with a bounded chronological output tail. Modes change
execution authority, not those primitive names.
- Codewhale no longer re-states the To-do list to the model. The model learns
what is on the list from the tool result its own `todo_write` call returned,
which is ordinary conversation history — the same way Pi's To-do works. The
transient `<codewhale:work_state>` block that used to ride the tail of every
parent turn-loop and sub-agent step request is gone, along with the stable
system prefix being disturbed by list changes. A snapshot is still shown once,
where a person asked for it: the `<codewhale:fork_state>` block a newly forked
sub-agent is handed, `/relay` handoff instructions, and the agent card. The
complete To-do stays visible in the UI. A structural test asserts real
outbound provider request bodies do not carry the list.
- Scout and Reviewer name the read-only investigator roles. Both expose exactly
one shell entry point — canonical lowercase `bash`, bounded by the strict
read-only classifier — and the legacy `Bash` alias stays denied in the catalog
and at dispatch. Previously a case-insensitive name match let a call spelled
`Bash` execute through that carve-out, returning raw shell to a read-only role.
### Fixed
- Sending more context while a lowercase `bash` command is running now moves
the command to `/jobs` and returns a successful running receipt instead of
falsely reporting `Command exited with code -1`; the process keeps running
and its completion still arrives through the normal runtime event.
- First-run usage disclosure now opens as a native Codewhale modal instead of a
shell questionnaire before application startup. Telemetry remains unarmed
until the native choice is made, and an in-memory Disable choice governs the
current session even when its preference cannot be saved.
- `/compact` completion, failure, queued, duplicate, and mailbox outcomes are
durable transcript receipts instead of short-lived toasts. A stray terminal
event can no longer leave every later compaction stuck as already running.
- Compaction now follows Codex's simple transcript shape: recent user context
followed by one ordinary history checkpoint. It never appends the summary,
the To-do list, or volatile shell/worker state to the standing system prompt;
reloads migrate the persisted carrier back into exactly one history item.
- Automatic compaction uses a percentage of the real context window, clamped to
the route's spendable ceiling. Pressure comes from the current parent-route
prompt, not cumulative billing or child-model usage.
- Compaction, review, verify, routing, setup, Fleet, MCP, RLM, vision,
translation, and sub-agent calls inherit the resolved route's normal output,
sampling, and reasoning policy. Small internal-task token caps no longer
truncate thinking routes or special-case individual providers.
- Incomplete provider responses fail truthfully across ordinary turns and every
internal model consumer. Partial text stays interrupted, pending tool calls do
not execute, and billed usage is retained.
- Transport-only `(reasoning omitted)` placeholders no longer enter new
transcripts and are filtered from restored sessions. Reasoning expand/collapse
actions stay attached to the exact rendered cell, including after replacement,
restore, filtering, and resize (#5291).
- Step-budget exhaustion is a typed failure and cannot release a pending
persistent service. Cancellation after terminal usage still charges the turn.
- Deferred tools now preserve a completed result when a provider reuses its
tool-call ID on the retry turn, preventing successful plugin calls from
entering a repeated execution loop.
- Website setup, provider, diagnostics, Fleet, and single-runtime claims now
match the source candidate.
- Opening the sub-agent register no longer hides the to-do list: the Agents
panel shows the full register and the durable checklist together, and the
register header is a two-way door that returns to Tasks on a second click.
- The ⌥V / Alt+V details chord opens the selected work-surface row's own
inspector instead of the transcript's nearest tool cell, so a selected
to-do row shows its own content rather than the latest reasoning.
- The first-run usage disclosure now asks a clear question — "Help improve
Codewhale?" — with unambiguous "Yes, keep anonymous counts" / "No, turn off
tracking" choices in every shipped locale, and states the persistent opt-out
command. Consent semantics are unchanged: telemetry stays unarmed until a
choice is made.
- macOS screencapture screenshots referenced in a message are copied to a
stable attachments directory the moment the message is received, and the
reference is rewritten to the stable path, so the image still exists when
the agent reads it. Only files under a screencapture "Temporary Items"
directory are touched; copies are idempotent and a failed copy keeps the
original reference.
- Manual `/compact` during an active turn now queues even when the engine's
bounded op mailbox is saturated. The request defers client-side, retries as
mailbox slots free, and cannot latch as already running after it settles.
- Interactive `/load`, startup `--resume`, and `/resume` picker paths preserve
the persisted provider, endpoint, and model identity; picker resume also
leaves a durable transcript receipt.
- Relative `mcp_config_path` values no longer depend on the launch directory or
silently load an empty server pool: Codewhale warns and falls back to the
user-global MCP configuration. Explicit absolute paths remain authoritative.
- Alibaba Model Studio `qwen3.8-max` and `qwen3.8-max-preview` still stream
their current reasoning, but no longer replay historical `reasoning_content`
that those routes do not accept. Historical reasoning replay is now gated by
the exact provider/API/model contract, so unknown `*-thinking` lookalikes
fail closed while documented Qwen, Kimi, DeepSeek, Mistral, Anthropic, and
Responses continuity rules remain intact.
- Compatibility File/patch calls retain optional content-hash guards when a
caller supplies them. The new direct `write` and `edit` schemas do not expose
hash or prior-read ceremony.
- Shell previews hold back incomplete UTF-8 sequences instead of emitting
replacement characters, and compaction receipts report token deltas.
- Nested agents may narrow but can never widen their inherited depth budget
(#5317 by @ousamabenyounes).
- Container publication now assembles AMD64 and ARM64 images in parallel on
native runners from the already-verified static release binaries, then
publishes and checks one multi-architecture manifest. It no longer rebuilds
both targets through the single long-running QEMU job that lost its runner.
### Removed
- The no-progress guard, repeated-read guard, and injected tool-error strategy
coaching. Productive polling, repeated inspection, and model-owned recovery
are no longer interrupted by runtime heuristics.
- Never-wired decision-card, keybinding, hover, shell-execution, engine-op, and
release-script paths were deleted so the supported runtime has one route for
each behavior.
### Contributors
- Xavier Pestel (@xavierpestel-ai) — Mistral AI provider route (#5295).
- Ben Younes (@ousamabenyounes) — inherited nested-agent depth cap (#5317).
- Rafael Cavalheri (@rafaelcavalheri) — ACP agentic tool turns (#5225).
## [0.9.5] - 2026-08-08
Codewhale v0.9.5 consolidates the terminal application into one compiled
runtime while preserving the familiar `codewhale` and `codew` commands. It
also expands the managed Runtime API, makes session and Fleet work easier to
inspect and resume, and removes the hidden local continuation backstop that
could end productive work without a final assistant response.
### Added
- **`model = "auto"` for prompt-based tier selection**: When set, the
dispatcher analyses the user's prompt before delegating to the TUI and
selects `deepseek-v4-pro` for complex tasks or `deepseek-v4-flash` for simple
tasks (PR #5257).
- Runtime API controls for persistent goals, bounded memory inspection, MCP
server and skill lifecycle management, and durable Fleet receipt evidence.
- Append-only session-tree history with `/tree`, `/branch`, `/fork`, and
`/resume`, plus `/rc` remote control and managed login.
- A unified Fleet roster for built-in dispatch postures and a pinned indicator
that keeps active background work visible above the composer.
- Incremental MCP registry refreshes that return the local snapshot immediately
and update it in the background.
- Scout and Reviewer agents can use a bounded direct-command evidence shell for
read-only workspace, Git, and GitHub inspection, and can keep private working
notes in their own To-do while the durable transcript retains their evidence.
### Changed
- `codewhale-cli` now contains the terminal runtime directly. Release installers
expose byte-identical `codewhale` and `codew` commands without a separate TUI
executable. The v0.9.5 asset set alone retains deprecated
`codewhale-tui-*` filenames as byte-identical compatibility copies so
installed v0.9.4 clients can discover and complete this upgrade.
- Startup release checks cache successful lookups for one hour. The updater
downloads and verifies the primary runtime once, then refreshes any existing
`codew` or legacy `codewhale-tui` command paths from the same bytes.
- Headless `codewhale exec` runs and verifier benchmark rollouts no longer
impose a 100-step default. `--max-turns` remains available as an explicit
opt-in ceiling; Fleet workers retain their separately configured budget.
- Goal token and time budgets are telemetry rather than default stop
conditions, and automatic goal continuation is unlimited unless the user
explicitly configures a continuation ceiling.
- Command-palette and slash-completion shadowing now share one alias-aware
discovery contract.
- The website install guidance, localized product copy, navigation controls,
social metadata, and Cloudflare build pipeline now describe and deploy the
same one-runtime release contract.
### Fixed
- The hidden 20-step no-user-input backstop no longer ends productive turns.
Tool results, queued steering, child completions, REPL feedback, and goal
continuations can all reach the next provider step and a final assistant
response; explicit user-configured limits and genuine stuck-loop guards remain.
- Complete error details are directly inspectable after a failure instead of
leaving the terminal with a clipped, unrecoverable error fragment.
- A newly minted OAuth credential is adopted in the same provider-selection
flow instead of requiring a second picker trip.
- Fresh session titles can replace a stale cached `New Session` placeholder,
unknown model context limits fail loudly, and release/source-install fallbacks
no longer request binaries removed by the single-runtime conversion.
### Contributors
- [Sh1Zuku](https://github.com/SparkofSpike) (`@SparkofSpike`) fixed stale
cached session titles that could pin the `New Session` placeholder.
- [Paulo Aboim Pinto](https://github.com/aboimpinto) (`@aboimpinto`) built the
shared alias-aware command discovery contract and acceptance coverage.
- [Sun Zhenyuan](https://github.com/bistack) (`@bistack`) contributed the
background incremental MCP Registry refresh.
- [SKY ZHAO](https://github.com/skyzhao1223) (`@skyzhao1223`) contributed
prompt-based `model = "auto"` routing in PR #5257.
## [0.9.4] - 2026-08-07
Codewhale v0.9.4 ships the release-train harness work: the familiar Fleet
roster/setup face with a clear operator-leader and user/folder scope, a
work strip that keeps actionable agents instead of a permanent archive,
@@ -19,6 +304,23 @@ File edits, terminal width, and Windows installation.
### Added
- Memory maintenance: `remember` gains `revise` and `retire` beside the
default `append`. Both name the exact note they target and both require
the evidence for the change. Append-only memory decays — a correction
sits behind the note it contradicts and both keep reaching the model —
so the model can now keep its own durable notes true instead of only
adding to them.
- An audit trail for durable state the model writes about you. Every
in-place memory edit is journalled to `memory/JOURNAL.md`, and every
continual-harness `refine` / `remove` to a `JOURNAL.md` beside its state,
each with before, after, and evidence. Harness removal previously left no
record at all even though the entry leaves state entirely, so the journal
is now the only place its content survives.
- A first-run tip that says so: the first time Codewhale saves something
durable it points at `/memory`, translated into all fifteen complete
locale packs. This state shaped later sessions and nothing ever mentioned
it existed.
- Sub-agent checkpoint resume: `agents/followup` resumes an
`interrupted_continuable` child from its checkpoint into a fresh agent loop —
new agent id, original prompt plus the prior conversation tail — when a
@@ -125,6 +427,39 @@ File edits, terminal width, and Windows installation.
- Acceptance-level Gherkin coverage locking the existing user-command
precedence, alias shadowing, fallback, and invalid-command error contract
(PR #4992).
- Agent Plugins v1.0.0: consume, publish, and slugify packaged sub-agent
briefs, with an install/update/uninstall on-ramp in the TUI (PR #5182). A
plugin bundles a prompt, posture, and routing as one shareable artifact;
on-disk migration of the older `plugin.toml` scaffold is deliberately out
of scope for this train.
- `send_later`: a model-callable one-shot delayed continuation tool, so the
model can schedule a single future nudge without an operator-approved
durable automation (PR #5138).
- `/advisor`: an opt-in background advisor watcher for live turns (PR #5139).
- Notification quiet mode with per-category switches and action-first copy
(PR #5066).
- Automation scheduling forms — one-shot `ONCE`, five-field cron, and honest
watcher modes — created through the approval-gated `automation` tool
(PR #5183).
- Sub-agent `resume_from` continuation chains (PR #5142), child-result
diff-tainting when a claimed diff is not visible to git, per-turn usage
receipts on the exec stream-json stream, and spawn receipts that report
the model each sub-agent actually ran on.
- Transport resilience: sub-agent exec transport retries with a 600 s
default (PR #5210), SSE header stalls retryable instead of fatal, and
headless turn resume after mid-stream network drops with an `EX_TEMPFAIL`
exit.
- Session durability and control: a deterministic compaction continuation
contract (PR #5064), persisting interrupted output (PR #5206), stop-word
cancellation (PR #5207), token-counter refresh (PR #5204), deny-by-default
approval cards (PR #5090), and the Operate completion gate (PR #5067).
- zh-Hant promoted to a full shipped locale with complete `en.json` parity
(PR #5143).
- A persistent update-available chip in the header, with the startup update
check throttled and naming the right command.
- RLM static intent extraction for code blocks (`rlm_block_intent.rs`)
landed as groundwork for a future code-mode approval flow; it is not yet
wired into the turn pipeline and ships dormant by design.
### Changed
@@ -171,12 +506,38 @@ File edits, terminal width, and Windows installation.
futures-util to 0.3.33, libc to 0.2.189, actions/stale to 11.0.0, and
docker/login-action to 4.5.2. The locked graph also includes the
event-listener 5.4.2 fix for RUSTSEC-2026-0221.
- The progress surface now speaks plainly everywhere: the last user-visible
"Work update is pending" notices say "To-do list", the tool constructor and
the docs name `todo_write` as the single canonical progress tool, and
`work_update`, `TodoWrite`, and `todo` stay registered as hidden
compatibility aliases so saved transcripts keep replaying.
- Sub-agent and `agents/wait` waits stay short by default and by cap:
blocking waits default to 30 s and refuse to block past 120 s, because a
blocked wait deafens the session to typed input and settled children
already report back as `<codewhale:subagent.done>` sentinels.
- `Bash` `action=wait` honors `timeout_secs` (seconds) and bare `timeout`
(milliseconds) alongside canonical `timeout_ms`, and `block` as an alias
for `wait`, so a habit formed on other wait tools gets the duration it
asked for instead of silently falling back to the 30 s default; the result
metadata reports the real `wait_timeout_ms` applied.
### Fixed
- The memory journal is no longer indexed as memory. It is Markdown in the
memory tree, so the source walk collected it and every retired note
re-entered the searchable set under its `before:` line — putting the
exact facts a revision had just removed back into the prompt.
- `memory_path` pointed at an already-native store no longer derives a
second store nested inside it, which silently wrote somewhere other than
the file the user named.
- `muse` and `muse-spark` resolved to `muse-spark-1.1` in the agent
registry while config had defaulted to `muse-spark-1.2`, so the CLI and
app-server routed those aliases somewhere the configured default never
pointed. The registry now carries 1.2 and the contributor variant.
- An explicit `type=builder` (or its `implementer` alias) plus
`write_authority=read_only` now fails closed at spawn instead of launching a
labeled write role that silently had only recon tools and then self-BLOCKED
labeled write role that silently had only read-only tools and then self-BLOCKED
after burning a turn (#5123). The check is deliberately narrow, because two
neighbouring combinations are legitimate and stay legal:
- `type=worker` + `read_only` — worker is the unnamed default (it renders as
@@ -196,7 +557,7 @@ File edits, terminal width, and Windows installation.
total the worker budget uses) instead of completion tokens alone; elapsed
time still freezes when the child settles.
- Live work-bar rows for sub-agents show how many to-dos they still have
left (`N left`) when the child's own ledger has unsettled items — never a
left (`N left`) when the child's own list has unsettled items — never a
fabricated zero when no list exists.
- Surfaces no longer claim an OS sandbox on platforms that cannot enforce one.
@@ -335,6 +696,56 @@ File edits, terminal width, and Windows installation.
- Transcript wheel scrolling under iTerm2: xterm alternate-scroll (DECSET
1007) now stays off while mouse capture is active, so wheel events arrive as
mouse events instead of being converted into arrow keys (#5223, PR #5234).
- A stalled model stream no longer ends the turn as `Completed` over a
frozen reasoning block: a mid-stream chunk-timeout now counts toward the
stream-error budget, so a stall with nothing streamed retries the request
transparently, and a stall that exhausts the retry budget fails the turn
with the real reason instead of reporting success.
- A finished background shell task now wakes the engine even when no goal is
active: the idle loop starts an ordinary runtime turn so the completion
reaches the model immediately instead of sitting unclaimed until the user
types (a dead provider route claims the completion once and reports where
the output lives instead of re-arming the same error every tick).
- Sub-agent final reports that exceed the summary budget are now spilled to
a session artifact, and the truncation footer names the
`retrieve_tool_result` ref for the elided middle instead of telling the
model the bytes are unrecoverable; write failures degrade to the honest
no-ref footer.
- An interactive mid-stream network drop after partial output no longer fails
the turn: the partial reply is preserved as a committed assistant message,
a runtime continuation message is appended, and the request is re-issued
bounded by the stream-retry budget.
- Large pasted input is no longer sent to the model twice as inline text and
as a backup `.md` paste file; the submitted message now carries only the
`@`-mention so the model reads the file once.
- A builder sub-agent can run ordinary shell writes again. Write claims
outlive the agents that register them, so a workspace accumulated one per
builder that ever ran — six completed agents left four standing claims in
testing — and the shared-checkout gate counted those long-finished children
as live contenders. Every later builder was refused `Bash` writes with
"cannot prove a bounded file target" and pushed toward worktree isolation,
which puts the work in a checkout the operator never looks at. The gate now
asks the question it meant to ask: is another *running* child writing in this
shared checkout. Concurrent writers are still gated; a lone builder writes in
the workspace you are actually watching.
- Ctrl-C during the first moments of startup no longer kills Codewhale
outright. The terminating-signal handlers were registered inside the task
that waits on them, and a spawned task does not run until the scheduler
first polls it, so a SIGINT arriving in that window hit the default
disposition — the process died with no exit code, no terminal restore, and
no session record. The handlers are now installed synchronously, before
the telemetry notice and before arming, so the window is closed.
- The documented tool list on the docs site named `update_plan` and
`work_update` as coordination tools. Neither is callable by the model —
`update_plan` replays older Plan artifacts and `work_update` is a hidden
compatibility alias — so the page listed two tools a reader cannot use and
omitted `todo_write`, the one they can.
### Security
- Bumped `nanoid` past GHSA-2v37-7h3g-55p8 (a custom generator given size
zero could loop indefinitely), restoring a zero-advisory `npm audit` for
the website.
### Removed
@@ -366,6 +777,8 @@ File edits, terminal width, and Windows installation.
- [vFONGv](https://github.com/vFONGv) (`@vFONGv`) wrote the zh-CN Windows
beginner guide with screenshots in PR #5229, harvested after its base branch
was accidentally deleted during maintainer cleanup.
- [mky](https://github.com/mky) (`@mky`) fixed the FreeBSD build (PR #5254, `rquickjs` `bindgen` on FreeBSD).
- [cacdcaecawae](https://github.com/cacdcaecawae) (`@cacdcaecawae`) contributed embedder-owned sub-agent state roots (PR #5252).
## [0.9.3] - 2026-07-31
@@ -3076,260 +3489,6 @@ folds in several community contributions.
- Config robustness: atomic permission-rule save, one-time config `.bak` backup before the first changed write, `CODEWHALE_HOME` as primary config home, and accepting the dispatcher-written config shape (camelCase aliases + `[features.enabled]` table) so legacy/dual-written configs parse cleanly
- Dependency/CI bumps: docker login/qemu actions, softprops gh-release, download-artifact, vitest, @opennextjs/cloudflare, form-data, js-yaml, dompurify, ws
## [0.8.60] - 2026-06-13
### Added
- **Agent Fleet real-run cutover (#3154/#3096).** `codewhale fleet run` now
launches durable workers through the headless `codewhale exec --output-format
stream-json` path instead of the local simulation interpreter, with terminal
worker events freeing leases so queued fleet tasks continue running.
- **Read-only shell parallelism (#2983).** The engine can now run conservative
read-only shell calls in parallel, including strict `bash`/`sh`/`zsh -c`
wrappers for whitelisted commands, while writes, stdin, background TTY work,
redirects, pipes, command substitution, and follow-mode tails stay serial.
- **Declarative JS/TS WhaleFlow authoring (#3097).** WhaleFlow now accepts a
compile-only `workflow({...})` JavaScript/TypeScript authoring form that
lowers into the existing `WorkflowSpec` validator without executing user
JavaScript.
- **Slash-menu Ctrl+P/Ctrl+N navigation (#3196).** The slash command menu now
supports Ctrl+P/Ctrl+N movement without letting the global file picker steal
focus while the menu is open. Thanks @1Git2Clone for the PR.
- **New models and first-party provider routes.** This release adds
**GLM-5.2** (selectable on the Z.ai Coding Plan and over OpenRouter as
`z-ai/glm-5.2`, alongside the existing GLM-5.1 default), a first-party
**Z.ai** provider route, a first-party **StepFun / StepFlash** route
(`step-3.7-flash`), and a first-party **MiniMax** route defaulting to
`MiniMax-M3` with the M2.7/M2.5/M2.1 family selectable (#3187/#3191).
### Changed
- **README and contributor credits.** The README now has a shorter public
overview and moves the full contributor ledger to `docs/CONTRIBUTORS.md`,
preserving public thanks for [DeepSeek](https://github.com/deepseek-ai),
[DataWhale](https://github.com/datawhalechina),
[OpenWarp](https://github.com/zerx-lab/warp), and
[Open Design](https://github.com/nexu-io/open-design).
- **Fleet-backed sub-agent direction.** Runtime docs now state the intended
cutover clearly: "sub-agent" is role/UX vocabulary, while durable detached
work should converge on the fleet-backed worker lifecycle with retries,
receipts, and ledgered inspection.
### Fixed
- **Sub-agent eval no longer blocks by default.** `agent_eval` now returns the
current projection immediately and delivers follow-up input without waiting
for a running child to finish its provider call. Pass `block:true` for an
intentional terminal wait.
- **Z.ai GLM thinking traces.** Direct Z.ai requests now use the documented
`thinking` shape, preserve and replay `reasoning_content`, classify GLM
reasoning streams as thinking output, and accept `ultracode` as a max-effort
alias.
- **Claude skill archive compatibility (#2743).** `/skill install` keeps
portable Claude-style skill folders supported while rejecting multi-skill
Claude plugin archives clearly instead of silently installing only one skill
and dropping plugin semantics. Thanks @AiurArtanis for the ecosystem request.
## [0.8.59] - 2026-06-12
### Added
- **Moonshot Kimi K2.7 Code model.** The Moonshot/Kimi provider now defaults to
`kimi-k2.7-code`, recognizes `kimi`/`kimi-k2` aliases for that model, keeps
explicit `kimi-k2.6` selectable, and adds the OpenRouter
`moonshotai/kimi-k2.7-code` registry row.
- **Concise verbosity mode (#3052).** CLI noninteractive launches now default
to concise prompt/output discipline unless overridden by config, env, or
`--verbosity`, while interactive TUI launches remain normal by default.
Thanks @cyq1017 for the PR.
- **Ephemeral generated project context (#3058).** Opening CodeWhale in a
directory with no instruction files now keeps the bounded generated project
overview in memory instead of creating `.codewhale/instructions.md`.
- **ACP registry auth metadata (#1447).** The ACP stdio adapter now advertises
terminal authentication setup in `initialize.authMethods`, matching the
registry's validation requirement.
- **Sidebar context menus (#3065).** Right-clicking the sidebar no longer shows
`Paste`; clickable sidebar rows now offer their row command as the first
context action.
- **Sidebar hover popovers (#3088).** Streaming turns now keep sidebar hover
popovers responsive while continuing to throttle transcript/body mouse
motion.
- **Dark-theme selection contrast (#3074, thanks @drpars).** Session, config,
help, context-menu, and approval selections now use the muted selection
background instead of the bright accent color.
- **Cursor-style activity metadata rows (#3146).** Dense successful tool-run
summaries now render as a single muted `Explored ...` / `Updated metadata`
row, include short command-family labels for successful generic verifier
groups, and keep keyboard/mouse expansion and detail inspection intact.
- **Provider-wait observability (#3095).** Footer stall reasons now name the
active provider/model route, idle seconds vs stream budget, and whether a
fanout plan is still at `0 running` or dispatch is pending. Structured
provider-wait incidents log once per turn from the main tick loop (not on
every footer redraw).
- **Interactive fanout launch gate (#3095).** Direct sub-agent children queue
behind a configurable semaphore (`[subagents] interactive_max_launch`,
default 4) with a visible `queued: waiting for an interactive fanout slot`
reason before their first model step.
- **Goal lifecycle controls.** `/goal` is now the primary command surface for
session goals, with `pause`, `resume`, `complete`, `blocked`, and `clear`
controls while `/hunt` remains a compatibility alias.
- **Persistent thread-goal API.** App-server clients can now set, get, and clear
durable thread goals through `thread/goal/set`, `thread/goal/get`, and
`thread/goal/clear`, backed by the state store with Codex-style status and
token/time accounting fields.
- **Command-boundary ownership layers (#2888/#3055).** Built-in slash command
metadata now lives in `commands/registry.rs`, slash parsing in
`commands/parse.rs`, and handlers under group-owned command areas, preserving
the existing dispatch surface while reducing future `commands/mod.rs` churn.
- **Approval-rule source metadata (#1186/#2971).** Runtime API
`approval.required` events now include optional `matched_rule` metadata when
an execution-policy rule caused the prompt. Thanks @greyfreedom for the PR
and @Ram9199 for the audit-semantics discussion.
- **Localized tool-family labels (#2901).** Tool activity labels for read,
patch, run, find, delegate, fanout, RLM, verify, think, and generic tool
work now route through the shipped locale tables. Thanks @gordonlu for the
PR.
- **Localized config section labels (#2918).** The interactive config view now
localizes section and session/saved scope labels while preserving English
search terms. Thanks @gordonlu for the PR.
- **Localized config editor labels (#2919).** The config editor modal now
localizes edit labels, default/unavailable placeholders, and effective
currency hints. Thanks @gordonlu for the PR.
- **Hotbar number-key dispatch (#3056).** Bare `1`-`8` now trigger bound
hotbar slots only when the composer is empty, while `Alt+1`-`Alt+8` trigger
slots regardless of composer text and overlays keep key ownership. Thanks
@reidliu41 for the PR.
- **Voice dictation commands (#3051).** `/voice`, `/voice-send`, and
`/voice-control` now record through `sox`/`rec`/`arecord`, transcribe via the
active provider's chat-completions API, and insert transcripts at the
composer cursor. The `voice.toggle` hotbar action dispatches the real voice
command, with help and status text localized across all seven shipped
locales. Thanks @huqiantao for the PR.
- **Thread rewind and snapshot restore API (#2808).** GUI clients can now call
`POST /v1/threads/{id}/undo`, `/patch-undo`, and `/retry` to fork, roll back,
or rerun recent thread turns, plus `POST /v1/snapshots/{id}/restore` to
restore a workspace snapshot by id. Thanks @bengao168 for the PR.
- **Active provider fallback chain (#2773).** Configured `fallback_providers`
now build an ordered primary-plus-fallback route that the TUI can report,
advance through, and reset with `/provider fallback reset`, including footer
visibility for fallback state. Thanks @idling11 for the PR.
- **Provider metadata registry (#3005).** Built-in provider ids, display names,
defaults, env vars, config keys, aliases, and wire formats now live in a
shared metadata registry, with the provider drift check covering the registry
contract. Thanks @sximelon for the PR.
- **Hugging Face provider route (#2879).** Hugging Face Inference Providers now
have first-class config, env, docs, and registry coverage for the
OpenAI-compatible router, including `huggingface`/`hugging-face`/
`hugging_face`/`hf` aliases and `HUGGINGFACE_*`/`HF_*` env fallbacks. Thanks
@mvanhorn for the PR.
### Fixed
- **SSE data lines without spaces (#3152).** Chat Completions, Responses, and
Anthropic stream readers now accept both `data: {...}` and `data:{...}` SSE
frames, matching the spec and preventing providers that omit the optional
space from streaming empty output. Thanks @wgeeker for the PR.
- **Runtime thread detail N+1 reads (#3141).** `get_thread_detail` now scans
persisted turn items once and groups them by turn instead of reading the
items directory once per turn, preserving item order while keeping large
thread detail loads responsive.
- **Project-local hook trust boundary (#3140).** `.codewhale/hooks.toml` is now
loaded only after the workspace is trusted in user-owned config, matching the
project-local MCP trust model while preserving the documented shell-command
hook contract.
- **Skill registry sync latency (#3139).** `/skills sync` now syncs registry
entries with bounded ordered concurrency, so network latency no longer stacks
one skill at a time while output order stays deterministic.
- **SiliconFlow China provider config (#2893/#2895).** `siliconflow-CN`
now reads its own `[providers.siliconflow_cn]` / `[providers.siliconflow-CN]`
table and falls back to `[providers.siliconflow]` only for unset
`api_key`/`base_url`/`model` fields. Thanks @Artenx for the report and
@idling11 for the PR.
- **Self-update download timeout (#3006).** `codewhale update` now applies a
five-minute HTTP client timeout so blocked or very slow GitHub release
downloads fail instead of hanging indefinitely. Thanks @New2Niu for the PR.
- **Legacy `deepseek` update migration (#2960/#3013/#3053).** Running
`deepseek update` or `deepseek-tui update` from a pre-rebrand install now
returns copy-pasteable npm, Cargo, Homebrew, and manual-binary migration
steps instead of trying to spawn a missing `codewhale` binary. README and
rebrand docs now cover the same upgrade path. Thanks @jazzi and
@tiangangQiu for the reports, @cyq1017 for the update-path PR, and
@angus-guo for the README PR.
- **Short `codew` shim delegation.** The `codew` convenience binary now
prefers the sibling `codewhale` dispatcher installed next to it before
falling back to `PATH`, preventing fresh local builds or installs from
accidentally invoking an older global dispatcher.
- **Constitution trust wording (#2950/#3008).** The base prompt now explains
that "begins with an A" means a baseline of trust, not a literal output
formatting rule. Thanks @cyq1017 for the PR.
- **TUI provider-source recovery (#3007/#3011).** Unsupported interactive
providers now report whether the value came from `--provider`, environment,
or config. Config-sourced unsupported providers fall back to DeepSeek without
forwarding stale keyring secrets. Thanks @cyq1017 for the PR.
- **Exec auto-model handoff (#3148).** `codewhale exec --model auto` now
survives the CLI/TUI boundary by honoring the CodeWhale model env alias and
legacy DeepSeek model handoff before falling back to provider defaults.
Thanks @hongchen1993 for the PR.
- **macOS shortcut modifiers (#2938/#2943).** Ctrl-like shortcuts that are
reported as `SUPER` by macOS terminals now work for backgrounding tasks and
sidebar-focus chords without rewriting clipboard shortcuts. Thanks @idling11
for the PR.
- **TUI mouse-report leak (#3063/#3067).** Strip raw SGR mouse coordinate
tails from the composer even when `use_mouse_capture` is false, covering
orphaned terminal reporting state after crashes or focus races.
- **Interrupted sub-agent lifecycle (#3080).** API-timeout interruptions now
emit `MailboxMessage::Interrupted`, render terminal interrupted cards, and
reconcile stale running fanout counts from manager snapshots.
- **OpenAI Codex stream diagnostics and active tool collapse (#3146).** The
Responses bridge now reports nested `response.failed` /
`response.incomplete` errors instead of `unknown`, and dense successful
in-flight tool bursts collapse into the same calm activity metadata row as
committed history.
- **OpenAI Codex reasoning tiers.** Switching from DeepSeek to `openai-codex`
now normalizes stale reasoning state into Responses-compatible
`low`/`medium`/`high`/`xhigh` tiers. Startup, `/config`, and the model
picker now display Codex labels instead of leaking DeepSeek
`off`/`max` names, while Codex still reports as a Responses payload
provider. The Responses request builder also clamps legacy `minimal` input
to `low` and has regression coverage that Codex requests use
`reasoning.effort`, not DeepSeek `thinking` fields.
- **OpenAI Codex context metadata (#3070).** The `gpt-5.5` default and
CodeWhale aliases now use OpenAI's documented 1,050,000-token context window
and 128,000 max-output metadata for context pressure, prompts, and doctor
capability output.
- **OpenAI Codex effective context budgeting.** The public OpenAI API metadata
for `gpt-5.5` remains 1,050,000 tokens, but the `openai-codex` OAuth route now
budgets prompts against the 400K Codex-family effective window so preflight
compaction runs before the backend returns `context_length_exceeded`.
- **OpenRouter Nemotron 3 Ultra preset.** The OpenRouter preset and model
registry now emit `nvidia/nemotron-3-ultra-550b-a55b` while keeping the old
Ultra aliases compatible.
- **OpenRouter auth after MiMo switches (#3064).** Switching from Xiaomi MiMo
to OpenRouter now has regression coverage for preflight key failures and
Bearer auth header isolation before any request can be dispatched.
- **Responses strict-tool schema compatibility (#3062/#3017/#1883).** Responses
function tools now preserve per-tool strict-mode compatibility, keep optional
strict-schema fields nullable, and append deterministic constraint notes when
root composition groups must be flattened for Responses.
- **Runtime prompt autonomous loop guard (#3061).** Runtime policy reference
now explicitly forbids initiating new work when `<runtime_prompt>` is the
only new turn content and no tool/sub-agent handoff is pending.
- **Goal runtime status sync.** Goal token budgets and active/paused/complete
status now sync into the engine alongside the objective, and model-visible
`update_goal` can only mark goals complete or blocked.
### Contributors
- Devin session work on #3080/#3095 (PRs #3103, #3104, #3106) — Hunter Bown
(maintainer integration/cherry-pick on `codex/v0.8.59-release-ready`).
- Nightt (@nightt5879) for the Responses strict-tool schema hardening in PR
#3062.
- yekern (@yekern) for the #3061 runtime-prompt loop safety report and repro
that shaped the dispatch guard.
- Paulo Aboim Pinto (@aboimpinto) for the staged command-boundary design and
Layer 3 registry/parser extraction in PR #2888, plus the #2851/#2791/#2870
architecture stream that guided the grouped command areas in #3055.
---
Older releases: [CHANGELOG.md](https://github.com/Hmbown/CodeWhale/blob/main/CHANGELOG.md) and [docs/CHANGELOG_ARCHIVE.md](https://github.com/Hmbown/CodeWhale/blob/main/docs/CHANGELOG_ARCHIVE.md).
+22 -18
View File
@@ -16,6 +16,10 @@ json = ["schemaui/json"]
toml = ["schemaui/toml"]
long-running-tests = []
[lib]
name = "codewhale_tui"
path = "src/lib.rs"
[[bin]]
name = "codewhale-tui"
path = "src/main.rs"
@@ -23,21 +27,22 @@ path = "src/main.rs"
[dependencies]
ahash = "0.8"
anyhow.workspace = true
codewhale-config = { path = "../config", version = "0.9.4" }
codewhale-execpolicy = { path = "../execpolicy", version = "0.9.4" }
codewhale-lane = { path = "../lane", version = "0.9.4" }
codewhale-paths = { path = "../paths", version = "0.9.4" }
codewhale-protocol = { path = "../protocol", version = "0.9.4" }
codewhale-release = { path = "../release", version = "0.9.4" }
codewhale-secrets = { path = "../secrets", version = "0.9.4" }
codewhale-telemetry = { path = "../telemetry", version = "0.9.4" }
codewhale-tools = { path = "../tools", version = "0.9.4" }
codewhale-workflow = { path = "../workflow", version = "0.9.4" }
codewhale-workflow-js = { path = "../workflow-js", version = "0.9.4" }
codewhale-config = { path = "../config", version = "0.9.6" }
codewhale-core = { path = "../core", version = "0.9.6" }
codewhale-execpolicy = { path = "../execpolicy", version = "0.9.6" }
codewhale-lane = { path = "../lane", version = "0.9.6" }
codewhale-paths = { path = "../paths", version = "0.9.6" }
codewhale-protocol = { path = "../protocol", version = "0.9.6" }
codewhale-release = { path = "../release", version = "0.9.6" }
codewhale-secrets = { path = "../secrets", version = "0.9.6" }
codewhale-telemetry = { path = "../telemetry", version = "0.9.6" }
codewhale-tools = { path = "../tools", version = "0.9.6" }
codewhale-workflow = { path = "../workflow", version = "0.9.6" }
codewhale-workflow-js = { path = "../workflow-js", version = "0.9.6" }
schemaui = { version = "0.12.0", default-features = false, optional = true }
async-stream = "0.3.6"
async-trait.workspace = true
base64 = "0.23.0"
base64 = "0.22.1"
axum.workspace = true
clap.workspace = true
clap_complete.workspace = true
@@ -55,7 +60,7 @@ oauth2 = "5"
ratatui = { version = "=0.30.0", features = ["unstable-rendered-line-info"] }
ratatui-core = "=0.1.0"
regex = "1.11"
reqwest = { workspace = true, features = ["blocking", "stream", "form", "http2", "gzip"] }
reqwest = { workspace = true, features = ["blocking", "stream", "form", "http2"] }
rusqlite.workspace = true
rmcp = { version = "2.2.0", default-features = false, features = ["auth", "client"] }
rustls.workspace = true
@@ -84,14 +89,12 @@ tower-http.workspace = true
wait-timeout = "0.2"
webbrowser = "1.0"
shlex = "1.3.0"
tiny_http = "0.12"
globset = "0.4"
ignore = "0.4"
image = { version = "0.25", default-features = false, features = ["png"] }
htmd = "0.5.4"
lru = "0.18"
lru = "0.16"
parking_lot = "0.12"
readability = { version = "0.3.0", default-features = false }
tar = "0.4"
flate2 = "1.1"
sha2.workspace = true
@@ -101,11 +104,12 @@ shell-words = "1.1.1"
mimalloc.workspace = true
[build-dependencies]
codewhale-build-support = { path = "../build-support", version = "0.9.4" }
codewhale-build-support = { path = "../build-support", version = "0.9.6" }
[dev-dependencies]
cucumber = "0.23.0"
wiremock = "0.6"
tiny_http = "0.12"
pretty_assertions = "1.4"
rio-vt = "0.5.1"
@@ -113,7 +117,7 @@ rio-vt = "0.5.1"
libc = "0.2"
[target.'cfg(windows)'.dependencies]
windows-sys = { version = "0.61.2", features = ["Win32_Foundation", "Win32_Security", "Win32_Security_Authorization", "Win32_Storage_FileSystem", "Win32_System_Threading"] }
windows-sys = { version = "0.61.2", features = ["Win32_Foundation", "Win32_Security", "Win32_Security_Authorization", "Win32_Storage_FileSystem", "Win32_System_Pipes", "Win32_System_Threading"] }
[target.'cfg(any(target_os = "macos", target_os = "windows", all(target_os = "linux", not(target_env = "ohos"))))'.dependencies]
arboard = "3.4"
+36 -4
View File
@@ -9,6 +9,19 @@
"HistoryHintAccept": "Enter acceptar",
"HistoryHintRestore": "Esc restaurar",
"HistoryNoMatches": " Cap coincidència",
"TranscriptReasoningExpand": "amplia",
"TelemetryNoticeHeadline": "Vols ajudar a millorar Codewhale?",
"TelemetryNoticeBody": "Codewhale compta: la versió que executes, el sistema operatiu i la família\nde CPU, la durada i el resultat de la sessió, i recomptes agregats de funcions\ni errors.\n\nNo recopila mai les teves converses, codi, prompts, fitxers, noms de\nfitxers, repositoris o branques, contingut del model ni credencials — i mai no\nenvia una cronologia de l'activitat de l'agent per torn o eina.\n\nNomés t'identifica un identificador aleatori desat en aquesta màquina, que se\nsubstitueix cada 90 dies. Pots canviar d'opinió en qualsevol moment:\n codewhale config set telemetry false\n\nEsquema complet, camp per camp: docs/TELEMETRY.md",
"TelemetryNoticeCompactBody": "Compta: versió, SO/CPU, temps/resultat, funcions/errors.\nID local rota/90d.\nMai: xats/codi/prompts/fitxers/noms; contingut del model/credencials; torns/eines.\nVegeu: docs/TELEMETRY.md\nDesact.: codewhale config set telemetry false",
"TelemetryNoticeChoiceKeep": "Sí, mantenir el recompte anònim",
"TelemetryNoticeChoiceDisable": "No, desactivar el seguiment",
"TelemetryNoticeActionChoose": "triar",
"TelemetryNoticeActionConfirm": "confirmar",
"TelemetryNoticeActionExit": "sortir",
"TelemetryNoticeReceiptEnabled": "Sí, el recompte anònim continua actiu.",
"TelemetryNoticeReceiptDisabled": "El recompte anònim d'ús està desactivat. No t'ho tornarem a preguntar.",
"TelemetryNoticeReceiptEnabledUnsaved": "El recompte anònim continua actiu en aquesta sessió. Codewhale no ha pogut desar l'elecció i tornarà a preguntar al pròxim inici.",
"TelemetryNoticeReceiptDisabledUnsaved": "El recompte anònim d'ús està desactivat en aquesta sessió. Codewhale no ha pogut desar l'elecció i tornarà a preguntar al pròxim inici.",
"StatusPickerTitle": " Línia d'estat ",
"StatusPickerInstruction": "Tria els elements que vols al peu:",
"StatusPickerActionToggle": "commutar ",
@@ -42,7 +55,7 @@
"HotbarActionSessionCompactDescription": "Compacta el context de la conversació actual.",
"HotbarActionModePlanName": "Mode Plan",
"HotbarActionModePlanDescription": "Pensa un pla abans d'actuar.",
"HotbarActionModeAgentName": "Mode Act",
"HotbarActionModeAgentName": "Mode Work",
"HotbarActionModeAgentDescription": "Treballa directament a la sessió actual.",
"HotbarActionModeYoloName": "Full Access (Act)",
"HotbarActionModeYoloDescription": "Compatibilitat: Act amb permisos de Full Access (no és un mode separat).",
@@ -259,6 +272,7 @@
"CmdModelsDescription": "Obtén els IDs de model en viu de l'API activa",
"CmdModelDbDescription": "Explora la base de dades de models inclosa",
"CmdNetworkDescription": "Gestiona les regles de xarxa de permís i denegació",
"CmdUpdateDescription": "Comprova i instal·la una nova versió de CodeWhale",
"CmdNoteDescription": "Afegeix, llista, edita o elimina notes de l'espai de treball",
"CmdThemeDescription": "Canvia de tema o obre el selector de temes",
"CmdProviderDescription": "Canvia el proveïdor i/o model actiu",
@@ -281,6 +295,12 @@
"CmdQueueIndexMin": "L'índex ha de ser >= 1",
"CmdRelayDescription": "Crea un relay de sessió (接力) per a un fil nou",
"CmdRemoteControlDescription": "Reprèn aquesta sessió exacta des del teu compte web de Codewhale",
"CmdRemoteEnvDescription": "Obre un Work allotjat nou des de la punta d'una branca de GitHub o CNB",
"CmdRemoteEnvOverview": "El Work allotjat inicia un entorn nou des de la punta de la branca disponible a GitHub o CNB.\n\nNo mou aquesta carpeta local ni inclou commits no enviats, fitxers modificats o ignorats, secrets ni l'estat de la sessió.\n\nUtilitza {command} per obrir el llançador de Work allotjat.",
"CmdRemoteEnvOpening": "S'està obrint el Work allotjat per a {repo} a la branca {branch}.\n\nAixò inicia un entorn nou des de la punta de la branca disponible a l'amfitrió Git configurat com a {origin}. L'estat exclusivament local no s'hi inclou.\n\nSi el navegador no s'obre, utilitza:\n{url}",
"CmdRemoteEnvUnavailable": "Codewhale no ha pogut resoldre alhora un origen compatible de GitHub o CNB i una branca activa per a aquesta carpeta. Canvia a una branca i configura {origin} amb un URL HTTPS o SSH; després torna a provar {command}. No s'ha creat ni assignat res.",
"CmdRemoteEnvSourceCustodyPolicy": "Codewhale no carrega, migra ni sincronitza codi font local amb el Work allotjat. Utilitza {command} per començar des de la punta de la branca disponible a GitHub o CNB. Els commits no enviats, els fitxers modificats o ignorats, els secrets i l'estat de la sessió es mantenen en local.",
"CmdRemoteEnvBrowserLabel": "Work allotjat de Codewhale",
"CmdRenameDescription": "Reanomena la sessió actual",
"CmdRestoreDescription": "Reverteix l'espai de treball a una instantània anterior pre/post-torn. Sense argument, llista les instantànies recents.",
"CmdRetryDescription": "Reintenta l'última petició",
@@ -288,6 +308,9 @@
"CmdRlmDescription": "Obre un context RLM persistent per a un fitxer o text",
"CmdSaveDescription": "Desa la sessió a un fitxer",
"CmdForkDescription": "Bifurca la conversació activa cap a una sessió germana",
"CmdTreeDescription": "Mostra l'historial de la sessió com un arbre (la fulla és la branca activa)",
"CmdBranchDescription": "Mou la branca activa a una entrada de sessió existent sense reescriure l'historial",
"CmdResumeDescription": "Reprèn una sessió, amb l'opció d'importar un fitxer JSON de sessió exportat",
"CmdNewDescription": "Inicia una sessió desada nova",
"CmdSessionsDescription": "Obre el selector de l'historial de sessions",
"CmdSettingsDescription": "Obre l'editor de configuració amb tipus",
@@ -583,7 +606,7 @@
"SetupConstitutionExistingLabel": "Fitxer existent:",
"SetupConstitutionExpertOverrideLabel": "Override expert:",
"SetupConstitutionGuidedHint": "1-6 ajusten l'esborrany, G previsualitza, G de nou ratifica. Només orientació: mai no canvia aprovacions del runtime, sandbox, shell, xarxa, confiança ni permisos MCP.",
"SetupConstitutionGuidedAnswersHint": "Les respostes guiades només desen preferències globals d'usuari. El nucli inclòs continua actiu; els mòduls de doctrina queden per als prompts de mode/futures adhesions.",
"SetupConstitutionGuidedAnswersHint": "Les respostes guiades només desen preferències globals d'usuari. El nucli inclòs continua actiu; l'execució queda en la política d'execució i futures adhesions.",
"SetupConstitutionPurposeLabel": "Propòsit:",
"SetupConstitutionAutonomyLabel": "Iniciativa:",
"SetupConstitutionEvidenceLabel": "Evidència:",
@@ -706,7 +729,7 @@
"CtxMenuHelp": "Ajuda",
"CtxMenuHelpDesc": "dreceres de teclat i ordres",
"FanoutCounts": "{done} fetes · {running} en curs · {failed} fallides · {pending} pendents",
"AppModeAgent": "Act",
"AppModeAgent": "Work",
"AppModeAuto": "Auto",
"AppModeYolo": "Full Access (etiqueta de mode obsoleta)",
"AppModePlan": "Plan",
@@ -768,6 +791,13 @@
"ElevationOptionWriteDesc": "Reintenta aquesta crida d'eina amb abast d'escriptura addicional del sistema de fitxers",
"ElevationOptionFullAccessDesc": "Reintenta sense límits de sandbox; concedeix accés sense restriccions al sistema de fitxers i a la xarxa",
"ElevationOptionAbortDesc": "Cancel·la aquesta execució d'eina",
"ContextAutoCompacting": "Compactant automàticament el context…",
"ContextManualCompacting": "Compactant el context…",
"ContextCompactionQueued": "La compactació del context s'ha posat a la cua; s'executarà després del torn actiu.",
"ContextCompactionAlreadyRunning": "La compactació del context ja està en curs.",
"ContextCompactionQueueFull": "No s'ha pogut posar la compactació del context a la cua perquè el motor està ocupat; torna-ho a provar quan acabi el torn.",
"ContextCompactionQueueClosed": "La compactació del context no està disponible perquè el motor ja no s'està executant.",
"ContextCompactionRouteInvalid": "No es pot compactar perquè la ruta del proveïdor actiu no és vàlida: {error}",
"CtxInspTitle": "Inspector de context",
"CtxInspSessionContext": "Context de la sessió",
"CtxInspSystemPrompt": "Estructura del prompt del sistema",
@@ -969,7 +999,7 @@
"PhaseDone": "fet",
"PhaseFailed": "fallit",
"PhaseFinishing": "acabant",
"ChipModeAct": "act",
"ChipModeAct": "work",
"ChipModePlan": "plan",
"ChipModeOperate": "operate",
"ChipPermissionReadOnly": "només lectura",
@@ -1203,6 +1233,8 @@
"BehavioralTipClearedInput": "Esborrat · {chord} restaura",
"BehavioralTipMcpValidation": "{command} inicia els servidors i mostra per què",
"BehavioralTipRepeatedCommand": "{command} ho pot fixar",
"BehavioralTipDurableStateWritten": "Desat · {command} per revisar",
"BehavioralTipTodoWrite": "Consell: segueix les tasques de diversos passos amb {command} per mantenir el progrés visible",
"SettingLockedDuringTurn": "{setting} està blocat mentre s'executa un torn — prem Esc per interrompre primer",
"SettingSubjectMode": "Mode",
"SettingSubjectThinking": "Raonament",
+36 -4
View File
@@ -9,6 +9,19 @@
"HistoryHintAccept": "Enter übernehmen",
"HistoryHintRestore": "Esc wiederherstellen",
"HistoryNoMatches": " Keine Treffer",
"TranscriptReasoningExpand": "erweitern",
"TelemetryNoticeHeadline": "Helfen Sie mit, Codewhale zu verbessern?",
"TelemetryNoticeBody": "Codewhale zählt: die ausgeführte Version, Betriebssystem und Prozessorfamilie,\nSitzungsdauer und -ergebnis sowie zusammengefasste Funktions- und\nFehlerzähler.\n\nEs erfasst nie Unterhaltungen, Code, Prompts, Dateien, Datei-, Repository-\noder Branch-Namen, Modellinhalte oder Zugangsdaten — und sendet nie eine\nAktivitätschronik des Agenten pro Runde oder Werkzeug.\n\nZur Identifikation dient nur eine zufällige, auf diesem Gerät gespeicherte\nKennung, die alle 90 Tage ersetzt wird. Sie können Ihre Meinung jederzeit\nändern:\n codewhale config set telemetry false\n\nVollständiges Schema, Feld für Feld: docs/TELEMETRY.md",
"TelemetryNoticeCompactBody": "Zählt: Version, OS/CPU, Dauer/Ergebnis, Funktionen/Fehler.\nLokale Zufalls-ID wechselt/90T.\nNie: Chat/Code/Prompts/Dateien/Namen; Modellinhalt/Zugangsdaten; Runden/Werkzeuge.\nSchema: docs/TELEMETRY.md\nAus: codewhale config set telemetry false",
"TelemetryNoticeChoiceKeep": "Ja, anonyme Zählung beibehalten",
"TelemetryNoticeChoiceDisable": "Nein, Nachverfolgung abschalten",
"TelemetryNoticeActionChoose": "wählen",
"TelemetryNoticeActionConfirm": "bestätigen",
"TelemetryNoticeActionExit": "beenden",
"TelemetryNoticeReceiptEnabled": "Ja, die anonyme Zählung bleibt eingeschaltet.",
"TelemetryNoticeReceiptDisabled": "Die anonyme Nutzungszählung ist ausgeschaltet. Sie werden nicht erneut gefragt.",
"TelemetryNoticeReceiptEnabledUnsaved": "Die anonyme Zählung bleibt für diese Sitzung eingeschaltet. Codewhale konnte die Auswahl nicht speichern und fragt beim nächsten Start erneut.",
"TelemetryNoticeReceiptDisabledUnsaved": "Die anonyme Nutzungszählung ist für diese Sitzung ausgeschaltet. Codewhale konnte die Auswahl nicht speichern und fragt beim nächsten Start erneut.",
"StatusPickerTitle": " Statuszeile ",
"StatusPickerInstruction": "Wähle die Chips für die Fußzeile:",
"StatusPickerActionToggle": "umschalten ",
@@ -42,7 +55,7 @@
"HotbarActionSessionCompactDescription": "Aktuellen Gesprächskontext komprimieren.",
"HotbarActionModePlanName": "Plan-Modus",
"HotbarActionModePlanDescription": "Erst einen Plan durchdenken, dann handeln.",
"HotbarActionModeAgentName": "Act-Modus",
"HotbarActionModeAgentName": "Work-Modus",
"HotbarActionModeAgentDescription": "Direkt in der aktuellen Sitzung arbeiten.",
"HotbarActionModeYoloName": "Full Access (Act)",
"HotbarActionModeYoloDescription": "Kompatibilität: Act mit Full-Access-Berechtigungen (kein eigener Modus).",
@@ -259,6 +272,7 @@
"CmdModelsDescription": "Live-Modell-IDs von der aktiven API abrufen",
"CmdModelDbDescription": "Mitgelieferte Modelldatenbank durchsuchen",
"CmdNetworkDescription": "Netzwerk-Allow- und Deny-Regeln verwalten",
"CmdUpdateDescription": "Neue CodeWhale-Version suchen und installieren",
"CmdNoteDescription": "Workspace-Notizen hinzufügen, auflisten, bearbeiten oder entfernen",
"CmdThemeDescription": "Theme wechseln oder Theme-Auswahl öffnen",
"CmdProviderDescription": "Aktiven Provider und/oder Modell wechseln",
@@ -281,6 +295,12 @@
"CmdQueueIndexMin": "Index muss >= 1 sein",
"CmdRelayDescription": "Session-Relay (接力) für einen frischen Thread erstellen",
"CmdRemoteControlDescription": "Diese exakte Sitzung aus deinem Codewhale-Webkonto fortsetzen",
"CmdRemoteEnvDescription": "Neues gehostetes Work von einer GitHub- oder CNB-Branch-Spitze öffnen",
"CmdRemoteEnvOverview": "Gehostetes Work startet eine neue Umgebung von der Branch-Spitze, die bei GitHub oder CNB verfügbar ist.\n\nDieser lokale Ordner wird nicht verschoben; nicht gepushte Commits, geänderte oder ignorierte Dateien, Geheimnisse und Sitzungsstatus werden nicht übernommen.\n\nMit {command} öffnest du den Launcher für gehostetes Work.",
"CmdRemoteEnvOpening": "Gehostetes Work für {repo} auf Branch {branch} wird geöffnet.\n\nDabei startet eine neue Umgebung von der Branch-Spitze, die beim als {origin} konfigurierten Git-Host verfügbar ist. Rein lokaler Status wird nicht übernommen.\n\nFalls sich der Browser nicht öffnet, verwende:\n{url}",
"CmdRemoteEnvUnavailable": "Codewhale konnte für diesen Ordner nicht sowohl einen unterstützten GitHub- oder CNB-Origin als auch einen ausgecheckten Branch ermitteln. Checke einen Branch aus, konfiguriere {origin} mit einer HTTPS- oder SSH-URL und versuche {command} erneut. Nichts wurde erstellt oder zugewiesen.",
"CmdRemoteEnvSourceCustodyPolicy": "Codewhale lädt lokalen Quellcode nicht in gehostetes Work hoch und migriert oder synchronisiert ihn nicht dorthin. Verwende {command}, um von der bei GitHub oder CNB verfügbaren Branch-Spitze zu starten. Nicht gepushte Commits, geänderte oder ignorierte Dateien, Geheimnisse und Sitzungsstatus bleiben lokal.",
"CmdRemoteEnvBrowserLabel": "Gehostetes Work von Codewhale",
"CmdRenameDescription": "Aktuelle Sitzung umbenennen",
"CmdRestoreDescription": "Workspace auf einen früheren Pre-/Post-Turn-Snapshot zurücksetzen. Ohne Argument werden die letzten Snapshots aufgelistet.",
"CmdRetryDescription": "Letzte Anfrage wiederholen",
@@ -288,6 +308,9 @@
"CmdRlmDescription": "Persistenten RLM-Kontext für eine Datei oder Text öffnen",
"CmdSaveDescription": "Sitzung in Datei speichern",
"CmdForkDescription": "Aktives Gespräch in eine Schwester-Sitzung forken",
"CmdTreeDescription": "Sitzungsverlauf als Baum anzeigen (das Blatt ist der aktive Zweig)",
"CmdBranchDescription": "Aktiven Zweig zu einem vorhandenen Sitzungseintrag verschieben, ohne den Verlauf neu zu schreiben",
"CmdResumeDescription": "Sitzung fortsetzen, optional durch Import einer exportierten Sitzungs-JSON-Datei",
"CmdNewDescription": "Neue gespeicherte Sitzung starten",
"CmdSessionsDescription": "Sitzungsverlauf-Auswahl öffnen",
"CmdSettingsDescription": "Typisierten Einstellungs-Editor öffnen",
@@ -583,7 +606,7 @@
"SetupConstitutionExistingLabel": "Bestehende Datei:",
"SetupConstitutionExpertOverrideLabel": "Experten-Override:",
"SetupConstitutionGuidedHint": "1-6 passt den Entwurf an, G zeigt die Vorschau, erneut G ratifiziert. Nur Leitlinien: Laufzeit-Freigabe, Sandbox, Shell, Netzwerk, Vertrauen oder MCP-Berechtigungen werden nie geändert.",
"SetupConstitutionGuidedAnswersHint": "Geführte Antworten speichern nur benutzer-globale Präferenzen. Der mitgelieferte Kern bleibt aktiv; Doktrin-Module bleiben in Modus-Prompts/künftigen Opt-ins.",
"SetupConstitutionGuidedAnswersHint": "Geführte Antworten speichern nur benutzer-globale Präferenzen. Der mitgelieferte Kern bleibt aktiv; Ausführung liegt bei der Laufzeitrichtlinie und künftigen Opt-ins.",
"SetupConstitutionPurposeLabel": "Zweck:",
"SetupConstitutionAutonomyLabel": "Initiative:",
"SetupConstitutionEvidenceLabel": "Belege:",
@@ -706,7 +729,7 @@
"CtxMenuHelp": "Hilfe",
"CtxMenuHelpDesc": "Tastenbelegung und Befehle",
"FanoutCounts": "{done} fertig · {running} läuft · {failed} fehlgeschlagen · {pending} ausstehend",
"AppModeAgent": "Act",
"AppModeAgent": "Work",
"AppModeAuto": "Auto",
"AppModeYolo": "Full Access (veralteter Modusname)",
"AppModePlan": "Plan",
@@ -768,6 +791,13 @@
"ElevationOptionWriteDesc": "Tool-Aufruf mit zusätzlichem beschreibbarem Dateisystembereich wiederholen",
"ElevationOptionFullAccessDesc": "Ohne Sandbox-Limits wiederholen; gewährt uneingeschränkten Dateisystem- und Netzwerkzugriff",
"ElevationOptionAbortDesc": "Diese Tool-Ausführung abbrechen",
"ContextAutoCompacting": "Kontext wird automatisch komprimiert…",
"ContextManualCompacting": "Kontext wird komprimiert…",
"ContextCompactionQueued": "Die Kontextkomprimierung wurde eingereiht; sie wird nach dem aktiven Zug ausgeführt.",
"ContextCompactionAlreadyRunning": "Die Kontextkomprimierung läuft bereits.",
"ContextCompactionQueueFull": "Die Kontextkomprimierung konnte nicht eingereiht werden, weil die Engine ausgelastet ist; versuche es nach Abschluss des Zugs erneut.",
"ContextCompactionQueueClosed": "Die Kontextkomprimierung ist nicht verfügbar, weil die Engine nicht mehr läuft.",
"ContextCompactionRouteInvalid": "Die Komprimierung ist nicht möglich, weil die aktive Provider-Route ungültig ist: {error}",
"CtxInspTitle": "Kontext-Inspektor",
"CtxInspSessionContext": "Sitzungskontext",
"CtxInspSystemPrompt": "System-Prompt-Struktur",
@@ -969,7 +999,7 @@
"PhaseDone": "fertig",
"PhaseFailed": "fehlgeschlagen",
"PhaseFinishing": "schließt ab",
"ChipModeAct": "act",
"ChipModeAct": "work",
"ChipModePlan": "plan",
"ChipModeOperate": "operate",
"ChipPermissionReadOnly": "nur lesen",
@@ -1203,6 +1233,8 @@
"BehavioralTipClearedInput": "Geleert · {chord} stellt wieder her",
"BehavioralTipMcpValidation": "{command} startet Server und zeigt warum",
"BehavioralTipRepeatedCommand": "{command} kann dies anpinnen",
"BehavioralTipDurableStateWritten": "Gespeichert · {command} ansehen",
"BehavioralTipTodoWrite": "Tipp: Mehrstufige Aufgaben mit {command} verfolgen — so bleibt der Fortschritt sichtbar",
"SettingLockedDuringTurn": "{setting} ist gesperrt, während ein Turn läuft — zuerst Esc zum Unterbrechen drücken",
"SettingSubjectMode": "Modus",
"SettingSubjectThinking": "Thinking",
+37 -5
View File
@@ -9,6 +9,19 @@
"HistoryHintAccept": "Enter accept",
"HistoryHintRestore": "Esc restore",
"HistoryNoMatches": " No matches",
"TranscriptReasoningExpand": "expand",
"TelemetryNoticeHeadline": "Help improve Codewhale?",
"TelemetryNoticeBody": "Codewhale counts: which version you run, OS and CPU family, session\nduration and outcome, and aggregate feature and error counters.\n\nIt never collects your conversations, code, prompts, files, repo or\nbranch names, model content, or credentials — and it never sends a\nper-turn or per-tool timeline of agent activity.\n\nYou are identified only by a random ID stored on this machine, replaced\nevery 90 days. Change your mind any time:\n codewhale config set telemetry false\n\nFull schema, field by field: docs/TELEMETRY.md",
"TelemetryNoticeCompactBody": "Counts: version, OS/CPU, session\ntime/result, feature/error totals.\nRandom local ID rotates every 90d.\nNever: chat/code/prompts/files/names;\nmodel content/credentials; turn/tools.\nSchema: docs/TELEMETRY.md\nOff: codewhale config set telemetry\nfalse",
"TelemetryNoticeChoiceKeep": "Yes, keep anonymous counts",
"TelemetryNoticeChoiceDisable": "No, turn off tracking",
"TelemetryNoticeActionChoose": "choose",
"TelemetryNoticeActionConfirm": "confirm",
"TelemetryNoticeActionExit": "exit",
"TelemetryNoticeReceiptEnabled": "Yes — anonymous counts stay on.",
"TelemetryNoticeReceiptDisabled": "Anonymous usage counting is off. You will not be asked again.",
"TelemetryNoticeReceiptEnabledUnsaved": "Counts stay on for this session. Codewhale could not save the choice, so it will ask again next launch.",
"TelemetryNoticeReceiptDisabledUnsaved": "Anonymous usage counting is off for this session. Codewhale could not save the choice, so it will ask again next launch.",
"StatusPickerTitle": " Status line ",
"StatusPickerInstruction": "Pick the chips you want in the footer:",
"StatusPickerActionToggle": "toggle ",
@@ -42,7 +55,7 @@
"HotbarActionSessionCompactDescription": "Compact the current conversation context.",
"HotbarActionModePlanName": "Plan mode",
"HotbarActionModePlanDescription": "Think through a plan before acting.",
"HotbarActionModeAgentName": "Act mode",
"HotbarActionModeAgentName": "Work mode",
"HotbarActionModeAgentDescription": "Do direct work in the current session.",
"HotbarActionModeYoloName": "Full Access (Act)",
"HotbarActionModeYoloDescription": "Compatibility: Act with Full Access permissions (not a separate mode).",
@@ -239,7 +252,7 @@
"CmdLogoutDescription": "Clear API key and return to setup",
"CmdMcpDescription": "Open or manage MCP servers",
"CmdPluginDescription": "Inspect and manage trusted plugin bundles; legacy executable tools stay separate",
"CmdPluginBundleUsage": "Usage: /plugin [list|show <name>|validate [name]|install <spec>|update <name>|uninstall <name>|trust <name> [review-token]|enable <name>|disable <name>|revoke <name>|reload|tools [name]]",
"CmdPluginBundleUsage": "Usage: /plugin [list|show <name>|validate [name]|export <name> <dir>|install <spec>|update <name>|uninstall <name>|trust <name> [review-token]|enable <name>|disable <name>|revoke <name>|reload|tools [name]]",
"CmdPluginBundleNoneFound": "No Codewhale plugin bundles discovered.",
"CmdPluginBundleListHeader": "Plugin bundles ({count}):",
"CmdPluginLegacyListHeader": "Legacy executable plugin tools ({count}) in {dir}:",
@@ -262,6 +275,7 @@
"CmdModelsDescription": "Fetch live model IDs from the active API",
"CmdModelDbDescription": "Browse the bundled model database",
"CmdNetworkDescription": "Manage network allow and deny rules",
"CmdUpdateDescription": "Check for and install a new CodeWhale release",
"CmdNoteDescription": "Add, list, edit, or remove workspace notes",
"CmdThemeDescription": "Switch theme or open the theme picker",
"CmdProviderDescription": "Switch the active provider and/or model",
@@ -284,6 +298,12 @@
"CmdQueueIndexMin": "Index must be >= 1",
"CmdRelayDescription": "Create a session relay (接力) for a fresh thread",
"CmdRemoteControlDescription": "Resume this exact session from your Codewhale web account",
"CmdRemoteEnvDescription": "Open new hosted Work from a GitHub or CNB branch tip",
"CmdRemoteEnvOverview": "Hosted Work starts a new environment from the branch tip available at GitHub or CNB.\n\nIt does not move this local folder or include unpushed commits, dirty or ignored files, secrets, or session state.\n\nUse {command} to open the hosted Work launcher.",
"CmdRemoteEnvOpening": "Opening hosted Work for {repo} on branch {branch}.\n\nThis starts a new environment from the branch tip available at the Git host configured as {origin}. Local-only state is not included.\n\nIf the browser does not open, use:\n{url}",
"CmdRemoteEnvUnavailable": "Codewhale could not resolve both a supported GitHub or CNB origin and a checked-out branch for this folder. Check out a branch and configure {origin} with an HTTPS or SSH URL, then try {command} again. Nothing was created or allocated.",
"CmdRemoteEnvSourceCustodyPolicy": "Codewhale does not upload, migrate, or sync local source into hosted Work. Use {command} to start from the branch tip available at GitHub or CNB. Unpushed commits, dirty or ignored files, secrets, and session state stay local.",
"CmdRemoteEnvBrowserLabel": "Codewhale hosted Work",
"CmdRenameDescription": "Rename the current session",
"CmdRestoreDescription": "Roll back the workspace to a prior pre/post-turn snapshot. With no arg, lists recent snapshots.",
"CmdRetryDescription": "Retry the last request",
@@ -291,6 +311,9 @@
"CmdRlmDescription": "Open a persistent RLM context for a file or text",
"CmdSaveDescription": "Save session to file",
"CmdForkDescription": "Fork the active conversation into a sibling session",
"CmdTreeDescription": "Show the session history as a tree (the leaf is the active branch)",
"CmdBranchDescription": "Move the active branch to an existing session entry without rewriting history",
"CmdResumeDescription": "Resume a session, optionally importing an exported session JSON file",
"CmdNewDescription": "Start a fresh saved session",
"CmdSessionsDescription": "Open session history picker",
"CmdSettingsDescription": "Open the typed settings editor",
@@ -606,7 +629,7 @@
"SetupConstitutionExistingLabel": "Existing file:",
"SetupConstitutionExpertOverrideLabel": "Expert override:",
"SetupConstitutionGuidedHint": "1-6 tune the draft, G previews, G again ratifies. Guidance only: it never changes runtime approval, sandbox, shell, network, trust, or MCP permissions.",
"SetupConstitutionGuidedAnswersHint": "Guided answers save user-global preferences only. The bundled core stays active; doctrine modules stay in mode prompts/future opt-ins.",
"SetupConstitutionGuidedAnswersHint": "Guided answers save user-global preferences only. The bundled core stays active; execution stays with runtime policy and future opt-ins.",
"SetupConstitutionPurposeLabel": "Purpose:",
"SetupConstitutionAutonomyLabel": "Initiative:",
"SetupConstitutionEvidenceLabel": "Evidence:",
@@ -729,7 +752,7 @@
"CtxMenuHelp": "Help",
"CtxMenuHelpDesc": "keybindings and commands",
"FanoutCounts": "{done} done · {running} running · {failed} failed · {pending} pending",
"AppModeAgent": "Act",
"AppModeAgent": "Work",
"AppModeAuto": "Auto",
"AppModeYolo": "Full Access (deprecated mode label)",
"AppModePlan": "Plan",
@@ -791,6 +814,13 @@
"ElevationOptionWriteDesc": "Retry this tool call with additional writable filesystem scope",
"ElevationOptionFullAccessDesc": "Retry without sandbox limits; grants unrestricted filesystem and network access",
"ElevationOptionAbortDesc": "Cancel this tool execution",
"ContextAutoCompacting": "Context automatically compacting…",
"ContextManualCompacting": "Compacting context…",
"ContextCompactionQueued": "Context compaction queued; it will run after the active turn.",
"ContextCompactionAlreadyRunning": "Context compaction is already in progress.",
"ContextCompactionQueueFull": "Context compaction could not be queued because the engine is busy; try again after the turn completes.",
"ContextCompactionQueueClosed": "Context compaction is unavailable because the engine is no longer running.",
"ContextCompactionRouteInvalid": "Cannot compact because the active provider route is invalid: {error}",
"CtxInspTitle": "Context inspector",
"CtxInspSessionContext": "Session Context",
"CtxInspSystemPrompt": "System Prompt Structure",
@@ -992,7 +1022,7 @@
"PhaseDone": "done",
"PhaseFailed": "failed",
"PhaseFinishing": "finishing",
"ChipModeAct": "act",
"ChipModeAct": "work",
"ChipModePlan": "plan",
"ChipModeOperate": "operate",
"ChipPermissionReadOnly": "read only",
@@ -1226,6 +1256,8 @@
"BehavioralTipClearedInput": "Cleared · {chord} restores",
"BehavioralTipMcpValidation": "{command} starts servers and shows why",
"BehavioralTipRepeatedCommand": "{command} can pin this",
"BehavioralTipDurableStateWritten": "Saved · {command} to inspect",
"BehavioralTipTodoWrite": "Tip: track multi-step work with {command} — it keeps progress visible",
"SettingLockedDuringTurn": "{setting} is locked while a turn is running — press Esc to interrupt first",
"SettingSubjectMode": "Mode",
"SettingSubjectThinking": "Thinking",
+36 -4
View File
@@ -9,6 +9,19 @@
"HistoryHintAccept": "Enter aceptar",
"HistoryHintRestore": "Esc restaurar",
"HistoryNoMatches": " Sin resultados",
"TranscriptReasoningExpand": "expandir",
"TelemetryNoticeHeadline": "¿Ayudas a mejorar Codewhale?",
"TelemetryNoticeBody": "Codewhale cuenta: la versión que ejecutas, el sistema operativo y la familia\nde CPU, la duración y el resultado de la sesión, y conteos agregados de\nfunciones y errores.\n\nNunca recopila tus conversaciones, código, prompts, archivos, nombres de\narchivos, repositorios o ramas, contenido del modelo ni credenciales — y nunca\nenvía una cronología de la actividad del agente por turno o herramienta.\n\nSolo te identifica un ID aleatorio almacenado en esta máquina, que se\nreemplaza cada 90 días. Cambia de opinión cuando quieras:\n codewhale config set telemetry false\n\nEsquema completo, campo por campo: docs/TELEMETRY.md",
"TelemetryNoticeCompactBody": "Cuenta: versión, SO/CPU, tiempo/resultado, funciones/errores.\nID local rota/90d.\nNunca: chats/código/prompts/archivos/nombres; datos del modelo/credenciales; turnos/herramientas.\nVer: docs/TELEMETRY.md\nApag.: codewhale config set telemetry false",
"TelemetryNoticeChoiceKeep": "Sí, mantener el conteo anónimo",
"TelemetryNoticeChoiceDisable": "No, desactivar el seguimiento",
"TelemetryNoticeActionChoose": "elegir",
"TelemetryNoticeActionConfirm": "confirmar",
"TelemetryNoticeActionExit": "salir",
"TelemetryNoticeReceiptEnabled": "Sí, el conteo anónimo sigue activo.",
"TelemetryNoticeReceiptDisabled": "El conteo anónimo de uso está desactivado. No volveremos a preguntarte.",
"TelemetryNoticeReceiptEnabledUnsaved": "El conteo anónimo sigue activo en esta sesión. Codewhale no pudo guardar la opción y volverá a preguntar en el próximo inicio.",
"TelemetryNoticeReceiptDisabledUnsaved": "El conteo anónimo de uso está desactivado en esta sesión. Codewhale no pudo guardar la opción, así que volverá a preguntar en el próximo inicio.",
"StatusPickerTitle": " Línea de estado ",
"StatusPickerInstruction": "Elige los elementos que quieres en el pie:",
"StatusPickerActionToggle": "alternar ",
@@ -42,7 +55,7 @@
"HotbarActionSessionCompactDescription": "Compactar el contexto de la conversación actual.",
"HotbarActionModePlanName": "Modo Plan",
"HotbarActionModePlanDescription": "Piensa el plan antes de actuar.",
"HotbarActionModeAgentName": "Modo Act",
"HotbarActionModeAgentName": "Modo Work",
"HotbarActionModeAgentDescription": "Trabaja directo en la sesión actual.",
"HotbarActionModeYoloName": "Acceso total (Act)",
"HotbarActionModeYoloDescription": "Compatibilidad: Act con acceso total (no es un modo aparte).",
@@ -262,6 +275,7 @@
"CmdModelsDescription": "Obtener IDs de modelo en vivo de la API activa",
"CmdModelDbDescription": "Explorar la base de datos de modelos integrada",
"CmdNetworkDescription": "Gestionar reglas de red permitidas y bloqueadas",
"CmdUpdateDescription": "Buscar e instalar una nueva versión de CodeWhale",
"CmdNoteDescription": "Agregar nota al archivo persistente (.codewhale/notes.md)",
"CmdThemeDescription": "Alternar entre tema claro y oscuro",
"CmdProviderDescription": "Cambiar o mostrar el backend LLM activo (deepseek | nvidia-nim | ollama)",
@@ -284,6 +298,12 @@
"CmdQueueIndexMin": "El índice debe ser >= 1",
"CmdRelayDescription": "Crear un relay de sesión (接力) para un hilo nuevo",
"CmdRemoteControlDescription": "Reanudar esta sesión exacta desde tu cuenta web de Codewhale",
"CmdRemoteEnvDescription": "Abrir un Work alojado nuevo desde la punta de una rama de GitHub o CNB",
"CmdRemoteEnvOverview": "El Work alojado inicia un entorno nuevo desde la punta de la rama disponible en GitHub o CNB.\n\nNo mueve esta carpeta local ni incluye commits sin enviar, archivos modificados o ignorados, secretos ni el estado de la sesión.\n\nUsa {command} para abrir el iniciador de Work alojado.",
"CmdRemoteEnvOpening": "Abriendo el Work alojado para {repo} en la rama {branch}.\n\nEsto inicia un entorno nuevo desde la punta de la rama disponible en el host Git configurado como {origin}. No se incluye el estado que solo existe localmente.\n\nSi el navegador no se abre, usa:\n{url}",
"CmdRemoteEnvUnavailable": "Codewhale no pudo resolver para esta carpeta tanto un origen compatible de GitHub o CNB como una rama activa. Cambia a una rama y configura {origin} con una URL HTTPS o SSH; después vuelve a intentar {command}. No se creó ni asignó nada.",
"CmdRemoteEnvSourceCustodyPolicy": "Codewhale no carga, migra ni sincroniza el código fuente local con el Work alojado. Usa {command} para iniciar desde la punta de la rama disponible en GitHub o CNB. Los commits sin enviar, los archivos modificados o ignorados, los secretos y el estado de la sesión permanecen locales.",
"CmdRemoteEnvBrowserLabel": "Work alojado de Codewhale",
"CmdRenameDescription": "Renombrar la sesión actual",
"CmdRestoreDescription": "Revertir el workspace a un snapshot pre/post-turno anterior. Sin argumento, lista los snapshots recientes.",
"CmdRetryDescription": "Repetir la última solicitud",
@@ -291,6 +311,9 @@
"CmdRlmDescription": "Abrir un contexto RLM persistente para un archivo o texto",
"CmdSaveDescription": "Guardar la sesión en archivo",
"CmdForkDescription": "Bifurcar la conversación activa a una sesión hermana",
"CmdTreeDescription": "Mostrar el historial de la sesión como árbol (la hoja es la rama activa)",
"CmdBranchDescription": "Mover la rama activa a una entrada existente de la sesión sin reescribir el historial",
"CmdResumeDescription": "Reanudar una sesión, con la opción de importar un archivo JSON de sesión exportado",
"CmdNewDescription": "Iniciar una nueva sesión guardada",
"CmdSessionsDescription": "Abrir el selector de sesiones",
"CmdSettingsDescription": "Abrir el editor de ajustes con tipos",
@@ -606,7 +629,7 @@
"SetupConstitutionExistingLabel": "Archivo existente:",
"SetupConstitutionExpertOverrideLabel": "Override experto:",
"SetupConstitutionGuidedHint": "1-6 ajustan el borrador, G previsualiza, G otra vez ratifica. Es solo guía: nunca cambia aprobaciones de runtime, sandbox, shell, red, confianza ni permisos MCP.",
"SetupConstitutionGuidedAnswersHint": "Las respuestas guiadas solo guardan preferencias globales de usuario. El núcleo incluido sigue activo; los módulos de doctrina quedan en prompts de modo/futuros opt-ins.",
"SetupConstitutionGuidedAnswersHint": "Las respuestas guiadas solo guardan preferencias globales de usuario. El núcleo incluido sigue activo; la ejecución queda en la política de ejecución y futuros opt-ins.",
"SetupConstitutionPurposeLabel": "Propósito:",
"SetupConstitutionAutonomyLabel": "Iniciativa:",
"SetupConstitutionEvidenceLabel": "Evidencia:",
@@ -729,7 +752,7 @@
"CtxMenuHelp": "Ayuda",
"CtxMenuHelpDesc": "atajos de teclado y comandos",
"FanoutCounts": "{done} completado · {running} ejecutando · {failed} falló · {pending} pendiente",
"AppModeAgent": "Act",
"AppModeAgent": "Work",
"AppModeAuto": "Automático",
"AppModeYolo": "Acceso total (etiqueta obsoleta)",
"AppModePlan": "Plan",
@@ -789,6 +812,13 @@
"ElevationOptionWriteDesc": "Reintenta esta llamada con ámbito adicional de sistema de archivos grabable",
"ElevationOptionFullAccessDesc": "Reintenta sin límites de sandbox; concede acceso sin restricciones al sistema de archivos y red",
"ElevationOptionAbortDesc": "Cancelar esta ejecución de herramienta",
"ContextAutoCompacting": "Compactando automáticamente el contexto…",
"ContextManualCompacting": "Compactando el contexto…",
"ContextCompactionQueued": "La compactación del contexto quedó en cola; se ejecutará después del turno activo.",
"ContextCompactionAlreadyRunning": "La compactación del contexto ya está en curso.",
"ContextCompactionQueueFull": "No se pudo poner en cola la compactación del contexto porque el motor está ocupado; inténtalo de nuevo cuando termine el turno.",
"ContextCompactionQueueClosed": "La compactación del contexto no está disponible porque el motor ya no está en ejecución.",
"ContextCompactionRouteInvalid": "No se puede compactar porque la ruta del proveedor activo no es válida: {error}",
"CtxInspTitle": "Inspector de contexto",
"CtxInspSessionContext": "Contexto de la sesión",
"CtxInspSystemPrompt": "Estructura del prompt del sistema",
@@ -992,7 +1022,7 @@
"PhaseDone": "listo",
"PhaseFailed": "falló",
"PhaseFinishing": "finalizando",
"ChipModeAct": "act",
"ChipModeAct": "work",
"ChipModePlan": "plan",
"ChipModeOperate": "operate",
"ChipPermissionReadOnly": "solo lectura",
@@ -1226,6 +1256,8 @@
"BehavioralTipClearedInput": "Borrado · {chord} restaura",
"BehavioralTipMcpValidation": "{command} inicia los servidores y muestra el motivo",
"BehavioralTipRepeatedCommand": "{command} puede fijar esto",
"BehavioralTipDurableStateWritten": "Guardado · {command} para ver",
"BehavioralTipTodoWrite": "Consejo: sigue el trabajo de varios pasos con {command} para mantener visible el progreso",
"SettingLockedDuringTurn": "{setting} está bloqueado mientras se ejecuta un turno: presiona Esc para interrumpir primero",
"SettingSubjectMode": "El modo",
"SettingSubjectThinking": "El razonamiento",
+36 -4
View File
@@ -9,6 +9,19 @@
"HistoryHintAccept": "Enter accepter",
"HistoryHintRestore": "Esc restaurer",
"HistoryNoMatches": " Aucun résultat",
"TranscriptReasoningExpand": "développer",
"TelemetryNoticeHeadline": "Aidez-nous à améliorer Codewhale ?",
"TelemetryNoticeBody": "Codewhale compte : la version exécutée, le système d'exploitation et la\nfamille du processeur, la durée et le résultat de la session, ainsi que les\ntotaux de fonctions et d'erreurs.\n\nIl ne collecte jamais vos conversations, votre code, vos prompts, fichiers,\nnoms de fichiers, de dépôts ou de branches, contenus du modèle ou identifiants\n— et n'envoie jamais de chronologie de l'activité par tour ou par outil.\n\nVous êtes identifié uniquement par un identifiant aléatoire stocké sur cette\nmachine, remplacé tous les 90 jours. Changez d'avis à tout moment :\n codewhale config set telemetry false\n\nSchéma complet, champ par champ : docs/TELEMETRY.md",
"TelemetryNoticeCompactBody": "Compte: version, OS/CPU, durée/résultat, fonctions/erreurs.\nID local aléatoire change/90j.\nJamais: chats/code/prompts/fichiers/noms; contenu modèle/identifiants; tours/outils.\nVoir: docs/TELEMETRY.md\nArrêt: codewhale config set telemetry false",
"TelemetryNoticeChoiceKeep": "Oui, conserver le comptage anonyme",
"TelemetryNoticeChoiceDisable": "Non, désactiver le suivi",
"TelemetryNoticeActionChoose": "choisir",
"TelemetryNoticeActionConfirm": "confirmer",
"TelemetryNoticeActionExit": "quitter",
"TelemetryNoticeReceiptEnabled": "Oui, les comptages anonymes restent actifs.",
"TelemetryNoticeReceiptDisabled": "Le comptage anonyme de l'utilisation est désactivé. La question ne sera plus posée.",
"TelemetryNoticeReceiptEnabledUnsaved": "Les comptages anonymes restent actifs pour cette session. Codewhale n'a pas pu enregistrer ce choix et reposera la question au prochain lancement.",
"TelemetryNoticeReceiptDisabledUnsaved": "Le comptage anonyme est désactivé pour cette session. Codewhale n'a pas pu enregistrer le choix et reposera la question au prochain lancement.",
"StatusPickerTitle": " Barre d'état ",
"StatusPickerInstruction": "Choisissez les éléments à afficher dans le pied de page :",
"StatusPickerActionToggle": "basculer ",
@@ -42,7 +55,7 @@
"HotbarActionSessionCompactDescription": "Compacter le contexte de la conversation en cours.",
"HotbarActionModePlanName": "Mode Plan",
"HotbarActionModePlanDescription": "Réfléchir à un plan avant d'agir.",
"HotbarActionModeAgentName": "Mode Act",
"HotbarActionModeAgentName": "Mode Work",
"HotbarActionModeAgentDescription": "Travailler directement dans la session en cours.",
"HotbarActionModeYoloName": "Full Access (Act)",
"HotbarActionModeYoloDescription": "Compatibilité : Act avec les permissions Full Access (pas un mode à part).",
@@ -259,6 +272,7 @@
"CmdModelsDescription": "Récupérer les IDs de modèles en direct depuis l'API active",
"CmdModelDbDescription": "Parcourir la base de modèles intégrée",
"CmdNetworkDescription": "Gérer les règles réseau d'autorisation et de refus",
"CmdUpdateDescription": "Rechercher et installer une nouvelle version de CodeWhale",
"CmdNoteDescription": "Ajouter, lister, modifier ou supprimer des notes du workspace",
"CmdThemeDescription": "Changer de thème ou ouvrir le sélecteur de thème",
"CmdProviderDescription": "Changer le fournisseur et/ou le modèle actif",
@@ -281,6 +295,12 @@
"CmdQueueIndexMin": "L'index doit être >= 1",
"CmdRelayDescription": "Créer un relais de session (接力) pour un nouveau fil",
"CmdRemoteControlDescription": "Reprendre cette session exacte depuis votre compte web Codewhale",
"CmdRemoteEnvDescription": "Ouvrir un nouveau Work hébergé depuis la pointe d'une branche GitHub ou CNB",
"CmdRemoteEnvOverview": "Le Work hébergé démarre un nouvel environnement depuis la pointe de la branche disponible sur GitHub ou CNB.\n\nIl ne déplace pas ce dossier local et n'inclut ni les commits non poussés, ni les fichiers modifiés ou ignorés, ni les secrets, ni l'état de la session.\n\nUtilisez {command} pour ouvrir le lanceur du Work hébergé.",
"CmdRemoteEnvOpening": "Ouverture du Work hébergé pour {repo} sur la branche {branch}.\n\nUn nouvel environnement démarre depuis la pointe de la branche disponible sur l'hôte Git configuré comme {origin}. L'état uniquement local n'est pas inclus.\n\nSi le navigateur ne s'ouvre pas, utilisez :\n{url}",
"CmdRemoteEnvUnavailable": "Codewhale n'a pas pu trouver à la fois une origine GitHub ou CNB prise en charge et une branche extraite pour ce dossier. Extrayez une branche, configurez {origin} avec une URL HTTPS ou SSH, puis réessayez {command}. Rien n'a été créé ni alloué.",
"CmdRemoteEnvSourceCustodyPolicy": "Codewhale ne téléverse, ne migre et ne synchronise pas le code source local dans le Work hébergé. Utilisez {command} pour démarrer depuis la pointe de la branche disponible sur GitHub ou CNB. Les commits non poussés, les fichiers modifiés ou ignorés, les secrets et l'état de la session restent locaux.",
"CmdRemoteEnvBrowserLabel": "Work hébergé de Codewhale",
"CmdRenameDescription": "Renommer la session en cours",
"CmdRestoreDescription": "Restaurer le workspace à un snapshot pré/post-tour antérieur. Sans argument, liste les snapshots récents.",
"CmdRetryDescription": "Réessayer la dernière requête",
@@ -288,6 +308,9 @@
"CmdRlmDescription": "Ouvrir un contexte RLM persistant pour un fichier ou un texte",
"CmdSaveDescription": "Enregistrer la session dans un fichier",
"CmdForkDescription": "Dupliquer la conversation active dans une session sœur",
"CmdTreeDescription": "Afficher lhistorique de la session sous forme darbre (la feuille est la branche active)",
"CmdBranchDescription": "Déplacer la branche active vers une entrée de session existante sans réécrire lhistorique",
"CmdResumeDescription": "Reprendre une session, avec la possibilité dimporter un fichier JSON de session exporté",
"CmdNewDescription": "Démarrer une nouvelle session enregistrée",
"CmdSessionsDescription": "Ouvrir le sélecteur d'historique des sessions",
"CmdSettingsDescription": "Ouvrir l'éditeur de paramètres typés",
@@ -583,7 +606,7 @@
"SetupConstitutionExistingLabel": "Fichier existant :",
"SetupConstitutionExpertOverrideLabel": "Dérogation experte :",
"SetupConstitutionGuidedHint": "1-6 ajustent le brouillon, G affiche l'aperçu, G à nouveau ratifie. Directives seulement : cela ne change jamais les approbations runtime, le sandbox, le shell, le réseau, la confiance ou les permissions MCP.",
"SetupConstitutionGuidedAnswersHint": "Les réponses guidées n'enregistrent que des préférences globales utilisateur. Le noyau intégré reste actif ; les modules de doctrine restent dans les prompts de mode/futures options.",
"SetupConstitutionGuidedAnswersHint": "Les réponses guidées n'enregistrent que des préférences globales utilisateur. Le noyau intégré reste actif ; l'exécution relève de la politique d'exécution et de futures options.",
"SetupConstitutionPurposeLabel": "Objectif :",
"SetupConstitutionAutonomyLabel": "Initiative :",
"SetupConstitutionEvidenceLabel": "Preuve :",
@@ -706,7 +729,7 @@
"CtxMenuHelp": "Aide",
"CtxMenuHelpDesc": "raccourcis clavier et commandes",
"FanoutCounts": "{done} terminé · {running} en cours · {failed} échoué · {pending} en attente",
"AppModeAgent": "Act",
"AppModeAgent": "Work",
"AppModeAuto": "Auto",
"AppModeYolo": "Full Access (étiquette de mode obsolète)",
"AppModePlan": "Plan",
@@ -768,6 +791,13 @@
"ElevationOptionWriteDesc": "Réessayer cet appel d'outil avec une portée inscriptible supplémentaire du système de fichiers",
"ElevationOptionFullAccessDesc": "Réessayer sans limites de sandbox ; accorde un accès illimité au système de fichiers et au réseau",
"ElevationOptionAbortDesc": "Annuler cette exécution d'outil",
"ContextAutoCompacting": "Compression automatique du contexte en cours…",
"ContextManualCompacting": "Compression du contexte en cours…",
"ContextCompactionQueued": "La compression du contexte a été mise en file dattente ; elle sexécutera après le tour actif.",
"ContextCompactionAlreadyRunning": "La compression du contexte est déjà en cours.",
"ContextCompactionQueueFull": "La compression du contexte na pas pu être mise en file dattente car le moteur est occupé ; réessayez une fois le tour terminé.",
"ContextCompactionQueueClosed": "La compression du contexte est indisponible car le moteur ne fonctionne plus.",
"ContextCompactionRouteInvalid": "Impossible de compacter, car la route du fournisseur actif nest pas valide : {error}",
"CtxInspTitle": "Inspecteur de contexte",
"CtxInspSessionContext": "Contexte de la session",
"CtxInspSystemPrompt": "Structure du prompt système",
@@ -969,7 +999,7 @@
"PhaseDone": "terminé",
"PhaseFailed": "échoué",
"PhaseFinishing": "finalisation",
"ChipModeAct": "act",
"ChipModeAct": "work",
"ChipModePlan": "plan",
"ChipModeOperate": "operate",
"ChipPermissionReadOnly": "lecture seule",
@@ -1203,6 +1233,8 @@
"BehavioralTipClearedInput": "Effacé · {chord} restaure",
"BehavioralTipMcpValidation": "{command} démarre les serveurs et montre pourquoi",
"BehavioralTipRepeatedCommand": "{command} peut épingler ceci",
"BehavioralTipDurableStateWritten": "Enregistré · {command} pour voir",
"BehavioralTipTodoWrite": "Astuce : suivez les tâches en plusieurs étapes avec {command} pour garder la progression visible",
"SettingLockedDuringTurn": "{setting} est verrouillé pendant qu'un tour est en cours — appuyez d'abord sur Esc pour interrompre",
"SettingSubjectMode": "Le mode",
"SettingSubjectThinking": "Le raisonnement",
+36 -4
View File
@@ -9,6 +9,19 @@
"HistoryHintAccept": "Enter चुनें",
"HistoryHintRestore": "Esc पुनर्स्थापित",
"HistoryNoMatches": " कोई मिलान नहीं",
"TranscriptReasoningExpand": "विस्तार",
"TelemetryNoticeHeadline": "क्या आप Codewhale को बेहतर बनाने में मदद करेंगे?",
"TelemetryNoticeBody": "Codewhale गणना करता है: चल रहा संस्करण, ऑपरेटिंग सिस्टम व प्रोसेसर परिवार,\nसत्र की अवधि व परिणाम, और सुविधाओं व त्रुटियों की कुल गणना।\n\nयह आपकी बातचीत, कोड, प्रॉम्प्ट, फ़ाइलें, फ़ाइल, भंडार या शाखा के नाम,\nमॉडल की सामग्री या परिचय प्रमाण कभी एकत्र नहीं करता, और प्रत्येक चरण या\nऔज़ार की एजेंट गतिविधि का क्रम भी कभी नहीं भेजता।\n\nपहचान के लिए केवल इस मशीन पर रखी एक आकस्मिक पहचान संख्या उपयोग होती है,\nजो हर 90 दिन में बदल दी जाती है। कभी भी अपना निर्णय बदलें:\n codewhale config set telemetry false\n\nहर क्षेत्र सहित पूरा प्रारूप: docs/TELEMETRY.md",
"TelemetryNoticeCompactBody": "गणना: संस्करण, सिस्टम/प्रोसेसर, सत्र अवधि/परिणाम और सुविधा/त्रुटि योग।\nआकस्मिक स्थानीय पहचान हर 90 दिन में बदलती है।\nकभी नहीं: बातचीत/कोड/प्रॉम्प्ट/फ़ाइल/नाम, मॉडल सामग्री/परिचय प्रमाण, चरण/औज़ार।\nप्रारूप: docs/TELEMETRY.md\nबंद: codewhale config set telemetry false",
"TelemetryNoticeChoiceKeep": "हाँ, अनाम गणना जारी रखें",
"TelemetryNoticeChoiceDisable": "नहीं, ट्रैकिंग बंद करें",
"TelemetryNoticeActionChoose": "चुनें",
"TelemetryNoticeActionConfirm": "पुष्टि",
"TelemetryNoticeActionExit": "बाहर",
"TelemetryNoticeReceiptEnabled": "हाँ, अनाम गणना चालू रहेगी।",
"TelemetryNoticeReceiptDisabled": "अनाम उपयोग गणना बंद है। दोबारा नहीं पूछा जाएगा।",
"TelemetryNoticeReceiptEnabledUnsaved": "इस सत्र में अनाम गणना चालू रहेगी। Codewhale चयन सहेज नहीं सका, इसलिए अगली बार शुरू होने पर फिर पूछेगा।",
"TelemetryNoticeReceiptDisabledUnsaved": "इस सत्र में अनाम उपयोग गणना बंद है। Codewhale चयन सहेज नहीं सका, इसलिए अगली बार शुरू होने पर फिर पूछेगा।",
"StatusPickerTitle": " स्टेटस लाइन ",
"StatusPickerInstruction": "फ़ुटर में चाहिए वाले चिप चुनें:",
"StatusPickerActionToggle": "टॉगल ",
@@ -42,7 +55,7 @@
"HotbarActionSessionCompactDescription": "वर्तमान वार्तालाप संदर्भ को कॉम्पैक्ट करें।",
"HotbarActionModePlanName": "Plan मोड",
"HotbarActionModePlanDescription": "कार्य करने से पहले योजना सोचें।",
"HotbarActionModeAgentName": "Act मोड",
"HotbarActionModeAgentName": "Work मोड",
"HotbarActionModeAgentDescription": "वर्तमान सत्र में सीधे काम करें।",
"HotbarActionModeYoloName": "Full Access (Act)",
"HotbarActionModeYoloDescription": "संगतता: Full Access अनुमतियों के साथ Act (अलग मोड नहीं)।",
@@ -259,6 +272,7 @@
"CmdModelsDescription": "सक्रिय API से लाइव मॉडल ID लाएँ",
"CmdModelDbDescription": "बंडल किया मॉडल डेटाबेस ब्राउज़ करें",
"CmdNetworkDescription": "नेटवर्क अनुमति और निषेध नियम प्रबंधित करें",
"CmdUpdateDescription": "नया CodeWhale रिलीज़ जाँचें और इंस्टॉल करें",
"CmdNoteDescription": "वर्कस्पेस नोट जोड़ें, सूचीबद्ध करें, संपादित करें या हटाएँ",
"CmdThemeDescription": "थीम बदलें या थीम चयनकर्ता खोलें",
"CmdProviderDescription": "सक्रिय प्रोवाइडर और/या मॉडल बदलें",
@@ -281,6 +295,12 @@
"CmdQueueIndexMin": "इंडेक्स >= 1 होनी चाहिए",
"CmdRelayDescription": "नए थ्रेड के लिए सत्र रिले (接力) बनाएँ",
"CmdRemoteControlDescription": "अपने Codewhale वेब खाते से यही सत्र जारी रखें",
"CmdRemoteEnvDescription": "GitHub या CNB ब्रांच टिप से नया होस्टेड Work खोलें",
"CmdRemoteEnvOverview": "होस्टेड Work, GitHub या CNB पर उपलब्ध ब्रांच टिप से नया एनवायरनमेंट शुरू करता है।\n\nयह इस लोकल फ़ोल्डर को नहीं ले जाता और इसमें पुश न किए गए कमिट, बदली हुई या इग्नोर की गई फ़ाइलें, सीक्रेट या सत्र स्थिति शामिल नहीं होती।\n\nहोस्टेड Work लॉन्चर खोलने के लिए {command} का उपयोग करें।",
"CmdRemoteEnvOpening": "{repo} के लिए {branch} ब्रांच पर होस्टेड Work खोला जा रहा है।\n\nयह {origin} के रूप में कॉन्फ़िगर किए गए Git होस्ट पर उपलब्ध ब्रांच टिप से नया एनवायरनमेंट शुरू करता है। केवल लोकल स्थिति शामिल नहीं होती।\n\nअगर ब्राउज़र नहीं खुले, तो इसका उपयोग करें:\n{url}",
"CmdRemoteEnvUnavailable": "Codewhale इस फ़ोल्डर के लिए समर्थित GitHub या CNB ओरिजिन और चेक आउट की गई ब्रांच, दोनों का पता नहीं लगा सका। किसी ब्रांच को चेक आउट करें, {origin} को HTTPS या SSH URL से कॉन्फ़िगर करें और फिर {command} आज़माएँ। कुछ भी बनाया या आवंटित नहीं किया गया।",
"CmdRemoteEnvSourceCustodyPolicy": "Codewhale लोकल सोर्स को होस्टेड Work में अपलोड, माइग्रेट या सिंक नहीं करता। GitHub या CNB पर उपलब्ध ब्रांच टिप से शुरू करने के लिए {command} का उपयोग करें। पुश न किए गए कमिट, बदली हुई या इग्नोर की गई फ़ाइलें, सीक्रेट और सत्र स्थिति लोकल ही रहती हैं।",
"CmdRemoteEnvBrowserLabel": "Codewhale होस्टेड Work",
"CmdRenameDescription": "वर्तमान सत्र का नाम बदलें",
"CmdRestoreDescription": "वर्कस्पेस को पूर्व pre/post-turn स्नैपशॉट पर वापस लाएँ। बिना आर्ग के हालिया स्नैपशॉट सूचीबद्ध करता है।",
"CmdRetryDescription": "अंतिम अनुरोध पुनः प्रयास करें",
@@ -288,6 +308,9 @@
"CmdRlmDescription": "फ़ाइल या टेक्स्ट के लिए स्थायी RLM संदर्भ खोलें",
"CmdSaveDescription": "सत्र को फ़ाइल में सहेजें",
"CmdForkDescription": "सक्रिय वार्तालाप को सिबलिंग सत्र में फ़ोर्क करें",
"CmdTreeDescription": "सत्र इतिहास को वृक्ष के रूप में दिखाएँ (अंतिम नोड सक्रिय शाखा है)",
"CmdBranchDescription": "इतिहास को दोबारा लिखे बिना सक्रिय शाखा को किसी मौजूदा सत्र प्रविष्टि पर ले जाएँ",
"CmdResumeDescription": "सत्र फिर से शुरू करें, और चाहें तो निर्यात की गई सत्र JSON फ़ाइल आयात करें",
"CmdNewDescription": "नया सहेजा गया सत्र शुरू करें",
"CmdSessionsDescription": "सत्र इतिहास चयनकर्ता खोलें",
"CmdSettingsDescription": "टाइप किया सेटिंग्स संपादक खोलें",
@@ -583,7 +606,7 @@
"SetupConstitutionExistingLabel": "मौजूदा फ़ाइल:",
"SetupConstitutionExpertOverrideLabel": "विशेषज्ञ ओवरराइड:",
"SetupConstitutionGuidedHint": "1-6 मसौदा समायोजित करते हैं, G पूर्वावलोकन, फिर G अंगीकार। केवल मार्गदर्शन: यह रनटाइम अनुमति, सैंडबॉक्स, शेल, नेटवर्क, ट्रस्ट या MCP अनुमतियाँ कभी नहीं बदलता।",
"SetupConstitutionGuidedAnswersHint": "निर्देशित उत्तर केवल उपयोगकर्ता-वैश्विक प्राथमिकताएँ सहेजते हैं। bundled कोर सक्रिय रहता है; सिद्धांत मॉड्यूल मोड प्रॉम्प्ट/भविष्य के opt-in में रहत है।",
"SetupConstitutionGuidedAnswersHint": "निर्देशित उत्तर केवल उपयोगकर्ता-वैश्विक प्राथमिकताएँ सहेजते हैं। bundled कोर सक्रिय रहता है; निष्पादन रनटाइम नीति और भविष्य के opt-in के पास रहत है।",
"SetupConstitutionPurposeLabel": "उद्देश्य:",
"SetupConstitutionAutonomyLabel": "पहल:",
"SetupConstitutionEvidenceLabel": "साक्ष्य:",
@@ -706,7 +729,7 @@
"CtxMenuHelp": "मदद",
"CtxMenuHelpDesc": "कीबाइंडिंग और कमांड",
"FanoutCounts": "{done} पूर्ण · {running} चल रहे · {failed} विफल · {pending} लंबित",
"AppModeAgent": "Act",
"AppModeAgent": "Work",
"AppModeAuto": "Auto",
"AppModeYolo": "Full Access (अप्रचलित मोड लेबल)",
"AppModePlan": "Plan",
@@ -768,6 +791,13 @@
"ElevationOptionWriteDesc": "अतिरिक्त लिखने योग्य फ़ाइलसिस्टम दायरे के साथ यह टूल कॉल फिर आज़माएँ",
"ElevationOptionFullAccessDesc": "सैंडबॉक्स सीमाओं के बिना फिर आज़माएँ; अप्रतिबंधित फ़ाइलसिस्टम और नेटवर्क एक्सेस देता है",
"ElevationOptionAbortDesc": "यह टूल निष्पादन रद्द करें",
"ContextAutoCompacting": "संदर्भ अपने आप संपीड़ित किया जा रहा है…",
"ContextManualCompacting": "संदर्भ संपीड़ित किया जा रहा है…",
"ContextCompactionQueued": "संदर्भ संपीड़न कतार में लगा दिया गया है; यह सक्रिय टर्न के बाद चलेगा।",
"ContextCompactionAlreadyRunning": "संदर्भ संपीड़न पहले से जारी है।",
"ContextCompactionQueueFull": "इंजन व्यस्त होने के कारण संदर्भ संपीड़न को कतार में नहीं लगाया जा सका; टर्न पूरा होने के बाद फिर कोशिश करें।",
"ContextCompactionQueueClosed": "संदर्भ संपीड़न उपलब्ध नहीं है क्योंकि इंजन अब नहीं चल रहा है।",
"ContextCompactionRouteInvalid": "संपीड़ित नहीं किया जा सकता क्योंकि सक्रिय प्रोवाइडर रूट अमान्य है: {error}",
"CtxInspTitle": "कॉन्टेक्स्ट इंस्पेक्टर",
"CtxInspSessionContext": "सत्र कॉन्टेक्स्ट",
"CtxInspSystemPrompt": "सिस्टम प्रॉम्प्ट संरचना",
@@ -969,7 +999,7 @@
"PhaseDone": "पूर्ण",
"PhaseFailed": "विफल",
"PhaseFinishing": "समापन हो रहा",
"ChipModeAct": "act",
"ChipModeAct": "work",
"ChipModePlan": "plan",
"ChipModeOperate": "operate",
"ChipPermissionReadOnly": "केवल पठन",
@@ -1203,6 +1233,8 @@
"BehavioralTipClearedInput": "साफ़ हुआ · {chord} से पुनर्स्थापित",
"BehavioralTipMcpValidation": "{command} सर्वर शुरू करता है और कारण दिखाता है",
"BehavioralTipRepeatedCommand": "{command} इसे पिन कर सकता है",
"BehavioralTipDurableStateWritten": "सहेजा गया · {command} से देखें",
"BehavioralTipTodoWrite": "सुझाव: कई चरणों वाले काम को {command} से ट्रैक करें—इससे प्रगति दिखाई देती रहती है",
"SettingLockedDuringTurn": "टर्न चलने के दौरान {setting} लॉक है — पहले Esc से बाधित करें",
"SettingSubjectMode": "मोड",
"SettingSubjectThinking": "थिंकिंग",
+36 -4
View File
@@ -9,6 +9,19 @@
"HistoryHintAccept": "Enter terima",
"HistoryHintRestore": "Esc pulihkan",
"HistoryNoMatches": " Tidak ada hasil",
"TranscriptReasoningExpand": "perluas",
"TelemetryNoticeHeadline": "Bantu tingkatkan Codewhale?",
"TelemetryNoticeBody": "Codewhale menghitung: versi yang dijalankan, sistem operasi dan keluarga CPU,\ndurasi dan hasil sesi, serta hitungan agregat fitur dan galat.\n\nIa tidak pernah mengumpulkan percakapan, kode, prompt, berkas, nama berkas,\nrepositori atau cabang, konten model, maupun kredensial Anda — dan tidak pernah\nmengirim linimasa aktivitas agen per giliran atau per alat.\n\nAnda hanya dikenali melalui ID acak yang disimpan di mesin ini, diganti setiap\n90 hari. Ubah keputusan kapan saja:\n codewhale config set telemetry false\n\nSkema lengkap, per bidang: docs/TELEMETRY.md",
"TelemetryNoticeCompactBody": "Hitungan: versi, sistem/CPU, durasi/hasil, serta total fitur/galat.\nID lokal acak diganti setiap 90 hari.\nTidak pernah: percakapan/kode/prompt/berkas/nama, konten model/kredensial, giliran/alat.\nSkema: docs/TELEMETRY.md\nNonaktifkan: codewhale config set telemetry false",
"TelemetryNoticeChoiceKeep": "Ya, pertahankan penghitungan anonim",
"TelemetryNoticeChoiceDisable": "Tidak, matikan pelacakan",
"TelemetryNoticeActionChoose": "pilih",
"TelemetryNoticeActionConfirm": "konfirmasi",
"TelemetryNoticeActionExit": "keluar",
"TelemetryNoticeReceiptEnabled": "Ya, penghitungan anonim tetap aktif.",
"TelemetryNoticeReceiptDisabled": "Penghitungan penggunaan anonim dinonaktifkan. Anda tidak akan ditanya lagi.",
"TelemetryNoticeReceiptEnabledUnsaved": "Penghitungan anonim tetap aktif untuk sesi ini. Codewhale tidak dapat menyimpan pilihan, sehingga akan bertanya lagi saat peluncuran berikutnya.",
"TelemetryNoticeReceiptDisabledUnsaved": "Penghitungan penggunaan anonim dinonaktifkan untuk sesi ini. Codewhale tidak dapat menyimpan pilihan, sehingga akan bertanya lagi saat peluncuran berikutnya.",
"StatusPickerTitle": " Baris status ",
"StatusPickerInstruction": "Pilih chip yang ingin ditampilkan di bilah bawah:",
"StatusPickerActionToggle": "alihkan ",
@@ -42,7 +55,7 @@
"HotbarActionSessionCompactDescription": "Padatkan konteks percakapan saat ini.",
"HotbarActionModePlanName": "Mode Plan",
"HotbarActionModePlanDescription": "Susun rencana sebelum bertindak.",
"HotbarActionModeAgentName": "Mode Act",
"HotbarActionModeAgentName": "Mode Work",
"HotbarActionModeAgentDescription": "Bekerja langsung di sesi saat ini.",
"HotbarActionModeYoloName": "Full Access (Act)",
"HotbarActionModeYoloDescription": "Kompatibilitas: Act dengan izin Full Access (bukan mode terpisah).",
@@ -259,6 +272,7 @@
"CmdModelsDescription": "Ambil ID model langsung dari API aktif",
"CmdModelDbDescription": "Telusuri database model bawaan",
"CmdNetworkDescription": "Kelola aturan izinkan dan tolak jaringan",
"CmdUpdateDescription": "Periksa dan pasang rilis CodeWhale baru",
"CmdNoteDescription": "Tambah, tampilkan, edit, atau hapus catatan workspace",
"CmdThemeDescription": "Ganti tema atau buka pemilih tema",
"CmdProviderDescription": "Ganti penyedia dan/atau model aktif",
@@ -281,6 +295,12 @@
"CmdQueueIndexMin": "Indeks harus >= 1",
"CmdRelayDescription": "Buat relay sesi (接力) untuk thread baru",
"CmdRemoteControlDescription": "Lanjutkan sesi persis ini dari akun web Codewhale Anda",
"CmdRemoteEnvDescription": "Buka Work terhosting baru dari ujung branch GitHub atau CNB",
"CmdRemoteEnvOverview": "Work terhosting memulai lingkungan baru dari ujung branch yang tersedia di GitHub atau CNB.\n\nTindakan ini tidak memindahkan folder lokal ini atau menyertakan commit yang belum di-push, file yang berubah atau diabaikan, rahasia, maupun status sesi.\n\nGunakan {command} untuk membuka peluncur Work terhosting.",
"CmdRemoteEnvOpening": "Membuka Work terhosting untuk {repo} pada branch {branch}.\n\nTindakan ini memulai lingkungan baru dari ujung branch yang tersedia di host Git yang dikonfigurasi sebagai {origin}. Status yang hanya ada secara lokal tidak disertakan.\n\nJika browser tidak terbuka, gunakan:\n{url}",
"CmdRemoteEnvUnavailable": "Codewhale tidak dapat menemukan sekaligus origin GitHub atau CNB yang didukung dan branch aktif untuk folder ini. Checkout sebuah branch dan konfigurasikan {origin} dengan URL HTTPS atau SSH, lalu coba {command} lagi. Tidak ada yang dibuat atau dialokasikan.",
"CmdRemoteEnvSourceCustodyPolicy": "Codewhale tidak mengunggah, memigrasikan, atau menyinkronkan kode sumber lokal ke Work terhosting. Gunakan {command} untuk memulai dari ujung branch yang tersedia di GitHub atau CNB. Commit yang belum di-push, file yang berubah atau diabaikan, rahasia, dan status sesi tetap lokal.",
"CmdRemoteEnvBrowserLabel": "Work terhosting Codewhale",
"CmdRenameDescription": "Ubah nama sesi saat ini",
"CmdRestoreDescription": "Kembalikan workspace ke snapshot pra/pasca-giliran sebelumnya. Tanpa argumen, menampilkan snapshot terbaru.",
"CmdRetryDescription": "Coba lagi permintaan terakhir",
@@ -288,6 +308,9 @@
"CmdRlmDescription": "Buka konteks RLM persisten untuk file atau teks",
"CmdSaveDescription": "Simpan sesi ke file",
"CmdForkDescription": "Fork percakapan aktif ke sesi saudara",
"CmdTreeDescription": "Tampilkan riwayat sesi sebagai pohon (daun adalah cabang aktif)",
"CmdBranchDescription": "Pindahkan cabang aktif ke entri sesi yang ada tanpa menulis ulang riwayat",
"CmdResumeDescription": "Lanjutkan sesi, dengan opsi mengimpor file JSON sesi yang diekspor",
"CmdNewDescription": "Mulai sesi tersimpan baru",
"CmdSessionsDescription": "Buka pemilih riwayat sesi",
"CmdSettingsDescription": "Buka editor pengaturan bertipe",
@@ -583,7 +606,7 @@
"SetupConstitutionExistingLabel": "File yang ada:",
"SetupConstitutionExpertOverrideLabel": "Override ahli:",
"SetupConstitutionGuidedHint": "1-6 menyesuaikan draf, G pratinjau, G lagi meratifikasi. Hanya panduan: tidak pernah mengubah persetujuan runtime, sandbox, shell, jaringan, kepercayaan, atau izin MCP.",
"SetupConstitutionGuidedAnswersHint": "Jawaban terpandu hanya menyimpan preferensi global pengguna. Inti bawaan tetap aktif; modul doktrin tetap di prompt mode/opt-in mendatang.",
"SetupConstitutionGuidedAnswersHint": "Jawaban terpandu hanya menyimpan preferensi global pengguna. Inti bawaan tetap aktif; eksekusi berada di kebijakan runtime dan opt-in mendatang.",
"SetupConstitutionPurposeLabel": "Tujuan:",
"SetupConstitutionAutonomyLabel": "Inisiatif:",
"SetupConstitutionEvidenceLabel": "Bukti:",
@@ -706,7 +729,7 @@
"CtxMenuHelp": "Bantuan",
"CtxMenuHelpDesc": "keybinding dan perintah",
"FanoutCounts": "{done} selesai · {running} berjalan · {failed} gagal · {pending} menunggu",
"AppModeAgent": "Act",
"AppModeAgent": "Work",
"AppModeAuto": "Auto",
"AppModeYolo": "Akses penuh (label mode usang)",
"AppModePlan": "Plan",
@@ -768,6 +791,13 @@
"ElevationOptionWriteDesc": "Coba lagi panggilan tool ini dengan cakupan filesystem tulis tambahan",
"ElevationOptionFullAccessDesc": "Coba lagi tanpa batas sandbox; memberikan akses filesystem dan jaringan tanpa batas",
"ElevationOptionAbortDesc": "Batalkan eksekusi tool ini",
"ContextAutoCompacting": "Konteks sedang dipadatkan secara otomatis…",
"ContextManualCompacting": "Sedang memadatkan konteks…",
"ContextCompactionQueued": "Pemadatan konteks telah masuk antrean; proses akan berjalan setelah giliran aktif selesai.",
"ContextCompactionAlreadyRunning": "Pemadatan konteks sedang berlangsung.",
"ContextCompactionQueueFull": "Pemadatan konteks tidak dapat dimasukkan ke antrean karena mesin sedang sibuk; coba lagi setelah giliran selesai.",
"ContextCompactionQueueClosed": "Pemadatan konteks tidak tersedia karena mesin tidak lagi berjalan.",
"ContextCompactionRouteInvalid": "Tidak dapat memadatkan karena rute penyedia aktif tidak valid: {error}",
"CtxInspTitle": "Inspektor konteks",
"CtxInspSessionContext": "Konteks Sesi",
"CtxInspSystemPrompt": "Struktur System Prompt",
@@ -969,7 +999,7 @@
"PhaseDone": "selesai",
"PhaseFailed": "gagal",
"PhaseFinishing": "menyelesaikan",
"ChipModeAct": "act",
"ChipModeAct": "work",
"ChipModePlan": "plan",
"ChipModeOperate": "operate",
"ChipPermissionReadOnly": "baca saja",
@@ -1203,6 +1233,8 @@
"BehavioralTipClearedInput": "Terhapus · {chord} memulihkan",
"BehavioralTipMcpValidation": "{command} memulai server dan menunjukkan alasannya",
"BehavioralTipRepeatedCommand": "{command} dapat menyematkan ini",
"BehavioralTipDurableStateWritten": "Tersimpan · {command} untuk melihat",
"BehavioralTipTodoWrite": "Tips: lacak pekerjaan bertahap dengan {command} agar progres tetap terlihat",
"SettingLockedDuringTurn": "{setting} terkunci saat giliran berjalan — tekan Esc untuk menginterupsi dulu",
"SettingSubjectMode": "Mode",
"SettingSubjectThinking": "Thinking",
+36 -4
View File
@@ -9,6 +9,19 @@
"HistoryHintAccept": "Enter 確定",
"HistoryHintRestore": "Esc 復元",
"HistoryNoMatches": " 一致なし",
"TranscriptReasoningExpand": "展開",
"TelemetryNoticeHeadline": "Codewhale の改善にご協力いただけますか?",
"TelemetryNoticeBody": "Codewhale が集計するのは次の情報です。実行中のバージョン、OS と CPU の種類、\nセッション時間と結果、機能およびエラーの集計カウンター。\n\n会話、コード、プロンプト、ファイル、リポジトリ名やブランチ名、モデルの内容、\n認証情報は収集しません。また、ターンごと、ツールごとのエージェント活動の\n履歴も送信しません。\n\n識別には、このマシンに保存されたランダム ID のみを使用します。この ID は\n90 日ごとに置き換えられます。あとからいつでも変更できます:\n codewhale config set telemetry false\n\n全フィールドのスキーマ: docs/TELEMETRY.md",
"TelemetryNoticeCompactBody": "集計: 版、OS/CPU、時間/結果、機能/エラー。\nローカルのランダムIDは90日ごとに更新。\n収集なし: 会話/コード/プロンプト/ファイル/名前、モデル内容/認証情報、ターン/ツール。\nスキーマ: docs/TELEMETRY.md\n無効: codewhale config set telemetry false",
"TelemetryNoticeChoiceKeep": "はい、匿名の利用状況集計を続けます",
"TelemetryNoticeChoiceDisable": "いいえ、利用状況集計をオフにします",
"TelemetryNoticeActionChoose": "選択",
"TelemetryNoticeActionConfirm": "確定",
"TelemetryNoticeActionExit": "終了",
"TelemetryNoticeReceiptEnabled": "はい。匿名の利用状況集計はオンのままです。",
"TelemetryNoticeReceiptDisabled": "匿名の利用状況集計はオフです。今後は確認しません。",
"TelemetryNoticeReceiptEnabledUnsaved": "このセッションでは匿名の利用状況集計はオンのままです。Codewhale は選択を保存できなかったため、次回の起動時に再度確認します。",
"TelemetryNoticeReceiptDisabledUnsaved": "このセッションでは匿名の利用状況集計がオフです。Codewhale は選択を保存できなかったため、次回の起動時に再度確認します。",
"StatusPickerTitle": " ステータス行 ",
"StatusPickerInstruction": "フッターに表示する項目を選択:",
"StatusPickerActionToggle": "切替 ",
@@ -42,7 +55,7 @@
"HotbarActionSessionCompactDescription": "現在の会話コンテキストを圧縮します。",
"HotbarActionModePlanName": "Plan モード",
"HotbarActionModePlanDescription": "実行前に計画を立てます。",
"HotbarActionModeAgentName": "Act モード",
"HotbarActionModeAgentName": "Work モード",
"HotbarActionModeAgentDescription": "現在のセッションで直接作業します。",
"HotbarActionModeYoloName": "フルアクセス (Act)",
"HotbarActionModeYoloDescription": "互換: Act + フルアクセス権限(独立モードではありません)。",
@@ -262,6 +275,7 @@
"CmdModelsDescription": "アクティブな API からライブのモデル ID を取得する",
"CmdModelDbDescription": "内蔵のモデルデータベースを閲覧する",
"CmdNetworkDescription": "ネットワーク許可・拒否ルールを管理",
"CmdUpdateDescription": "新しい CodeWhale リリースを確認してインストールします",
"CmdNoteDescription": "ワークスペースノートの追加、一覧、編集、削除",
"CmdThemeDescription": "テーマを切り替え(ダーク/ライト/グレースケール/システム)",
"CmdProviderDescription": "現在の LLM バックエンドを切り替え・確認(deepseek | nvidia-nim | ollama",
@@ -284,6 +298,12 @@
"CmdQueueIndexMin": "インデックスは 1 以上である必要があります",
"CmdRelayDescription": "新しいスレッド用のセッションリレー(接力)を作成",
"CmdRemoteControlDescription": "Codewhaleウェブアカウントからこのセッションを再開",
"CmdRemoteEnvDescription": "GitHub または CNB のブランチ先端から新しいホスト型 Work を開く",
"CmdRemoteEnvOverview": "ホスト型 Work は、GitHub または CNB で利用可能なブランチ先端から新しい環境を開始します。\n\nこのローカルフォルダーは移動されず、未プッシュのコミット、変更済みまたは無視対象のファイル、シークレット、セッション状態も含まれません。\n\n{command} でホスト型 Work のランチャーを開きます。",
"CmdRemoteEnvOpening": "{repo} のブランチ {branch} 用にホスト型 Work を開いています。\n\n{origin} に設定された Git ホストで利用可能なブランチ先端から新しい環境を開始します。ローカルにしかない状態は含まれません。\n\nブラウザーが開かない場合は、次を使用してください:\n{url}",
"CmdRemoteEnvUnavailable": "このフォルダーについて、対応する GitHub または CNB の origin とチェックアウト済みブランチの両方を解決できませんでした。ブランチをチェックアウトし、{origin} に HTTPS または SSH URL を設定してから、{command} を再試行してください。何も作成または割り当てられていません。",
"CmdRemoteEnvSourceCustodyPolicy": "Codewhale はローカルソースをホスト型 Work にアップロード、移行、同期しません。{command} を使用して、GitHub または CNB で利用可能なブランチ先端から開始してください。未プッシュのコミット、変更済みまたは無視対象のファイル、シークレット、セッション状態はローカルに残ります。",
"CmdRemoteEnvBrowserLabel": "Codewhale ホスト型 Work",
"CmdRenameDescription": "現在のセッションの名前を変更",
"CmdRestoreDescription": "ワークスペースを以前のターン前/後スナップショットへロールバック。引数なしで最近のスナップショットを一覧表示。",
"CmdRetryDescription": "直前のリクエストを再試行",
@@ -291,6 +311,9 @@
"CmdRlmDescription": "ファイルまたはテキスト用の永続 RLM コンテキストを開く",
"CmdSaveDescription": "セッションをファイルに保存",
"CmdForkDescription": "現在の会話を兄弟セッションに fork",
"CmdTreeDescription": "セッション履歴をツリー表示(末端がアクティブなブランチ)",
"CmdBranchDescription": "履歴を書き換えず、アクティブなブランチを既存のセッション項目へ移動",
"CmdResumeDescription": "セッションを再開(エクスポート済みセッション JSON の読み込みにも対応)",
"CmdNewDescription": "新しい保存済みセッションを開始",
"CmdSessionsDescription": "セッション履歴ピッカーを開く",
"CmdSettingsDescription": "型付き設定エディターを開く",
@@ -606,7 +629,7 @@
"SetupConstitutionExistingLabel": "既存ファイル:",
"SetupConstitutionExpertOverrideLabel": "上級者オーバーライド:",
"SetupConstitutionGuidedHint": "1-6 で草案を調整、G でプレビュー、もう一度 G で承認します。これは指針だけであり、実行時の承認、サンドボックス、シェル、ネットワーク、信頼、MCP 権限は変更しません。",
"SetupConstitutionGuidedAnswersHint": "ガイド回答はユーザーグローバル設定だけを保存します。同梱コアは有効のまま、doctrine モジュールはモードプロンプト/将来の opt-in に残ります。",
"SetupConstitutionGuidedAnswersHint": "ガイド回答はユーザーグローバル設定だけを保存します。同梱コアは有効のまま、実行はランタイムポリシーと将来の opt-in が担います。",
"SetupConstitutionPurposeLabel": "目的:",
"SetupConstitutionAutonomyLabel": "主体性:",
"SetupConstitutionEvidenceLabel": "根拠:",
@@ -729,7 +752,7 @@
"CtxMenuHelp": "ヘルプ",
"CtxMenuHelpDesc": "キー操作とコマンド",
"FanoutCounts": "{done} 完了 · {running} 実行中 · {failed} 失敗 · {pending} 保留",
"AppModeAgent": "Act",
"AppModeAgent": "Work",
"AppModeAuto": "自動",
"AppModeYolo": "フルアクセス(非推奨ラベル)",
"AppModePlan": "Plan",
@@ -789,6 +812,13 @@
"ElevationOptionWriteDesc": "追加の書き込み可能ファイルシステム範囲で再試行",
"ElevationOptionFullAccessDesc": "サンドボックス制限なしで再試行(ファイルシステムとネットワークへの無制限アクセス)",
"ElevationOptionAbortDesc": "このツール実行をキャンセル",
"ContextAutoCompacting": "コンテキストを自動圧縮しています…",
"ContextManualCompacting": "コンテキストを圧縮しています…",
"ContextCompactionQueued": "コンテキスト圧縮をキューに追加しました。アクティブなターンの完了後に実行されます。",
"ContextCompactionAlreadyRunning": "コンテキスト圧縮はすでに進行中です。",
"ContextCompactionQueueFull": "エンジンがビジーなため、コンテキスト圧縮をキューに追加できませんでした。ターンの完了後にもう一度お試しください。",
"ContextCompactionQueueClosed": "エンジンがすでに停止しているため、コンテキスト圧縮を利用できません。",
"ContextCompactionRouteInvalid": "アクティブなプロバイダールートが無効なため圧縮できません: {error}",
"CtxInspTitle": "コンテキストインスペクタ",
"CtxInspSessionContext": "セッションコンテキスト",
"CtxInspSystemPrompt": "システムプロンプト構造",
@@ -992,7 +1022,7 @@
"PhaseDone": "完了",
"PhaseFailed": "失敗",
"PhaseFinishing": "仕上げ中",
"ChipModeAct": "act",
"ChipModeAct": "work",
"ChipModePlan": "plan",
"ChipModeOperate": "operate",
"ChipPermissionReadOnly": "読み取り専用",
@@ -1226,6 +1256,8 @@
"BehavioralTipClearedInput": "クリアしました · {chord} で復元",
"BehavioralTipMcpValidation": "{command} はサーバーを起動し、原因を表示します",
"BehavioralTipRepeatedCommand": "{command} でこれを固定できます",
"BehavioralTipDurableStateWritten": "保存しました · {command} で確認",
"BehavioralTipTodoWrite": "ヒント: 複数ステップの作業は {command} で追跡すると、進捗を確認できます",
"SettingLockedDuringTurn": "ターンの実行中は{setting}を変更できません。まず Esc で中断してください",
"SettingSubjectMode": "モード",
"SettingSubjectThinking": "思考",

Some files were not shown because too many files have changed in this diff Show More