Compare commits

...
Author SHA1 Message Date
CodeWhale BotandClaude Opus 5.5 1c25cd0448 Merge origin/main into fix/7096-opencode-go-context-window
Maintainer merge so the required checks run against current main
(main was red when this branch last ran CI; CW-7525).

Signed-off-by: CodeWhale Bot <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 15:19:50 -07:00
CodeWhale BotandClaude Opus 5.5 715bb2ed02 fix(ci): unbreak the Windows build and rustfmt gate on main
Every PR's required checks were red for reasons on main itself:
- Lint: `cargo fmt --check` diffed two hunks in crates/tui/src/mcp.rs
  from e1acc381ab; formatted.
- Test (windows-latest): dead code is denied on Windows, where code that
  is Unix-only or compiled out by the debug test-isolation gate leaves
  helpers unused. `native_snapshot_roots` (read only under
  cfg(not(windows))) and the cfg(test) `load_global_context_file` get
  `cfg_attr(windows, expect(dead_code))`; the Ollama probe helpers
  (`TAGS_PROBE_TIMEOUT`, `probe_client`, `fetch_text`,
  `fetch_tag_profile`, `ollama_v1_base_url`,
  `record_ollama_tags_into_lake`, `probe_live_local_ollama_catalog`) and
  `acquire_runtime_chat_inference_ownership`, which S23 placed behind the
  shared test-isolation predicate, get `cfg_attr(windows,
  allow(dead_code))`. The supervisor attribute was written by the #7091
  session and left uncommitted; taken over here.
The Windows "dsh-message-compat.js no longer names dsh-brand" failure is
CRLF checkout changing the pinned source hash; contributor PR #7109 fixes
that and lands separately.

Verified: rustfmt --check clean on mcp.rs; the attributes match the exact
items in Test (windows-latest) on PR #7105 (job 122399216069). A local
x86_64-pc-windows-msvc check could not complete (aws-lc-sys needs MSVC
headers), so Windows proof comes from the next PR CI run on this main.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 15:16:12 -07:00
CodeWhale BotandClaude Opus 5.5 3a72047c1b docs(dsh): record the live surface walkthrough of a DSH plugin (CW-7054)
Adds "Surfaces walked through live" to docs/DSH_PLUGIN_COMPAT.md, from a
DSH-format probe plugin (tool using sessions.flush + userQuestions.ask, two
commands) against a loopback fixture provider on macOS:
- Runtime API: install/review/trust/enable, first-turn tool, approval,
  question answered via POST /v1/user-input, command result.
- codewhale exec --auto: first-turn tool ran, flush true, question refused
  NO_PROVIDER (no human present).
- Desktop app (codewhale-app 7e0e0ae7 debug build, codewhale-gpui, Engine
  fff5a476f9+ debug build): approval card "Use the dsh_probe tool", native
  question card (Blue/Calm, Green, free text, Skip, Send answers), plugin
  received selected ["Green"], /probe-count in the palette returned 2 with
  "extension:dsh-probe-bundle · Completed".
Interactive TUI not walked through.

Paths: docs/DSH_PLUGIN_COMPAT.md
Verified: docs only; evidence is local (screenshots and receipts kept outside
the repo).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 15:03:12 -07:00
Claude 0da2b3d704 feat(runtime): hunk-level git writes, fork parent links, suggested tasks
Hunks: POST /v1/git/stage, /unstage and /discard take an optional
`patch` for the single path in `paths`. Its file headers must name that
workspace-relative path on both sides; renames, copies, mode changes,
creations, deletions and binary patches are refused, as are hunks whose
bodies are shorter than their @@ counts. The patch goes through a temp
file to `git apply [--cached] [--reverse]` under the existing write lock
and expect preconditions. It is applied from the repository top with
--directory=<workspace prefix>: run inside a subdirectory, git apply
silently skips paths outside it and reports success having written
nothing. Checked by hand in a scratch repo with a subdirectory
workspace: stage, unstage and discard each change exactly the one hunk,
and re-applying a stale hunk fails with git's message.

Forks: ThreadRecord gains parent_thread_id, set by both fork paths and
returned in /v1/threads/summary, so clients can nest forks under the
thread they came from. Older records have none.

Suggestions: the tasks tool gains a `suggest` action (no approval,
starts nothing) returning {title, prompt}; the logged tool call is the
suggestion, which the desktop shows as a Start/Hide card. Registry row
task-suggestions.

Evidence (RUST_MIN_STACK=8388608):
cargo test -p codewhale-tui --lib -- git_routes_drive git_write_preconditions
  runtime_api::git fork thread_summary tools::tasks registry
test result: FAILED. 326 passed; 1 failed; 3 ignored
The failure, runtime_api::git::tests::unreadable_file_only_withholds_affected_row,
chmods a file to 0o000 and expects it unreadable; this container runs as
uid 0, which reads it anyway. cargo fmt clean for the touched files.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_018whUMPrn9PSoxXwWwX7dsp
2026-10-27 21:47:07 +00:00
CodeWhale BotandClaude Opus 5.5 983ab92e4f docs(dsh): planned child-agent mapping onto the existing sub-agent manager (CW-7054)
Adds the DSH child-agent call -> Codewhale owner table to
docs/DSH_PLUGIN_COMPAT.md: getProvider, startActivation, legacy start,
agent/created, child-scoped systemPrompt.section persona, tools.restrict /
toolFilter, tools.get, activation.result/dispose and caller delivery all map
onto SharedSubAgentManager prepare/commit, WorkerRuntimeProfile narrowing and
the one turn loop; nothing adds a second executor or prompt authority.
States plainly that subagents stays refused: the rebased manager patch
(8bdffba2) and Core activation hook draft now type-check against main, but
the plugin bridge (host shim, Engine protocol methods, compatibility row)
does not exist, and the remaining approval-planning and `agents` surface
questions are listed. The compiling rebase is kept off main as an artifact
(dead-code lints fire without the bridge consumer).

Paths: docs/DSH_PLUGIN_COMPAT.md
Verified: docs only.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 14:43:19 -07:00
CodeWhale BotandClaude Opus 5.5 98104b9737 refactor(split): move workshop budget, route evidence and usage persistence below billing (S24a, N12, CW-6129) [skip ci]
N12: the ACTIVE_WORKSHOP slot, its install/read helpers and the
ActiveWorkshopTestGuard move verbatim from tools/large_output_router.rs to
`codewhale-runtime::active_workshop`; the test gate becomes the FD-04
predicate any(test, all(feature = "test-thread-scoped-state",
debug_assertions)) with the #6306 lock order unchanged; ~30 callers
repointed, no facade; route_budget no longer depends on tools.
S24a: `AutoRouteDecisionEvidence` moves to
`codewhale_models::auto_route_evidence`; the cost_status persistence
cluster (persistent usage sink registration and interactive-origin
reporting through SessionManager) moves to tui `usage_persistence.rs`, so
production cost_status has no SessionManager or model_routing references.
Unblocks S25 billing together with S24 proper (still open).

Implemented on Boat VM vm10 by the split-s22-provider session; integrated
by the coordinator on d9ccf8f3e4 (Linux x86_64):
- cargo check --workspace --all-targets --locked: clean
- codewhale-runtime 850, codewhale-models 106, feature_registry 2
  passed; tui filtered (cost_status, usage_persistence,
  large_output_router, route_budget, tools::file, model_routing,
  superfast, activity_detail, purge, compaction, subagent) 1449 passed,
  0 failed
Not run: full tui suite on this commit, macOS, Windows, clippy.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 21:40:11 +00:00
CodeWhale BotandClaude Opus 5.5 d9ccf8f3e4 perf(session): share journal entry bodies and read a resumed document once (#6842)
Snapshot: `SessionEntry.kind` is now `Arc<SessionEntryKind>` (serde `rc`
feature on codewhale-runtime; the JSON is unchanged), so the journal clone
that `build_session_snapshot` takes on the UI task for every save copies
ids, timestamps and a pointer per entry instead of every message body.
The persistence actor then serializes the shared bodies. Entry mutation
goes through `Arc::make_mut` (one test fixture).

Resume: `promote_interrupted_checkpoint` compares a crash checkpoint with
the saved document's metadata (the bounded prefix read) instead of parsing
the whole document, and returns whether the checkpoint is stale. attach
then reads the document once in `resume_session` and clears a stale
checkpoint only after that read succeeds; `-r` does the same after its
bounded load. Only when the checkpoint is newer is the old document still
read in full first, so an unreadable document is never replaced (the
existing guard). Before this, `-r` parsed the document twice.

Not done: `usage_source_fingerprints` stays unbounded. It is the money
replay-dedupe set: late-usage ledger records are never removed and every
metadata load re-checks them against it, and the set has no insertion
order, so any cap could evict a live entry and double-count spend. A safe
bound needs an ordered set that always keeps ledger fingerprints. The
snapshot is still built on the UI task (now without deep copies), and the
lifecycle metadata read in it is still UI-task I/O.

Evidence (macOS arm64):
- Clone cost, throwaway release bench (not committed) on a synthetic
  journal of 4,500 entries with 4 KB bodies (1,500 active, 3,000
  superseded): SessionJournal::clone 0.19 ms vs 1.24 ms with deep bodies.
- Resume of that 25.3 MB session through a loopback provider, `-r`, time
  to the first frame showing the last message: installed 0.10.2 19.32 s
  (2.82 s CPU); release build of this tree 15.72 s (3.45 s CPU). Per-turn
  process CPU was ~0.7 s on both, because it includes the persistence
  actor writing the 25 MB document. RSS is not compared: the after binary
  carries all of 0.10.3, so it is not an A/B of this change.
- scripts/dev-test.sh runtime session_tree: 22 tests run: 22 passed.
- scripts/dev-test.sh tui idle_poll_tests terminal_input session_manager
  persistence_actor thread_history runtime_store_convergence tui::approval
  snapshot checkpoint resume motion: 784 tests run: 784 passed, 13351
  skipped (session_manager 137, including
  attach_promotes_the_sessions_newer_crash_checkpoint, which covers the
  newer and stale branches; runtime_store_convergence 14).

Refs #6842

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 14:19:02 -07:00
CodeWhale BotandClaude Opus 5.5 ccda707286 perf(tui): an idle session relaxes its poll during prompts and background jobs (#6728)
The event loop only reached its 250 ms quiet poll when nothing at all was
live, so the states an idle user actually sits in kept the 24-48 ms poll:
- an approval, elevation or question card on top (the turn is still
  loading, so the active 24 ms poll applied);
- a running background job (durable task) or a once-a-second label, which
  counted as "animating" or "busy" even though the frame requester already
  wakes the loop at the next frame deadline.

Now a waiting decision prompt takes the idle path and counts as quiescent;
only motion faster than the quiet poll counts as animation (slower motion
is woken by its frame deadline); a live durable task no longer blocks the
quiet poll (the task panel refreshes on its own 2.5 s cadence). The
terminal input thread polls every 200 ms instead of 50 ms after 5 s
without input, which keeps the child-terminal pause ack well inside its
500 ms timeout. `decision_prompt_waiting` replaces the duplicated
ModalKind match in status_animation_interval_ms.

Known limits: the loop is still polled, not woken, so an engine event, a
remote decision or a prompt timeout lands up to 250 ms late once the UI
has been quiet for 5 s. The input pump still wakes 5x/s at rest.

Measured on macOS arm64, release builds, codewhale process only, 60 s
windows (30 s where noted), timer wakeups from proc_pid_rusage
interrupt_wkups, through a loopback OpenAI-compatible provider (no
provider spend). Before: installed 0.10.2 (3400ebb5c9; this poll logic is
unchanged between it and main). After: release build of this tree.
| state                     | before wakeups/s, CPU | after wakeups/s, CPU |
| launch screen             | ~25 (composer), 0.20% | 10.4, 0.22% (30 s)   |
| after a finished turn     | 25.0, 0.23% (30 s)    | 10.8, 0.17% (30 s)   |
| approval card waiting     | 29.4, 0.35% (30 s)    | 12.6, 0.17%          |
| background `sleep 900`    | 32.4, 0.45%           | 15.9, 0.21%          |
The approval card still decided normally (`y` ran the command and wrote
its file). Not measured: FreeBSD, where the reporter's ktrace showed the
per-wake clock_gettime cost dominating.

Tests: scripts/dev-test.sh tui idle_poll_tests terminal_input
session_manager persistence_actor thread_history runtime_store_convergence
tui::approval snapshot checkpoint resume motion: 784 tests run: 784
passed, 13351 skipped (idle_poll_tests 5, tui::approval 128). The
idle-poll test row for durable tasks was removed with the field.
Release build: Finished release in 8m 43s, 0 warnings.

Refs #6728, CW-6993

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 14:19:02 -07:00
Hunter Bown a4bbb21564 Merge pull request #7104 from Aboysky/docs/readme-i18n-stamps
docs(i18n): refresh README source stamps after 4cf2d2cff. Verified: scripts/check-readme-translations.py fails on main (18 stale stamps) and reports OK, 18 translations in sync with README.md (sha256:1e61f10f7706), with this PR applied. CW-7525.
2026-10-28 05:13:23 +08:00
Claude a4f089ffe6 fix(extension-host): import HostRuntime from codewhale-dependencies
ecc5deb moved dependencies into its own crate; fff5a47 still imported
crate::dependencies, so codewhale-tui did not compile on main.
cargo check -p codewhale-tui --lib --locked: clean.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_018whUMPrn9PSoxXwWwX7dsp
2026-10-27 21:07:41 +00:00
CodeWhale BotandClaude Opus 5.5 7dc430e354 docs: DSH plugin compatibility checklist (CW-7054)
New docs/DSH_PLUGIN_COMPAT.md: feature-by-feature DeepSeek Harness 0.2.1-rc.2
plugin behavior versus Codewhale, with the landing commit for each verified
row (25225ef051 first-turn tools, 17ebeb5f61 sessions.flush + 409 install
conflict, fff5a476f9 userQuestions.ask), what is refused or unimplemented
(subagents, agents, systemPrompt, tools.get/restrict, rich output, plugin UI),
the WebRTC reason plugin UI stays off, and why the published code-review
plugin still cannot activate. Linked from docs/README.md.

Paths: docs/DSH_PLUGIN_COMPAT.md docs/README.md
Verified: relative links resolve (EXTENSIONS.md, INTEGRATIONS_DSH.md). Docs
only; no code or registry change.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 13:47:22 -07:00
CodeWhale BotandClaude Opus 5.5 fff5a476f9 feat(extension-host): DSH userQuestions.ask through the Engine question owner (CW-7054)
Lands the DSH user-questions candidate stack on current main. A plugin tool
injecting `userQuestions` can call ctx.userQuestions.ask({agent: exec.agent,
questions}) during its own invocation; the host sends user-questions/ask with
the invocation's question ticket and the existing Engine request_user_input
owner presents it (terminal, Runtime API pending_user_inputs) with
require_typed_answers. Answers come back typed (choice / custom / skipped),
0..1000 options with optional descriptions, plan-review intent, exact
UserInputClosed cleanup, pinned UserQuestionError / HarnessError identities
(ASK_ABORTED, CALLER_NOT_LIVE, DELEGATED_CALLER, EMPTY_QUESTIONS, BAD_INTENT).
Compatibility table advertises `userQuestions` (ask only, no wait).

Applied, in order (CW/artifacts/dsh-user-questions-20261026): packet-a
f0ac4100, legacy-custom b0119f6a, packet-b ae2e614c, host-v2 236221a4,
packet-c-runtime 62b7ad2a, packet-c-mechanical 193d7118 (two rustfmt-only
hunks dropped: main already matched), tui 12fdba8d, tui-mechanics 96ab84fd,
longscroll 23685f64, typed-menu edb62d35, preserve-input 49a3e68e,
host-tests b861dafd, error-contract-v2 f24754338, advertisement a83ef8dd.
protocol.generated.ts regenerated from Rust (CODEWHALE_CONFORMANCE_UPDATE=1),
dist/ rebuilt, THIRD_PARTY_NOTICES.md lists the new dsh-user-questions
excerpt. event_loop.rs and supervisor.rs staged without peers' uncommitted
hunks (idle-cpu poll change; a windows cfg_attr).

Paths: CHANGELOG.md THIRD_PARTY_NOTICES.md crates/protocol/src/event_msg.rs crates/protocol/src/lib.rs crates/protocol/tests/parity_protocol.rs crates/tui/CHANGELOG.md crates/tui/extension-host/dist/LICENSES.txt crates/tui/extension-host/dist/agent-presets.mjs crates/tui/extension-host/dist/codewhale-extension-host.mjs crates/tui/extension-host/dist/dsh-compatibility.json crates/tui/extension-host/dist/dsh-composition-review.mjs crates/tui/extension-host/dist/protocol.mjs crates/tui/extension-host/dist/rpc.mjs crates/tui/extension-host/src/dsh/compatibility.json crates/tui/extension-host/src/dsh/dsh-message-compat.js crates/tui/extension-host/src/main.ts crates/tui/extension-host/src/protocol.generated.ts crates/tui/extension-host/src/root.ts crates/tui/extension-host/test/harness.mjs crates/tui/extension-host/test/host.test.mjs crates/tui/src/core/engine.rs crates/tui/src/core/engine/approval.rs crates/tui/src/core/engine/tests/runtime_state.rs crates/tui/src/core/engine/tests/sse_turn_recovery.rs crates/tui/src/core/engine/tests/test_cases_12.rs crates/tui/src/core/engine/turn_loop.rs crates/tui/src/core/events.rs crates/tui/src/core/protocol_parity.rs crates/tui/src/exec_agent.rs crates/tui/src/extension_host/core_call.rs crates/tui/src/extension_host/core_call_tests.rs crates/tui/src/extension_host/mod.rs crates/tui/src/extension_host/protocol.rs crates/tui/src/extension_host/protocol/tests.rs crates/tui/src/extension_host/supervisor.rs crates/tui/src/extension_host/tests.rs crates/tui/src/extension_host/tool.rs crates/tui/src/runtime_api.rs crates/tui/src/runtime_api/tests.rs crates/tui/src/runtime_threads.rs crates/tui/src/runtime_threads/tests.rs crates/tui/src/runtime_threads/tests/task_ownership.rs crates/tui/src/tools/codemode.rs crates/tui/src/tools/user_input.rs crates/tui/src/tui/pager.rs crates/tui/src/tui/pet_watch/mod.rs crates/tui/src/tui/phase_strip/tideline_tests.rs crates/tui/src/tui/plan_handoff.rs crates/tui/src/tui/ui/apply.rs crates/tui/src/tui/ui/event_loop.rs crates/tui/src/tui/ui/frame.rs crates/tui/src/tui/ui/observer_hooks.rs crates/tui/src/tui/ui/tests.rs crates/tui/src/tui/user_input.rs

Verified (macOS; tree also held peers' uncommitted edits at test time):
- extension-host typecheck; Node 420: 413 pass, 7 skip, 0 fail; Bun 420:
  414 pass, 6 skip, 0 fail.
- CODEWHALE_EXT_HOST_TESTS=1 + compiled Bun host, scripts/dev-test.sh tui
  extension_host:: tools::codemode:: tools::user_input:: tui::user_input
  tui::pager tui::plan_handoff tui::pet_watch tui::phase_strip
  tui::persistence_actor:: runtime_threads:: runtime_api:: plugins::install::
  exec_agent:: core::engine::tests:: core::protocol_parity tui::ui::tests::
  -> 2656 run, 2655 passed, 1 failed (licence notice missing; fixed in this
  commit, rerun 1/1 passed). scripts/dev-test.sh protocol: 78/78. CLI build.
- Live Engine HTTP (artifacts/dsh-e2e-20261027/logs/q1, loopback provider):
  DSH bundle tool flushed (true), asked one question; Runtime listed it in
  pending_user_inputs with require_typed_answers; answering Blue via
  POST /v1/user-input returned delivered:true and the plugin received
  [{"id":"colour","selected":["Blue"]}].
Not verified: TUI and desktop App rendering live, Linux/Windows, hosted CI.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 12:53:47 -07:00
CodeWhale BotandClaude Opus 5.5 96498ce2a7 refactor(split): extract codewhale-skill-install (S16, CW-6129) [skip ci]
The community-skill installer, package content digest and SKILL.md
frontmatter reader move into `crates/skill-install`. The SSRF DNS pin and
shortened timeouts are relaxed only under its dev-dependency-only
`test-support` feature, so production behavior is unchanged; the
unfulfilled `expect(dead_code)` pairs are gone. Cargo.toml/crates.sh/
ARCHITECTURE conflicts with S15 resolved as unions; Cargo.lock refreshed
by cargo metadata. Implemented on Boat VM vm6 by the split session.

Series verification (S23 + S15 + S16 on e66998bec3, Linux x86_64 Boat VM
vm7 with bubblewrap installed), then rebased onto current main:
- cargo check --workspace --all-targets --locked: exit 0
- codewhale-snapshot 91, lsp 64, repl 36, skill-install 42, util 49,
  models 106, runtime 849, feature_registry 2 passed, 0 failed
- codewhale-dependencies 54/55 once, then 55/55 on four reruns: the
  untrusted_search_candidates test is intermittently racy (it writes and
  executes probe scripts while sibling tests fork) now that it no longer
  shares tui's test lock; follow-up to add a crate-local lock.
- codewhale-tui lib: 14098 passed, 11 failed; the same 11 tests fail on
  unmodified e66998bec3 on the same VM image (MCP boot/extension-host
  tests on Linux), so no new tui failures.
Not run: macOS suite on this series, Windows, clippy.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 17:25:41 +00:00
CodeWhale BotandClaude Opus 5.5 ecc5deb692 refactor(split): extract dependencies, snapshot, lsp and repl crates (S15, CW-6129) [skip ci]
New crates `codewhale-dependencies` (external-tool resolution and bounded
subprocess probes), `codewhale-snapshot` (pre/post-turn side-repo
snapshots), `codewhale-lsp` (language-server client) and `codewhale-repl`
(Python REPL runtime and RLM context files; `write_context_file` lives
here). Callers rewired, no facades; FD-04 forks gated like S14.
Implemented on Boat VM vm3 by the split session; integrated by the
coordinator. Verification in the S16 commit.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 17:25:41 +00:00
CodeWhale BotandClaude Opus 5.5 847fdf3c3a refactor(split): provider pre-cuts B inside tui client modules (S23, CW-6129) [skip ci]
Client wire helpers hoisted out of client.rs (ERROR_BODY_MAX_BYTES, the SSE
constants, the stream buffer pool, bounded_error_text, API tool-name
mapping, system_to_instructions, apply_reasoning_effort) so chat.rs and
responses.rs no longer `use super::` from client.rs; system_one.rs stays
with the client; local_ollama catalog parsers moved into their own module
(N11); test-only provider gates use the shared test-isolation predicate.
No behavior change. Implemented on Boat VM vm4 by the split session;
integrated by the coordinator. Verification in the S16 commit.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 17:25:40 +00:00
CodeWhale BotandClaude Opus 5.5 eaff21a8b8 docs(changelog): composer history honors CODEWHALE_HOME (Refs #7069, CW-7525)
The #7069 fix landed in 2947e4c51d without an Unreleased entry.
Changelog only; crates/tui/CHANGELOG.md regenerated with
scripts/sync-changelog.sh (--check clean).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 09:08:28 -07:00
CodeWhale BotandClaude Opus 5.5 f75bb12d62 fix(dependencies): a version probe that outlives its deadline means installed, not missing (CW-7525)
tools::subagent::tests::root_operate_dispatch_delegates_builtin_verification_but_not_shell
failed under load: Cargo::resolve() (and resolve_executable_path for
git/pandoc) run `<tool> --version` with the 3 s VERSION_PROBE_TIMEOUT,
and on a busy machine the rustup `cargo` shim can take longer. The
timeout was treated as "not installed" and cached in the resolver's
OnceLock for the whole process, so run_tests reported cargo missing for
the rest of the session. That is a product bug, not a test bug.

probe_executable_with_flag and resolve_executable_path now share
probe_found(): a successful exit means present, a failed launch or a
failing exit means absent, and a probe that launched and was still
running at its deadline means present (it is killed with its process
tree as before). The deadline still bounds how long resolution waits;
it no longer decides presence. Banner capture
(probe_executable_capturing) is unchanged and still returns None on
timeout.

Verification: same nextest batch as 8084ead09c (frozen source) ->
142 passed, including 55 dependencies:: tests and the subagent test.
The load-dependent failure itself was not reproduced locally.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 09:08:26 -07:00
CodeWhale BotandClaude Opus 5.5 3f4c4f2c34 fix(action): require an explicit provider and pin the review workflow to v0.10.2 (CW-7525)
action.yml defaulted `provider` to `codewhale`, the account machine-key
relay, while docs/GITHUB_ACTION.md says to use explicit BYOK unless the
account route has been separately verified. `provider` is now a required
input with no default, and scripts/github-action/review.mjs no longer
falls back to `codewhale`: an empty provider stops with
configuration_missing / provider_required before any release download
or model call. The OrcaRouter and other BYOK key handling from
6f92f0d5e2 is unchanged; `codewhale` remains selectable explicitly.

.github/workflows/codewhale-review.yml (disabled_manually on GitHub)
falls back to CLI v0.10.2, the current release, instead of v0.10.0. It
still names the account provider explicitly. No secret, variable or
workflow state was changed.

Docs: the BYOK section is no longer labelled optional and the bounds
table lists `provider` as required.

Verification: node --test scripts/github-action/review.test.mjs -> 8
pass, 0 fail. Direct settings() call with no CW_PROVIDER stopped with
{"outcome":"configuration_missing","reason":"provider_required"}.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 09:08:24 -07:00
CodeWhale BotandClaude Opus 5.5 8084ead09c fix(review): drop fixed per-tier output caps; reviews use the route's output limit (Refs #7067, CW-7525)
Review v2 slice F1. budget_for() no longer hard-codes 4,000 / 8,000 /
16,000 output tokens for Low / Medium / High risk. It takes the route's
effective output limit (client.effective_max_output_tokens) and every
review pass may use all of it, so a review is never truncated by a tier
cap. Tiers still set steps and tool calls; the money cap, step budget and
tool budget keep stopping a review early as a named partial review
(skip, not truncate).

with_reasoning_floor() is deleted: it only existed to lift the tier cap
to 8,192 for reasoning routes, and the route limit already is at least
that. Both callers (`codewhale review` in lib.rs and the review tool)
delete their `.min(budget.max_output_tokens)`; the allowance they send,
the reserve and the exhausted-pass message all read
budget.max_output_tokens, which is the value actually sent. The
"numbers that were never sent" error was already fixed by 2d158d78b1;
this keeps the reported and sent values the same source.

The review_budget receipt field max_output_tokens now records the real
route allowance instead of the tier cap.

CHANGELOG: rewrote the Unreleased #7067 entry, which described the
removed 8,192 floor.

Verification: scripts/dev-test.sh tui review_triage tools::review
dependencies:: root_operate_dispatch_delegates_builtin_verification_but_not_shell
-> 142 tests run: 142 passed (86 tools::review incl. 6 review_triage,
55 dependencies::, 1 subagent). No live provider review was run.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 09:08:05 -07:00
Boyce c79f719acb docs(i18n): refresh README source stamps after 4cf2d2cff
4cf2d2cff reworded the Computer Use paragraph in README.md and refreshed
the content of all 18 translations, but left every 'source: README.md'
stamp on the previous hash, so scripts/check-readme-translations.py
fails on main with 'stale' for all 18 files. Bump the stamps; no prose
changes.

Signed-off-by: Boyce <[email protected]>
2026-10-27 22:43:04 +08:00
CodeWhale BotandClaude Opus 5.5 e66998bec3 feat(uninstall): Windows receipts, package-manager commands and --purge-data (Refs #7091, CW-7525)
`codewhale uninstall` now covers every supported install route:

- Windows archive install: install.ps1 writes the
  .codewhale-install.sha256 receipt, which joins its existing rollback.
  Uninstall removes codewhale.exe, codew.exe and codewhale.bat by
  receipt. The running exe is renamed through its open handle
  (NtSetInformationFile) and marked delete-on-close; if that fails, a
  detached powershell deletes it after the process exits. Self-update
  re-captures and renews the receipt after replacing the exe, because a
  held handle would block the MoveFileExW replace.
- Package-managed copies are never deleted. They print the exact
  command: npm (with --prefix), cargo (with --root), the Homebrew
  formula from Cellar, scoop, winget, or the NSIS Uninstall.exe.
  InstallMethod gains Scoop and WinGet detection.
- Every other codewhale/codew still on PATH is listed as preserved.
- --purge-data previews, then deletes with --yes, only ~/.codewhale
  (or an explicit CODEWHALE_HOME named .codewhale) and
  ~/.deepseek/snapshots. It refuses symlinks, home and its ancestors,
  and other users' directories.

Two pre-existing bugs fixed along the way:
- Unix: remove_checked's retirement tempdir inherited the umask (0755),
  so unlink_opened refused it and left `codew` stranded in
  .codewhale-uninstall-*. It is now created 0700.
- Windows: codewhale-util (from the S14 split) did not build. It lacked
  the `windows` dependency and referred to itself as codewhale_util::.

Verified:
- macOS arm64 temp-HOME script against install.sh with a file:// release
  (binary bdd94d1bcfbd): 36 passed, 0 failed. It covers preview,
  --yes, a repeat no-op, reinstall and re-removal; two receipted copies;
  a tampered alias refused; a manual copy preserved; the cargo,
  cargo-root, npm, brew, brew-legacy, scoop, winget and nsis layouts
  printing their exact command; and purge preview, --yes, a symlink
  refusal and a CODEWHALE_HOME=HOME refusal.
- check -p codewhale-cli --target x86_64-pc-windows-msvc passes,
  using stand-in CC/AR because there is no Windows SDK; check never
  links.
- cargo test -p codewhale-release: 57 passed, 0 failed.
- cargo test -p codewhale-tui --test feature_registry: 2 passed.

Not verified: nothing ran on a Windows host. install.ps1 was not
executed (no pwsh). Scoop, WinGet and NSIS are detection-only checks
against synthetic layouts.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 07:18:03 -07:00
Claude 3824cdea0c feat(runtime): deliver scheduled messages as turns in an existing thread
Automations, send_later triggers and tasks gain an optional
destination_thread_id. With one set, a run starts a turn in that thread
(tagged input_provenance=scheduled with its task_id) instead of creating
a new thread; a busy thread is retried every 2 s up to the task wall
time, and a missing or archived destination fails the task naming it,
never falling back to a new thread. send_later from a thread now
defaults to that thread ("" keeps the new-thread behaviour). From a
tool, thread_id may name only the calling thread, so one thread's model
cannot start turns in another under that thread's permissions; other
threads are API-only.

Adds GET/POST /v1/triggers, GET/DELETE /v1/triggers/{id} (Idempotency-Key
honoured as for automations), and automation.created/updated plus
trigger.scheduled/canceled/fired thread events when a destination is set.
Registry row thread-scheduled-turns (experimental, api preview).

Delivery uses the normal turn-start path, not Agent Mail: Agent Mail
requires distinct sender and receiver threads.

Known limits: explicit thread ids from tools are checked only at fire
time; an Engine restart mid-delivery can start the turn twice (today the
same case creates a second thread).

Evidence (RUST_MIN_STACK=8388608 as CI sets it):
cargo test -p codewhale-tui --lib -- automation send_later task_manager trigger
test result: FAILED. 247 passed; 2 failed; 2 ignored
The two failures, task_manager::ownership_tests::failed_running_persistence_*
and failed_event_persistence_*, make a directory read-only (0o500) to force a
write error; this container runs as uid 0, which ignores the mode, so the
write succeeds. Neither test touches the changed code paths.
cargo fmt --all -- --check: clean. feature_registry not run (the separate
lib build exceeds this container's memory); the new row was checked by hand
against its rules. No live Engine run.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_018whUMPrn9PSoxXwWwX7dsp
2026-10-27 13:54:56 +00:00
CodeWhale BotandClaude Opus 5.5 e1acc381ab feat(mcp): authorization-scoped catalog freshness and the Skills extension (CW-7547, CW-7545)
CW-7547 — catalog freshness through the existing connection owner.
Each server's tools/resources/templates/prompts/skills stay on its own
McpConnection, so nothing is shared across servers, credentials,
sessions or reconnects (cacheScope public and private both get the
private rule). McpCatalogBudget now records the earliest ttlMs across
pages (capped at 24h). The supervisor sweep calls the new
McpPool::revalidate_catalogs, which rediscovers in place when that TTL
has passed or a tools/resources/prompts list_changed notice arrived,
seen by the host broker's frame observer (Session::observe) or the Rust
backend's recv_reply. A changed catalog bumps a per-connection
catalog_revision that tool routes now carry, so a call resolved against
the old catalog is refused, and the existing tools() membership check
keeps a removed tool from running. Failed revalidation marks the
connection Disconnected and ordinary supervision redials it; the stale
catalog is not served. McpSupervisorUpdate gains `refreshed` so the
engine emits a snapshot; the next turn's refresh_current_mcp_catalog
picks up the tools and declares the prefix change. Not done (stated in
docs/MCP.md): 2026-07-28 subscriptions/listen and resources/subscribe.

CW-7545 (Engine half) — published Skills extension (SEP-2640).
When initialize/discovery advertises
capabilities.extensions["io.modelcontextprotocol/skills"], skills/list
is read with the catalog under the same budget and freshness rules.
Entries are admitted only with a listed SKILL.md root, name and
description, files confined under the root (no ., .., %, ?, #),
sha256 digests and the 512-file/16 MiB limits; "dynamic" entries are
skipped. Skills appear in list_mcp_resources; listing never activates
one. read_mcp_resource on a listed skill file verifies URI, size and
digest and refuses a mismatch. skills/list and skills/get join the
Rust SUPPORTED_METHODS and the host builtin, which passes extension
methods to the SDK with a pass-through result schema so Rust stays the
validator. Native /skills catalog integration remains open (CW-7177).
CU's server half landed as codewhale-cu-plugin 47a7365.

The host bundle and builtin/mcp.mjs were rebuilt with node build.mjs
from HEAD plus only this mcp.ts, in a scratch copy, and staged
index-only. The tier.rs pin is the new builtin digest 50516f55.... The
changelogs and runtime_state.rs were staged the same way, so other
sessions' uncommitted hunks stay out of this commit.

Verified on macOS, against the shared tree at c5cd7c87f0 plus dirty work:
- scripts/dev-cargo.sh check -p codewhale-tui --all-targets: exit 0
- scripts/dev-test.sh tui mcp extension_host conformance::mcp:
  736 run, 736 passed, 0 failed (13686 skipped)
- extension-host npm test: 420 tests, 413 pass, 0 fail, 7 skipped;
  tsc --noEmit clean
Not run: cargo check --workspace, a live Engine against a TTL or
list_changed server, CU through the Engine end to end.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 05:17:53 -07:00
CodeWhale BotandClaude Opus 5.5 17ebeb5f61 feat(extension-host): DSH sessions.flush through an acknowledged Engine checkpoint (CW-7054)
Lands the DSH caller-checkpoint candidate (previously only artifact patches
qualified on a Boat snapshot) on current main. A plugin tool injecting
`sessions` can call ctx.sessions.flush(exec.agent.session) during its own
live invocation; the host sends session/checkpoint with the invocation's
Engine ticket, the Engine captures the committed prefix, and the owning
Runtime / terminal / exec store acknowledges persistence before the call
resolves. Runtime checkpoint turns use schema 5 (older readers refuse);
restore, compaction, fork and crash repair honour the private reference.
Also: install of plugin bytes that changed after review returns a typed 409
(PluginContentMismatch) instead of 500. Compatibility table advertises
`sessions` (flush only), which invalidates earlier trust by design.

Applied, in order: checkpoint-bridge-7e7c2842, digest-hex-repair-7001d139,
terminal-checkpoint-integration-v4 (a80a918c), exec-checkpoint-7c218953,
runtime-prefix-checkpoint-4e8e93d4, runtime-api-checkpoint-fixtures-07a8413f,
sessions-flush-adapter-v2 (2649f2a1), install-content-conflict-c1a9c47d,
checkpoint-generated-protocol, checkpoint-cancel-wait-repair-8980519a
(artifacts under CW/artifacts/dsh-*). Rebase repairs: two hunks hand-merged
(core_call.rs imports, install/mod.rs PluginContentMismatch) and
crate::utils::spawn_supervised -> codewhale_util::utils after S14. dist/
rebuilt from HEAD + these sources (builtin modules unchanged).

Paths: CHANGELOG.md crates/tui/CHANGELOG.md crates/tui/extension-host/dist/agent-presets.mjs crates/tui/extension-host/dist/codewhale-extension-host.mjs crates/tui/extension-host/dist/dsh-compatibility.json crates/tui/extension-host/dist/dsh-composition-review.mjs crates/tui/extension-host/dist/protocol.mjs crates/tui/extension-host/dist/rpc.mjs crates/tui/extension-host/src/dsh/compatibility.json crates/tui/extension-host/src/protocol.generated.ts crates/tui/extension-host/src/root.ts crates/tui/extension-host/test/harness.mjs crates/tui/extension-host/test/host.test.mjs crates/tui/src/core/engine.rs crates/tui/src/core/engine/turn_loop.rs crates/tui/src/core/events.rs crates/tui/src/core/protocol_parity.rs crates/tui/src/exec_agent.rs crates/tui/src/extension_host/core_call.rs crates/tui/src/extension_host/core_call_tests.rs crates/tui/src/extension_host/mod.rs crates/tui/src/extension_host/protocol.rs crates/tui/src/extension_host/protocol/tests.rs crates/tui/src/extension_host/supervisor.rs crates/tui/src/extension_host/tests.rs crates/tui/src/extension_host/tool.rs crates/tui/src/lib.rs crates/tui/src/plugins/install/mod.rs crates/tui/src/plugins/install/tests.rs crates/tui/src/runtime_api/plugins.rs crates/tui/src/runtime_api/tests.rs crates/tui/src/runtime_threads.rs crates/tui/src/runtime_threads/tests.rs crates/tui/src/tools/codemode.rs crates/tui/src/tui/persistence_actor.rs crates/tui/src/tui/ui/event_loop.rs crates/tui/src/tui/ui/frame.rs crates/tui/src/tui/ui/tests.rs docs/EXTENSIONS.md docs/zh_hans/EXTENSIONS.md

Verified (macOS, this tree incl. peers' uncommitted MCP work at test time):
- extension-host typecheck; Node 420 tests: 413 pass, 7 skip, 0 fail;
  Bun 420: 414 pass, 6 skip, 0 fail.
- CODEWHALE_EXT_HOST_TESTS=1 + compiled Bun host: scripts/dev-test.sh tui
  extension_host:: tools::codemode:: tui::persistence_actor:: runtime_threads::
  runtime_api:: plugins::install:: exec_agent:: core::engine::tests::
  core::protocol_parity tui::ui::tests:: -> 2493 run, 2493 passed (includes
  typescript_protocol_is_generated_from_the_rust_types and compiled Native
  containment). CLI build passed.
- Live Engine HTTP (artifacts/dsh-e2e-20261027/logs/c1, loopback provider, no
  spend): DSH bundle tool calling ctx.sessions.flush returned true on turn one;
  saved turn is schema 5 with prefix_checkpoint; after Engine restart the
  checkpointed turn reloads with the plugin result and the killed turn is
  interrupted, not replayed.
Not verified: TUI/App UI, Linux/Windows on this source, hosted CI.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 04:48:01 -07:00
CodeWhale BotandClaude Opus 5.5 57adfdfea6 test(tui): align pricing and picker-action tests with the 7d2308ac81 provider fix
Five codewhale-tui tests failed after 7d2308ac81 (reported on Linux at
0cf390f01, confirmed by the same filters on macOS). In each case the
product behaviour is the intended one and the test encoded the old
behaviour:

- pricing::tests::stepfun_billing_surface_keeps_payg_separate_from_step_plan
  and unpublished_cache_write_fails_closed_but_documented_same_rate_prices
  used DEFAULT_STEPFUN_MODEL as a fixture while asserting Step 3.7 Flash's
  PAYG rate (0.735) and its unpublished cache-write policy. The default is
  now step-5-preview, whose documented cache-write is the input rate. The
  tests now name step-3.7-flash, the model their numbers belong to.
- tui::ui::tests::model_picker_actions: Shift+P / Shift+F are query text
  while searching (so `Step-5-Preview` can be typed), and Ctrl+P / Ctrl+F
  act on the highlighted row. shift_p/shift_f_after_a_search_* become
  ctrl_p/ctrl_f_after_a_search_* driving Ctrl chords, and
  pin_and_fleet_hints_hide_when_no_catalog_route_is_highlighted now
  expects the Ctrl hints while searching and no shifted hints.

Checks (macOS, on c5cd7c87f0 plus these two files): `scripts/dev-cargo.sh
test -p codewhale-tui --lib -- pricing::tests:: tui::ui::tests::model_picker_actions
tui::model_picker` 123 passed, 0 failed.

Touched paths: crates/tui/src/pricing.rs,
crates/tui/src/tui/ui/tests/model_picker_actions.rs.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 04:35:20 -07:00
CodeWhale BotandClaude Opus 5.5 c5cd7c87f0 refactor(split): provider pre-cuts into codewhale-models and runtime (S22, CW-6129) [skip ci]
Into codewhale-models: `schema_sanitize` (production extent re-measured:
old lines 1-448 + 982-1988, incl. `sanitize_for_kimi_parameters`),
`schema_canonicalize`, `turn_classify` (wire-compaction checkpoint and
prompt classifier closure; models, not core, because core cannot name
Message), `image_window` (pure image helpers), `tool_projection`
(S22 owns it, not S33) and `reasoning_effort_tier` (from work_graph).
Into codewhale-runtime: `stall_report`, and `safe_receipt_field` /
`sanitize_persisted_route_label` from cost_status (cuts
provider_catalog_live -> cost_status). `client::redact_url_for_display`
wrapper removed. Not done in this slice: image_attach decode helpers and
the P6-2 back-edge cuts.

Implemented on Boat VM vm4 by the split-s22-provider session.

Series verification (S14 + S29/S30 + S22 integrated on 0cf390f01, Linux
x86_64 Boat VM vm3), then rebased cleanly onto 25225ef051:
- cargo check --workspace --all-targets --locked: exit 0, no warnings
- codewhale-util 49, codewhale-models 106, codewhale-runtime 849,
  codewhale-execpolicy 237, codewhale-tools 26, codewhale-protocol 78,
  codewhale-agent-contract 14 passed, 0 failed; feature_registry 2 passed
- codewhale-tui lib: 14381 passed, 16 failed, 22 ignored. 15 of the 16
  fail identically on unmodified 0cf390f01 on Linux (MCP/extension-host
  tests needing bwrap on that VM, StepFun pricing and model-picker tests
  from 7d2308ac81); the 16th
  (full_access_auto_approves_non_bypassable_registered_tools) passed 5/5
  when rerun alone (timing under full load).
Not run: macOS tui suite on this exact series, Windows, clippy.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 10:04:53 +00:00
CodeWhale BotandClaude Opus 5.5 19616707f8 refactor(split): move canonical_action and tool contract helpers out of tui (S29/S30, CW-6129) [skip ci]
S30: the `canonical_action` policy table moves to
`codewhale-execpolicy::canonical_action` (serde_json added);
`required_action` to `codewhale_tools`; path-alias folding
(`with_canonical_path_argument`, `PATH_ALIASES`, `apply_param_aliases`) to
`codewhale_tools::param_alias`; the `apply_patch` preflight parser closure
to `codewhale_tools::patch_preflight`, so `core/authority/auto_review.rs`
no longer needs tui tool implementations. Inline tests stay in tui as
`tools/canonical_action_tests.rs`.
S29 (moves only): `DEFAULT_SPAWN_DEPTH`, `PermissionCeiling` and
`ShellCeiling` live in `codewhale_protocol::fleet`, so
`codewhale-agent-contract` no longer depends on codewhale-config or
codewhale-workflow. FD-12-gated deletions are held.
Approval and authority behavior is unchanged. About 60 callers rewired,
no facades.

Implemented on Boat VM vm6 by the split-s29-s30 session; integrated by the
coordinator. Verification in the S22 commit of this series.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 10:04:53 +00:00
CodeWhale BotandClaude Opus 5.5 ff17095783 refactor(split): extract codewhale-util from codewhale-tui (S14, CW-6129) [skip ci]
New crate `crates/util` (`codewhale-util`): `utils` (+ response_body),
`fs_confined`, `fleet/files` -> `util::files`, `plugins/path_identity` ->
`util::path_identity`, bundle file readers -> `util::bundle_file`,
`runtime_api::open_workspace_directory` -> `util::workspace_dir` (removes
the S42 back-edge, N1), `.git` name rule -> `util::git_metadata`, and the
owner file lock -> `util::lock` (duplicate `is_contention` deleted).
One-way: callers rewired, no re-export facades. The
`spawn_blocking_supervised` env-scope fork is gated
`any(test, feature = "test-support")` with an optional runtime dep.
Workspace members, Cargo.lock, release crate order, dev-test areas,
ARCHITECTURE docs and the blocking-calls budget updated.

Implemented on Boat VM vm3 by the split-s14-util session; integrated with
S29/S30 and S22 and rebased onto main by the coordinator. Verification in
the S22 commit of this series.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 10:04:53 +00:00
CodeWhale BotandClaude Opus 5.5 25225ef051 fix(extension-host): offer reviewed plugin tools on the first turn (CW-7054)
The turn tool build started plugin reconciliation in the background, so a
newly enabled DSH/Native plugin's tools were missing from turn one (and from
the deferred catalog tool_search reads). The attachment now records the
plugin snapshot a finished reconcile activated; until the current snapshot is
settled, the tool build waits up to 10 s for a reconcile (run as its own task,
so a timeout never cancels it mid-activation). Settled snapshots keep the
background path. The DSH import receipt now says why `dsh.client` UI stays
off (WebKit restrictions do not stop WebRTC; isolated renderer is CW-7549).

Paths: crates/tui/src/core/engine.rs, crates/tui/src/extension_host/mod.rs,
crates/tui/src/plugins/install/dsh.rs, CHANGELOG.md, crates/tui/CHANGELOG.md
(via scripts/sync-changelog.sh --check: up to date).

Verified:
- scripts/dev-cargo.sh build -p codewhale-cli --bin codewhale: passed.
- scripts/dev-test.sh tui core::engine::tests:: extension_host:: :
  712 run, 712 passed. scripts/dev-test.sh tui plugins::install:: :
  66 run, 66 passed. Doc-comment removal and rustfmt line join only after.
- Live macOS Engine HTTP probe (artifacts/dsh-e2e-20261027, loopback fixture
  provider, no provider spend): DSH bundle preview -> exact-hash install ->
  review -> trust -> enable. Installed 3400ebb5c9: turn-one tool_search did
  not offer dsh_probe. This build: offered, approval card shown and allowed,
  tool ran, /probe-count reported 1.
Not verified: TUI/App UI walkthrough, Windows/Linux, questions, child agents.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 02:20:55 -07:00
CodeWhale BotandClaude Opus 5.5 0cf390f01e feat(tui): change mode and permissions mid-turn; record steering in Unreleased (CW-7552)
Mode and permission posture can now change while a turn is running. Tab,
Shift+Tab, Alt shortcuts, /mode, the hotbar, /auto and /config mode|approval*
no longer hit the #2982 turn lock; model, thinking and provider stay locked
because they change the route the in-flight request is on.

Decision: a change applies at the engine's next step in the same turn, not at
the next turn and not by restarting the streaming response. The engine already
drains the live authority snapshot before each tool batch, at every approval
gate and at the per-step preparation boundary; a call nobody approved, or one
whose approval is narrowed, is returned to the model to retry. New: the
preparation boundary now records the mode notice (and Operate contract) via
Engine::record_mode_context, so the next model request is told the new mode
and the notice is a session message (model-visible means logged). While a turn
runs, the TUI shows "<Mode> · <permissions> — applies at the agent's next step
in this turn"; the engine's existing "Permissions: … · <mode>" status confirms
the apply.

Reference comparison (CW/refs): Codex steers at the next safe point and applies
/model and /permissions to the next turn; Grok Build queues by default, injects
steers at tool/model safe points and allows Plan/permission toggles mid-turn
with turn-start reminders. Neither preempts the in-flight request.

Steering itself: the preemption source from 4cf2d2cfff was proven, not changed.
Changelog entry added for it (it had none).

Touched paths:
  CHANGELOG.md, crates/tui/CHANGELOG.md (sync-changelog.sh output)
  crates/tui/src/core/engine.rs (record_mode_context; turn start uses it)
  crates/tui/src/core/engine/turn_loop/preparation.rs
  crates/tui/src/tui/app.rs, crates/tui/src/tui/app/tests.rs
  crates/tui/src/commands/config_main_host.rs
  crates/tui/src/tui/ui/provider_routes.rs (sync_mode_update takes &mut App)
  docs/MODES.md
Not included: uncommitted extension-host first-sync hunks in engine.rs,
extension_host/mod.rs and plugins/install/dsh.rs belong to another writer.

Evidence (candidate = 3400ebb5c9 + this diff + then-dirty provider files,
debug codewhale sha256 9f6d1934…052e):
- Live TUI (private tmux, sandboxed, loopback provider holding a reasoning
  stream): Tab×2 mid-turn moved Work→Plan with the pending notice, no lock
  message; double-Enter steer reached the provider 143 ms later before the held
  response was released; next request carried the steer once and the Plan
  notice once; obsolete text never shown. PASS_SCOPED. Installed 3400ebb5c9
  baseline: steer 130 ms OK, mode refused "Mode is locked while a turn is
  running". CW/artifacts/cw7552-steer-20261027/tui-runs/{baseline-3400-b,candidate-1}
- Runtime steering-probe.py on candidate: headers, reasoning, text, tool_json
  all PASS_SCOPED (also PASS on installed 3400ebb5c9, 28-33 ms).
- Tests (hermetic home, crate dir): tui::app::tests 238 passed 0 failed;
  commands::config_main_host 109/0; runtime_handoff 22/0;
  core::queued_approval_tests 1/0; tui::ui::tests::plan_handoff 13/0;
  core::engine::tests 524 passed 9 failed: 8 MCP extension-host fixtures
  ("built-in extension host is down"), failing identically before the test
  edits and outside touched code, plus recursive_rlm_round_waits_on_the_
  code_execution_gate, flaky under load (5/5 alone).
- Updated obsolete lock tests in app/tests.rs and config_main_host.rs; removed
  approval_aliases_are_inert_while_a_turn_is_running.
- rustfmt --check clean on touched files. No CI run (release step only).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 01:38:37 -07:00
CodeWhale BotandClaude Opus 5.5 9ff7099b90 feat(web): 0.10.3 home highlights and Unreleased changelog entries
The changelog parser dropped bullets that sit directly under a release
heading, so /changelog rendered the Unreleased (0.10.3) section empty. Such
bullets now land in an implicit "Changes" section; 0.10.2 output is
unchanged (same section counts before and after).

The home page shows "Coming in v0.10.3" while that version is ahead of the
latest published release (0.10.2): MCP via the official SDK, MCP
compatibility fixes, DeepSeek Harness plugins, Computer Use as an installed
plugin, image-heavy session recovery, per-scope instruction budgets (#7086),
Unix uninstall and /rc. Copy is taken from CHANGELOG.md Unreleased only.

Paths: web/scripts/changelog-lib.mjs, web/lib/content/release-highlights.ts,
web/app/[locale]/page.tsx.

Verified: npm run check (web) exit 0, 993 static pages; vitest changelog,
public-copy, public-surface-contract: 3 files, 24 passed; dev server /en and
/zh show the v0.10.3 block and /en/changelog shows Unreleased 49 entries,
no console errors. Not deployed.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 01:04:30 -07:00
CodeWhale BotandClaude Opus 5.5 7d2308ac81 fix(provider): switching provider lands on its current model; /model lists the route's roster
Founder report: `/provider stepfun` silently selected step-3.7-flash, far
older than step-5-preview, and `/model` did not show step-5-preview even
when searching for it.

Root causes:
- StepFun's built-in default was step-3.7-flash in the descriptor and the
  seed spec. `catalog_models_dev.py defaults audit` never flagged it
  because it treats `-preview` rows as non-replacements. Step 5 Preview is
  StepFun's current flagship and is fully wired (reasoning effort map,
  1M context, pricing), so the descriptor, seed spec and rendered seed
  `default` flag move to step-5-preview. A saved [providers.stepfun].model
  is still honoured. The audit's other recorded holds (openai, google,
  xai, meta) stay as recorded; meta was briefly moved and reverted to
  respect its DEFAULT_AUDIT_REASONS entry.
- The default /model view (#6533) showed only the current model, pins,
  recents and one default per provider; the current route's other models
  were behind search. The existing custom-relay roster rule now covers the
  current route of any provider: up to 20 rows of its own roster (bundled
  plus live), newest first, in a new trailing `Roster` section so the
  one-default-per-provider invariant holds.
- In the picker search, Shift+P and Shift+F pinned or added the
  highlighted row to Fleet instead of typing, so `Step-5-Preview` became
  `Step-5-review` and matched nothing (#6500 made them act during search).
  While searching they are now query text; Ctrl+P / Ctrl+F act on the
  highlighted row at any time and the footer advertises them while
  searching. Search also matches ids with separators removed (`step5`).

Known gap, not fixed here: live rows on a non-default official host (e.g.
StepFun Step Plan `/step_plan/v1`) are not completed from bundled facts,
because `CatalogOffering::complete_from` requires the live fingerprint to
match the bundled row's published base; they show "reasoning unknown" and
no context. Widening that needs a per-fact rule (pricing must not cross
PAYG/plan hosts).

Proof (dev build of this tree, founder-shaped CODEWHALE_HOME copy with the
Step Plan route and its cached live roster, saved stepfun model removed):
`/provider stepfun` -> status line `StepFun / StepFlash · step-5-preview`;
`/model` lists step-5-preview, step-3.7-flash, step-3.5-flash-2603,
step-3.5-flash; searches `step5` and `Step-5-Preview` both find
step-5-preview; Ctrl+P during search pins it.

Checks: codewhale-config 805 passed, 1 failed (the StepFun default
assertion in catalog/tests.rs, updated; rerun 1 passed); route::export
golden regenerated with WRITE_GOLDEN=1 (1 passed); codewhale-agent 70
passed; codewhale-tui tui::model_picker 33 passed; commands::groups::core::
provider 37 passed; scripts/catalog_models_dev_test.py 18 passed.

Touched paths: CHANGELOG.md, crates/tui/CHANGELOG.md, crates/agent/src/lib.rs,
crates/config/assets/models_dev.bundled.json,
crates/config/assets/provider_descriptors.json,
crates/config/src/catalog/tests.rs,
crates/config/src/route/providers-export.golden.json,
crates/tui/src/tui/model_picker.rs, docs/PROVIDERS.md,
scripts/catalog/models_dev_seed.toml. No provider_lake/provider_catalog_live
edits.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 01:01:36 -07:00
CodeWhale BotandClaude Opus 5.5 3400ebb5c9 docs(changelog): record that remote control attaches again [skip ci]
Platform 932b873e/c231a299 replaced the retired 0.9.13 runtime identity
pin, which refused every 0.10.x Engine's `codewhale rc` with
runner_runtime_identity_unavailable. The pin is exact: the 0.10.3 release
must bump Platform's CWC_CODEWHALE_RUNTIME_VERSION (and its provider
export) to 0.10.3 and deploy it with the release commit/image/digest, or
0.10.3 remote control is refused again. Tracked on CW-7525.

Verified: scripts/sync-changelog.sh --check passes. Production rc proof
pending the founder's deploy.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 23:35:02 -07:00
CodeWhale BotandClaude Opus 5.5 4cf2d2cfff feat: integrate the verified 0.10.3 combined Engine batch [skip ci]
Lands the 0.10.3 program work that two Codex sessions left uncommitted on
canonical main (chats 01a176ff "Review latest MCP updates" / 01a176e0 "DSH
plugin compatibility"), on top of contributor PR #7086, which merged as itself (5c3d461d8a)
immediately before this commit. The pieces share compiler inputs
(the crate-split moves touch most of them), so they land as one verified
tree rather than as intermediate commits that would not build.

Contents:
- Crate split slices: `codewhale-net`, `codewhale-notify` and
  `codewhale-sandbox` crates; runtime config, config persistence, oauth,
  settings, catalog cache and Codex model roster moved from tui to runtime;
  hooks, extension host config and instruction budget to config; callers
  rewired, re-export facades removed (CW-6129).
- Follow-up repairs to #7086 instruction budgets (per-scope budgets,
  canonical source labels, `instruction_assembly` session snapshot).
- MCP: official SDK 2.4 host backend by default with `server/discover`
  and legacy fallback, metadata/output schema handling, OAuth issuer
  binding, Engine/CU server modernization (CW-6713, CW-7543, CW-7544,
  CW-7546).
- DSH plugin compatibility from the authorized exception lane `26ba80b06a`:
  shared compatibility table, native command lifecycle, reviewed imports,
  Native-only filesystem sandbox (CW-7054).
- Computer Use plugin-only cutover: vendored bundle removed, marketplace
  install from the independent release (CW-6732).
- Image-heavy sessions: 256 KiB read budget, outbound image omission with
  receipts, one smaller retry on HTTP 413 (CW-7550).
- Typed review operation (`core/engine/review.rs`, `runtime_api/review.rs`)
  used by Platform structured reviews.
- Receipt-owned Unix uninstall (Refs #7091).
- Steering (CW-7552) and Linux Shift+Enter (CW-7551, Refs #7093) source
  changes; runtime/manual proof still pending.

Fixes made during takeover verification:
- MCP: Rust now admits the legacy handshake whenever the SDK classifies the
  discovery probe as legacy (any JSON-RPC error, or a result without the
  modern supported version); before, servers answering the probe with a
  result were refused after it.
- MCP: an admitted legacy handshake reports initialize's own failure (e.g.
  401) instead of the probe's -32601.
- MCP: OAuth reactive refresh covers the side-effect-free probe.
- MCP: broker launch failures keep their diagnostic (missing command).
- Tool images must decode before delivery (truncated PNG was accepted).
- THIRD_PARTY_NOTICES lists MCP SDK 2.4.0 and DSH 0.2.1-rc.2 excerpts.
- Changelog entries for MCP, DSH, Computer Use and image validation.
- `#[cfg(test)]` on three test-only items; `cargo fmt --all`; tests updated
  to post-cutover behavior (no built-in CU, `$schema` manifests, canonical
  paths, lazy CU tools, SyncSession resets instruction assembly).

Verified on this exact tree (macOS, source sha 4353faf1 before changelog
edits; changelog/generated-doc edits only after):
- cargo check --workspace --all-targets --locked: exit 0, no warnings
- codewhale-config 806 passed; codewhale-runtime 850 passed;
  codewhale-net 52 passed; codewhale-notify 31 passed; codewhale-cli 501
  passed (nextest, 0 failed)
- codewhale-tui lib: 14515 passed, 0 failed, 21 skipped
- extension host npm test: 420 tests, 413 passed, 0 failed, 7 skipped
- web check-facts OK
Linux x86_64 (Boat VM, same source before the #7097 fold-in):
cargo check --workspace --all-targets --locked exit 0; codewhale-config
808, codewhale-runtime 849, codewhale-net 52, codewhale-notify 31,
codewhale-sandbox 100, codewhale-cli 501 passed, 0 failed. The tui lib
test binary was still compiling on the 16 GB VM at commit time.
#7097 (main) was three-way merged into approval_log.rs and clipboard.rs;
re-check after the fold-in: workspace check exit 0; tui approval_log,
clipboard, session_export, file_mention, pet_watch 213 passed.
Not run: hosted CI, clippy -D warnings, release packaging, live provider
or real MCP servers beyond local fixtures, Deepin run for #7093.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 23:28:43 -07:00
Hunter Bown 5c3d461d8a feat(context): per-scope instruction budgets with visible truncation (#7086)
Thanks @7jrxt42BxFZo4iAnN4CX for the design and implementation of per-scope instruction budgets (#6526).
2026-10-27 14:26:50 +08:00
CodeWhale BotandClaude Opus 5.5 363f03a2bb Merge main into fix/instruction-budget-6526
Maintainer merge to make the PR mergeable again. Conflicts were limited to
CHANGELOG.md and crates/tui/CHANGELOG.md (kept both sides: main's entries
and this PR's instruction-budget entry; the two Unreleased sections stay in
sync) and the generated web/data/{agent-docs,features}.generated.json
(main's copy; regenerated from source on main afterwards).

Verified (Linux, Boat VM) on the same merge against main 7fe0cc2b4f:
cargo check --workspace --all-targets --locked exit 0; codewhale-config
808 passed, 0 failed. This merge adds only main's two newer commits
(#7097 and the changelog sync, 7 files).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 23:05:29 -07:00
CodeWhale BotandClaude Opus 5.5 c8d0e14de1 fix(catalog): inherit model facts across a provider's host move (#7096)
OpenCode Go's roster still lives at https://opencode.ai/zen/go/v1 while
models.dev now publishes its api as https://opencode.ai/inference/go/openai/v1.
complete_from required the lower row's published base to fingerprint-match
the live roster before copying anything, so the models.dev limit for
deepseek-v4.1-flash (context 1,000,000) was discarded and the window fell
back to LEGACY_DEEPSEEK_CONTEXT_WINDOW_TOKENS (128K).

A context window belongs to the model, not the host serving it. For a live
row completed from a model-describing layer (bundled, models.dev, cloud
facts, Codewhale corrections), the configured base must still match the
live roster, but the published-base match now gates only endpoint-scoped
facts: transport, provider_api, provider_npm, status and cost. Limits,
modalities, reasoning and the other model facts are inherited. Live-to-live
and override arms are unchanged; complete_facts_from (signed cloud patch)
still copies everything.

Not in this slice: moving the opencode-go descriptor to the inference/
host. The new docs split the chat base (/inference/go/openai/v1), the
Anthropic base (/inference/go/anthropic/v1) and the models base
(/inference/go/v1), which the single model-aware base_url cannot express;
zen/go/v1 still answers /models with 200 (checked 2026-10-26).

Verified:
- scripts/dev-test.sh config: 806 run, 806 passed, 1 skipped
- scripts/dev-test.sh tui provider_lake catalog context_window opencode:
  365 run, 365 passed
- Throwaway reproduction (not committed): live opencode-go row at zen/go/v1
  plus models.dev row with api inference/go/openai/v1 and limit 1M.
  Without fix: FAILED (context stays None). With fix: passed; context is
  1,000,000, provider_api and cost not inherited, and a mismatched
  configured base still returns false.
- cargo fmt -p codewhale-config --check clean

Fixes #7096

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 22:26:41 -07:00
Sh1ZukuandHunter Bown ef30199b50 fix(tui): resolve the state root before opening session-side state (#7097)
* fix(tui): resolve the state root before opening session-side state

A user who relocates application state to another volume cannot save a
session at all. Every save fails with the red "cannot save session"
notice and the log records

  persistence write failed what=session:<id> error_kind=InvalidInput
  error=Fleet artifact path must stay within the workspace

`SessionManager::save_session_inner` hydrates approval receipts before it
serializes anything, and `ApprovalReceiptStore::log_file` opened
`<sessions>/<id>/approval_receipts.jsonl` through the strict
`WorkspaceFile::open`. On Windows that opener walks the root component by
component and refuses the junction that is the state root, so the save died
before producing a document.

Storing session receipt data went from plain file I/O (0.10.2) to the
confined opener, which is the right direction, but the state root is a root
the user owns and may relocate. `open_resolved_root` exists for exactly
that (#6947): it resolves the root once and then runs the same link-refusing
walk below it. These call sites now use it:

- approval receipt log (the one that blocked saving)
- session archive artifact reads and the two directory checks
- clipboard image sink, screencapture attachment sink
- pet habitat store, descriptor and lifetime lock

No boundary is widened: a link below the state root is still refused, and
`WorkspaceFile::open` stays the opener for caller-supplied workspace roots.

Verified with cargo check -p codewhale-tui (exit 0) and cargo fmt. The
0.10.2 binary with the same junction layout reproduces the failure, so this
is not a regression from an unreleased branch.

* fix(tui): keep the strict root rule on the workspace fallback

The review of the previous commit found that two of the converted call sites
do not always hold the state root. `clipboard_images_dir_for_home` and
`screenshot_stabilization_dir` fall back to a directory under the workspace
when no home directory resolves, so `dir.parent()` is then a caller-supplied
workspace root — and resolving it would have widened the rule that a link at
the workspace root is refused.

Both sites now pick the opener from the root they actually hold: the resolved
opener only for `<home>/.codewhale`, the strict opener otherwise. The normal
path (a home directory exists) keeps the junction fix; the narrow fallback
keeps the boundary it had.

---------

Co-authored-by: Hunter Bown <[email protected]>
2026-10-27 05:22:28 +00:00
CodeWhale BotandClaude Opus 5.5 b1a6191e79 chore(changelog): sync crates/tui/CHANGELOG.md with root Unreleased
81558024c added three Unreleased entries to CHANGELOG.md without running
scripts/sync-changelog.sh, so the Version drift check fails on every PR
based on main (seen on #7097 and #7094). Regenerated from origin/main's
root CHANGELOG.md; scripts/sync-changelog.sh --check passes on the result.
Built with a temporary index so the shared checkout's uncommitted work is
untouched.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 22:02:21 -07:00
CodeWhale Bot 7fe0cc2b4f docs(web): prepare simpler Computer Use onboarding (CW-6732) [skip ci]
Describe the ordinary Computer Use plugin and persistent native host approval across apps, models and tasks. Make the background diagnostic optional by removing it from the default onboarding sequence in all 18 locales. Explain the compact controller, global Stop and saved-access settings; retain the optional live preview.

Validation: release validators 26/26, dictionary checks 20/20 after updating the existing onboarding step-count assertion, locale completeness and GT catalog parity pass. Final headline edits only simplify prose; keys and flow structure are unchanged.

Source preparation only. Deploy this wording with the qualified CU 0.13.0 release and normal-plugin installation route; the current published 0.12.0 helper does not implement this flow. No website deployment or CU publication performed.
2026-10-26 03:16:03 -07:00
CodeWhale Bot 88772172b6 docs(actions): distinguish account configuration from optional BYOK [skip ci]
CW-6864: document account-owned provider/model selection and missing-configuration refusal. OrcaRouter remains an explicit example, not a hosted default. Standalone Actions still require explicit inputs; no automatic account-settings lookup is claimed. Documentation diff and whitespace reviewed; no runtime, provider, CI or deployment action.
2026-10-26 01:08:48 -07:00
CodeWhale Bot 6f92f0d5e2 fix(action): accept OrcaRouter keys and recommend the free route [skip ci]
The explicit provider allowlist omitted OrcaRouter. Pass only ORCAROUTER_API_KEY to its existing Engine route and keep no-key failure closed. The setup example uses our existing public referral and orcarouter/free, with required authentication and rate/runner limits disclosed. No provider or credential fallback.

Validation:8 existing Action cases pass, expanded existing assertions cover missing Orca key, exact route, selected-secret isolation and receipt redaction. Actual unauthenticated POST orcarouter/free returned401 Invalid API key on2026-10-26; no authenticated provider completion. Whitespace pass. No workflow enabled or deployed.
2026-10-26 00:46:11 -07:00
CodeWhale Bot 81558024ca feat(integrations): reuse Engine reviews and durable chat admission [skip ci]
GitHub Action exports validated structured findings for a separate trusted publisher with current-head checks and dedupe. Telegram persists prompt identity before cursor advancement, reconciles the same Engine operation and retains uncertain final replies for explicit status recovery. DingTalk internal-app Stream adapter reuses bridge-core admission, approvals and question parsing. Extracted existing Weixin question helpers into shared core; no second turn loop.

Local evidence:168 existing bridge/Telegram/Weixin/Feishu/WeCom checks pass,1existing skip;9 actual local HTTP bridge-process scenarios pass including restart/identity/question/uncertain-final delivery. Action8 existing tests plus30 injected publisher/contract scenarios pass. DingTalk20 injected transport scenarios pass plus syntax/SDK import; no live connection. Feature derivation/whitespace pass. Staged shared registry/changelog/facts exclude concurrent instruction-budget work. No new tests/comments, native build, provider completion, workflow activation or deploy. Refs CW-6864.
2026-10-26 00:30:04 -07:00
CodeWhale BotandCodex fb10884b4f feat(engine): opt into bounded answerless output-limit recovery [skip ci]
Refs #6990. Add default-off reasoning_only.on_output_limit using the
existing max_reprompts counter. Capture settings in exec, Runtime API,
TUI and ordinary Fleet children; keep RLM's separate stop protocol.
Length stops get a request-scoped nudge immediately, empty custom nudges
stay bare, and whitespace-only text is never a successful answer.
Output/context/turn/tool controls remain authoritative; this does not
preserve hidden reasoning, raise caps or guarantee long-proof completion.

Persist owner-bound child nudge receipts before dispatch in the existing
private artifact. V2 validates sequence/anchors and separates display
from conversation/resume; v1 stays readable. Bound receipt waits and keep
the owned guard through cancellation. A busy writer fails loudly with
the previous checkpoint rather than hanging settlement. Critical live
forwarding uses the existing bounded queue pattern.

Actual loopback proof exposed existing child transport replay after a
terminal length frame plus a broken body:2requests,Completed,two reported
usage sources. Generic recovery now requires no meaningful terminal stop.
The same case makes1request and fails after repair, retaining reported
usage. Partial text/tools remain unadmitted.

Verification: CLI build PASS (initial814.896s; guard repair53.083s).
Focused nextest:124tests run,124passed,15232skipped (70TUI/54locale),
4.669s after compilation. Initial test compilation was stopped303.972s
in to repair the reproduced replay; no initial test pass is claimed.
All2396frozen source hashes match;38explicit paths. Existing registry1/1,
locale2616keys x15, dead-code186, blocking543sites/165files, and local
facts/vocabulary/version/changelog/whitespace checks pass.

Manual receipts:11root,11Fleet and1same-boot140x40TUI cases. Exact bounds,
caps, shared exhaustion, empty/custom nudges, v1/v2 worker resume, durable
predispatch records and visible receipts verified. Root and5unaffected
Fleet cases reuse the initial CLI; the sole changed child-error branch
was re-observed on the repair. Recorded request prefixes/catalogs remain
unchanged across retries, without claiming provider cache hits.
All inference was synthetic loopback; the TUI proxy denied background
catalogue/update connections. No paid provider, hosted CI, release or
deploy. Preserve existing prior-boot roster filters and report limits.

Both changelogs credit @M-Maciej; docs/feature row and15packs updated.
Receipts: CW/artifacts/codex-0103-reasoning-limit-20261025.

Co-Authored-By: Codex <[email protected]>
2026-10-25 21:24:26 -07:00
CodeWhale Bot 6378f3b6b9 fix(docs): keep the OrcaRouter registry summary within its limit [skip ci]
The new provider description was141characters against the registry's120
limit. Shorten it to105 and regenerate the public feature projection.
The existing feature_registry_matches_code check fails before with the
141-character diagnostic, then passes1/1 after the correction.

Compiled the unchanged existing feature_registry.rs independently with the
actual built runtime/serde/toml libraries under the existing build lock;
no new tests, shim or whole TUI rebuild. Node facts and whitespace pass.
The uncompiled reasoning-recovery packet remains unstaged and is excluded.
2026-10-25 20:28:59 -07:00
CodeWhale Bot eea9f1ea45 docs(changelog): credit the verified NoNewPrivs container fix [skip ci]
Refs #7021. Record the existing test-only source fix e7c336d634 and credit
@asto18089 for diagnosing inherited NoNewPrivs. Both changelogs updated.

Local verification now completed against source bytes identical to the
carrying commit: compiled the unchanged canonical process_hardening.rs
as an independent test module with the workspace-pinned real libc/tracing
libraries, edition2024 and the same stable Rust1.99.0 compiler. Static
ARM64 Linux musl ELF has no interpreter or needed shared libraries.
Dependency and module compilation ran under the existing host build lock
and cache; no new manifest, checkout, cache, shim or tests.

The same harness passed3existing tests in each unpinned/pinned local
Bookworm container, parent NoNewPrivs0/1. Two substantive matrices cover
7child rows per state. Pinned run printed exactly2ratchet disclosures;
unpinned printed0. Child/mode/override markers were unset. Source/binary/
dependency/compiler/kernel hashes and logs retained in the receipt.

No full Linux TUI/GNU compilation, startup-order, status UI, sudo, hosted
CI, provider call, release or deployment claim. Changelog-only source
change: whitespace, changelog synchronization and version checks pass;
passing kernel evidence reused without a repeat run.
2026-10-25 20:05:07 -07:00
CodeWhale Bot 579c8d92b3 feat(tui): recommend the OrcaRouter partner offer in route pickers [skip ci]
No-Issue: founder-requested provider/model recommendation for 0.10.3.

Reuse one recommendation row in /provider, /model and OrcaRouter sign-in
choices. F7 or a click opens the existing non-closing pager with the exact
redemption URL, 30% top-up match for DeepSeek V4.1 Flash, current-terms
pointer and public referral disclosure. Keep provider/model/auth choices
explicit. Clear previous click latches before opening the offer. Brand-first
titles retain OrcaRouter at narrow widths. All 15 locale packs, key catalog,
keybindings guide, provider feature row/docs and both changelogs updated;
regenerated public docs/features projections.

Verification: final production CLI build passed14m10s with the retained
large-__eh_frame linker warning. Focused existing batch ran273 tests:
272 passed,1 failed because F7 was missing from the keybindings guide.
Corrected only that documentation and its generated projection, compiled
unchanged keybinding_table.rs with the actual built localization library,
and reran the exact failed existing test:1 passed,0 failed. Reused the272
unchanged passing tests; no new tests or code comments. Original failed
run retained.2394 source hashes were unchanged before this docs repair;
all production Rust/locale inputs remain unchanged afterward.

Actual isolated PTY proof:9 cases, F7 and clicks on provider/model/auth
surfaces, en80x24/fr40x12/zh-Hans140x40. Exact URL survives wrapping and
return preserves the picker. Zero model POSTs, external browser/account or
redemption actions. Observer startup/rail/coordinate corrections retained.
Read-only Codex review clean after title/click-latch corrections.

Locale parity2611keys/15packs, G1 boundary0/0/1/0/0 (121 closure modules),
dead186, blocking543sites/165files, vocabulary/facts/version/changelog
checks pass. No hosted CI, release or provider inference claimed. An initial
own CLI compiler was stopped at435s to correct title truncation before the
final build; its receipt is retained separately.
2026-10-25 20:02:34 -07:00
CodeWhale Bot a129eb544a fix(tui): localize question footers and portable command feedback [skip ci]
Refs #7071. Translate question/approval/handoff/waiting chrome, shorten
localized numeric selectors to fit, and carry locale through the existing
portable /color presentation capability. Restore the contextual-handler
assertion and correct the two stale command ownership descriptions. Both
changelogs describe this first slice; remaining untranslated surfaces stay
open. No new tests or code comments.

Final combined source: CLI build passed; 479/479 focused existing tests
passed (425 TUI, 54 localization); locale parity 2607 keys/15 packs. Actual
80x40 PTY single/multi question footers passed in fr/es-419/uk/pt-BR/ru,
10 cases, including more than nine options and visible cancel tail. Actual
Russian /color red traversed the portable bridge and produced localized
feedback. Synthetic loopback fixture only, no real provider calls.
Prebuilt existing metric probes passed all 55 runtime-contract ceilings;
G1 boundary/dead-code/blocking/vocabulary/facts checks passed.
Intermediate commit trees not compiled separately; no hosted CI.
2026-10-25 19:05:01 -07:00
CodeWhale Bot 8c2bf6b49e fix(tui): localize the seeded plan handoff to-do reminder [skip ci]
Refs #7068. Use the existing locale catalog for the seeded plan title and
todo_write reminder across all 15 packs; adapt the existing assertions and
record the behavior in both changelogs. The prompt remains a reminder, not
a claim that model tasks have completed.

Final combined four-slice UI source: CLI build passed; 479/479 focused
existing tests passed, including 16 plan-handoff and 54 localization tests
(overlapping groups). Locale parity: 2607 keys in each of 15 packs.
Intermediate trees not compiled separately. No real provider or hosted CI.
2026-10-25 19:04:59 -07:00
CodeWhale Bot 9f4dce1fc5 fix(tui): offer the external editor when the draft is clipped [skip ci]
Refs #7049. Use the native composer's actual source rows and visible budget
to show a localized F4 hint only for clipped editable drafts, preserving
queue/busy hints. Route F4/Ctrl+Shift+O through the existing binding table
and editor action; keep the full chord catalog authoritative. Credit
@Lstarsky0 in both changelogs.

Final combined UI source: CLI build passed with an existing-style large
__eh_frame linker warning; 479/479 focused existing tests passed, including
composer, mounted-shell and key-routing checks. Initial test compilation
found a missing enum arm in the existing key-probe helper; fixed that arm.
Actual isolated PTY: hint absent when text fits, appears after resizing to
40x12, clears with the draft, and handles CJK/combining/ZWJ and 20,400-char
paste. F4 delivered the whole draft to VISUAL and restored the edited text.
Retained attachment-preview exclusion remains source-reviewed only.
Intermediate commit trees not compiled separately. No real provider/CI.
2026-10-25 19:04:57 -07:00
CodeWhale Bot 513718b30e fix(tui): use the active palette for archived-context markdown [skip ci]
Refs #7043. Carry the archived-context rendering options through the existing
markdown renderer and retire its last production fallback callers. Keep
render_markdown/render_parsed only for existing test consumers. Credit
@Statter for the black-on-black report; both changelogs record this slice.

Verified the final combined four-slice UI source: CLI build passed; 479/479
focused existing localization/TUI tests passed, including 65 history and
63 markdown tests (overlapping groups). One test-only exhaustive-match
repair followed the initial compile failure; no production rebuild needed.
No exact archived-context terminal reproduction is claimed. Intermediate
commit trees were not compiled separately. No hosted CI or provider calls.
2026-10-25 19:04:53 -07:00
CodeWhale BotandOpenAI Codex bf028bcc0e docs(changelog): credit the goal-clear receipt correction [skip ci]
Record PR #7089 by @Lstarsky0 in both Unreleased changelogs. The contributor PR merged as itself in bb5ad1f1824fe0a12d7da624eed68b97461e2853; its GitHub-linked author and original commit remain in history.

The GitHub merge tree equals the locally verified tree138ad9ddea42b7ed17710d958535a84f78dc17ba. Canonical source plus these exact changelog bytes matches all2,177 frozen hashes: TUI library check passed6m58s;147 existing goal tests passed,0failed,15,155skipped,6.299s run after1m44s build. Independent read-only Codex and parent review clean. Never-goal/fallback-only notification branches are source-reviewed, not directly asserted by these tests; no agent PTY/provider/hostedCI evidence. Format, changelog sync, version state0.10.2 and whitespace pass. No test rerun for unchanged source.

No release/tag/publication or provider call. Receipt CW/artifacts/codex-0103-pr7089-20261025/acceptance.json says PASS and retains limits.

Co-Authored-By: OpenAI Codex <[email protected]>
2026-10-25 18:14:06 -07:00
Hunter Bown bb5ad1f182 Merge pull request #7089 from Lstarsky0/fix/clear-without-goal-no-receipt [skip ci]
Only report a goal clear when an objective or configured fallback existed. Preserve the canonical empty snapshot, prompt refresh and continuation cancellation.

Human approval verified at exact e11bd43 head. Local acceptance PASS on canonical main478b29eae5 plus the exact merge content: TUI library check passed,147 goal tests passed,0failed. Independent read-only Codex and parent source reviews clean. Existing tests cover positive clear flows; never-goal and fallback-only notification branches are source-reviewed. No agent PTY/provider/hosted CI claim.

Contributor commit and GitHub-linked authorship retained. No dependent PRs; source-branch auto-delete remains disabled. Changelog credit follows separately. Full0.10.3 remains under implementation; this is not a release.
2026-10-26 09:13:54 +08:00
CodeWhale BotandOpenAI Codex 478b29eae5 refactor(config): retire the TUI upstream re-export facade [skip ci]
S06/W3 routes consumers directly to codewhale-config, runtime and notification owners, retires config/models.rs and privately imports the settings theme. Its seven locally defined constants/helpers retain exact definitions and active consumers in config.rs. Registry drift guard keeps the same Antigravity exclusion at its direct owner. Update both Unreleased changelogs.

Initial plan: 229 root upstream exports (three test-only), 213 overlapping model-wrapper names, 1,124 direct lines/143 files plus 68 grouped lists/60 files. One repair fixes the missed root-relative ProviderKind reference and its registry literal. Initial E0603 and exit101 retained. Final positive-controlled full-source audit finds zero retired relative refs and zero upstream public facades; seven retained definitions and three separately reordered helpers match old source exactly. Independent read-only Codex review found no actionable issue.

Local verification: workspace all-target check passed in12m44s; 554/554 existing config/readiness/route tests (baseline554 active/1ignored;14,748 skipped), 2.175s execution after20m06s build. skill_cli16/16 (204skipped),0.636s after14m02s build. All2,177 Cargo hashes and both auxiliary script hashes unchanged. Final source compiled, intermediate commits not separately compiled. G1/G2, provider registry and web facts pass; budgets186 suppressions and543 blocking sites/165files unchanged/pass; pre-repair budget evidence remains valid for spelling-only repair. Changelog sync/whitespace pass.

TUI strict[M]634556→634613(+57), file[F]943349→943735(+386); explicit owner imports add lines while preparing extraction. Runtime9526/15121 unchanged. Largest module SCC79→78; tools→core29,client→tools4,mcp→core7 unchanged. Config→core/sandbox0; hooks/plugins1each still await later windows. G1 0/0/1/0/0,121closure modules. No production-line reduction claimed. Eleven pre-existing standalone test fragments retain their old rustfmt-unstable layout; other Rust formatted on stdin.

No new tests/comments, hosted CI, provider call, deploy or release. HumanDecision mint unchanged; FD12 removals held. Full0.10.3 and later split windows remain open. Receipts CW/artifacts/codex-0103-takeover-20261025/w3-final-receipt.json and coordination/CW-0.10.3-CRATE-SPLIT-W3-REPORT-20261025.md.

Co-Authored-By: OpenAI Codex <[email protected]>
2026-10-25 17:58:26 -07:00
CodeWhale BotandOpenAI Codex 8b82b747e6 refactor(tools): place shared helpers before test modules [skip ci]
Move tool_call_denied, enforce_tool_denial and prepare_tools_for_strict_mode before their existing test modules. Byte-for-byte definition comparison against 995f2a88f9 passes for all three; no helper body or authority change.

Verified in the final W3 batch: workspace all-target check passes after one config import repair, 554 existing config/readiness/route tests and 16 skill_cli integration tests pass. All 2,177 Cargo source hashes unchanged through checks; no new tests/comments. The final W3 source, rather than each intermediate commit, was compiled. Receipt CW/artifacts/codex-0103-takeover-20261025/w3-final-receipt.json.

Co-Authored-By: OpenAI Codex <[email protected]>
2026-10-25 17:58:22 -07:00
CodeWhale BotandOpenAI Codex 492c3d5b30 fix(split): resolve test aliases and report root references [skip ci]
Include cfg(test) upstream aliases in re-export discovery and audit root-module relative paths even without an explicit crate::config use. W3 exposed both gaps: one root-local ProviderKind path escaped the tool, and test-only aliases could be left at a retired facade. Preserve cfg attributes on rewritten imports.

Manual before/after artifact fixtures: root report zero to one site; test alias unrewritten to direct owner, guarded by cfg(test). Python syntax passes. Independent read-only Codex W3 review found no actionable issue. No native build or Rust behavior claim for this Python-only commit.

Co-Authored-By: OpenAI Codex <[email protected]>
2026-10-25 17:58:20 -07:00
CodeWhale BotandOpenAI Codex 995f2a88f9 chore(tui): remove uncalled plan and journal helpers [skip ci]
Delete SavedSession::active_entries, PlanState::steps, StepStatus::symbol,
and the never-issued CancelReason::Preempted. These private-module helpers
have no callers or public root re-export; cancellation reasons are typed,
not serialized or numeric. Keep active mailbox/cancellation builders and
remove their obsolete unused-code suppressions. No supported feature is
retired. Hand-ratchet the budget 192->186 and update both changelogs.

Local evidence covers the combined W2a source snapshot (base 916d3227e8,
2,178 input hashes, canonical map sha256
cd34d125770ebfc7f3ebaa921131c54b82781b469eb1cd27fd7c39350476cad0).
Intermediate commit trees were not separately compiled. A pre-follow-up
all-target check passed in 13m42s; final library tests ran 2,116 and passed
2,116 (13,540 skipped). Original 1,309-test selection is accounted for after
mapping four moved test names. Final skill_cli integration: 16 run/16 passed/204 skipped, 0.643s
execution after its 24m08s test-profile build. All recorded source hashes
stayed unchanged through both final runs.

Final batch metrics: tui strict/file lines 634691/943616 -> 634556/943349;
runtime 9526/15121 unchanged; SCC 79 unchanged; tools->core 46->29,
client->tools 5->4; mcp->core 7 held. Dead-code suppressions/budget 195->186;
blocking 543/165 unchanged. G1, G2, ratchets, web facts, provider registry,
changelog sync and diff whitespace checks passed. First check exposed an
over-broad FleetRole import rewrite, corrected in one repair round. Logs
and frozen-source receipts are retained outside the repository.

No new permanent tests or comments, no CI, provider call, deployment or
release. HumanDecision retains the engine-only mint boundary; published
runtime API removals stay held under FD-12. Engineering defaults follow the
coordinating handoff and remain overridable by the founder.

Co-Authored-By: OpenAI Codex <[email protected]>
2026-10-25 17:03:15 -07:00
CodeWhale BotandOpenAI Codex 4d4ca25b61 refactor(tools): retire core facades and dead context fields [skip ci]
S04 moves tool names to their shared leaf owner, denial checks beside the
execution envelope, and strict-tool catalog preparation with its four
existing tests beside the catalog. Drop private FleetRole/tool_parser
facades, unused MCP/sandbox context fields, and the one-variant policy.
Every consumer imports its actual owner. The paired changelogs record the
removals. Human approval attestations remain in their original engine scope.

Local evidence covers the combined W2a source snapshot (base 916d3227e8,
2,178 input hashes, canonical map sha256
cd34d125770ebfc7f3ebaa921131c54b82781b469eb1cd27fd7c39350476cad0).
Intermediate commit trees were not separately compiled. A pre-follow-up
all-target check passed in 13m42s; final library tests ran 2,116 and passed
2,116 (13,540 skipped). Original 1,309-test selection is accounted for after
mapping four moved test names. Final skill_cli integration: 16 run/16 passed/204 skipped, 0.643s
execution after its 24m08s test-profile build. All recorded source hashes
stayed unchanged through both final runs.

Final batch metrics: tui strict/file lines 634691/943616 -> 634556/943349;
runtime 9526/15121 unchanged; SCC 79 unchanged; tools->core 46->29,
client->tools 5->4; mcp->core 7 held. Dead-code suppressions/budget 195->186;
blocking 543/165 unchanged. G1, G2, ratchets, web facts, provider registry,
changelog sync and diff whitespace checks passed. First check exposed an
over-broad FleetRole import rewrite, corrected in one repair round. Logs
and frozen-source receipts are retained outside the repository.

No new permanent tests or comments, no CI, provider call, deployment or
release. HumanDecision retains the engine-only mint boundary; published
runtime API removals stay held under FD-12. Engineering defaults follow the
coordinating handoff and remain overridable by the founder.

Co-Authored-By: OpenAI Codex <[email protected]>
2026-10-25 17:02:58 -07:00
CodeWhale BotandOpenAI Codex 276a2b2585 refactor(config): consume shared home and approval owners [skip ci]
S03 removes the home wrapper and integration stub, routes every free-function
caller to codewhale_paths::user_home, centralizes legacy-bit approval posture
in execpolicy, and moves pure legacy-role migration to protocol::fleet.
The unrelated WorkspaceHost home facet retains its nine Rust occurrences.

Local evidence covers the combined W2a source snapshot (base 916d3227e8,
2,178 input hashes, canonical map sha256
cd34d125770ebfc7f3ebaa921131c54b82781b469eb1cd27fd7c39350476cad0).
Intermediate commit trees were not separately compiled. A pre-follow-up
all-target check passed in 13m42s; final library tests ran 2,116 and passed
2,116 (13,540 skipped). Original 1,309-test selection is accounted for after
mapping four moved test names. Final skill_cli integration: 16 run/16 passed/204 skipped, 0.643s
execution after its 24m08s test-profile build. All recorded source hashes
stayed unchanged through both final runs.

Final batch metrics: tui strict/file lines 634691/943616 -> 634556/943349;
runtime 9526/15121 unchanged; SCC 79 unchanged; tools->core 46->29,
client->tools 5->4; mcp->core 7 held. Dead-code suppressions/budget 195->186;
blocking 543/165 unchanged. G1, G2, ratchets, web facts, provider registry,
changelog sync and diff whitespace checks passed. First check exposed an
over-broad FleetRole import rewrite, corrected in one repair round. Logs
and frozen-source receipts are retained outside the repository.

No new permanent tests or comments, no CI, provider call, deployment or
release. HumanDecision retains the engine-only mint boundary; published
runtime API removals stay held under FD-12. Engineering defaults follow the
coordinating handoff and remain overridable by the founder.

Co-Authored-By: OpenAI Codex <[email protected]>
2026-10-25 17:02:24 -07:00
CodeWhale BotandOpenAI Codex d904b22c6d fix(split): preserve import guards and contain carved output [skip ci]
Retiring a facade can split one cfg(test)-guarded import into several
statements. Both flattening and upstream regrouping previously left the
extra statements unconditional. Reuse the existing mover attribute helper
and repeat the guard on each resulting statement.

Refuse carved output whose existing parent symlinks resolve outside the
canonical repository, matching the source-path containment boundary.

Local evidence: both import shapes lost their second cfg guard before this
change and preserve it after; four extra attribute cases and the existing
mover shape passed. Four Python modules parse; mover help exits 0. The
symlink-output probe accepted before and refuses after, without writing any
outside file. A formatted carve CLI dry-run on a real function exits 0.
No Rust build, module move, CI, provider call, or release claim for this
Python-only change. The active W2a Cargo source remains frozen separately.

Co-Authored-By: OpenAI Codex <[email protected]>
2026-10-25 16:40:22 -07:00
916d3227e8 fix(review): reserve reasoning output and report balance failures [skip ci]
Refs #7067: Low-risk review passes previously capped reasoning and visible
text together at 4,000 output tokens. Raise the tier allowance to 8,192 for
reasoning/unknown routes, retaining the route or explicit output ceiling.
Use the same floor and normalized bounded reasoning effort in ReviewTool;
share incomplete-output wording with the CLI and restore the existing
accept_single_review documentation to its function. This is the intermediate
review repair; removing caps and implementing skip-before-start money control
remain planned Review v2 work.

Refs #7055: preserve balance HTTP/parse failures through Result and show
them in explicit /balance output. Distinguish timeouts and connection errors
without exposing the request URL. Automatic chip refresh keeps its previous
value. The DeepSeek URL fix predates this slice; real-key proof remains open.
Update both changelogs. Based on the stopped issue drafter batch 3, including
its reviewed effort normalization; transport classification fixes review m1.

Verified on a0053edcda plus this seven-file diff:
- Hermetic dev-cargo nextest, codewhale-tui --lib, focused review/triage/core,
  reserve and balance filters: 74 tests run, 74 passed, 15,228 skipped.
- All 2,180 recorded source hashes were unchanged during compilation.
- Single-file Rust formatting and lib.rs stdin comparison passed;
  git diff --check and sync-changelog.sh --check passed.
- Linker emitted its existing-size warning: __eh_frame exceeds the 16 MB
  compact-unwind limit; compilation and all selected tests completed.

No new tests/comments, provider call, CI dispatch, release or deployment.
These existing checks do not exercise a live reasoning route or key.
Full local receipt: CW/artifacts/codex-0103-takeover-20261025/batch3-result.json.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Co-Authored-By: OpenAI Codex <[email protected]>
2026-10-25 15:45:38 -07:00
CodeWhale BotandOpenAI Codex a0053edcda tooling(split): carve whole Rust items through journalled writes [skip ci]
Complete the stopped lane B carving helper for S51. Derive child imports
from existing use leaves and moved items; widen private definitions only
to the selected visibility and preserve test module attributes. Use the
existing splitlib compare-and-swap journal for writes. Dry runs now parse
and format the proposed Rust too. Refuse outside sources, unsafe or duplicate
module names, invalid visibility, empty/cut ranges and duplicated test items.

Verified against canonical main 035bd86a26: Python syntax passed; 7 manual
invalid-plan checks were refused; carve.py crates/cli/src/lib.rs --plan
/Volumes/VIXinSSD/CW/artifacts/codex-0103-takeover-20261025/carve-live-plan.json
--dry-run --json exited 0, formatting one real CLI function into a child
without writing source. git diff --check passed. This is tooling-only:
no Rust compilation, no full S51 move, no cross-platform or hosted checks.
Token-based import inference still needs compiler validation on a real move.

Refs CW-6129

Co-Authored-By: OpenAI Codex <[email protected]>
2026-10-25 15:41:08 -07:00
CodeWhale BotandOpenAI Codex 035bd86a26 tooling(split): support crate-scoped visibility for in-crate carving [skip ci]
Complete stopped lane B widening work for S51. --vis pub(crate) now
widens private items and fields without exporting them outside the crate;
the default remains pub for cross-crate moves. Reject unsupported visibility
values rather than producing malformed Rust.

Verified on canonical main b1fa6193b9: Python syntax parsing for widen.py
and carve.py passed; 8 manual widening cases passed, including unchanged
public and already crate-visible items. widen.py --help exited 0 and
git diff --check passed. No Rust compilation or refactor-output build was
run for this Python-only slice.

Refs CW-6129

Co-Authored-By: OpenAI Codex <[email protected]>
2026-10-25 15:40:20 -07:00
CodeWhale BotandClaude Sonnet 5.5 b1fa6193b9 W1 / S02: delete zero-reference tui code, stale dead-code allows and three empty files
First slice of the crate-split execution brief (S02), run as one window. Tested
at base 7478d51f9c; landed on f0c0bf2665 (two upstream files, tui/diff_render.rs
and tui/pager.rs, are disjoint from this change and were not recompiled with it).
Engineering defaults for the open founder decisions (FD-12: hold published-crate
pub removals) were set by the coordinating session under AGENTS.md source
authority; the founder can overrule them.

Deleted after re-proving zero references with git grep over the whole repo
(crates, web, scripts, docs), not only crates/*.rs:
- session_manager.rs journal_append_message (and its expect(dead_code));
- fleet/alerts.rs FleetAlertEvent::{restart_exhausted, needs_human, budget_exceeded};
- fleet/worker_runtime.rs explicit_fleet_provider (explicit_fleet_provider_id stays);
- sandbox/policy.rs SandboxPolicy::has_full_disk_read_access, and the read_guard.rs
  module comment that named it;
- plugins/manifest.rs ResolvedPluginComponents::all_paths (the now-empty impl),
  plugins/registry.rs list_enabled and enabled_plugins, plugins/marketplace/types.rs
  to_install_source (and its now-unused PluginInstallSource import);
- tui/pet_watch/worker.rs Raster::same_picture (the now-empty impl);
- client.rs OpenRouterModelItem name, created, expiration_date (never read, three
  expect(dead_code) gone; serde ignores the extra response keys);
- lib.rs pub use of maybe_load_persisted_cache and all_catalog_models_for_provider;
- ten superfluous allow(dead_code) in core/prefix_cache.rs (5), models/lib.rs (3),
  localization/lib.rs (2);
- empty tracked files tui/src/model_registry.rs, tui/src/main.rs (autobins=false)
  and models/src/model_catalog.rs, plus the false model_registry entries in
  AGENTS.md and docs/skills/cw-slice/SKILL.md and two "model_registry follow-up"
  comments in core/engine.rs and core/engine/tool_catalog.rs.

Kept on purpose (brief findings): sandbox/mod.rs PUBLIC_SANDBOX_BACKENDS (the
website facts gate parses it); the pub use ... as pet alias. Removing the alias
makes rustc report seven items in tui/ambient_life/pet_sim.rs as dead, two of
which (braille_text, legacy_whale) the separately built pet/ crates use through
#[path]; it needs a pet/ edit and pet/verify.sh, so it is deferred. FD-12 holds:
session_tree children_of and append_*, goal_loop DEFAULT_GOAL_MAX_STEPS and the
resource_telemetry trim are untouched.

Measured (before at 00be3d2e4f [lane B counter], after on this tree):
- tui strict production lines 634,761 to 634,614 (-147); file-level [F] 943,686
  to 943,539 (-147); count_lines.py.
- largest cycle: 79 modules before and after (partition_report.py); no cycle edge
  was touched.
- dead-code budget 209 to 195 (allow 132 to 122, expect 77 to 73), hand-edited in
  scripts/dead-code-budget.json to the measured number, no --update; blocking-call
  budget unchanged at 543 sites in 165 files; module_graph --check PASS (121
  closure modules); node web/scripts/check-facts.mjs OK; rustfmt --check of
  tui/src/lib.rs (991 files), models, localization and core files clean.

Verified (macOS, local only; no hosted CI, no Linux/Windows compile):
- clippy -p codewhale-tui -p codewhale-core -p codewhale-models -p codewhale-localization
  -p codewhale-runtime --all-targets --all-features --locked -D warnings with CI's
  allowed lints: exit 0 (wall 954.9 s, user 938.7 s). The first pass found the orphaned
  import and the pet_sim exposure; both repaired before the final pass.
- nextest of the same crates, filters core, models, localization and tui alerts,
  session_manager, openrouter, plugins::registry, marketplace, sandbox::policy,
  read_guard, pet_watch: 485 run, 485 passed, 14975 skipped (core 62, models 42,
  localization 54, alerts 9, session_manager 137, openrouter 40, plugins::registry 20,
  marketplace 48, sandbox::policy 15, read_guard 26, pet_watch 35). The test-profile tui
  build took 2634.9 s wall and 1340.6 s user at load average 27 to 40 from other
  sessions' processes, over the brief's 17 to 25 minute cap; user CPU is in line with a
  normal build.
- The committed tree differs from the tested tree by one blank line removed in
  plugins/marketplace/types.rs (rustfmt, checked by rustfmt only) and by the two
  upstream files named above.

Not verified: pet/verify.sh (untouched), web build, runtime-crate tests (no runtime file
changed), the production-feature cli check.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-25 15:15:54 -07:00
Hunter Bown f0c0bf2665 Merge pull request #7087 from Lstarsky0/fix/diff-pager-stable-origins
fix(tui): keep the /diff pager's place when the terminal is resized
2026-10-26 06:03:57 +08:00
CodeWhale BotandClaude Sonnet 5.5 7478d51f9c tooling(split): make rustfmt failures and unstable bases visible in the rewrite tools (W0 follow-up)
Follow-up to 3b35f098f7 after a note from the issue-drafts session: stdin-mode
`rustfmt --check` exits 0 even over a diff, so no exit status is used anywhere
in scripts/split. format_many now writes the formatted bytes of
`rustfmt --edition 2024 --emit stdout`, treats any rustfmt error as fatal,
requires the formatted text to be a fixed point of a second pass, and returns
the files whose base was not rustfmt-stable (left exactly as rewritten, so no
foreign line is reflowed). move-modules.py reports them as
`rustfmt_unstable_base` (null in a dry run, which does not format) and
`--strict-format` turns them into an error before anything is written.

Proof on a throwaway git repo (artifacts/crate-split-w0-20261025/mini):
`printf 'fn  main( ){}' | rustfmt --check` exits 0 (the pitfall);
--strict-format on a tree with an unformatted file exits 2 and writes nothing;
a rewritten file rustfmt cannot parse ("unclosed delimiter") exits 2 and
writes nothing; a normal run formats the formatted-base file and leaves the
unformatted-base file untouched except for the rewritten path, listing both.
The 334/351 stable result of the W0 proof is unchanged in meaning: the 17
others are the include!d test_cases_* fragments whose base is unstable.
Python only; nothing compiled.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-25 14:46:36 -07:00
CodeWhale BotandClaude Sonnet 5.5 3b35f098f7 tooling(split): split tooling v2 (S01/W0): journalled mover, re-export resolver, G11 preflight, widen v2, strict+file counters
Scripts only under scripts/split/. No Rust, no manifest, no budget touched.
Base 00be3d2e4f. tui strict production [M] 634,761 -> 634,761, file level [F]
943,686 -> 943,686, largest SCC 79 -> 79 (partition_report --non-test: [79, 8,
1, 1, 1]). Nothing was compiled: this is python; the rewrites it produces were
checked with rustfmt parse/format, module_graph.py, the path-keyed budget
scripts and counts, on a scratch `git init` export of this base (never the
shared tree). A compile of a rewritten tree is the first Lane A window.

What changed and why (reasoning lives here, not in comments):

move-modules.py (rewritten). R2.5: the old tool exited 0 in a tree without
.git after "moved 6 module(s)" (git failures were check=False), and its
dry-run headline (354 files / 3,487 lines) counted 12 files / 212 lines
inside the moved set that a real run never rewrites.
- --dest-crate/--src-crate replace every hard-coded runtime literal; --json;
  --require-clean (dirty set from `git status --porcelain -z` plus files
  younger than --quiet-minutes, default 15); --apply-cfg (flips only
  `#[cfg(test)]` items that code outside the moved set uses; never touches
  cfg(not(test)) forks, which are an FD-04 decision); --restore <journal>.
- No git mv (the shared index is not touched): plain file operations. Every
  write is compare-and-swap (stat+sha against the bytes the plan was computed
  from, temp file, os.replace) with a pre-image journal under CW/backups/;
  a change between plan and write refuses before writing (exit 3); a change or
  SIGTERM/SIGHUP/SIGINT during the write phase restores every pre-image and
  removes created files and directories (exit 4). git/git grep failures and a
  missing or foreign .git are fatal (exit 2). --require-clean refusals exit 5.
- Rewritten Rust goes through `rustfmt --edition 2024 --emit stdout` on stdin
  per file; a file whose base text is not rustfmt-stable (the 17 include!d
  test_cases_*.rs fragments) is left exactly as rewritten, so no foreign line
  is reflowed.
- Coverage: plain and grouped `use crate::{..}` (3 sites), alias, unqualified
  `<m>::` in the source lib (5 sites; cfg(test)-only use gets a cfg(test)
  import), `<dest>::` -> `crate::` inside moved files (code only), `codewhale_tui::<m>`
  in every other Rust file (cli config_keys 15 lines), sweep of path strings
  (plain, `tui/src/..`, segmented `"tui" / "src" / "<m>"`: this adds
  scripts/check_orcarouter_contract.py, which the old sweep missed) with
  vendor/, crates/config/, scripts/split/, CHANGELOG* and web/data/*.generated.json
  skipped; --derive-facts regenerates web/data (journalled pre-images).
- Reports (and plans, never applies): residue edges from moved files to
  staying modules, super-root residue, manifest needs (deps, features,
  consumer crates), cfg sites by class.
reexports.py (--resolve-reexports): resolves `pub use` facade names (globs
through the upstream sources and the build.rs-generated constants via
catalog_corrections.json / provider_descriptors.json) and rewrites
`crate::config::NAME` and grouped lists to the upstream path.
preflight.py (G11): whole-repo include_str!/include_bytes!/include!/#[path]
resolution against the old-to-new map, features.toml owners, path strings the
sweep cannot fix, `../src/<m>` references, CI test identities; exit 1 on residue.
widen.py: --dest-crate/--src-crate/--packages, signal-safe lints restore
(SIGTERM/SIGHUP/SIGINT and atexit; a manifest left capped by a killed run is
restored first), atomic writes, --prepass static widening, unused_imports
removed with the compiler's machine-applicable suggestions.
count_lines.py promoted from the plan's scratch (identical [M]) with the [F]
counter beside it. testsplit.py: function-level move/stay classifier with call
closure. partition_report/closure_report --non-test; module_graph --tui-src/
--runtime-src and a regex-driven lexer (11.0 s -> 2.5 s over 1,585 files,
0 mismatches against the old lex_mask on every tracked .rs).

Acceptance (scratch export of 00be3d2e4f, config settings config_persistence
config_keys oauth codex_model_cache):
- dry run: staying callers 342 files / 3,275 lines + inside the moved set 12 /
  212 = the old tool's 354 / 3,487; grouped 3; self paths 105 + 5 in comments
  = 110; other crates 2 files / 16 lines (15 cli + 1 runtime doc comment);
  sweep 31 files (+15 skipped); 81 cfg sites (53 test items, 11 test
  mods, 17 not(test) forks = the brief's 17); 9 s.
- real rewrite: 403 paths, 18.6 s, counts identical to the dry run line for
  line; 334 of 351 changed .rs files rustfmt-stable (the 17 others are the
  base-unstable fragments); no `crate::<moved>` left in tui, no
  `codewhale_tui::<moved>` in cli; module_graph.py --check refuses this
  premature move with 43 residue edges (it is the closedness gate working).
- closed module (remote_setup) real move: module_graph --check PASS with the
  unchanged baseline, check-blocking-calls-budget PASS (543 sites, 165 files;
  the sweep re-keyed the path-keyed JSON), dead-code budget PASS 209,
  check-provider-registry PASS, G11 unresolved 0.
- guards on the scratch repo: change between plan and write -> exit 3 nothing
  written; change at write 201 of 403 -> exit 4 and `git status` back to only
  the concurrent edit; SIGTERM at 201 of 403 -> exit 4, status empty; dirty
  file and a file touched < 15 min -> exit 5; no .git -> exit 2.
- widen.py: SIGTERM and SIGHUP while a compile round runs -> exit 130 and
  crates/runtime/Cargo.toml back to `[lints] workspace = true`; a capped
  manifest from an earlier run is restored at start; with a fake cargo JSON
  stream the E0603 widening, --prepass and the unused-import removal applied.
- reexports: 226 config names (213 via models) -> 1,109 direct lines in 142
  files and 68 grouped lists in 60 files outside the cluster, 166 files; the
  ProviderKind figure matches the plan exactly (339 lines / 95 files); the
  plan's 1,189 / 190 and 89 / 75 are not reproduced and likely include the
  cluster's own files; module_graph --check unchanged afterwards.
- preflight on the cluster: residue 8 = lib.rs:20230 include_str, tests/
  integration/main.rs:16 (path + relative), 5 skip-listed mentions in
  crates/config (catalog_corrections.json:4584 and models_dev.bundled.json:11769
  key a source receipt `crates/tui/src/config/models.rs`), vendor picker.rs.
- testsplit: config/tests.rs 407 tests (378 move, 29 stay), scope_tests.rs 8
  (0 move: needs provider_readiness/fleet), whole cluster 687 tests (649 move,
  38 stay); count_lines [M]/[F]: tui 634,761/943,686, cli 20,204/33,761,
  runtime 9,526/15,121, config 29,415/35,830, app-server 7,572/13,095.

Not proven: no cargo build of any rewritten tree; widen.py's compile loop ran
only against a fake cargo; --apply-cfg flipping is planned, not applied or
compiled; Windows/FreeBSD cfg code; whether any test reads the source-receipt
keys in crates/config assets as paths; the re-export resolver does not
rewrite relative `config::NAME` uses (none found at this base); SCC/edge
counts were not re-run after a rewrite (the cargo-free G2 stays Lane A's).

Evidence: /Volumes/VIXinSSD/CW/artifacts/crate-split-w0-20261025/proof-w0.log
and guard-demos.log. Dirty work of other writers in the checkout was left
untouched (about 20 tui/core/models files, window W1).

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-25 14:31:42 -07:00
CodeWhale BotandClaude Sonnet 5.5 00be3d2e4f Regenerate the web agent-docs and feature facts that main left stale
node web/scripts/check-facts.mjs failed at d4d50f730d and at e6c400e294 with
"generated agent docs are stale". Two committed sources changed without their
generated copies:

- docs/RUNTIME_API.md (the #7074 merge) was newer than its sha256 and embedded
  content in web/data/agent-docs.generated.json.
- the cloud-computer-dispatch summary in docs/features.toml, shortened in
  46aa86634d for the 120 character limit, was newer than
  web/data/features.generated.json.

Regenerated with node web/scripts/derive-facts.mjs only (version=0.10.2 crates=30
providers=50 models=91 tools=79); no hand edit. The diff is those two files, 4
lines each. This clears the one baseline gate failure that the crate-split windows
would otherwise inherit, so a later docs gate can tell its own failure from this one.

Verified: node web/scripts/check-facts.mjs exits OK ("committed facts.generated.ts
matches workspace") after this change; it printed FAIL before it. No cargo, no source
file touched.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-25 13:48:09 -07:00
Hunter Bown f87fba6d8e Merge branch 'main' into fix/diff-pager-stable-origins 2026-10-26 04:47:26 +08:00
Hunter Bown e6c400e294 Merge pull request #7090 from Lstarsky0/fix/status-cost-unknown-not-zero 2026-10-26 04:28:56 +08:00
CodeWhale BotandClaude Sonnet 5.5 d4d50f730d P3: cut the config to sandbox and config to core edges
Two of the six remaining config-cluster edges to other tui modules, both one-way:

- Config::read_denylist and Config::effective_sandbox_denied_read_paths are deleted.
  ReadDenylist::from_config(&Config) in sandbox/read_guard.rs builds the same list
  from the same three fields, and its four callers use it directly (exec_agent,
  runtime_threads x2, ui/frame) plus lib.rs, which now asks the list for
  subtree_paths(). The deny-list doc comment moved with the constructor.
- apply_requirements no longer calls core::authority::sandbox_policy_for_turn just to
  name the Agent baseline. ApprovalMode::agent_baseline_sandbox_mode() in
  codewhale-execpolicy returns "danger-full-access" for Full Access and
  "workspace-write" otherwise, apply_requirements reads it, and
  sandbox_policy_for_turn derives its non-Plan default from the same method, so the
  baseline is still defined once. The network argument the old call passed never
  affected the returned config value. The comment that said "the engine's resolver" now
  says where the baseline comes from.

Measured after this change (re-run, not carried over): largest SCC still 79 modules
(this cut does not fragment it); config now has 7 outbound module edges instead of 9.
Remaining non-cluster edges from config/config_persistence: plugins::providers x5,
provider_readiness x2, hooks::HooksConfig x1, codex_model_cache::model_roster_for x1.
tui production lines with inline tests stripped: 640,313 (old metric incl. inline tests:
948,634+); runtime 10,109; the monolith is unchanged in size.

Verified locally (macOS; no hosted CI): CLI build exit 0; nextest -p codewhale-execpolicy
-p codewhale-tui --lib focused set: 992 run, 992 passed, 0 failed, 14546 skipped (read_guard
26, read_denylist 1, sandbox 125, requirements 6, sandbox_policy_for_turn 1, config::tests
423, authority 202, execpolicy 236); workspace clippy with CI's exact flags exits 0 on the
final tree (two needless borrows it found in lib.rs and ui/frame.rs were fixed after the
test run; those are token-only changes, covered by clippy's compile, tests not re-run for
them); fmt clean; dead-code 209, blocking 543/165, boundary 121.

Not verified: a live sandboxed shell under the OS wrapper (only the existing unit tests that
build the deny-list and the policy table), Linux/Windows, hosted CI.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-25 11:32:46 -07:00
7jrxt42BxFZo4iAnN4CX 8e98af2f6d Merge main into fix/instruction-budget-6526
`main` has now landed its own versions of every side fix this branch carried:
46aa86634 took the feature-summary and installer-assertion repairs, and
65ef1ea70 renamed `FleetRole::from_str` to `parse` and cleared the five
`crates/tui` lints plus the two `crates/cli` collapsible `if`s. Their versions
win on all five conflicting files; this branch is now the instruction-budget
feature only.
2026-10-25 21:29:27 +03:00
CodeWhale BotandClaude Sonnet 5.5 65ef1ea70b Fix the clippy lints the rename exposed so the hosted Lint job can pass
With the should_implement_trait error gone, clippy reached the dependents and
reported six more under CI's flags. One was mine: 7d84d0be6b removed the setter
that followed an early `return;` in apply_engine_error_to_app, which left that
`return;` as the last statement.

- tui/ui/apply.rs: drop the unneeded trailing `return;` (the code after it is
  now only comments, so behaviour is unchanged).
- tui/ui/frame.rs: pass `config` instead of `&config` to
  AutoReviewPolicy::from_config and UserInputLimits::from_config (needless borrow).
- tui/ui/handlers.rs: trim_start_matches/replace take a char array instead of a
  matches! closure.
- tui/work_surface/terminal.rs: sort_by_key with Reverse(finished_at).
- cli/cloud/computer.rs: collapse two nested `if`s into let-chains (introduced with
  the cloud key handoff work).
No behaviour changes.

Verified: workspace clippy with the exact CI flags exits 0 (first pass listed
exactly these; the final run has no diagnostics); rustfmt clean; 2082 tests
passed in the shared frozen batch including frame 98, handlers 5, work_surface
164 and cli computer 4 matching tests.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-25 11:15:26 -07:00
CodeWhale BotandClaude Sonnet 5.5 acd520cb99 Rename FleetRole::from_str to FleetRole::parse
Hosted CI run 40208377293 (PR #7087, base f98cb165a3) failed its Lint job on
clippy::should_implement_trait at crates/agent-contract/src/role.rs:148: an
inherent `from_str(&str) -> Option<Self>` shadows std::str::FromStr's name. It
returns Option, not Result, so implementing FromStr would change every caller.

Renamed to `parse` and migrated all 33 call sites in one pass, no alias:
agent-contract role.rs (definition, the serde Deserialize impl, three internal
calls and their doc links), tools/subagent/mod.rs (7), tools/subagent/tests.rs
(18, unchanged assertions), tools/workflow/mod.rs (1) and tui/views/mod.rs (1).
Behaviour is identical.

Of the three hosted failures from that run: the features.toml summary length and
the stale legacy_tui assertion in release-workflows.test.js were fixed in
46aa86634d; this commit and the next one fix the Lint job. Clippy stops at the
first failing crate, so the rename exposed further lints in dependents; they are
fixed in the following commit.

Verified: workspace clippy with the exact CI flags (--workspace --all-targets
--all-features --locked -D warnings -A uninlined_format_args -A too_many_arguments
-A unnecessary_map_or) exits 0 after both commits; nextest agent-contract 14 passed,
tools::subagent 914 and tools::workflow 248 matching tests passed (2082 passed in the
shared batch, 0 failed).

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-25 11:15:25 -07:00
CodeWhale BotandClaude Sonnet 5.5 8c6c3985ac Re-observe the served Ollama context when a turn starts and it is unknown
Refs #5848 (stays open).

A thread or TUI session created while the local model was unloaded froze the
explicitly unverified 8192-token floor, because the /api/ps context is observed
only at admission and startup. Turn one loaded the model; turn two still used
5120 usable input tokens and failed with "qwen3:4b's context window (~5120
tokens usable) is smaller than Codewhale's working instructions".

refresh_unverified_local_context(config, identity, model) reads the exact route's
catalog row off the Tokio worker and, only when that route has no fresh served
context, awaits the existing refresh_local_ollama_catalog bounded to 3 seconds.
It runs before the per-turn route is resolved in the two places that resolve one:
RuntimeThreadManager::start_turn_with_narrowing (Runtime API, mobile and desktop
threads; non-Auto models) and the TUI's spawned_dispatch_inner (non-Auto). A loaded
model already observed within 60 s costs nothing; an unloaded or expired one costs
one tags+ps round trip. No warmup, no num_ctx change, no polling, and the explicit
8192 floor still applies when the daemon reports nothing.

Verified locally (macOS; no hosted CI; Ollama 0.40.9, qwen3:4b):
- Reproduced before the change on a binary built without it: a Runtime API thread
  created while qwen3:4b was unloaded completed turn 1, then turns 2 and 3 each
  returned in 0.5 s with item.failed. Same thread flow on the changed binary: turns
  1, 2 and 3 all completed with no failed items; /api/ps then showed 32768.
- TUI, same two prompts on an unloaded model: before, the second turn showed the
  context-window error immediately; after, no error appeared and the turn was still
  running when the capture ended (so completion of turn two in the TUI was not
  observed, only the absence of the failure).
- nextest -p codewhale-agent-contract -p codewhale-cli -p codewhale-tui --lib
  (frozen batch shared with the next two commits): 2082 run, 2082 passed, 13735
  skipped. Matches: provider_lake 38, local_ollama 10, runtime_threads:: 294,
  dispatch 268, api_provider_default_and_model_list_follow_exact_local_catalog 1.

Not verified: auto-routed turns (the refresh is skipped for Auto), a stalled
endpoint (bounded to 3 s, not exercised), Windows/Linux, hosted CI.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-25 11:15:23 -07:00
7jrxt42BxFZo4iAnN4CX 73786f4356 docs: regenerate agent docs for the merged CONFIGURATION text
The second merge brought main's documentation edits; the generated agent-docs
JSON is regenerated from them so the published page matches the source.

Refs #6526
2026-10-25 21:06:39 +03:00
7jrxt42BxFZo4iAnN4CX 4c88f89464 Merge main into fix/instruction-budget-6526
`main` landed 46aa86634, which fixes the same two gate breaks this branch had
fixed independently: the 143-character `cloud-computer-dispatch` summary and
the stale `legacy_tui` assertion in `release-workflows.test.js`. Their version
wins for both files; this branch keeps its own commits for the clippy lints
(`FleetRole::from_str`, five `crates/tui` lints, two `crates/cli` collapsible
`if`s) that `main` has not taken.

`web/data/features.generated.json` is regenerated from the merged registry.
`node .github/scripts/release-workflows.test.js` exits 0 on the merged tree.
2026-10-25 21:04:23 +03:00
7jrxt42BxFZo4iAnN4CX 4338d7ba81 fix(context): answer the first review round on the budget feature
Thirteen findings from the automated review, all addressed.

Reads and bounds:

- The configured `instructions = [...]` loader read the file whole after a
  metadata check, so `/dev/zero` (length zero) or a file growing after the
  check could exhaust memory. It now reads at most `file_max_bytes + 1` bytes
  from the open handle, refuses anything that is not a regular file, and
  reports an over-ceiling skip.
- The first-turn notice emitted one line per loss, so a rules directory past
  the 50-file cap could produce a message larger than the budget it described.
  It is bounded to 12 lines / 2,000 bytes with an "and N more losses" line.
- Budgets now subtract the route's own output reservation
  (`max_output_tokens_for_model`) instead of the 2,048-token fallback.

Reporting:

- `/context` counted the raw instructions in the source row and the retained
  tokens again in the budget/loss rows. It now assembles once, counts the
  assembled block in the source row, and gives the budget/loss rows no token
  cost of their own.

Static-prefix guard:

- Rebuilds from the scope's retained usage minus the overage instead of
  subtracting from the configured ceiling, which did nothing for a scope that
  was already under its ceiling.
- The settled ceiling is kept on the engine, so later compositions cannot put
  the trimmed files back.
- The load-and-assemble pass runs on a blocking worker instead of the engine's
  async task.
- Its recomposed assembly's per-source losses are published with the
  scope-level sentence.
- A newly installed conversation (`Op::SyncSession`) re-arms the guard and
  rebuilds the notice from its own assembly.
- The notice is captured from the assembly that installs the header, not only
  from the construction-time one.

Startup checks (`exec` and the TUI) run on `spawn_blocking`, and `exec`
includes `--append-system-prompt` in the sources it checks, so the gate and the
engine budget the same list.

Locally on this tree:

    cargo test -p codewhale-tui --lib project_context    124 passed
    cargo test -p codewhale-tui --lib prompts::tests      99 passed
    cargo test -p codewhale-tui --lib debug_diagnostics   87 passed (fixtures re-recorded)
    cargo test -p codewhale-tui --lib conformance::       33 passed
    cargo test -p codewhale-tui --test feature_registry    2 passed

Refs #6526
2026-10-25 21:01:51 +03:00
CodeWhale BotandClaude Sonnet 5.5 46aa86634d fix(ci): shorten the cloud-dispatch feature summary and retire the stale installer assertion [skip ci]
Two of the hosted-red checks seen on main (run 40208377293, PR #7087 at base
f98cb165a3) are repo-side drift, not the PR's:

- docs/features.toml: the cloud-computer-dispatch summary was 143 characters;
  the feature_registry test allows 1-120 on one line. Reworded to fit.
- .github/scripts/release-workflows.test.js: e9c9de775f deliberately dropped
  the codewhale-tui compatibility-path guard from scripts/release/install.sh
  (the installer never writes that name), but this script still demanded a
  legacy_tui validation, so the "Version drift" job failed with "archive
  installs must validate the retired TUI path". The stale match and its
  now-false comment are removed, and the companion doesNotMatch (which tested
  a variable that no longer exists, so it could never fail) now asserts the
  installer never installs a file to $BIN_DIR/codewhale-tui.

Still red and NOT fixed here: Lint fails on clippy::should_implement_trait for
FleetRole::from_str at crates/agent-contract/src/role.rs:148 (about 15 call
sites in crates/tui). That edit recompiles codewhale-tui and belongs to the
Engine owner's next batch.

Verified (own runs on the current tree):
  node .github/scripts/release-workflows.test.js: exit 0 (was: AssertionError
    at the legacy_tui assertion on the same tree before the edit).
  python tomllib over docs/features.toml: 79 rows, 0 summaries outside 1-120
    characters or containing a newline.
Not verified: cargo test -p codewhale-tui --test feature_registry (a tui test
build; the only reported problem was the summary length), hosted CI.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-25 10:00:36 -07:00
7jrxt42BxFZo4iAnN4CX a64b7548f9 fix(context): surface configured-entry losses, restore loader warnings
The first hosted test run found four real defects behind the feature. Each was
in this branch, not in `main`.

Configured `instructions = [...]` skips and duplicates were collected into a
local vector and never merged into `ProjectContext::notices`, so a missing or
empty configured entry was dropped silently — the exact failure the notice
contract exists to remove. They are now appended with the trim notices.

A refused candidate only produced a structured notice; the loader's
`warnings` vector — what `/context report` and the Runtime API listing read —
stayed empty, so an empty or symlinked `AGENTS.md` reported nothing. Both
loaders now carry the refusal text into `warnings` again, alongside the notice.

The conformance goldens and the `debug::diagnostics` baselines were re-recorded
for the intended layout change: configured entries moved out of the volatile
world-state "permissions" fragment into the global instruction scope, and
`/context` gained an "Instruction budget" row (source total 2367 -> 2506).

`context_expectations_preserve_counts_for_each_host` now derives the host
difference only from fixtures that actually carry a host-dependent count: the
top-source list can stop naming the environment block, which makes a fixture
identical on both hosts.

`docs/features.toml`: `cloud-computer-dispatch` (on `main`) had a 143-character
summary and failed the registry's 120-character rule; shortened to 120 with the
same meaning.

Verified locally on this tree:

    cargo test -p codewhale-tui --lib project_context         124 passed
    cargo test -p codewhale-tui --lib prompts::tests           99 passed
    cargo test -p codewhale-tui --lib debug_diagnostics        87 passed
    CODEWHALE_CONFORMANCE_UPDATE=1 … conformance::             33 passed
    cargo test -p codewhale-tui --lib                        15302 passed, 15 failed

The 15 remaining local failures are sandbox, terminal-size and network cases
(`bwrap_*`, `enforced_readonly_*`, `theme_picker_*`, `read_file_home_path_*`,
dsh install, subagent timing). The hosted Ubuntu job, running the same suite,
reported exactly seven failures before this commit and none in those areas, so
these are artifacts of this read-only sandbox rather than of the change; the
hosted run is the authority for that.

Refs #6526
2026-10-25 19:32:58 +03:00
CodeWhale BotandClaude Sonnet 5.5 134dadc7bb Add 0.10.3 changelog entries for live turn usage and the fatal-error queue fix
Two landed changes had no [Unreleased] entry.

- #7074 (@gaord): the Runtime API's turn.usage receipts now carry each call's
  priced cost and billed prompt size and the mobile control page shows live
  usage with a partial cost estimate. The contributor's PR did not touch the
  changelog and the merge range has no other mention, so this entry carries the
  credit.
- #6561: a generic fatal engine error no longer turns on offline queueing by
  itself (7d84d0be6b). The environment-only authentication branch is unchanged.

crates/tui/CHANGELOG.md is regenerated by scripts/sync-changelog.sh.

Verified: scripts/release/check-versions.sh exits 0 after this change
(workspace, npm and lockfile 0.10.2; 26 linked issue references checked in
v0.10.1..HEAD) and sync-changelog reports the mirror up to date. Docs only; no
source file changed.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-25 09:30:00 -07:00
CodeWhale BotandClaude Sonnet 5.5 d8231c5fb8 Cite the superseded PRs and the reporter in the 0.10.3 changelog entries
scripts/release/check-versions.sh failed its feature-release-note audit on
three commits in v0.10.1..HEAD whose bodies reference #7082, #7084 and #4955
while the matching [Unreleased] entries cited only the tracking issues.

- The multi-line Live card entry (#6436) now also cites #7082, the closed
  draft PR it replaced.
- The Ctrl+C composer-clear entry (#6237) now also cites #7084, the closed
  draft PR it replaced.
- The --full-access entry (#6992) now also cites #4955 and thanks @eugenicum,
  who asked for a zero-sandbox mode.

#7082 and #7084 were opened from the maintainer's own account, so they carry
no outside contributor credit. crates/tui/CHANGELOG.md is regenerated by
scripts/sync-changelog.sh.

Verified: scripts/release/check-versions.sh now exits 0 ("Feature release-note
receipts OK: 26 linked issue reference(s) checked in v0.10.1..HEAD", workspace,
npm and lockfile all 0.10.2) and sync-changelog --check reports up to date.
It exited 1 with three errors before this change. No version bump, tag or
release helper was run; no source file changed.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-25 09:29:35 -07:00
7jrxt42BxFZo4iAnN4CX 0337afdefa fix(cli): collapse two nested ifs flagged by clippy
CI's clippy gate removed the `collapsible_if` allowance, and
`crates/cli/src/cloud/computer.rs` had two nested `if` statements that the
lint now reports. Both are let-chain collapses with no behaviour change.

Refs #6526
2026-10-25 16:54:21 +03:00
7jrxt42BxFZo4iAnN4CX b4f43688b9 chore(ci): unbreak the clippy and release-workflows gates on main
Neither failure came from the instruction-budget change; both reproduce on a
pristine `main` checkout at b4103aa7b. They are fixed here as a separate
commit so a maintainer can drop it without losing the feature.

Clippy (stable 1.99):

- `crates/agent-contract/src/role.rs` — the inherent `FleetRole::from_str`
  trips `clippy::should_implement_trait`. Renamed to `parse_label`, which is
  what it does (parse a role label/alias, `Option<Self>`), with the 27 call
  sites and the doc links updated. No behaviour change.
- `crates/tui/src/tui/ui/handlers.rs` — two `manual_pattern_char_comparison`
  closures became char arrays.
- `crates/tui/src/tui/ui/frame.rs` — two `needless_borrow` arguments.
- `crates/tui/src/tui/work_surface/terminal.rs` — `sort_by` on a reversed
  key became `sort_by_key(Reverse(..))`.

`cargo clippy -p codewhale-tui --all-targets -- -D warnings` (with the three
project allowances) now exits 0, and `-p codewhale-agent-contract` is clean.

Release workflows:

- `.github/scripts/release-workflows.test.js` pinned `legacy_tui` inside
  `scripts/release/install.sh`, but the installer rewrite (e9c9de775) stopped
  managing the retired TUI path entirely — `scripts/release/install.sh` no
  longer mentions `codewhale-tui` at all. The assertion now pins what is true
  and still matters: the archive installer must not mention the retired path,
  and every payload is validated against its destination before the first
  write. `node .github/scripts/release-workflows.test.js` exits 0.

Refs #6526
2026-10-25 16:16:30 +03:00
CodeWhale BotandClaude Sonnet 5.5 f98cb165a3 Read MCP secret variables through one resolver
Refs #5637 (slice 1 only; the injected-secret-store request stays open).

Four MCP sites each decided where a secret variable comes from: ${NAME}
placeholder expansion, HTTP env_headers, the bearer-token variable, and the
OAuth default-header builder. The first three already read a reviewed plugin's
captured pre-dotenv HostEnvironment; the OAuth header builder read the process
environment for every server, including reviewed plugin servers.

mcp::mcp_secret_var(reviewed_plugin, name) is now the only place that picks
the source: a reviewed plugin reads only its captured host_environment, any
other server keeps the exact std::env::var behaviour. The four sites call it,
and oauth::build_default_headers takes the server config (its four callers
already held one) so the OAuth path gets the same rule. Error context strings
are unchanged and the resolver logs and returns nothing new. Ordinary servers
still share process-environment semantics; no provider credential store or
keyring is consulted and none is added.

Verified locally (macOS; no hosted CI): the same frozen tui lib nextest batch
as the preceding #6561 commit, 426 run, 426 passed, 0 failed, 14876 skipped.
Of those, mcp:: matched 408 tests, expand_env_placeholders 2, mcp_http_auth_ 2,
reviewed_plugin_environment_uses_only_the_pre_dotenv_snapshot,
reviewed_plugin_oauth_is_disabled_without_network_or_token_mutation and
configured_mcp_credentials_refuse_remote_http_before_header_resolution 1 each.
dead-code 209, blocking 543/165 and the runtime boundary check still pass.

Not verified: no existing test passes an env-header value through the OAuth
header builder or the HTTP env_headers path, so those two sites rest on source
audit plus compilation. No running MCP server was observed. The injected store
for embedded hosts is deliberately not part of this slice.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-25 05:59:14 -07:00
CodeWhale BotandClaude Sonnet 5.5 7d84d0be6b Keep the session online after a generic fatal engine error
Refs #6561 (U02-01; the audit tracker stays open).

apply_engine_error_to_app set offline_mode for every non-recoverable error.
Three engine paths emit a fatal authentication error without starting or
completing a model turn (a finished background shell that cannot resolve its
route, a goal continuation that cannot resolve its route, and a child
completion that cannot resolve the resume route), so no TurnComplete follows
and neither lifecycle clearer runs. The flag then queued every later submit and
restored every explicitly selected queued message instead of sending it, so the
routing refusal blocked the next healthy attempt from even reaching route
validation.

Only the generic setter is removed. The environment-only authentication branch
still sets offline_mode, reopens provider setup and posts its targeted notice;
explicit offline onboarding, provider setup completion and failed-switch
rollback are untouched. The fatal error still finalizes the same cells, ends
loading, records failed status for an active turn and leaves the categorized
error in history. A still-broken route can fail again and the existing recovery
path keeps the unsent message; there is no automatic retry or provider rotation.

Two existing expectations flip and one existing test is renamed
(non_recoverable_engine_error_enters_offline_mode to ..._stays_online); no new
test was added. The comments that described the old behaviour on the offline
field, the error projection, the renamed test and fatal_auth are corrected.

Verified locally (macOS; no hosted CI): frozen tui lib nextest batch, 426 run,
426 passed, 0 failed, 14876 skipped, shared with the MCP change. Every named
existing test matched: non_recoverable_engine_error_stays_online,
keyless_engine_error_stays_visible_after_a_config_ack,
env_only_auth_failure_reopens_provider_onboarding,
recoverable_engine_error_does_not_enter_offline_mode,
offline_explore_selects_no_provider_and_writes_no_credential, the credential_
rejected_turn_ pair, credential_rejection_after_output_keeps_the_composer_empty,
recoverable_stream_error_keeps_active_turn_for_pending_approval,
provider_switch_auth_error_restores_previous_provider_and_model,
real_engine_client_preflight_failure_leaves_dispatch_state_atomic,
immediate_submit_closed_mailbox_restores_composer_and_skill and
submit_disposition_scenario (1 each), plus 8 engine_error tests.

Not verified: the trap itself was not reproduced in the running product. A TUI
run against a local daemon that answers every chat request with 401 shows the
in-turn authentication error and the existing resend note, which does not take
the no-TurnComplete path, so it cannot tell the old and new behaviour apart. The
three fatal paths above are known from source, not from a live run.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-25 05:58:15 -07:00
CodeWhale BotandClaude Sonnet 5.5 fa659b9858 Format the three files whose paths the credentials move rewrote
0c2ab7d577 moved credentials into codewhale-runtime and rewrote crate::credentials
to codewhale_runtime::credentials in config.rs, config/credential_resolve.rs and
provider_readiness.rs. The longer paths made rustfmt want different line breaks
and I pushed that commit without re-running the format check after the move.

This is rustfmt output only: `scripts/dev-cargo.sh fmt --all -- --check` reported
12 differences in exactly these three files before and none after. No token was
added, removed or reordered.

Verified: fmt check clean. The tests for that move (774 passed) ran on the
unformatted text; the formatted tree is rebuilt and re-run in the next batch.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-25 05:58:12 -07:00
Hunter Bown 0c9af1a36e Merge pull request #7074: feat(runtime): show a turn's cost and context while it runs
Contributor @gaord's live per-call usage, with maintainer review fixes on the PR branch (e454685f22): compaction no longer blanks the figure, settlement keeps priced-call estimates, the next turn resets instead of accumulating, replayed receipts count once, and money is labeled as a partial estimate. Verified locally: 512 tui runtime_threads/mobile/usage tests passed, and the corrected page was exercised in a browser against a local Engine turn. No hosted CI ran before this merge.
2026-10-25 20:49:33 +08:00
CodeWhale BotandClaude Sonnet 5.5 e454685f22 fix(runtime): keep mobile usage estimates across compaction and turns
Review fixes on top of the contributor's live-usage change, kept on the
contributor's branch so the PR lands as itself.

- Automatic compaction items no longer blank the conversation's figure, and a
  compaction pass is never presented as the conversation's spend. A compaction
  that owns a separate Runtime turn resets the figure like any new turn.
- turn.completed replaces the token counts with the authoritative
  payload.turn.usage but keeps the priced-call estimates already observed, so
  the price no longer disappears at settlement.
- The figure carries its turn id and replay high-water sequence per thread. A
  new turn resets it, so the next turn no longer accumulates onto the last
  one, and a replayed start, receipt or completion cannot reset or re-add it.
- Money reads "~$0.10 partial" with a tooltip that names what it excludes
  (unpriced, child and compaction work); an unpriced call accrues tokens but
  never invents free money, and amounts require the matching *_priced flag.
- The four tests that matched the page's source strings and the new code
  comments are removed to follow the repository's no-new-tests, no-new-comments
  rule for this change; the existing recovery-test receipt assertions stay.
  docs/RUNTIME_API.md and its zh_hans mirror describe the final behaviour.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-25 05:42:01 -07:00
CodeWhale BotandClaude Sonnet 5.5 0c2ab7d577 Move the credentials module from tui into codewhale-runtime
First placement-independent step of the S7=(c) decision (config, credentials
and OAuth move toward codewhale-runtime). credentials has no dependency on any
other tui module (0 outbound edges) and is the only module the config cluster
was blocked on at module level (23 references), so it moves first and alone.

- scripts/split/move-modules.py credentials rewrote the seven callers
  (config, config/credential_resolve, config/tests, provider_readiness,
  runtime_api/secrets, tui/provider_picker, client test cases) to
  codewhale_runtime::credentials. No shim or re-export stays in tui.
- scripts/split/widen.py widened exactly the 38 items the compiler named
  (pub(crate) to pub) and deleted three expect(dead_code) attributes the export
  fulfils. Token and key material is unchanged: Credential keeps its
  hand-written Debug that prints only the variant tag, and secrets stay behind
  the existing store trait.
- MapAuthContext is gated on any(test, feature = "test-support") instead of
  cfg(test) so tui tests can still use it across the crate boundary; the tui
  dev-dependency on runtime already enables test-support.
- codewhale-runtime gains a codewhale-secrets dependency (Cargo.toml and
  Cargo.lock). codewhale-secrets is a leaf; the command-crate-boundaries check
  still passes with runtime free of UI packages.
- One module comment that named crate::config::credential_resolve now names
  codewhale_tui::config::credential_resolve. THIRD_PARTY_NOTICES and the old
  decomposition roadmap carry the path sweep; the roadmap still plans a later
  move into codewhale-secrets, which S7=(c) supersedes.
- dead-code budget lowered 212 to 209.

Size: codewhale-runtime production lines 14,699 to 15,416 (closure_report),
tui 965,801 to 965,081. This is NOT progress on the 79-module SCC (credentials
was never in it) and the crate split is not done. Honest headroom note for the
founder: moving the rest of the S7=(c) cluster (config 14,105, settings 5,935,
config_persistence 2,180, oauth 9,896, codex_model_cache 608 = 32,724
production lines) would take runtime to about 48,140, within about 1,900 of
the 50k ceiling and far from the 10k typical target, with no room for the 13
other wave-0 modules (5,988 lines).

Verified (macOS, local only; no hosted CI, Linux/FreeBSD/Windows compile):
- scripts/dev-cargo.sh build -p codewhale-cli --bin codewhale --locked: exit 0
  (known ld __eh_frame warning only).
- nextest -p codewhale-runtime -p codewhale-tui --lib, filters runtime
  credentials:: and tui credential / secrets / provider_readiness /
  provider_picker / config::tests: 774 run, 774 passed, 0 failed, 14792
  skipped (10 runtime credentials tests, 755 tui tests).
- module_graph --check 121 closure modules PASS, blocking calls 543/165,
  command-crate-boundaries PASS, dead-code 209 (budget updated).

Not verified: a signing-in or key-saving run through the product. The
exercised paths are the existing unit and API tests only.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-25 05:41:03 -07:00
7jrxt42BxFZo4iAnN4CX f96c47d3f3 Merge main into fix/instruction-budget-6526
Resolves the conflict with 10ce9cde ("part 1"). `crates/tui/src/project_context.rs`
keeps this branch's loader: it supersedes the truncating read with per-scope
token budgets, segment-level trimming, and the notice contract. The 100 KiB
truncation that 10ce9cde introduced is *not* in this resolution, which is the
open question raised in the pull request — if truncation is the intended
content policy, the follow-up keeps 8 MiB as the memory guard that skips and
adds the 100 KiB head-truncation on top.

CHANGELOG keeps main's entries and re-adds this branch's bullet;
`crates/tui/CHANGELOG.md` and the generated web data are regenerated from their
owners rather than merged by hand.
2026-10-25 15:36:09 +03:00
CodeWhale BotandClaude Sonnet 5.5 b4103aa7bb Elect the local Ollama default from completion-capable native tags
Refs #5848 (kept open: see "Not verified").

The alphabetical-first election in the runtime resolver is deleted. The
provider lake stays the single catalog and default authority.

- /api/tags is read natively, carrying completion, tools and thinking
  capabilities. Tags that omit capabilities reuse the bounded /api/show pass
  with the listing client's authorization and redirect policy. Only rows with
  positive completion capability become chat rows; embedding rows never reach
  the picker, and choose_chat_tag ranks the filtered roster and marks
  default_for_provider. valid_catalog_model_id runs before election, known
  secret-bearing tags are rejected before any authenticated profile call, and
  the whole profile-future error is redacted before shared logging.
- The first-run probe builds the same delta and returns the elected default,
  so there is no second election. A daemon reporting no completion capability
  gets no automatic default; an explicit real model still passes through.
- /provider ollama refreshes before resolving. The CLI exec route and the
  Runtime API (tasks, threads, stream), including an explicit
  model_provider/model_provider_id and blank, auto or placeholder models, await
  a targeted refresh. refresh_local_ollama_catalog serializes through one
  async mutex whose guard moves into the blocking worker, so a cancelled caller
  cannot release it before publication; a queued call reuses only an exact
  successful cache entry fetched since it queued, with an elected default and
  an authoritative same-endpoint partition (this includes A to B to A). Startup
  gets a background refresh; no config read waits on the network.
- Served context is the positive loaded /api/ps context_length, never the
  trained /api/show maximum. The observation is current for 60 seconds, then
  only that context fact clears; the 24h roster TTL is unchanged. The Ollama
  runtime resolver cache is bypassed so an expired fact cannot survive in it.
  Unloaded or unknown context stays the explicitly unverified 8192 floor;
  configured context overrides keep precedence. Frozen active routes remain
  snapshots: no mid-turn revocation, warmup, num_ctx change or polling.

The 60 s window is a proposal, not release qualification.

Verified locally (macOS; Ollama 0.40.9 on localhost, qwen3:4b
completion/tools/thinking and nomic-embed-text embedding; no hosted CI, no
paid route, no Linux/FreeBSD/Windows compile; every check below ran once on
the combined tree of this commit and the preceding browser/OAuth commit, on
base 62bbcebf2b, before the two were split for landing):
- nextest -p codewhale-runtime -p codewhale-tui --lib, focused filters
  (local_ollama, route_runtime, model_inventory, provider_catalog_live,
  unresolved_ollama_client_waits_for_catalog_but_probe_can_bootstrap,
  api_provider_default_and_model_list_follow_exact_local_catalog,
  api_config_reports_local_default_availability_without_blocking_config_reads,
  oauth::, plugins::providers::, browser::): 251 run, 251 passed, 0 failed,
  15315 skipped. Every filter matched nonzero tests (10, 37, 28, 25, 1, 1, 1).
  Six existing fixtures now carry default_for_provider because the implicit
  election they leaned on no longer exists.
- Built binary, isolated HOME and CODEWHALE_HOME, env -i:
  * real daemon, fresh home, exec --provider ollama with no model: wire model
    qwen3:4b (not the embedding tag), turn completed with output "OK".
  * real daemon, Runtime API ready in 0.23 s, immediate no-model, blank,
    explicit model_provider, and model_provider plus id admissions all
    resolved qwen3:4b with the model unloaded; /v1/providers/ollama/models
    listed only qwen3:4b at the 8192 fallback. With qwen3:4b loaded the same
    list showed 32768 (from /api/ps) against a trained 262144. A no-model task
    and a no-model stream both ran to completion on qwen3:4b.
  * first-run TUI (pty, no config): "Provider switched: deepseek -> ollama,
    Model: deepseek-flash -> qwen3:4b"; configured deepseek then /provider
    ollama switched to qwen3:4b. Embedding tag never adopted.
  * mock daemon with 1 s per request (race forced): first admission waited
    2.0 s and succeeded; two concurrent fresh admissions both succeeded with
    exactly one /api/tags and one /api/ps fetch; an explicit Ollama target
    while deepseek was active did the same. Embedding-only roster: 400
    "no fresh model catalog", empty model list, model_available false. Legacy
    tags without capabilities used /api/show and still elected the chat tag.
    Context 16384 from /api/ps held for the 60 s window, cleared to 8192 at
    about 65 s with the default still available, and returned on the next
    admission. Endpoint A to B to A within 60 s: B showed 8192, A 16384.
- dead-code 212 (at budget), blocking calls 543 sites / 165 files,
  module_graph --check 121 closure modules, reqwest builder, provider registry
  and changelog sync checks pass.

Not verified: a baseline binary was not built, so the admission race is shown
passing on this tree rather than failing on the previous one. An unreachable or
stalled local endpoint during /provider ollama (the switch awaits a bounded
refresh) was not exercised. Configured context override precedence was not run
in the product. The embedding-only error text reads "no fresh model catalog",
which is accurate but unhelpful; improving it is left open. Model execution was
shown only for exec, API task and API stream on qwen3:4b; this is not release
qualification.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-25 04:30:26 -07:00
CodeWhale BotandClaude Sonnet 5.5 cd6203e1d2 Move browser opening to runtime and plugin OAuth login to plugin providers
Two prerequisites for the S7 decision (config, credentials and OAuth move
toward codewhale-runtime, not codewhale-config). They remove the two edges
that would otherwise force oauth.rs to keep a dependency on tui-only modules.

- utils::open_url and browser_open_command move to codewhale_runtime::browser
  with their two existing tests. All four production callers migrate
  (oauth orcarouter login, runtime_api web bootstrap, overlays open_external_url,
  commands/contract browser open). No shim and no re-export remain in utils.
- plugin_oauth_login moves from oauth.rs to plugins/providers.rs, next to the
  verify_provider_binding it calls. Both callers migrate (lib.rs TuiAuthCommand::
  PluginLogin, event_loop run_plugin_oauth_from_tui). The body is unchanged:
  PolicyScope propagation, the three binding checks, PKCE state and issuer
  validation, and the secure-store write after the last binding check keep their
  order. Six oauth.rs items widen to pub(crate) (wait_for_callback_validated,
  PluginOAuthConfig::callback_params and authorize_url, PluginOAuthTokens,
  plugin_oauth_slot, plugin_token_response); token fields stay private.
- docs/design/TUI_DECONSTRUCTION.md records the founder's 2026-10-25 S7=(c)
  placement, the 10k-typical/50k-ceiling crate target, and the two-slot build
  rule. Nothing here is a claim that the crate split is done: the largest SCC
  is still 79 modules before and after.

Verified (macOS, local only; no hosted CI, Linux/FreeBSD or Windows compile):
- scripts/dev-cargo.sh build -p codewhale-cli --bin codewhale --locked: exit 0,
  only the known ld __eh_frame warning. A first pass failed on one unused
  anyhow::Result import in utils.rs, removed in this commit.
- nextest -p codewhale-runtime -p codewhale-tui --lib, focused filter set:
  251 run, 251 passed, 0 failed, 15315 skipped. Of those, browser:: 2 (the moved
  tests), oauth:: 143, plugins::providers:: 3.
- module_graph.py --check: 121 closure modules, PASS. check-dead-code-budget 212
  (exactly at budget). check-blocking-calls-budget 543 sites / 165 files.

Not verified: the interactive browser launch and the plugin OAuth round trip
against a real issuer were not exercised; only the existing unit tests ran.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-25 04:29:51 -07:00
7jrxt42BxFZo4iAnN4CX 2f5ab3e1f3 docs: record the route-window fix in the design notes 2026-10-25 14:19:02 +03:00
7jrxt42BxFZo4iAnN4CX 8e7e614cae fix(context): budget the startup notice for the route window, not a default
Three review findings from an independent pass over the first two commits.

The startup instruction notice resolved its budgets for an unknown window
(128k). On a large-window route that can report a trim that will never
happen, and under `policy = "refuse"` it can stop a session whose
instructions would have fitted. Both call sites now pass the route's real
window: `exec` derives it from the validated route, the TUI from
`App::effective_model_for_budget`. `startup_instruction_notice` keeps the
`Option` so a caller without a route still works.

A trimmed notice said "kept N of M bytes and ~K of ~T tokens"; the design
requires the numbers to be labelled as estimates. It now says "estimated
tokens", matching the marker embedded in the prompt.

`unimported_foreign_warnings` stopped naming `CLAUDE.md` as unloaded once
the default fallback started reading it — but it skipped the whole Claude
format, so a workspace with only `.claude/instructions.md` (which is not a
fallback candidate) lost its opt-in hint entirely. The excuse is now scoped
to a directory that actually has a fallback candidate.

Refs #6526
2026-10-25 14:17:14 +03:00
7jrxt42BxFZo4iAnN4CX 32fb2d1205 docs: document the instruction budget and register the feature
`docs/CONFIGURATION.md` gains an "Instruction budget" section: the two
scopes, the `[instruction_budget]` keys with the effective-budget table for
32k/128k/1M windows, what is trimmed versus skipped, `policy`, the
static-prefix guard, the `claude_fallback` selection rule, and the notice
channels. The `instructions = [...]` rules are corrected to match the new
behaviour (entries are global-scope authority, oversized entries are
skipped with a notice instead of being truncated).

`config.example.toml` replaces the fixed 48 KiB comment with the documented
table. `CHANGELOG.md` and `crates/tui/CHANGELOG.md` (via
scripts/sync-changelog.sh) carry the entry. `docs/zh_hans/CONFIGURATION.md`
gets an explicit deferral note with the sync date plus the one selection
change it must not get wrong. `docs/features.toml` registers
`instruction-budget`, and the generated web data is regenerated with
`web/scripts/derive-facts.mjs`.

Refs #6526
2026-10-25 13:56:47 +03:00
7jrxt42BxFZo4iAnN4CX 7b9d55d651 feat(context): per-scope instruction budgets with visible truncation
Standing instructions were one 48 KiB byte pool shared by the workspace
chain, the global fallback, the configured `instructions` array and the
rules block, trimmed from an opaque joined string and reported nowhere an
operator would see it.

They are now two independent scopes:

- `global` — the global AGENTS.md/instructions.md chain, `~/.claude/CLAUDE.md`
  as a last resort, and the configured `instructions = [...]` entries;
- `project` — the repository-root → workspace chain, a co-read `CLAUDE.md`,
  and the rules block.

Each scope is capped at `min(absolute_tokens, percent × usable_window)` in
estimated tokens, where the usable window is the route window minus the
response ceiling and a 10% safety reserve. Defaults are 100k tokens / 25%
per scope, so a huge project can no longer shrink the user's global
preferences and vice versa. Rules are charged to the project scope after
the instruction chain.

Selection follows OpenCode's model with codewhale's sources preserved:
AGENTS.md wins; a directory without one falls back to CLAUDE.md
(`claude_fallback = false` opts out); an explicit `claude` import co-reads.
A candidate that is skipped for size or emptiness counts as absent and the
next one is tried; a file already loaded from a wider scope is reported as
a duplicate and does not trigger the fallback.

Skips are never trims. The per-file ceiling (`file_max_bytes`, 8 MiB), an
empty file, the 50-file rules cap and a symlinked duplicate are all
reported with the limit that produced them, so a single bad file cannot
make a workspace unopenable.

Every loss reaches the user (a system cell at TUI start, stderr in `exec`),
the model (a runtime-provenance message on the first turn, which is part of
the logged transcript) and `/context report` per source, with path, scope,
source, action, bytes, tokens and limit kind. `policy = "refuse"` stops the
session on a trim instead of proceeding; skips stay skips.

A static-prefix guard keeps the stable prefix — base prompt, instructions,
skills index and tool schemas — under `static_prefix_guard_percent` (80%)
of the usable window, trims the largest scope by the overage once, and
re-pins the header under a declared reason.

Config is a new `[instruction_budget]` table: `deny_unknown_fields`, user
config only (the project overlay ignores it and warns, like `instructions`),
validated at load so a broken table fails the launch instead of applying a
nonsensical budget.

The project-context cache still keys on files: raw segments are cached and
trimmed per request, so a context-window change never invalidates the cache
and a content change never reuses trimmed text. `ProjectContext` carries
`instruction_segments` and `notices`, and the joined string is rebuilt
after trimming.

Verification: `cargo check --profile test -p codewhale-tui --lib` exits 0
with `-D warnings`, which typechecks the crate and every new test. Test
execution is still pending in this environment — the shared cargo registry
and target directory are read-only here, so the build runs against an
isolated `/tmp` target where this crate needs roughly an hour of codegen.

Refs #6526
2026-10-25 13:56:15 +03:00
CodeWhale BotandClaude Opus 5.5 62bbcebf2b docs: correct FreeBSD source-build claims (#1097)
FreeBSD stays a source-only target, with no prebuilt asset and no
release-matrix row (founder decision 2026-10-25, item 14). This change
fixes the claims the issue sweep found to be false or stale:

- packaging/freebsd/README.md said x86_64-unknown-freebsd "is validated in
  CI ... on the release/0.9.5 branch". No job does this:
  `git grep -i freebsd -- .github scripts` matches only a YAML comment. The
  README now says no CI job checks the target and gives the cargo check
  command to run on a FreeBSD host.
- The README pointed to crates/tui/build.rs for the rquickjs bindgen fix.
  1582ba9653/5eb0385e8f touch only crates/workflow-js/Cargo.toml, which
  holds the target_os = "freebsd" bindgen dependency. It now names that
  file.
- The README now states the Rust minimum, 1.89 (Cargo.toml rust-version),
  and its verification block no longer says ">= 1.88". The matrix list now
  says Linux musl arm64, which is the target the matrix builds, not gnu.
- The release-artifacts.yml build-job comment no longer claims a FreeBSD
  cargo check. It now says FreeBSD has no prebuilt asset, no npm binary and
  no CI check.
- "OpenBSD" is dropped from the support-table row in docs/INSTALL.md and
  docs/zh_hans/INSTALL.md. workflow-js has no rquickjs bindgen entry for
  openbsd, and no OpenBSD build has been verified.

Ignore the vendor Ratatui target directory produced by its separate-workspace
test command; generated output is preserved in the SSD cache, not committed.

Docs, ignore rules and a YAML comment only; no product behaviour changes, so no CHANGELOG
bullet. Verified by read-only greps against the worktree. No FreeBSD compile was
attempted.

Verified as one frozen batch at 7db526a725 plus consensus patch
sha256 d6418055436eaada55f4cbedc954a0eeeb51d9d922716a3588006cf7932fab45:
- scripts/with-hermetic-test-home.sh scripts/dev-cargo.sh nextest run
  -p codewhale-tui -p codewhale-localization --lib --locked:
  15343 run, 15341 passed, 2 failed, 25 skipped; build 3m41s.
  The two fixed-width rendered-fixture failures came from the long SSD
  TMPDIR wrapping paths mid-needle. Same source/binary, shorter SSD TMPDIR,
  only those two tests rerun: 2 passed, 15312 skipped. Distinct passing
  coverage across both runs: 15289 TUI + 54 localization tests.
- scripts/with-hermetic-test-home.sh scripts/dev-cargo.sh test
  --manifest-path vendor/codewhale-ratatui/Cargo.toml --locked --all-targets:
  145 passed, 0 failed (128 library and 17 example tests).
- fmt and diff checks passed; changelog sync passed; runtime boundary PASS
  (120 closure modules); dead-code budget 212; blocking-call budget 543
  across 165 files; locale parity 2583 keys in all 15 packs.
- The vocabulary guard initially failed on an already-published advertised
  legacy alias. Both refuters confirmed the docs-only correction; the
  unchanged guard then passed. Web prebuild and check-facts passed.
Before peer reconciliation, only .gitignore, docs/MODES.md and generated web
documentation changed after native tests; frozen compiler inputs were identical.
Published peer commits aa45bf84ef/485382f81d then advanced main. Their exact
diff was integrated, preserving their ancestry and regenerating web docs.
Focused reconciliation nextest (TUI ops/runtime_info/spawn and CLI cloud
callers): 117 passed, 0 failed, 15698 skipped; source remained frozen
(104 TUI, 13 CLI). Command/filter and source SHA256
are retained in coordination/land-0103-20261025/reconciliation-test-run.json.
Final integrated tree f297b67c73 is checked against the regenerated commits.
macOS ld emitted its __eh_frame size warning. No hosted CI, FreeBSD/Linux
compile, paid provider call, install, deploy, or release is claimed.

This does not resolve #1097: the npm prebuilt and pkg port asks remain open.

Refs #1097

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 02:15:50 -07:00
CodeWhale BotandClaude Opus 5.5 9f4993f820 Let an explicit --provider move a resumed session (#6996)
A saved session whose provider table was deleted could not be reopened.
`codewhale resume <id>` runs prepare_mounted_session before the TUI
starts. That function resolved the saved provider identity and refused,
with no recovery hint. exec --resume already had an explicit way out: a
launch --provider keeps the named route (resolve_exec_resume_route).
The TUI resume had none.

The founder chose option A (FOUNDER-DECISIONS-20261025 item 8): an
explicit launch provider moves the session, the same way exec does. When
the launch names a provider (explicit_launch_provider_override, the
signal exec_resume_route_overrides reads), prepare_mounted_session now
resolves the live active_provider_identity() instead of the saved one.
The model is the explicit --model if given, otherwise that route's
default. The explicit launch route is credential-preflighted before any canonical
history operation is reserved. The new route rides the existing
CanonicalHistoryOptions.overrides (model_provider, model_provider_id,
model). decode_history_overrides already validates those fields, and
the history create path now replaces provider kind and exact ID together,
clearing an inherited named ID for a root custom route. The update path
preflights the named provider's credentials. The committed session
document then records the new route, so the post-commit admission and
the event loop's apply_loaded_session_with_goal admit it unchanged.

There is still no silent fallback. Without --provider the resume
refuses as before. The mount path refusal, shared by `resume` and
`fork`, now carries the recovery hint as well. The shared
export_recovery_hint names `codewhale --provider <name> resume <id>`
before `codewhale sessions export <id>`, so the TUI /resume, exec
--resume, Runtime API and mount path refusals all show the way out.
Fork routing is unchanged.

Verified as one frozen batch at 7db526a725 plus consensus patch
sha256 d6418055436eaada55f4cbedc954a0eeeb51d9d922716a3588006cf7932fab45:
- scripts/with-hermetic-test-home.sh scripts/dev-cargo.sh nextest run
  -p codewhale-tui -p codewhale-localization --lib --locked:
  15343 run, 15341 passed, 2 failed, 25 skipped; build 3m41s.
  The two fixed-width rendered-fixture failures came from the long SSD
  TMPDIR wrapping paths mid-needle. Same source/binary, shorter SSD TMPDIR,
  only those two tests rerun: 2 passed, 15312 skipped. Distinct passing
  coverage across both runs: 15289 TUI + 54 localization tests.
- scripts/with-hermetic-test-home.sh scripts/dev-cargo.sh test
  --manifest-path vendor/codewhale-ratatui/Cargo.toml --locked --all-targets:
  145 passed, 0 failed (128 library and 17 example tests).
- fmt and diff checks passed; changelog sync passed; runtime boundary PASS
  (120 closure modules); dead-code budget 212; blocking-call budget 543
  across 165 files; locale parity 2583 keys in all 15 packs.
- The vocabulary guard initially failed on an already-published advertised
  legacy alias. Both refuters confirmed the docs-only correction; the
  unchanged guard then passed. Web prebuild and check-facts passed.
Before peer reconciliation, only .gitignore, docs/MODES.md and generated web
documentation changed after native tests; frozen compiler inputs were identical.
Published peer commits aa45bf84ef/485382f81d then advanced main. Their exact
diff was integrated, preserving their ancestry and regenerating web docs.
Focused reconciliation nextest (TUI ops/runtime_info/spawn and CLI cloud
callers): 117 passed, 0 failed, 15698 skipped; source remained frozen
(104 TUI, 13 CLI). Command/filter and source SHA256
are retained in coordination/land-0103-20261025/reconciliation-test-run.json.
Final integrated tree f297b67c73 is checked against the regenerated commits.
macOS ld emitted its __eh_frame size warning. No hosted CI, FreeBSD/Linux
compile, paid provider call, install, deploy, or release is claimed.

Fixes #6996

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 02:15:48 -07:00
CodeWhale BotandClaude Opus 5.5 f6b7af39c6 fix(tui): treat a child silent for 10m as suspect again (#6184)
CW-7500 (574dc76e26) removed SUBAGENT_SUSPECT_AFTER, which treated a
Running child past its wall budget as suspect, together with the default
child wall time. Since then, every current-session Running row in
subagent_cache counts in live_running_agent_count. Every recovery
branch of reconcile_turn_liveness_with requires !has_running_agents,
and the engine heartbeat enters TurnPhase::Tools with no bound, leaving
that bound to the UI tool-hang watchdog. So a tool batch waiting on a
wedged child, or on a ghost Running row whose AgentComplete was lost,
could again sit silent forever while follow-up messages queued behind
it.

Founder decision (FOUNDER-DECISIONS-20261025 item 7): a running child
that has reported no progress for 10 minutes counts as suspect again.
"No progress" is measured from AgentProgressMeta.last_progress_at,
which each mailbox envelope and Engine AgentProgress event refreshes. If no envelope has
arrived yet, it is measured from the row's started_at. It is not
measured as elapsed wall time. The bound reuses
max(TOOL_HANG_WATCHDOG_TIMEOUT, child heartbeat_timeout_ms), preserving
legitimate child operations whose configured heartbeat bound exceeds 600s. A row with neither clock is never
suspect. Prior-session rows stay suspect as before. Child wall time
stays unbounded: a child that keeps reporting progress is live however
long it runs. This sits above the engine's own per-child heartbeat
auto-cancel, so in practice it catches rows the manager cannot settle:
ghosts, and children whose executor is wedged.

suspect_running_agents and live_running_agent_count take `now` again,
as they did before CW-7500. The stall-record detail no longer claims
that every suspect was retained from a prior session.

The existing test
stall_prior_session_subagent_is_suspect_but_long_running_child_is_live
now gives the 2h-old current child fresh progress, so it stays live.
The new silent-child scenario was removed under the no-new-tests rule;
only the existing scenario setup and signature updates remain.

Verified as one frozen batch at 7db526a725 plus consensus patch
sha256 d6418055436eaada55f4cbedc954a0eeeb51d9d922716a3588006cf7932fab45:
- scripts/with-hermetic-test-home.sh scripts/dev-cargo.sh nextest run
  -p codewhale-tui -p codewhale-localization --lib --locked:
  15343 run, 15341 passed, 2 failed, 25 skipped; build 3m41s.
  The two fixed-width rendered-fixture failures came from the long SSD
  TMPDIR wrapping paths mid-needle. Same source/binary, shorter SSD TMPDIR,
  only those two tests rerun: 2 passed, 15312 skipped. Distinct passing
  coverage across both runs: 15289 TUI + 54 localization tests.
- scripts/with-hermetic-test-home.sh scripts/dev-cargo.sh test
  --manifest-path vendor/codewhale-ratatui/Cargo.toml --locked --all-targets:
  145 passed, 0 failed (128 library and 17 example tests).
- fmt and diff checks passed; changelog sync passed; runtime boundary PASS
  (120 closure modules); dead-code budget 212; blocking-call budget 543
  across 165 files; locale parity 2583 keys in all 15 packs.
- The vocabulary guard initially failed on an already-published advertised
  legacy alias. Both refuters confirmed the docs-only correction; the
  unchanged guard then passed. Web prebuild and check-facts passed.
Before peer reconciliation, only .gitignore, docs/MODES.md and generated web
documentation changed after native tests; frozen compiler inputs were identical.
Published peer commits aa45bf84ef/485382f81d then advanced main. Their exact
diff was integrated, preserving their ancestry and regenerating web docs.
Focused reconciliation nextest (TUI ops/runtime_info/spawn and CLI cloud
callers): 117 passed, 0 failed, 15698 skipped; source remained frozen
(104 TUI, 13 CLI). Command/filter and source SHA256
are retained in coordination/land-0103-20261025/reconciliation-test-run.json.
Final integrated tree f297b67c73 is checked against the regenerated commits.
macOS ld emitted its __eh_frame size warning. No hosted CI, FreeBSD/Linux
compile, paid provider call, install, deploy, or release is claimed.

Refs #6184

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 02:15:48 -07:00
CodeWhale BotandClaude Opus 5.5 1cefbca1fc Composer: grow past the density cap for long drafts
A long draft or pasted text now grows the composer past its per-density
row cap (Compact 7 / Comfortable 9 / Spacious 12), so it can be read and
edited before sending. The frame sets the bound: composer room is
min(existing frame room, max(density rows, body_height / 2)), so short
terminals never get a smaller composer than before and the transcript
keeps MIN_CHAT_HEIGHT.

native_composer_height (vendored kit) clamps to `available` only when no
menu is open. With a slash, mention or history-search menu open it keeps
the previous clamp, available.min(density.max_rows()). That keeps the
worst-case menu reservation and stable layout from the v0.8.8 jitter fix
exactly as before. ComposerWidget::desired_height passes the frame's
bound unchanged, and frame.rs reuses widgets::composer_max_height (now
pub(crate)) as the density floor of that ceiling.

This builds on PR #7078 and applies its review fixes:
- menus no longer inflate the composer. The PR counted menu rows when
  deciding to grow, which broke the mounted/legacy parity test (left 9,
  right 7)
- the frame ceiling is max(density cap, body/2), not min(body/2), so
  Spacious/Comfortable keep their height on short terminals
- the frozen legacy painter (composer_legacy.rs) passes the same bound
  as the mounted widget, so the parity test still compares buffer,
  caret, submit rect and height for the intended growth case
- the PR's new `bounded` assertion and added test comments are dropped.
  Existing expectations change only where growth is intended
  (composer_chrome content_growth test: 9/12 -> 21; widgets
  composer_auto_fits test: 40-line draft -> AVAILABLE rows). The
  composer_chrome content_growth test takes the PR's name because the
  old name became false

apply_external_edit now clears pending_paste_reference and
oversized_paste_full_text the way clear_input does, so a stale paste
attachment can never replace an $EDITOR edit at submit. Those fields are
set only when an oversized draft is consolidated (submit_input, and
launch_submit_held on the startup screen) and are normally taken by the
same submit, so this guards a narrow path rather than fixing an observed
loss.

The standalone starter and README example pass half the terminal height
as composer room, preserving conversation rows when content grows. The
existing density comment now describes its menu-only cap.

vendor/codewhale-ratatui/UPSTREAM.json is intentionally left alone. Its
`files` hashes record the original upstream snapshot (version_note), 11
locally patched files already differ from them, and writing this edit's
hash would falsely claim the file matches upstream 2ad795b.

Not included: the Ctrl+Shift+O / F4 hint for long drafts (CW-7525
section B), so this refs rather than fixes #7049.

Verified as one frozen batch at 7db526a725 plus consensus patch
sha256 d6418055436eaada55f4cbedc954a0eeeb51d9d922716a3588006cf7932fab45:
- scripts/with-hermetic-test-home.sh scripts/dev-cargo.sh nextest run
  -p codewhale-tui -p codewhale-localization --lib --locked:
  15343 run, 15341 passed, 2 failed, 25 skipped; build 3m41s.
  The two fixed-width rendered-fixture failures came from the long SSD
  TMPDIR wrapping paths mid-needle. Same source/binary, shorter SSD TMPDIR,
  only those two tests rerun: 2 passed, 15312 skipped. Distinct passing
  coverage across both runs: 15289 TUI + 54 localization tests.
- scripts/with-hermetic-test-home.sh scripts/dev-cargo.sh test
  --manifest-path vendor/codewhale-ratatui/Cargo.toml --locked --all-targets:
  145 passed, 0 failed (128 library and 17 example tests).
- fmt and diff checks passed; changelog sync passed; runtime boundary PASS
  (120 closure modules); dead-code budget 212; blocking-call budget 543
  across 165 files; locale parity 2583 keys in all 15 packs.
- The vocabulary guard initially failed on an already-published advertised
  legacy alias. Both refuters confirmed the docs-only correction; the
  unchanged guard then passed. Web prebuild and check-facts passed.
Before peer reconciliation, only .gitignore, docs/MODES.md and generated web
documentation changed after native tests; frozen compiler inputs were identical.
Published peer commits aa45bf84ef/485382f81d then advanced main. Their exact
diff was integrated, preserving their ancestry and regenerating web docs.
Focused reconciliation nextest (TUI ops/runtime_info/spawn and CLI cloud
callers): 117 passed, 0 failed, 15698 skipped; source remained frozen
(104 TUI, 13 CLI). Command/filter and source SHA256
are retained in coordination/land-0103-20261025/reconciliation-test-run.json.
Final integrated tree f297b67c73 is checked against the regenerated commits.
macOS ld emitted its __eh_frame size warning. No hosted CI, FreeBSD/Linux
compile, paid provider call, install, deploy, or release is claimed.

Builds on PR #7078 by @Hmbown.
Refs #7049
Refs CW-7525

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 02:15:46 -07:00
CodeWhale BotandClaude Opus 5.5 755b9da6e7 tui: render thinking-cell markdown with the active theme's palette mode
Reasoning cells parsed their markdown through render_markdown, which
picks the syntect theme from terminal or OS detection. With a dark theme
such as Shoreline in a light-profile terminal, a code fence inside a
thinking block got InspiredGitHub's near-black tokens on the charcoal
reasoning surface.

render_thinking_with_preview_limit now takes a palette_mode, and
HistoryCell::lines_with_options_folded passes the caller's
TranscriptRenderOptions.palette_mode, which
App::transcript_render_options sets from the active ui_theme.mode. Both
the expanded body and the collapsed preview render through the existing
render_markdown_tagged_with_palette, so there is still one palette-aware
entry point and no second wrapper. render_thinking_with_analysis serves
HistoryCell::lines() and transcript_lines(), which have no render
options, so it passes markdown_render::detected_palette_mode(). Pager and
clipboard output therefore behaves as before. The transcript caches
compare the whole TranscriptRenderOptions, so a theme change also
invalidates thinking cells.

The render_markdown doc comment named the Thinking body and the
immediate message body as its callers. Neither is true now, so it names
the archived-context body, its only remaining non-test caller.

Still open for #7043: archived_context.rs, render_message,
TranscriptRenderOptions::default, the background-only overlays and
fills, and the contrast-floor skip for theme-remap presets in
color_compat.rs.

Based on PR #7079 by @Hmbown, with the review fixes applied: no new
render_markdown_with_palette wrapper or doc comment, reuse of
render_markdown_tagged_with_palette, and the cached detected mode on the
path without render options.

Verified as one frozen batch at 7db526a725 plus consensus patch
sha256 d6418055436eaada55f4cbedc954a0eeeb51d9d922716a3588006cf7932fab45:
- scripts/with-hermetic-test-home.sh scripts/dev-cargo.sh nextest run
  -p codewhale-tui -p codewhale-localization --lib --locked:
  15343 run, 15341 passed, 2 failed, 25 skipped; build 3m41s.
  The two fixed-width rendered-fixture failures came from the long SSD
  TMPDIR wrapping paths mid-needle. Same source/binary, shorter SSD TMPDIR,
  only those two tests rerun: 2 passed, 15312 skipped. Distinct passing
  coverage across both runs: 15289 TUI + 54 localization tests.
- scripts/with-hermetic-test-home.sh scripts/dev-cargo.sh test
  --manifest-path vendor/codewhale-ratatui/Cargo.toml --locked --all-targets:
  145 passed, 0 failed (128 library and 17 example tests).
- fmt and diff checks passed; changelog sync passed; runtime boundary PASS
  (120 closure modules); dead-code budget 212; blocking-call budget 543
  across 165 files; locale parity 2583 keys in all 15 packs.
- The vocabulary guard initially failed on an already-published advertised
  legacy alias. Both refuters confirmed the docs-only correction; the
  unchanged guard then passed. Web prebuild and check-facts passed.
Before peer reconciliation, only .gitignore, docs/MODES.md and generated web
documentation changed after native tests; frozen compiler inputs were identical.
Published peer commits aa45bf84ef/485382f81d then advanced main. Their exact
diff was integrated, preserving their ancestry and regenerating web docs.
Focused reconciliation nextest (TUI ops/runtime_info/spawn and CLI cloud
callers): 117 passed, 0 failed, 15698 skipped; source remained frozen
(104 TUI, 13 CLI). Command/filter and source SHA256
are retained in coordination/land-0103-20261025/reconciliation-test-run.json.
Final integrated tree f297b67c73 is checked against the regenerated commits.
macOS ld emitted its __eh_frame size warning. No hosted CI, FreeBSD/Linux
compile, paid provider call, install, deploy, or release is claimed.

Refs #7043

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 02:15:44 -07:00
CodeWhale BotandClaude Opus 5.5 a25ac2f4f6 fix(tui): send DeepSeek /balance to the API root, not /beta
The DeepSeek descriptor defaults base_url to https://api.deepseek.com/beta.
fetch_deepseek_user_balance appended /user/balance to the configured base
URL, so both /balance and the background balance chip requested
/beta/user/balance. DeepSeek serves its balance endpoint at the API root,
not under the /beta inference prefix. balance_get_json logs the non-2xx
status only at debug level, so the user saw only "Could not fetch DeepSeek
account balance. Check the provider dashboard."

The URL is now built from the base URL with trailing slashes trimmed and
one trailing /beta or /v1 removed. Only ProviderKind::Deepseek reaches
this function (balance_api_for). Custom or proxy hosts without those
suffixes are unchanged. The match is case-sensitive.

Based on PR #7077, with the reviewers' fixes applied. The strip is
inlined into the private fetch_deepseek_user_balance instead of a
pub(crate) helper that would be glob re-exported through
`pub(crate) use provider_routes::*`, and the PR's new unit test is
dropped under the no-new-tests rule. Adds an [Unreleased] CHANGELOG
entry.

Verified as one frozen batch at 7db526a725 plus consensus patch
sha256 d6418055436eaada55f4cbedc954a0eeeb51d9d922716a3588006cf7932fab45:
- scripts/with-hermetic-test-home.sh scripts/dev-cargo.sh nextest run
  -p codewhale-tui -p codewhale-localization --lib --locked:
  15343 run, 15341 passed, 2 failed, 25 skipped; build 3m41s.
  The two fixed-width rendered-fixture failures came from the long SSD
  TMPDIR wrapping paths mid-needle. Same source/binary, shorter SSD TMPDIR,
  only those two tests rerun: 2 passed, 15312 skipped. Distinct passing
  coverage across both runs: 15289 TUI + 54 localization tests.
- scripts/with-hermetic-test-home.sh scripts/dev-cargo.sh test
  --manifest-path vendor/codewhale-ratatui/Cargo.toml --locked --all-targets:
  145 passed, 0 failed (128 library and 17 example tests).
- fmt and diff checks passed; changelog sync passed; runtime boundary PASS
  (120 closure modules); dead-code budget 212; blocking-call budget 543
  across 165 files; locale parity 2583 keys in all 15 packs.
- The vocabulary guard initially failed on an already-published advertised
  legacy alias. Both refuters confirmed the docs-only correction; the
  unchanged guard then passed. Web prebuild and check-facts passed.
Before peer reconciliation, only .gitignore, docs/MODES.md and generated web
documentation changed after native tests; frozen compiler inputs were identical.
Published peer commits aa45bf84ef/485382f81d then advanced main. Their exact
diff was integrated, preserving their ancestry and regenerating web docs.
Focused reconciliation nextest (TUI ops/runtime_info/spawn and CLI cloud
callers): 117 passed, 0 failed, 15698 skipped; source remained frozen
(104 TUI, 13 CLI). Command/filter and source SHA256
are retained in coordination/land-0103-20261025/reconciliation-test-run.json.
Final integrated tree f297b67c73 is checked against the regenerated commits.
macOS ld emitted its __eh_frame size warning. No hosted CI, FreeBSD/Linux
compile, paid provider call, install, deploy, or release is claimed.

Based on PR #7077 by @Hmbown.
Refs #7055

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 02:15:43 -07:00
CodeWhale BotandClaude Opus 5.5 10ce9cde28 fix(context): truncate oversized AGENTS.md and global instruction files instead of dropping them
Refs #6526 (part 1: truncate instead of drop).

Before this change, a file over the 100 KiB per-file cap failed with
TooLarge and was skipped. That applied to an AGENTS.md (or an opted-in
foreign file) in the repository-root-to-workspace chain, and to a global
~/.codewhale|.agents|.deepseek AGENTS.md or instructions.md. A sibling
candidate could then load silently in its place, and the global search
moved on to the next path. Such a file now keeps its first 100 KiB. It
ends with the existing sized marker "[...truncated: N of M bytes omitted
- consider splitting AGENTS.md]". The label is the bare file name, so
no absolute path enters the pinned prompt.

Reuse instead of a copy. prompts::read_prompt_file_bounded is split.
The new pub(crate) core, read_prompt_text_bounded(fs::File, cap), takes
a file that is already open. The path wrapper stays for its two
prompts.rs callers. The chain loader keeps fs_confined::open_read and
the global layer keeps open_user_read, so symlink confinement is
unchanged. prompts::cap_prompt_text becomes pub(crate) and builds the
marker. The strict read_context_file and its TooLarge error stay for
the constitution JSON loader and for rules files, which must not be
truncated silently.

Operator visibility. A truncated file pushes a sized warning into the
chain and global-layer warnings: "Context file <path> is too large
(<size> bytes, max 102400); truncated to its first 102400 bytes". That
warning reaches /context report and GET /v1/workspace/instructions.
The instruction source map uses the same truncating loader and carries
the warning on the Loaded entry. The discovery probe for foreign files
that are not imported uses it too, so an oversized CLAUDE.md that has
not been imported still produces the hint. load_dir_instructions' doc
comment now says its warnings include truncated candidates.

Honest aggregate marker. A head-truncated chain file is still
front-trimmed at the 48 KiB aggregate budget. The broader-scope marker
is now chain_truncation_marker(dropped, total), which gives the bytes
dropped and the assembled length before the trim, so the two markers
together describe what the model lost. It is sized before the cut, so
the result stays within the budget. keep_tail_within still keeps the
tail, and the per-file cap is unchanged. The founder's 2026-10-25
decision keeps nearest-scope precedence. docs/CONFIGURATION.md now
documents it, along with the per-file and aggregate limits. The docs
say plainly that instructions claim the budget first and rules are cut
from the end, and that a file larger than the budget keeps only its
end.

The workspace-only loader now assembles unbudgeted content; the parent
and global loader applies the aggregate ceiling exactly once after all
sources are assembled. Its direct existing tests keep a budgeted test-only
wrapper. This preserves truthful aggregate truncation counts.

Regenerate web/data/agent-docs.generated.json from the owning documentation.

The existing test nearest_scope_survives_when_the_budget_forces_a_cut
now matches the sized marker's stable text instead of the removed
const.

Not in this change: part 2 (a session-start notice backed by a session
event), part 3 (the [context] budget keys and their hard maximum), and
the "[...elided]" wording for the `instructions = [...]` loader (#454).

Builds on PR #7081 by @Hmbown (CodeWhale Boat run). Reviewer fixes:
reuse the bounded reader, drop the PR's three new tests and comments,
put sizes in the aggregate marker, restore sized warnings.

Verified as one frozen batch at 7db526a725 plus consensus patch
sha256 d6418055436eaada55f4cbedc954a0eeeb51d9d922716a3588006cf7932fab45:
- scripts/with-hermetic-test-home.sh scripts/dev-cargo.sh nextest run
  -p codewhale-tui -p codewhale-localization --lib --locked:
  15343 run, 15341 passed, 2 failed, 25 skipped; build 3m41s.
  The two fixed-width rendered-fixture failures came from the long SSD
  TMPDIR wrapping paths mid-needle. Same source/binary, shorter SSD TMPDIR,
  only those two tests rerun: 2 passed, 15312 skipped. Distinct passing
  coverage across both runs: 15289 TUI + 54 localization tests.
- scripts/with-hermetic-test-home.sh scripts/dev-cargo.sh test
  --manifest-path vendor/codewhale-ratatui/Cargo.toml --locked --all-targets:
  145 passed, 0 failed (128 library and 17 example tests).
- fmt and diff checks passed; changelog sync passed; runtime boundary PASS
  (120 closure modules); dead-code budget 212; blocking-call budget 543
  across 165 files; locale parity 2583 keys in all 15 packs.
- The vocabulary guard initially failed on an already-published advertised
  legacy alias. Both refuters confirmed the docs-only correction; the
  unchanged guard then passed. Web prebuild and check-facts passed.
Before peer reconciliation, only .gitignore, docs/MODES.md and generated web
documentation changed after native tests; frozen compiler inputs were identical.
Published peer commits aa45bf84ef/485382f81d then advanced main. Their exact
diff was integrated, preserving their ancestry and regenerating web docs.
Focused reconciliation nextest (TUI ops/runtime_info/spawn and CLI cloud
callers): 117 passed, 0 failed, 15698 skipped; source remained frozen
(104 TUI, 13 CLI). Command/filter and source SHA256
are retained in coordination/land-0103-20261025/reconciliation-test-run.json.
Final integrated tree f297b67c73 is checked against the regenerated commits.
macOS ld emitted its __eh_frame size warning. No hosted CI, FreeBSD/Linux
compile, paid provider call, install, deploy, or release is claimed.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 02:15:42 -07:00
CodeWhale BotandClaude Opus 5.5 423dff6af5 skills: tell the model when the ambient index shortens descriptions
The ambient skills index cuts each description at 400 chars (tier 1). It
shortens rows in proportion when the block is over budget (tier 2), and
drops to names only after that (tier 3). Before this change only tier 3
left a model-visible trace, so the model was never told that
`load_skill name="list"` would show it more. When any rendered row was
shortened, the index now ends with:

  - ... descriptions shortened for N of M skills; `load_skill` with
    `name="list"` returns full text.

M counts only skills the model can invoke, so explicit-only skills are
never counted. The footer's worst-case length is set aside only when
shortening can happen: tier 1 with at least one capped row, or tiers 2
and 3. A pack that fits unshortened, including the bundled pack, keeps
its full budget and renders exactly as before. The budget debug_assert
still holds.

One rule now decides shortening. `IndexRow::exceeds_index_cap` (summary
plus trigger chars over MAX_SKILL_DESCRIPTION_CHARS) matches what
`description_split` does at the cap. `description_exceeds_index_cap`
wraps it for `/skills inspect`, which used to make its own
whitespace-collapsed count of the raw description and could disagree
with the renderer. The inspect note no longer prints a count of its own.

/context report and /context json read the count back through `shortened_skills_note`, which
matches the exact footer prefix the renderer writes (a shared constant).
It puts "descriptions shortened for N of M skills" on the SkillsBlock
entry, instead of searching the block for a phrase. A skill row cannot
collide with the prefix, because skill names must start with [a-z0-9].

PR #7075 also added a discovery warning for over-length descriptions.
That warning is left out. Registry warnings go into the ambient
`### Skill load warnings` section, and any warning reserves up to
about 3.3k chars of the index budget at a 128k window. So one long
imported description would have shortened every other row, and the
same warning would also have reached plugin diagnostics and the skills
audit state. The footer, /context report, /context json and /skills inspect carry
the signal.

The MAX_SKILL_DESCRIPTION_CHARS doc comment said over-length
descriptions were reported as a load warning; none ever were. It now
names where shortening is actually reported.

The founder approved the footer text on 2026-10-25. Config knobs for
the row cap and the budget share, and a per-skill name-only state, are
still open (#7073 pieces 3 and 4, with #7066). Hence Refs, not Fixes.

No tests were added.

Verified as one frozen batch at 7db526a725 plus consensus patch
sha256 d6418055436eaada55f4cbedc954a0eeeb51d9d922716a3588006cf7932fab45:
- scripts/with-hermetic-test-home.sh scripts/dev-cargo.sh nextest run
  -p codewhale-tui -p codewhale-localization --lib --locked:
  15343 run, 15341 passed, 2 failed, 25 skipped; build 3m41s.
  The two fixed-width rendered-fixture failures came from the long SSD
  TMPDIR wrapping paths mid-needle. Same source/binary, shorter SSD TMPDIR,
  only those two tests rerun: 2 passed, 15312 skipped. Distinct passing
  coverage across both runs: 15289 TUI + 54 localization tests.
- scripts/with-hermetic-test-home.sh scripts/dev-cargo.sh test
  --manifest-path vendor/codewhale-ratatui/Cargo.toml --locked --all-targets:
  145 passed, 0 failed (128 library and 17 example tests).
- fmt and diff checks passed; changelog sync passed; runtime boundary PASS
  (120 closure modules); dead-code budget 212; blocking-call budget 543
  across 165 files; locale parity 2583 keys in all 15 packs.
- The vocabulary guard initially failed on an already-published advertised
  legacy alias. Both refuters confirmed the docs-only correction; the
  unchanged guard then passed. Web prebuild and check-facts passed.
Before peer reconciliation, only .gitignore, docs/MODES.md and generated web
documentation changed after native tests; frozen compiler inputs were identical.
Published peer commits aa45bf84ef/485382f81d then advanced main. Their exact
diff was integrated, preserving their ancestry and regenerating web docs.
Focused reconciliation nextest (TUI ops/runtime_info/spawn and CLI cloud
callers): 117 passed, 0 failed, 15698 skipped; source remained frozen
(104 TUI, 13 CLI). Command/filter and source SHA256
are retained in coordination/land-0103-20261025/reconciliation-test-run.json.
Final integrated tree f297b67c73 is checked against the regenerated commits.
macOS ld emitted its __eh_frame size warning. No hosted CI, FreeBSD/Linux
compile, paid provider call, install, deploy, or release is claimed.

Builds on PR #7075 by @Hmbown.

Refs #7073

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 02:15:40 -07:00
CodeWhale BotandClaude Opus 5.5 2c8c8be9c3 feat(tui): show multi-line shell commands on the Live card
Refs #6436. The founder chose option (a) on 2026-10-25: no new setting.

ae357d63df fixed the main complaint in #6436: the running card's header
now names the command instead of showing the Ctrl+B hint. But
command_header_summary keeps only the first line. Several cards also
returned before reaching render_command_mode:
- a running foreground shell wait, in both render_with_locale and
  render_calm
- a non-calm Live success card, including one whose output spilled over

As a result, the second and later lines of a heredoc, a loop or a
multi-line && chain never appeared on the Live card. A running
command's body was missing from Transcript too.

Changes in ExecCell:
- render_with_locale draws a multi-line command through the existing
  render_command_mode before its compact-card returns. This covers a
  foreground wait (Live and Transcript) and every Live success, the
  spillover case included. Live is capped at TOOL_COMMAND_LINE_LIMIT (6)
  wrapped lines with the existing "command" details affordance;
  Transcript is uncapped. A Transcript success still draws the command
  once, from the existing full body.
- render_calm shows its command block, the one failed cards already use
  (calm "$ " prompt), for multi-line Running and Success cards. The
  condition is now an exhaustive match on ToolStatus. Failed keeps its
  existing rule. Hydrated, Stopped and Warning are unchanged. The calm
  dispatch's TOOL_SUMMARY_CARD_LINES cap still bounds the whole card.
- Single-line commands take none of the new paths, so their cards are
  unchanged.

There is no new settings key. Draft PR #7082 (an opt-in
running_shell_command enum) is superseded. It was an agent-authored
skeleton that did not compile and never reached the render paths.

The doc comment on a_foreground_shell_wait_names_its_command_without_a_key_hint
said the card does not re-print the command, and that it tells the user
how to take the terminal back. Both are now false, so the comment is
corrected. The test's assertions are unchanged because it uses a
single-line command.

Verified as one frozen batch at 7db526a725 plus consensus patch
sha256 d6418055436eaada55f4cbedc954a0eeeb51d9d922716a3588006cf7932fab45:
- scripts/with-hermetic-test-home.sh scripts/dev-cargo.sh nextest run
  -p codewhale-tui -p codewhale-localization --lib --locked:
  15343 run, 15341 passed, 2 failed, 25 skipped; build 3m41s.
  The two fixed-width rendered-fixture failures came from the long SSD
  TMPDIR wrapping paths mid-needle. Same source/binary, shorter SSD TMPDIR,
  only those two tests rerun: 2 passed, 15312 skipped. Distinct passing
  coverage across both runs: 15289 TUI + 54 localization tests.
- scripts/with-hermetic-test-home.sh scripts/dev-cargo.sh test
  --manifest-path vendor/codewhale-ratatui/Cargo.toml --locked --all-targets:
  145 passed, 0 failed (128 library and 17 example tests).
- fmt and diff checks passed; changelog sync passed; runtime boundary PASS
  (120 closure modules); dead-code budget 212; blocking-call budget 543
  across 165 files; locale parity 2583 keys in all 15 packs.
- The vocabulary guard initially failed on an already-published advertised
  legacy alias. Both refuters confirmed the docs-only correction; the
  unchanged guard then passed. Web prebuild and check-facts passed.
Before peer reconciliation, only .gitignore, docs/MODES.md and generated web
documentation changed after native tests; frozen compiler inputs were identical.
Published peer commits aa45bf84ef/485382f81d then advanced main. Their exact
diff was integrated, preserving their ancestry and regenerating web docs.
Focused reconciliation nextest (TUI ops/runtime_info/spawn and CLI cloud
callers): 117 passed, 0 failed, 15698 skipped; source remained frozen
(104 TUI, 13 CLI). Command/filter and source SHA256
are retained in coordination/land-0103-20261025/reconciliation-test-run.json.
Final integrated tree f297b67c73 is checked against the regenerated commits.
macOS ld emitted its __eh_frame size warning. No hosted CI, FreeBSD/Linux
compile, paid provider call, install, deploy, or release is claimed.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 02:15:39 -07:00
CodeWhale BotandClaude Opus 5.5 f421390771 feat(tui): Ctrl+C clears a non-empty idle composer before arming exit
Founder decision on #6237 (option A, 2026-10-25): plain Ctrl+C on an idle
composer that holds a draft clears it recoverably, and Ctrl+Z restores it
through the existing ClearedInputRestore path. The two-tap exit prompt arms
only once the composer is empty. Cancel still beats clear: a selection
copies first, then an in-flight turn, compaction or goal-continuation wait
is cancelled, and only after that does a non-empty composer clear. This
reverses the #1757 one-meaning-per-key split for Ctrl+C. It matches the
muscle memory from sibling tools that the issue describes.

ctrl_c_disposition gains a ClearInput branch between CancelTurn and
ConfirmExit. The clear body that the Esc ClearInput arm had inline is now
App::clear_composer_draft: it resets backtrack, disarms quit, drops
edit_in_progress, makes the recoverable clear and shows the restore tip.
Esc and the new Ctrl+C arm both call it. Disarming quit on clear means a
cleared draft restarts the order clear, then arm, then exit. Without the
disarm, Ctrl+C on an empty composer, then typing, then Ctrl+C twice would
exit right after the clear. On main, an armed prompt plus a draft exited
on the very next Ctrl+C. Esc-clear now disarms a pending exit too.

PR #7084 also added a ClearInput arm to the launch-screen Ctrl+C block.
That arm is left out because the block cannot be reached:
LaunchState.composer_focus is always true. handle_launch_composer_key
returns ComposerAuthority for Ctrl+C, so the key falls through to the
session-shell arm, which also clears a launch-screen draft.

The PR's three new tests and its added rationale comments are not carried.
The existing doc comments are updated to the new priority order.
docs/KEYBINDINGS.md now lists the global Ctrl-C row in real priority order
and adds an editing-table row. The [Unreleased] CHANGELOG entry is added. In-app key help now names
draft clearing in all 15 locale packs; the install guide agrees. The
pre-existing vocabulary failure is corrected by advertising only the
canonical --full-access flag in the TUI guide; the parser alias remains.

Verified as one frozen batch at 7db526a725 plus consensus patch
sha256 d6418055436eaada55f4cbedc954a0eeeb51d9d922716a3588006cf7932fab45:
- scripts/with-hermetic-test-home.sh scripts/dev-cargo.sh nextest run
  -p codewhale-tui -p codewhale-localization --lib --locked:
  15343 run, 15341 passed, 2 failed, 25 skipped; build 3m41s.
  The two fixed-width rendered-fixture failures came from the long SSD
  TMPDIR wrapping paths mid-needle. Same source/binary, shorter SSD TMPDIR,
  only those two tests rerun: 2 passed, 15312 skipped. Distinct passing
  coverage across both runs: 15289 TUI + 54 localization tests.
- scripts/with-hermetic-test-home.sh scripts/dev-cargo.sh test
  --manifest-path vendor/codewhale-ratatui/Cargo.toml --locked --all-targets:
  145 passed, 0 failed (128 library and 17 example tests).
- fmt and diff checks passed; changelog sync passed; runtime boundary PASS
  (120 closure modules); dead-code budget 212; blocking-call budget 543
  across 165 files; locale parity 2583 keys in all 15 packs.
- The vocabulary guard initially failed on an already-published advertised
  legacy alias. Both refuters confirmed the docs-only correction; the
  unchanged guard then passed. Web prebuild and check-facts passed.
Before peer reconciliation, only .gitignore, docs/MODES.md and generated web
documentation changed after native tests; frozen compiler inputs were identical.
Published peer commits aa45bf84ef/485382f81d then advanced main. Their exact
diff was integrated, preserving their ancestry and regenerating web docs.
Focused reconciliation nextest (TUI ops/runtime_info/spawn and CLI cloud
callers): 117 passed, 0 failed, 15698 skipped; source remained frozen
(104 TUI, 13 CLI). Command/filter and source SHA256
are retained in coordination/land-0103-20261025/reconciliation-test-run.json.
Final integrated tree f297b67c73 is checked against the regenerated commits.
macOS ld emitted its __eh_frame size warning. No hosted CI, FreeBSD/Linux
compile, paid provider call, install, deploy, or release is claimed.

Based on PR #7084 by @Hmbown.
Fixes #6237

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 02:15:38 -07:00
CodeWhale BotandClaude Opus 5.5 2c4f090a9f fix(tools): write says when it kept the existing file's line endings
Refs #7070.

Since #5909, whole-file `write` restores the existing file's line
endings. That stops accidental CRLF-to-LF flips, but the result still
said only "Successfully wrote N bytes", so after a deliberate LF request
the agent told the user the file was LF while every line stayed CRLF.

When restoring endings actually changes the supplied content and the
existing file has an observable newline style, the success summary now adds "kept the existing file's CRLF (or
LF) line endings (write does not change a file's line-ending style)".
Bytes on disk are unchanged, and new files and same-style overwrites
get the same summary as before. A newline-free old file no longer gets
a false LF attribution, and lone-CR input conversions are reported.

Actually converting endings through `write` would change the
model-visible schema (PR #7076's `line_endings` field) and is deferred
until users ask for it (founder decision 2026-10-25, item 4).

Verified as one frozen batch at 7db526a725 plus consensus patch
sha256 d6418055436eaada55f4cbedc954a0eeeb51d9d922716a3588006cf7932fab45:
- scripts/with-hermetic-test-home.sh scripts/dev-cargo.sh nextest run
  -p codewhale-tui -p codewhale-localization --lib --locked:
  15343 run, 15341 passed, 2 failed, 25 skipped; build 3m41s.
  The two fixed-width rendered-fixture failures came from the long SSD
  TMPDIR wrapping paths mid-needle. Same source/binary, shorter SSD TMPDIR,
  only those two tests rerun: 2 passed, 15312 skipped. Distinct passing
  coverage across both runs: 15289 TUI + 54 localization tests.
- scripts/with-hermetic-test-home.sh scripts/dev-cargo.sh test
  --manifest-path vendor/codewhale-ratatui/Cargo.toml --locked --all-targets:
  145 passed, 0 failed (128 library and 17 example tests).
- fmt and diff checks passed; changelog sync passed; runtime boundary PASS
  (120 closure modules); dead-code budget 212; blocking-call budget 543
  across 165 files; locale parity 2583 keys in all 15 packs.
- The vocabulary guard initially failed on an already-published advertised
  legacy alias. Both refuters confirmed the docs-only correction; the
  unchanged guard then passed. Web prebuild and check-facts passed.
Before peer reconciliation, only .gitignore, docs/MODES.md and generated web
documentation changed after native tests; frozen compiler inputs were identical.
Published peer commits aa45bf84ef/485382f81d then advanced main. Their exact
diff was integrated, preserving their ancestry and regenerating web docs.
Focused reconciliation nextest (TUI ops/runtime_info/spawn and CLI cloud
callers): 117 passed, 0 failed, 15698 skipped; source remained frozen
(104 TUI, 13 CLI). Command/filter and source SHA256
are retained in coordination/land-0103-20261025/reconciliation-test-run.json.
Final integrated tree f297b67c73 is checked against the regenerated commits.
macOS ld emitted its __eh_frame size warning. No hosted CI, FreeBSD/Linux
compile, paid provider call, install, deploy, or release is claimed.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 02:15:35 -07:00
CodeWhale BotandClaude Opus 5.5 06530af1bc fix(tui): the git probe backs off from input and engine events, not UI motion
Refs #6728.

git_status.rs documents that the git probe backs off after 30 s with "no
input and no engine event". Both probe consumers (set_probe_backoff and
probe_due) read last_ui_activity instead, which any non-quiescent UI
state also moves. A running background job keeps an exec cell
animating, so that clock was reset on every tick and the probe stayed on
its 2 s cadence for as long as the job lived: the reporter's FreeBSD
ktrace shows 89 git execs in about 10 s, in bursts about 2.1 s apart.

A second clock, last_input_or_engine_event, is moved only by terminal
and engine events, and both probe consumers now read it. The idle poll
and the automation scan stay on last_ui_activity. Showing the Git panel
still forces the fast cadence through git_probe_quiet_for. The two
comments that described the old shared clock are corrected.

The second cause in #6728, the fixed event-loop wake rate during an
approval wait or a background job, needs a design slice and the
reporter's FreeBSD measurement, so the issue stays open.

The dual-refuter batch review also found two already-published #7068
explanations that overstated title selection. Correct the changelog and
existing test comment without rewriting those published commits: the
title uses the first non-heading line outside code fences.

Verified as one frozen batch at 7db526a725 plus consensus patch
sha256 d6418055436eaada55f4cbedc954a0eeeb51d9d922716a3588006cf7932fab45:
- scripts/with-hermetic-test-home.sh scripts/dev-cargo.sh nextest run
  -p codewhale-tui -p codewhale-localization --lib --locked:
  15343 run, 15341 passed, 2 failed, 25 skipped; build 3m41s.
  The two fixed-width rendered-fixture failures came from the long SSD
  TMPDIR wrapping paths mid-needle. Same source/binary, shorter SSD TMPDIR,
  only those two tests rerun: 2 passed, 15312 skipped. Distinct passing
  coverage across both runs: 15289 TUI + 54 localization tests.
- scripts/with-hermetic-test-home.sh scripts/dev-cargo.sh test
  --manifest-path vendor/codewhale-ratatui/Cargo.toml --locked --all-targets:
  145 passed, 0 failed (128 library and 17 example tests).
- fmt and diff checks passed; changelog sync passed; runtime boundary PASS
  (120 closure modules); dead-code budget 212; blocking-call budget 543
  across 165 files; locale parity 2583 keys in all 15 packs.
- The vocabulary guard initially failed on an already-published advertised
  legacy alias. Both refuters confirmed the docs-only correction; the
  unchanged guard then passed. Web prebuild and check-facts passed.
Before peer reconciliation, only .gitignore, docs/MODES.md and generated web
documentation changed after native tests; frozen compiler inputs were identical.
Published peer commits aa45bf84ef/485382f81d then advanced main. Their exact
diff was integrated, preserving their ancestry and regenerating web docs.
Focused reconciliation nextest (TUI ops/runtime_info/spawn and CLI cloud
callers): 117 passed, 0 failed, 15698 skipped; source remained frozen
(104 TUI, 13 CLI). Command/filter and source SHA256
are retained in coordination/land-0103-20261025/reconciliation-test-run.json.
Final integrated tree f297b67c73 is checked against the regenerated commits.
macOS ld emitted its __eh_frame size warning. No hosted CI, FreeBSD/Linux
compile, paid provider call, install, deploy, or release is claimed.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 02:15:34 -07:00
Claude 485382f81d fix(runtime): name a started Fleet member's child by its member id (CW-7182)
917e6b2 passed the member's display_name as the child's session name. The
spawn path validates session names as ASCII letters, digits, '-', '_' and
'.', so any whale whose name has a space or non-ASCII text ("Ana Lovelace",
小鲸) failed every member start with "name must not contain whitespace". The
child is now named by member_id (the whale's Agent id), which is always a
valid session name and is unique per whale, so the existing "one live child
per name" rule still refuses a second start for a busy member. The platform's
members inspection maps that id back to the whale's name (platform 6ea8a0f).
docs/RUNTIME_API.md says the session name is the member id.

Also moves the FleetMemberStarted alias above SessionSnapshot's doc comment;
917e6b2 had inserted it between the comment's two lines, so the alias took
SessionSnapshot's first doc line.

Verification: cargo test -p codewhale-tui --lib -- protocol_covers_engine_ops
runtime_info spawn_ plus a throwaway test (not committed) of
parse_spawn_request: 105 passed, 0 failed. The throwaway test showed
"Ana Lovelace" refused ("name must not contain whitespace…"), 小鲸 refused,
and an agent_<uuid> member id accepted as the session name. The run needed
RUST_MIN_STACK=16777216 in this container: with the default test-thread
stack, the unrelated
child_permission_gate::typed_denies_updated_after_spawn_reach_children_grandchildren_and_background
overflows its stack and aborts the binary. No live provider or Boat call.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01PsruMwr3b8fWh1sfKL7JTQ
2026-10-25 08:45:53 +00:00
Claude aa45bf84ef feat(cli): connect each group member's provider in the cloud key handoff (CW-7182)
`dispatch --prepare-model --thread` (and a send that uses the local model)
only connected the lead's provider. A group member on another provider then
failed at spawn on the guest. After the lead's `key` operation, the CLI now
reads the conversation's `group` (GET /v1/threads/:id), resolves each active
non-lead member's saved route from /api/agents/:id, and sends one more `key`
operation per distinct provider from the local configuration for it, under the
same portability checks as the lead (HTTPS endpoint, API-key auth, no
device-bound sign-in). Keys stay memory-only on the guest through the existing
session `key` operation. Removed members are ignored; a member whose provider
is the lead's is not sent twice. A member with no saved model, or whose
provider is not configured locally, stops the handoff with that member's name
rather than being skipped. The prepare receipt gains `members` (agentId,
provider) for the connections it added; the desktop already prepares before
every cloud send, so it picks this up with no app change.

Verification: a throwaway test (not committed) drove computer::run with the
existing FakeTransport for a group (lead deepseek; Ana anthropic; Dee
deepseek; Old removed): key operations went to deepseek then anthropic only,
receipt listed Ana/anthropic; 1 passed, 0 failed. cargo test -p codewhale-cli
--lib cloud: 122 passed, 1 failed — the failure,
login_selects_managed_route_when_provider_is_default, fails identically
without this change in this container (pre-existing, environment-dependent).
cargo fmt clean. No live account, Boat computer or provider call.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01PsruMwr3b8fWh1sfKL7JTQ
2026-10-25 08:45:53 +00:00
CodeWhale BotandClaude Opus 5.5 e7c336d634 test(sandbox): NoNewPrivs child-matrix rows hold under an NNP parent
Refs #7021.

PR_SET_NO_NEW_PRIVS is inherited and irreversible. In a container whose
parent already holds it (docker --security-opt no-new-privileges,
hardened Kubernetes pods), every child reports NNP=1, so the two
child-matrix rows that expect NNP=0 failed every time. Those rows now
check the parent's flag first: when it is already set they assert the
kernel contract (the child keeps NNP=1) and print one line naming the
substituted row. Production code is unchanged. The module doc now names
PR_GET_NO_NEW_PRIVS, which is how the child reports the flag.

These tests compile only on Linux. They were not compiled or exercised
on this macOS host, and not inside an NNP container.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 01:30:52 -07:00
CodeWhale BotandClaude Opus 5.5 a7dcd47ee1 feat(tui): offer Open in editor for a bare workspace file path
Refs #2342.

The right-click "Open in editor" entry was offered only for a
`path:line` reference. A bare path in prose or tool output
(`src/main.rs`) could not be opened. first_file_line_reference and the
context-menu resolver now fall back to the first bare workspace file
path, opened at line 1, after looking for a `path:line`. The token
trimming is shared (reference_tokens). Every candidate still goes
through workspace_file, which refuses `..`, absolute paths outside the
workspace and links, and open_file_in_editor checks again.

The in-TUI file preview the issue also asks for is not built; it needs
a product decision.

Verified at the tree of the ten sweep commits (9ac47bb0ec): scripts/dev-test.sh
tui session_manager plan_handoff skills composer_history tui::history
mouse_ui process_hardening tools::workflow tools::file large_output_router
oauth tls: 1219 tests run, 1219 passed, 14095 skipped. rustfmt clean.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 01:30:52 -07:00
CodeWhale BotandClaude Opus 5.5 8d7282a11e fix: composer history keeps a file it could not read; an empty workflow fails
Refs #6559 (U01-m2 and the empty-workflow half of R06-06).

Composer history treated any read failure as an empty or truncated file:
map_while(Result::ok) stopped at the first bad line, and the append
path then atomically rewrote the file from that partial view, losing
every entry after it. read_history_from now returns the I/O error. The
load path logs it and shows no history; the append path logs it and
leaves the file alone instead of rewriting it. A missing file is still
an empty history.

The workflow validator accepted a declarative workflow with no nodes,
and the execution then reported success with no work done.
validate_workflow_nodes now refuses it with
WorkflowExecutionError::EmptyWorkflow ("workflow must contain at least
one node"). No checked-in workflow or example uses empty nodes.

R01-06, the rest of R06-06 and the partial items keep #6559 open.

Verified: scripts/dev-test.sh workflow: 237 tests run, 237 passed.
Verified at the tree of the ten sweep commits (9ac47bb0ec): scripts/dev-test.sh
tui session_manager plan_handoff skills composer_history tui::history
mouse_ui process_hardening tools::workflow tools::file large_output_router
oauth tls: 1219 tests run, 1219 passed, 14095 skipped. rustfmt clean.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 01:30:51 -07:00
CodeWhale BotandClaude Opus 5.5 b7803d8636 fix(release): republish checks each asset's digest against its checksum manifest
Refs #6557 (O01-m1).

In --manifest mode (release-republish), verify-release-inventory used
the release's checksum manifest only as a list of names and threw the
SHA-256 column away. A stale or edited manifest that named the right
files therefore passed, and republish then wrote its hashes into
Homebrew and the other channels.

manifestInventory now returns each row's digest (and refuses a name
listed twice). assertInventory refuses an asset whose GitHub `digest`
is missing or differs from the manifest row. This fails closed: an old
tag whose assets predate GitHub's digest field can no longer be
republished from its manifest. The existing republish test now gives
each asset a real SHA-256.

P01-01 needs a design slice and D03-m3 a policy decision; #6557 stays
open for those.

Verified: node --test scripts/release/verify-release-inventory.test.js:
7 tests, 7 passed.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 01:30:51 -07:00
CodeWhale BotandClaude Opus 5.5 97bb16321b fix(skills): refuse a SKILL.md that is not a regular file
Refs #6554.

Global skill discovery read SKILL.md with fs::read_to_string whatever
the path was, so a FIFO named SKILL.md in a skills directory stalled
discovery. The global root now refuses a non-regular SKILL.md with a
"not a regular file" load warning (fs::metadata follows symlinks, so a
symlink to a regular file still loads). The audit's bounded parser
reports the same case as Broken. Workspace roots already read through
fs_confined.

The rest of #6554 (a shared ingest-time bounded reader and a guard
against new uncapped reads) needs design and stays open.

Verified at the tree of the ten sweep commits (9ac47bb0ec): scripts/dev-test.sh
tui session_manager plan_handoff skills composer_history tui::history
mouse_ui process_hardening tools::workflow tools::file large_output_router
oauth tls: 1219 tests run, 1219 passed, 14095 skipped. rustfmt clean.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 01:30:51 -07:00
CodeWhale BotandClaude Opus 5.5 cbde2c3f43 fix(web): admin post route answers a non-JSON body with 415
Refs #6560 (W02-01).

The admin post route already capped the body at 65,536 bytes but
accepted any Content-Type. After auth, it now refuses a request whose
media type is not application/json (compared case-insensitively, as
media types are) with 415. The one real client (admin-client.tsx) and
both test helpers already send application/json.

R05-11 (telemetry limiter keyed on the client-chosen install_id) and
W02-03 (one shared admin-login rate-limit key) wait on a policy call and
keep #6560 open. Not deployed.

Verified: in web/, npx tsc --noEmit -p . clean; npx vitest run
lib/security-boundary-behavior.test.ts lib/community-agent-review-state.test.ts
lib/middleware.test.ts: 3 files, 75 tests passed; lib/admin-login.test.ts
lib/admin-login-integration.test.ts lib/public-api-security.test.ts:
14 tests passed.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 01:30:51 -07:00
CodeWhale BotandClaude Opus 5.5 9c53943a19 fix(app-server): interrupt a turn whose response_start could not be written
Refs #6558 (R03-05).

message_thread propagated a failed response_start write with `?` after
the runtime had accepted the turn, but before the turn was registered
and before the existing orphan-interrupt branch. A dead stdio writer
therefore left the runtime turn running with nothing able to interrupt
it. The write failure now sends the same best-effort interrupt the
stream-break path uses (InFlightTurn + interrupt_turn_request), logs if
that interrupt fails, and returns the original error. The success path
is unchanged.

R04-09 (process-group kill after the leader is reaped) and R06-05 (an
unrecorded lane worktree after a pre-launch I/O failure) need design and
keep #6558 open.

Verified: scripts/dev-test.sh app-server stdio_runtime_bridge: 3 tests
run, 3 passed, 174 skipped. No existing test makes the response_start
write fail, so the new branch is covered by compiling it and by the
neighbouring stream-break test.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 01:30:50 -07:00
CodeWhale BotandClaude Opus 5.5 59d035546a fix(tui): title a plan-handoff To-do with the plan's first line
Refs #7068.

A Plan-to-Work handoff seeded its To-do with the whole plan response as
the title: headings, fences and newlines included. The work-surface row
puts a title on one line, so the lines ran together
("## GoalAdd ...").

plan_handoff_seed_title now takes the first line that is not a heading
and not inside a code fence, strips list and quote markers and inline
emphasis/code marks, and collapses whitespace. It falls back to the
collapsed whole text, and keeps the 1024-char cap. Seed and rollback
share this one derivation. The two plan_handoff tests that compared
against the raw text now expect the derived title.

What happens to the seed afterwards (nothing completes it if the model
never writes its own checklist) is a product decision and is not in
this change.

Verified at the tree of the ten sweep commits (9ac47bb0ec): scripts/dev-test.sh
tui session_manager plan_handoff skills composer_history tui::history
mouse_ui process_hardening tools::workflow tools::file large_output_router
oauth tls: 1219 tests run, 1219 passed, 14095 skipped. rustfmt clean.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 01:30:50 -07:00
CodeWhale BotandClaude Opus 5.5 1e66ad3837 fix(session): parse a session with a stale crash checkpoint once on attach
Refs #6842.

attach_session called promote_interrupted_checkpoint, which fully
loaded the saved document (load_session) only to compare its timestamp
with the checkpoint. resume_session then loaded and repaired the same
document again. On a long session that doubled the attach-time parse
of a document the reporter measured at 98.7 MB.

When the saved document is the newer one, promote_interrupted_checkpoint
now returns the recovery it already computed with
recover_session_for_resume, and attach_session persists any repair from
it (persist_resumed_repair, split out of resume_session) instead of
loading a second time. The other branches are unchanged:
- a newer checkpoint is promoted and saved, and attach reloads it through
  resume_session;
- a missing document is replaced by the checkpoint the same way;
- a document that exists but cannot be read is left alone, the
  checkpoint stays, and attach reports the error;
- the stale checkpoint is cleared only after a readable newer document.

The rest of #6842 stays open for design: the snapshot is still built and
the journal still cloned on the UI task, and the journal archive and
usage_source_fingerprints still grow without bound.

Verified at the tree of the ten sweep commits (9ac47bb0ec): scripts/dev-test.sh
tui session_manager plan_handoff skills composer_history tui::history
mouse_ui process_hardening tools::workflow tools::file large_output_router
oauth tls: 1219 tests run, 1219 passed, 14095 skipped. rustfmt clean.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 01:30:50 -07:00
CodeWhale BotandClaude Opus 5.5 10c687c683 refactor(tui): call codewhale_release::tls directly and drop the crate::tls alias
The TLS bootstrap and platform client builders moved to
codewhale-release in the crate split. lib.rs kept
`use codewhale_release::tls;` so that `crate::tls::*` still resolved for
every caller. That alias is a shim, and the split's migrations are
one-way. All callers now name codewhale_release::tls directly, and the
alias and its comment are deleted. No behavior change.

This also removes oauth.rs's `tls` edge into the tui crate, one of the
four small edges that S7 option (c) cuts before oauth and config move
into codewhale-runtime.

Verified at the tree of the ten sweep commits (9ac47bb0ec): scripts/dev-test.sh
tui session_manager plan_handoff skills composer_history tui::history
mouse_ui process_hardening tools::workflow tools::file large_output_router
oauth tls: 1219 tests run, 1219 passed, 14095 skipped. rustfmt clean.

Refs CW-6382

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 01:30:50 -07:00
CodeWhale BotandClaude Opus 5.5 fe80c74b63 docs(changelog): sync crates/tui/CHANGELOG.md with the root changelog
Recent commits edited the root CHANGELOG.md without running
scripts/sync-changelog.sh, so the copy that /change embeds had fallen
behind and the Version drift check failed. This regenerates it; no
other change.

Verified: scripts/sync-changelog.sh --check passes on this tree.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 01:30:46 -07:00
CodeWhale BotandClaude Opus 5.5 58760608f4 feat(tui): the Terminal dock shows the model's shell commands, not only interactive shells
Founder report, 2026-10-25: while the model ran a shell command such as a
cargo test, the dock's Terminal tab said "No live terminals. Start one with
Ctrl+N." That read as broken. The dock listed only interactive PTY shells
(Ctrl+N or the model's terminal tool). Ordinary shell tool calls lived only
in the transcript and the Jobs count.

The dock now also lists the session's tracked shell commands. They come
from the same `ShellManager::list_jobs_for_session` snapshots the Jobs
projection uses, so nothing new is recorded:
- The dock's existing blocking worker thread, which already holds the
  shared ShellManager, returns the command snapshots with each refresh, so
  the UI thread never takes the manager lock.
- Every running command is shown, plus the five most recently finished.
- Each command is a read-only tab, `$ <command>` (truncated at 32 chars),
  with its running or exit status and elapsed seconds.
- Selecting a command shows its stdout tail, then its stderr tail, through
  the same SGR-only rendering as PTY output.
- Key input to a command tab is ignored. Next and previous tab cycle across
  shells and commands, and mouse selection works on both.
- PTY tabs, input, resize and creation are unchanged.
- The empty state now appears only when there are neither shells nor
  commands.

Verified:
- scripts/dev-cargo.sh check --workspace --all-targets --locked: 0
  warnings, 0 errors.
- nextest codewhale-tui, filtered to work_surface, terminal_session and
  review: 768 passed.
- Not verified: a live render in a running TUI with a model executing a
  command. Check this in the next build.

Refs CW-7525

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 00:40:46 -07:00
CodeWhale BotandClaude Opus 5.5 2d158d78b1 fix(review): reject leaked tool-call markup and size Low-risk reviews to the cap actually sent
#7072: `codewhale review` printed DeepSeek's leaked DSML tool-call markup
(`<||DSML|| calls>…`) as the whole review and exited 0 with no tally.
This happens on Low-risk diffs, whose budget offers zero tools. The review
path builds its own requests in tools/review_core.rs and never ran the
engine's existing detector.
- run_review_core now classifies a tool-free final reply as
  ReviewCoreOutcome::LeakedToolCall when it contains a fake tool-call wrapper.
  The detection reuses core::engine::streaming::contains_fake_tool_wrapper
  and its marker table; there is no second detector. That module becomes
  pub(crate) for this.
- Only text outside fenced code blocks and inline code spans is checked, so
  a review that quotes such markup from the diff is still accepted.
- Both consumers treat that outcome as a failed pass: the `codewhale review`
  CLI (report_failure, so nothing is posted and there is no tally) and the
  agent's review tool (review_error_with_usage). They share one message,
  `leaked_tool_call_message`. The tool's metadata reports
  `review_outcome: "leaked_tool_call"`.
- ReviewCoreOutcome derives Copy: it is fieldless, and the tool path reads
  it after the move.

#7067: a Low-risk review sends max_tokens = min(route allowance, 4000), but
three things still used the uncapped 65,536 allowance:
- the reported allowance and reserve;
- the reasoning-effort cap;
- the failure text, which said "produced no review text" even when about
  1k visible tokens came back.
So 3 of 4 DeepSeek runs exhausted the cap reasoning and failed with numbers
that were never sent. The changes:
- The CLI computes triage and budget first, then derives review_allowance
  from the min'd cap. It sends exactly that, and the reserve and the message
  use it.
- bounded_review_reasoning_effort takes the allowance. A reasoning model
  (reserve > 0) with an allowance of 8,192 or less caps at Low. Low is a
  real DeepSeek wire tier, while medium rounds up to high there. The doc
  comment states the new rule.
- The exhausted-pass message says "was cut off before the review finished
  (N visible tokens came back)" when output exceeded reasoning, and
  "produced no review text" only when nothing visible came back.
- Existing bound tests pass the 65,536 allowance they always assumed.

Verified:
- scripts/dev-cargo.sh check --workspace --all-targets --locked: 0
  warnings, 0 errors.
- nextest codewhale-tui, filtered to review, work_surface, terminal_session
  and route_budget: 768 passed.
- That run also included a temporary local check, removed before this
  commit and not committed. It confirmed the issue's exact DSML output is
  detected, while a review quoting `<tool_call>` inline and an `<invoke>`
  block inside a code fence, and a plain JSON review, are not.
- Not run: a live DeepSeek review, clippy, hosted CI.

Refs #7072 #7067

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 00:40:45 -07:00
CodeWhale BotandClaude Opus 5.5 e24c050b87 build(lock): two machine build slots instead of one
Founder decision, 2026-10-25: at most two native builds per machine.

On this 36 GB M4 Max (14 cores), the single exclusive build lock queued every
agent behind one tui compile. That compile is a ~965k-line crate whose front
end keeps most cores idle; its unit-test rustc peaks near 8 GB
(BUILD_PERFORMANCE.md). Today, sessions waited 15 to 30 minutes behind
each other's tui builds while the machine was mostly idle.

scripts/build-lock.py now holds one of CODEWHALE_BUILD_SLOTS (default 2)
exclusive flocks:
- `build.lock` is slot 0, so an older single-slot copy of the script, or a
  CI runner pointed at the same file, still excludes the build in that slot.
- `build.lock.1` and up are the extra slots.
- A build beyond the slot count prints every holder and polls once a second.
- CODEWHALE_BUILD_SLOTS=1 restores the old behaviour, and a non-integer
  value exits 2 with an error.

docs/BUILD_PERFORMANCE.md's "One build per machine" section is updated to
match. The workspace AGENTS.md (operator file, outside any repo) and
codewhale-app/AGENTS.md now say "at most two".

Verified with a scratch lock file and three concurrent 3-second commands:
- Jobs 1 and 2 ran together and finished at 3s and 4s.
- Job 3 printed both holders, acquired a slot after 3s, and finished at 7s.
- CODEWHALE_BUILD_SLOTS=1 ran.
- CODEWHALE_BUILD_SLOTS=x exited 2.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 00:18:28 -07:00
CodeWhale BotandClaude Opus 5.5 917e6b2969 feat(runtime): start a saved Fleet member as a typed session child (CW-7182)
POST /v1/threads/{id}/members {operation_key, member_id, display_name,
prompt, fleet} starts one member of a complete Fleet v2 document as an
ordinary child of the thread's session. It reuses the `agent` tool's spawn
path (spawn_subagent_from_input) with a per-request roster built by
roster_from_fleet, so the member's exact provider/model is bound through the
existing profile pin, its instructions join the child prompt as its Fleet
profile, and session permissions and spawn limits apply unchanged. Provider
fallback is refused for these starts, as for exact Workflow spawns. The
document is used for this start only; the selected Fleet is untouched.

The host sends Op::StartFleetMember (wire twin `start_fleet_member`, reply
channel kept off the wire like GetSubAgentSettlement); the engine handles it
at its idle boundary with the off-turn child runtime used for follow-ups. The
route answers 200 with the child agent_id, or 202 "queued" when the thread is
mid-turn. The manager records operation_key -> (request, agent_id): the same
key and body return the original child, a different body is refused.
Runtime capability `thread_fleet_members` advertises it. Stop stays
POST /v1/agent-runs/{id}/cancel. docs/RUNTIME_API.md "Thread Fleet members".

Consumer: codewhale-platform a09ead01 (group member addressing).

Verification: cargo test -p codewhale-protocol 96 passed, 0 failed;
cargo test -p codewhale-tui --lib -- protocol_covers_engine_ops runtime_info
spawn_ fleet_member: 106 passed, 0 failed (the four existing
spawn_subagent_from_input callers in tests.rs updated for the new parameter).
No live provider or Boat call; the route was not exercised end to end against
a real model.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 00:15:03 -07:00
CodeWhale BotandClaude Opus 5.5 2947e4c51d refactor(config): second batch of P3 edge cuts, plus #7069 composer history isolation
Continues P3, moving tui's config out of codewhale-tui. The destination is
codewhale-runtime; 8b5baa7a5f explains why. Each move is one-way, with every
caller migrated and no re-export left behind.

Edges cut from config:
- tools:
  - WorkshopConfig (the `[workshop]` schema) moves to codewhale-config.
  - Its methods become free functions in large_output_router
    (install_active_workshop, active_read_result_max_bytes,
    active_tool_result_max_bytes, workshop_threshold_for), because the
    orphan rule forbids inherent impls on a foreign type.
  - Config::user_input_limits becomes UserInputLimits::from_config.
  - Two intra-doc links into tools become plain code spans.
- plugins: PluginId (with as_str and Display) and PluginAuthority move to
  codewhale-config, and all 30 using files import them from there.
- provider_lake:
  - The provider-capability matrix (ProviderCapability, RequestPayloadMode,
    provider_capability, provider_capability_with_wire) moves out of
    config into provider_lake, its only home of use.
  - configured_model_for_route moves into config, which is what it reads.
  - deepseek_alias_deprecation becomes pub(crate) for the moved code.
  - wire_config_prefers_responses is now unused and is deleted.
- settings/config_keys -> reasoning_preference: the reasoning-effort
  spelling table moves to
  codewhale_config::settings_schema::canonical_reasoning_effort.
  ReasoningEffort::parse_strict delegates to it and maps the canonical name
  back through as_setting, so there is one table. settings and config_keys
  call the table directly.
- config_keys -> commands: edit_distance and best_suggestion_score move to
  codewhale_config::suggest. The duplicate edit_distance in
  core/engine/tool_catalog.rs is folded into it.
- credentials -> doctor: structural_url_authority moves to codewhale-config,
  which gains a `url` dependency. url is already in the lockfile through
  reqwest.
- Four doc links from config and settings into skills and session_resume
  become plain code spans; rustdoc can't resolve them once config leaves
  tui.

#7069: composer history ignored CODEWHALE_HOME. With an explicit
CODEWHALE_HOME, history now reads and writes
$CODEWHALE_HOME/composer_history.jsonl, with no ~/.deepseek fallback,
through the existing codewhale_paths resolver. Without one, resolution is
unchanged. An invalid CODEWHALE_HOME resolves to no history rather than
the ambient home.

Remaining config edges, all outside this batch:
- oauth (33 refs), apply_startup_providers (5), codex_model_cache (1) and
  provider_readiness (2): these wait for founder decision S7.
- read_denylist and HooksConfig.
- The sandbox requirements table.

Verified:
- scripts/dev-cargo.sh check --workspace --all-targets --locked: 0
  warnings, 0 errors (11m 38s). That ran on 135319aa1a.
- After rebasing onto 4d2b3fdb1b, `check -p codewhale-cli --all-targets`:
  no errors. The later main commits touched only cli/src/update.rs, the
  installers, web and docs; none of the moved symbols.
- cargo fmt --check: clean. module_graph --check: PASS. Dead-code budget:
  exactly at 212. Blocking-call budget: within 543.
- nextest:
  - codewhale-config: 806 passed, 1 skipped.
  - codewhale-tui, filtered to large_output_router, user_input, plugins,
    provider_lake, config::tests, composer_history, reasoning_preference,
    settings, config_keys, doctor, commands::tests, tool_catalog,
    extension_host plugin tests, model_profile and route_budget: 1139
    passed.
- Not run: clippy, doc, Windows, hosted CI.

Refs #7069 CW-6129 CW-6382 CW-7525

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-24 23:24:13 -07:00
CodeWhale BotandClaude Opus 5.5 a43f4c1e78 fix(web): unblock the website deploy after the audit move and the installer change
The Web Frontend run (40168744583) for main at b63e5af3ac failed its test
job before deploy, with two stale expectations:
- docs/public-surface-facts.json still listed crates/tui/src/audit.rs as a
  fact-matrix source. P3 (8b5baa7a5f) moved that file to
  crates/runtime/src/audit.rs.
- public-installer-contract.test.ts still required the installer to refuse
  when a leftover `codewhale-tui` sits beside the commands. e9c9de775f
  deliberately removed that guard, because the installer never writes that
  name. The test now asserts the new contract: codewhale and codew install,
  and the legacy file is left byte-for-byte untouched.

`npm run check` also reported the generated agent docs and feature data as
stale, after today's doc edits (INSTALL.md, CLOUD_DISPATCH.md,
features.toml). They were regenerated with `npm run prebuild`.

Verified locally in web/:
- `npm test`: 74 files, 772 tests passed.
- `npm run check`: facts, docs, tokens, lint, tsc and next build all pass;
  993 static pages.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-24 23:23:38 -07:00
CodeWhale BotandClaude Opus 5.5 b63e5af3ac chore(installer): drop the new code comments added with the install fix
Repository rule: no new code comments; the reasoning belongs in commit
messages. e9c9de775f's message already carries it:
- `(dev)` in CODEWHALE_BUILD_VERSION marks an unstamped checkout. A release
  artifact carries its short SHA, and a Cargo source package carries the
  plain version.
- A forced reinstall that fails puts the moved-aside command back.

This removes the comment added in update.rs and the forced-backup comment
blocks added in both installers. The edited comment that explains when
existing binaries are replaced stays, because it now describes
CODEWHALE_FORCE_REINSTALL.

Comment-only change. Verified: `bash -n scripts/release/install.sh` and
`sh -n web/public/install.sh` pass.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-24 23:17:26 -07:00
Ben Gao 27f2b240e3 feat(runtime): show a turn's cost and context while it runs
A tool that listens to a running turn could only report what it cost after the
turn ended, so long agentic work looked frozen and then jumped at the end. Each
model call now reports the price of that call on the route that served it and
the prompt the provider billed, so a client can follow the work as it happens.
The turn's own totals remain the record once it ends.

The mobile control page shows that reading in its header, keeps each
conversation's figure when you switch away and come back, and never presents a
compaction pass as the conversation's spend.
2026-10-25 12:51:56 +08:00
Lstarsky0 6aebca1b89 fix(tui): don't print an unpriced session cost as $0.0000 in /status
/status printed the session's own cost through the precise money
formatter whatever the billing state, so a session whose turns have no
authoritative price read "Session cost: $0.0000" while the footer said
"cost: unknown (billing basis unknown)" and /cost and /tokens said
"unknown". Give the status view a label from the same cumulative usage
chip the footer reads: "unknown", the word /cost and /tokens print, when
no turn is priced, and the chip's subtotal text when only some are.
Fully priced sessions keep the precise amount.

Signed-off-by: Lstarsky0 <[email protected]>
2026-10-24 00:24:54 +08:00
Lstarsky0 e11bd43a42 fix(tui): only say "Goal cleared." when there was a goal to clear
/clear and /new always queue a goal clear so the engine's goal state
can't outlive the conversation, and the engine answered every clear
with a "Goal cleared." status. In a session that never had a goal the
footer said so right under "Conversation cleared". Note whether a goal
existed (in the shared goal state, or the configured fallback the clear
also wipes) before clearing, and skip the status when there was none.
The empty goal snapshot is still published, and clearing a goal that
was set, through /goal clear or /clear, still says "Goal cleared.".

Signed-off-by: Lstarsky0 <[email protected]>
2026-10-23 21:43:38 +08:00
Lstarsky0 ec2f51c32b fix(tui): keep the /diff pager's place when the terminal is resized
Since the /diff pager renders its diff at the body width, its rows are
numbered by rendered diff line, and that numbering changes with the
width. On a resize the pager remaps the top row and the current search
match through those numbers, so it landed on an earlier line: scrolled
to "Line 31" at 100 columns, shrinking to 60 showed "Line 25", and a
search for "Line 47" came back on "Line 38".

Have the diff renderer record which source row of the diff produced
each row it returns (one per header, hunk header or diff line; the
summary block counts as one), and number the pager's rows with that. A
resize then keeps the same diff line at the top and on the current
match.

Signed-off-by: Lstarsky0 <[email protected]>
2026-10-23 07:04:52 +08:00
1128 changed files with 64277 additions and 57740 deletions

No files matched your search

+2 -10
View File
@@ -750,18 +750,10 @@ assert.equal(
2,
"both glibc recovery branches must name codewhale-cli",
);
// The archive installer never overwrites an existing command: it validates the
// retired TUI path against the consolidated bytes and leaves upgrades to
// `codewhale update`, which migrates `codewhale-tui` beside the canonical pair.
assert.match(
archiveInstaller,
/legacy_tui="\$BIN_DIR\/codewhale-tui"[\s\S]*check_destination "\$SCRIPT_DIR\/codewhale" "\$legacy_tui"/,
"archive installs must validate the retired TUI path against consolidated bytes",
);
assert.doesNotMatch(
archiveInstaller,
/install_binary "\$SCRIPT_DIR\/codewhale" "\$legacy_tui"/,
"archive installs must not overwrite an existing retired TUI command",
/install_binary "\$SCRIPT_DIR\/[^"]+" "\$BIN_DIR\/codewhale-tui"/,
"archive installs must not write the retired codewhale-tui command",
);
assert.doesNotMatch(
cliDispatcher,
-15
View File
@@ -320,21 +320,6 @@ jobs:
(cd "integrations/${bridge}" && npm test)
done
- name: Build computer-use test desktop
# Spawn assertions have short request deadlines; keep the cold image
# build outside those deadlines and fail image preparation explicitly.
timeout-minutes: 10
run: >-
docker build --tag codewhale-cu-linux
--file crates/tui/plugins/computer-use/docker/Dockerfile
crates/tui/plugins/computer-use
- name: Run computer-use plugin suites
# The bundled plugin is dependency-free too; its suites cover the
# manifest contract, the registry, the exec/ssh transport, the four
# platform backends, and the MCP stdio protocol. No GUI input runs.
run: (cd crates/tui/plugins/computer-use && npm test)
- name: Run extension host suites and check the committed bundle
# The TypeScript extension host (experimental, [features]
# extension_host). Tests exercise both the committed bundle and
+1 -1
View File
@@ -50,7 +50,7 @@ jobs:
id: review
uses: ./
with:
version: ${{ vars.CODEWHALE_REVIEW_VERSION || 'v0.10.0' }}
version: ${{ vars.CODEWHALE_REVIEW_VERSION || 'v0.10.2' }}
provider: codewhale
model: ${{ vars.CODEWHALE_REVIEW_MODEL }}
pr-number: ${{ inputs.pr-number }}
+1 -2
View File
@@ -45,11 +45,10 @@ jobs:
run: node marketplace/scripts/check-marketplace.mjs
- name: Check bundled catalog snapshot
run: python3 core/scripts/sync-marketplace.py --marketplace marketplace --check
- name: Check current skill and Computer Use mirrors
- name: Check current skill mirrors
if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
run: |
node marketplace/scripts/check-marketplace.mjs --core core
node marketplace/scripts/check-cu-sync.mjs --core core
- name: Explain a drift failure
if: failure()
run: |
+2 -3
View File
@@ -87,9 +87,8 @@ jobs:
# Both cold macOS artifact builds on 0.10.1 exhausted 90 minutes inside
# compilation. Keep the full CI build budget; packaging stays capped below.
timeout-minutes: 210
# 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.
# FreeBSD is a source-build target with no prebuilt asset, no npm binary and no CI check
# (see packaging/freebsd/README.md and docs/INSTALL.md#freebsd).
strategy:
fail-fast: false
matrix:
-1
View File
@@ -39,7 +39,6 @@ jobs:
- { name: web, directory: web }
- { name: telemetry-ingest, directory: telemetry-ingest }
- { name: extension-host, directory: crates/tui/extension-host }
- { name: embedded-computer-use, directory: crates/tui/plugins/computer-use }
- { name: vscode, directory: extensions/vscode }
- { name: feishu-bridge, directory: integrations/feishu-bridge }
- { name: telegram-bridge, directory: integrations/telegram-bridge }
+1
View File
@@ -1,6 +1,7 @@
# Build artifacts
/target
/target-*
/vendor/codewhale-ratatui/target/
.entire/
.cursor/
.plans/
+1 -1
View File
@@ -183,7 +183,7 @@ casually it reads as taking the work even when credit is preserved.
KV-cache effect: frozen prefix vs. append-only history. Never splice a
volatile fact into the prefix; append it as a user-role message.
- These active modules are repeatedly misidentified as dead; verify consumers
before removal: `runtime/src/context_budget.rs`, `tui/src/model_registry.rs`,
before removal: `runtime/src/context_budget.rs`,
`runtime/src/prompt_zones.rs`, `tui/src/tools/remember.rs`, and
`config/src/route/`. Native memory lives in `runtime/src/native_memory.rs`;
`tools/remember.rs` is its capture path.
+188 -1
View File
@@ -7,19 +7,206 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
- **Crate split S24a (internal):** the workshop byte-budget slot moves to `codewhale-runtime`, `AutoRouteDecisionEvidence` to `codewhale-models`, and cost-status session persistence to a tui `usage_persistence` module, so billing no longer reaches the session manager or model routing. No behavior change. (CW-6129)
- **Nested and deletable threads (Runtime API):** a fork (including a backtrack fork) records `parent_thread_id`, `POST /v1/threads` accepts one, and `GET /v1/threads/summary` returns it so clients can nest child conversations. `DELETE /v1/threads/{id}` removes an idle thread with its turns, items, events and goal, and its saved session unless another thread shares it or a terminal holds it open; a running thread answers 409.
- **Hunk actions in Review (Runtime API):** `POST /v1/git/stage`, `/v1/git/unstage` and `/v1/git/discard` take `hunks` (the `@@ … @@` headers from `GET /v1/diff`) with one path and apply only those hunks through `git apply`, honouring the same `expect` preconditions as whole-path writes. A hunk that no longer exists answers `409 git_state_changed` with `stale_hunks`, and nothing is written unless git accepts the whole patch.
- **Provider pre-cuts B (internal):** client wire helpers, reasoning-effort mapping and the Ollama catalog parsers move into their own client modules, and test-only provider gates use the shared test-isolation predicate. No behavior change. (CW-6129)
- **`codewhale-dependencies`, `codewhale-snapshot`, `codewhale-lsp`, `codewhale-repl` crates:** external-tool resolution, workspace snapshots, the language-server client and the Python REPL runtime moved out of `codewhale-tui` into their own crates; `write_context_file` now lives in `codewhale-repl`. No behavior change. (CW-6129)
- **`codewhale-skill-install` crate:** the community-skill installer, package content digest and `SKILL.md` frontmatter reader move out of `codewhale-tui` into a new crate; the install SSRF guard and timeouts are unchanged in production builds. (CW-6129)
- **`codewhale-util` crate:** shared utilities, confined workspace file access, link and file-identity checks, bundle file readers, the `.git` name rule and the owner file lock move out of `codewhale-tui` into a new crate; no behavior change. (CW-6129)
- **Crate-split contract hygiene:** the `canonical_action` policy table now lives in `codewhale-execpolicy`, `required_action`, the file path-alias folding and the `apply_patch` preflight parser in `codewhale-tools`, and `DEFAULT_SPAWN_DEPTH`, `PermissionCeiling` and `ShellCeiling` in `codewhale_protocol::fleet`, so `codewhale-agent-contract` no longer depends on `codewhale-config` or `codewhale-workflow`. Approval and authority behavior is unchanged. (CW-6129)
- **Provider pre-cuts (internal):** schema sanitizing, prompt-turn classification, image-window capping, tool-projection warning names and the reasoning-effort tier moved into `codewhale-models`; stall records and route-label sanitizing moved into `codewhale-runtime`. No behavior change. (CW-6129)
- **Change mode and permissions while the agent works:** Tab, Shift+Tab, `/mode`, the hotbar and `/config mode` no longer refuse while a turn is running. A response that is already streaming is not restarted; the change applies at the agent's next step in the same turn (before its next tool batch, at every approval gate, and before its next model request, which carries the new mode notice). The status line says so until it applies. A call still waiting for approval goes back to the model under the new posture, and a call you already approved runs only if the new posture is equal or broader. Model, thinking level and provider stay locked until the turn ends. (CW-7552)
- **Steering interrupts thinking:** a steer (Ctrl+Enter, or Enter twice after queueing) now closes the in-flight model request at once, whether the model is waiting on headers, reasoning, writing text or emitting a tool call, and continues the same turn with your message. Text already shown is kept as interrupted output; unfinished reasoning and partial tool calls are discarded, never run. (CW-7552)
- **Switching provider lands on its current model:** `/provider stepfun` now selects Step 5 Preview instead of the older Step 3.7 Flash; a model you saved for a provider is still kept. `/model` now lists the current provider's own models (bundled and live, newest first, up to 20) instead of only the selected one. Search also matches ids typed without separators (`step5`). While you search, Shift+P and Shift+F type their letters rather than pinning or adding to Fleet, so `Step-5-Preview` can be typed in full; Ctrl+P and Ctrl+F pin or add the highlighted row at any time.
- **Remote control works again:** `codewhale rc` (`/rc`) can attach to the hosted control plane for web Chat again. The control plane had only accepted the retired 0.9.13 runtime identity, so every 0.10.x Engine was refused with `runner_runtime_identity_unavailable`; it now accepts the exact released runtime identity (commit, image and digest) and fails closed when they are not configured.
- **Modern MCP client:** MCP servers now connect through the official MCP SDK (2.4) behind Codewhale's Rust broker by default (`mcp_backend = "host"`; `"rust"` stays available for one release). Servers that speak the 2026-07-28 revision are discovered with `server/discover`; other stdio and HTTP servers fall back to the legacy `initialize` handshake, and configured SSE servers keep theirs. Rust keeps process, network, credential, catalog, approval and cancellation authority. (CW-6713)
- **MCP compatibility fixes:** older servers that answer the discovery probe with an error or an unrelated result now connect through the legacy handshake instead of being refused after the probe. A server that rejects sign-in reports its real 401 and the `/mcp login` hint rather than the probe's "method not found". A stale OAuth token is refreshed once when the probe itself is rejected. A server whose command cannot start now says why (for example, command not found) instead of a generic broker refusal.
- **DeepSeek Harness plugins:** reviewed DSH plugins (definition API pinned to DSH 0.2.1-rc.2 in a shared compatibility table) run in the Native extension host. Tool output is validated against its declared schema, unsupported options are refused instead of ignored, and plugin commands can submit a prompt with visible plugin attribution. Terminal command receipts are kept in thread snapshots without entering model history. Changing the compatibility table invalidates earlier trust. A newly enabled plugin's tools are offered on the first turn: the turn waits up to 10 seconds for the plugin host to finish loading instead of adding them only from the next turn. Plugin UI views stay disabled, and the import receipt says why: WebKit's network restrictions do not stop WebRTC, so plugin UI waits for an isolated renderer. A plugin tool can call `ctx.sessions.flush(exec.agent.session)` to have the Engine save the conversation so far before it continues (Runtime, terminal and `exec` sessions); it reports failure instead of claiming a save. Installing a plugin whose bytes changed after review now returns a 409 conflict instead of a server error. A plugin tool can ask you a question with `ctx.userQuestions.ask` and gets back the options you picked, your own text, or a skip; the question shows like any other Codewhale question in the terminal and through the Runtime API. (CW-7054, CW-7549)
- **MCP catalogs stay fresh:** a server's tools, resources, prompts and skills are rediscovered on their own connection when an advertised `ttlMs` expires or the server sends a list-changed notice. A tool resolved against the old catalog is refused with "retry the call", a removed tool cannot run, and a failed refresh reconnects instead of serving stale entries. Catalogs are never shared across servers, credentials or reconnects. (CW-7547)
- **MCP Skills extension:** servers that advertise the published Skills extension have their skills listed in `list_mcp_resources`. Listing never activates a skill, and reading a skill file is refused unless its size and sha256 digest match the listing. MCP skills do not yet join `/skills`. (CW-7545)
- **Computer Use is installed, not bundled:** the Computer Use plugin is no longer vendored into the binary. Install it from the `codewhale` marketplace, which points at its independent release; its consent tools load on first use, and decisions still require a human approval card. (CW-6732)
- **Tool image validation:** an image returned by a tool must decode before it is sent, so truncated or corrupt files are omitted with a receipt even when they are already within the delivery budget.
- **Image-heavy session recovery:** ordinary image reads use a 256 KiB delivery budget. Requests measure serialized bytes, omit older images from an outbound copy with receipts, and retry an initial HTTP 413 once only with a smaller body. Saved images and executed tool actions remain intact. (CW-7550)
- **Uninstall on every install route:** `codewhale uninstall` previews receipt-owned release files and `--yes` removes only checked files, with the running command last. Both Unix installers and the Windows archive installer record command ownership; self-update renews existing receipts on both. On Windows the running `codewhale.exe` is renamed and deleted once the command exits. Package-managed copies are left alone, and the command prints their exact removal command: npm with its prefix, Cargo with its root, the Homebrew formula, `scoop uninstall codewhale`, `winget uninstall --id HunterBown.CodeWhale`, or the Windows setup's `Uninstall.exe`. Every other `codewhale`/`codew` still on PATH is listed. User data is kept unless you add `--purge-data`, which previews and then deletes only `~/.codewhale` (or a `CODEWHALE_HOME` named `.codewhale`) and `~/.deepseek/snapshots`, refusing symlinks, home ancestors and directories owned by another user. Windows removal is compile-checked but not yet exercised on a Windows host. (Refs #7091)
- **GitHub Action needs an explicit provider:** the Action's `provider` input no longer defaults to the `codewhale` account relay. Name your BYOK provider (or `codewhale` for a separately verified account machine key); a missing provider stops with `configuration_missing` before any download or model call. The repository's own (disabled) review workflow now pins CLI `v0.10.2` by default instead of `v0.10.0`.
- **Composer history honors `CODEWHALE_HOME`:** with `CODEWHALE_HOME` set, prompt history reads and writes `$CODEWHALE_HOME/composer_history.jsonl` instead of sharing `~/.codewhale` across isolated homes; an invalid `CODEWHALE_HOME` keeps no history rather than falling back to your real home. (#7069)
- **Slow tools are not reported missing:** a tool whose `--version` probe is still running at its 3-second deadline (for example the `cargo` rustup shim on a busy machine) now counts as installed instead of being cached as missing for the rest of the session.
- **GitHub Action reviews:** analysis can export a structured Engine review for a separate trusted publisher job. The publisher checks the current PR revision and line anchors, deduplicates its findings, and keeps uncertain or preexisting claims out of inline comments.
- **Telegram recovery:** prompts and their initiating identity are durable before the polling cursor advances. Restart recovery reuses the original Engine operation, questions preserve human ownership, and uncertain final replies remain available through `/status` without an automatic resend.
- **DingTalk bridge:** an optional internal-app Stream adapter reuses the same Engine, shared admission and approval helpers. Tenant/staff allowlists, durable acceptance before acknowledgement and retained uncertain deliveries protect recovery. Managed account linking is separate from this advanced self-hosted bridge.
- **Answerless output-limit recovery:** opt into `[reasoning_only] on_output_limit = true` to request an answer again after a length stop with no answer or tool call, including hidden reasoning and ordinary Fleet workers. Retries share `max_reprompts`, retain existing output and control limits, and record request-scoped nudge receipts without adding them to resumed conversation context. RLM keeps its existing stop protocol. Thanks [@M-Maciej](https://github.com/M-Maciej) for the long-proof Fleet report. (#6990)
- **Linux hardening tests:** the NoNewPrivs child matrices now respect a parent that already holds the irreversible flag, keeping the full assertions on unpinned hosts and checking the kernel ratchet on pinned hosts. Thanks [@asto18089](https://github.com/asto18089) for diagnosing the hardened-container failure. (#7021)
- **OrcaRouter recommendation:** `/provider` and `/model` offer a shared partner recommendation with the DeepSeek V4.1 Flash 30% top-up-match link, public referral disclosure and localized offer details. F7 or a click opens the existing pager while retaining the current route.
- **Per-scope instruction budgets:** global and project standing instructions now have separate token budgets (100,000 estimated tokens each, capped at 25% of the usable context window), every trim, skip or refusal reaches the model and `/context report`, and `[instruction_budget] policy = "refuse"` stops a request before dispatch. Other tools' instruction files stay opt-in through `project_instruction_imports` or `claude_fallback`, and the table is user config only. Thanks [@7jrxt42BxFZo4iAnN4CX](https://github.com/7jrxt42BxFZo4iAnN4CX) for the design and implementation. (#6526, #7086)
- Long drafts now show a localized external-editor hint when the actual composer
viewport clips their text. The hint uses the shared F4 binding and preserves
queue, running-turn, history, and menu hints (#7049). Thanks @Lstarsky0.
- Archived context markdown now uses the active theme's palette when rendered
in the transcript, preventing unreadable dark text on dark backgrounds (#7043).
Thanks @Statter.
- Plan-to-Work handoff now names the seeded To-do item in a localized reminder
to keep it on the list and complete it after the plan (#7068).
- Localized question footers, approval and plan-handoff chrome, tool waiting
status, and /color receipts use the shared locale packs (#7071).
- Clearing a conversation with no goal no longer shows a misleading "Goal cleared."
receipt. Clearing an existing or configured goal still reports it (#7089).
Thanks @Lstarsky0.
- Removed the internal config re-export facade and its model-constant wrapper;
provider types, shared constants, credential helpers, and notification types
now come directly from their shared owners. Configuration and saved settings
keep their existing behavior.
- Removed superseded home-directory and Fleet-role re-export shims, unused
tool-context sandbox/MCP fields, and the redundant core tool-parser facade.
Consumers now call their existing owners directly; tool denial rules, approval
posture, strict tool schemas, home resolution, and legacy saved-role migration
retain their behavior. The cleanup removes three dead-code suppressions and
keeps the engine as the sole owner of human approval attestations.
- Removed unused plan/journal accessors, an unused plan-status glyph helper,
and a cancellation case that no execution path issued. Active mailbox and
cancellation wiring stays in place; its stale unused-code suppressions are
removed. No supported command or feature is retired by this cleanup.
- Local Ollama defaults now come from chat-capable tags, and `/provider ollama`
refreshes the live catalog before switching. Exec and Runtime API defaults use
the same discovery path. Fresh context observations come from loaded models'
served windows; unloaded models retain an explicitly unverified fallback (#5848).
- The Runtime API's `turn.usage` receipts now carry each model call's priced
cost on the route that served it and the prompt size the provider billed, so
a client can follow a long turn while it runs. The mobile control page shows
live token usage and a partial cost estimate in its header, keeps the
estimate after the turn settles, starts fresh for the next turn, and counts a
replayed receipt once (#7074). Thanks @gaord.
- A fatal engine error no longer turns on offline queueing by itself. When the
error came from a step that never starts a turn (a finished background
shell, a goal continuation or a child completion that cannot resolve its
route), every later message used to be queued behind it; it is now sent
normally. An environment-only authentication failure still reopens provider
setup and queues until you fix it (#6561).
- `codewhale --provider <name> resume <id>` now moves the saved session to the
named provider, as `exec --resume` already did, so a session whose provider
table was deleted can be reopened on a provider you still have. It takes
that provider's default model unless `--model` is also given. Without
`--provider` the resume still refuses rather than fall back, and the refusal,
now also on the `codewhale resume` and `fork` mount path, names this command
beside `codewhale sessions export <id>` (#6996). Thanks @luestr.
- The model's skills index now says when it shortens skill descriptions.
When a description hits the 400-char row cap, or rows are cut to fit the
context budget, the index ends with "descriptions shortened for N of M
skills" and points to `load_skill` with `name="list"` for the full text;
`/context report` and `/context json` show the same count on the skills entry.
`/skills inspect` now
flags a description by the same rule the index uses (#7073).
- The TUI Terminal dock now also shows the model's shell commands for the
session: each running or recent command is a read-only tab with its live
output, beside the interactive shells. "No live terminals" appears only when
there are neither.
- A shell command that spans several lines (a heredoc, a loop, an `&&` chain
split across lines) now shows those lines on its Live card while it runs and
after it succeeds, within the card's usual line limit, instead of only the
first line. Transcript shows the whole command. Single-line cards are
unchanged (#6436, #7082).
- `codewhale review` no longer accepts leaked tool-call markup as a review: a
pass whose reply is a DeepSeek DSML (or other) tool-call wrapper outside code
now fails as incomplete, with no tally and nothing posted (#7072). Reviews no
longer have fixed per-risk output caps (a Low-risk review used to get 4,000
tokens, which a reasoning pass could spend entirely on thinking): every pass
may use the route's own output limit, and the step, tool and money budgets
still stop a review early as a named partial review rather than truncating
it. The `review` tool bounds its reasoning level to leave room for the review,
and an exhausted pass reports the allowance and reserve actually sent and
says "cut off" when visible text came back (#7067).
- `--full-access` is now the documented flag for Full Access (`--yolo` stays an
alias), and `serve` accepts it after the subcommand: `codewhale serve --acp
--yolo` used to fail with "unexpected argument". `serve --acp` also writes one
stderr line naming the permission and sandbox it runs with, and the ACP
permission option's Full Access hint names the working command. A configured
`sandbox_mode = "danger-full-access"` still never widens Ask or Auto-Review
(#6992).
(#6992, #4955). Thanks @eugenicum for the request.
- Preview: `dispatch --computer` starts an account cloud conversation that can
also be opened in desktop. `--thread` continues or inspects it; `--stop`,
`--approve` and `--deny` control its current task. Explicit `--key-stdin`
installs provider access in the running computer's memory. Retries retain
one operation key and the account/VM binding. Existing Agent Work dispatch
remains available when no computer conversation is selected.
- A delegated agent still marked running that has reported no progress for 10
minutes no longer holds its parent turn open: the turn's stall watchdogs can
recover again, and the stall record names the agent. A child's running time
stays unlimited; only silence counts (#6184).
- A long draft or paste now grows the composer past its density row limit, up
to half the screen (never less than that limit) while leaving room for the
transcript, so the text can be read and edited before sending. While a
slash, mention or history-search menu is open the composer keeps its usual
height (#7049).
- Code blocks inside reasoning cells now take their syntax colours from the
active theme instead of the terminal's detected light or dark mode, so a dark
theme such as Shoreline in a light-profile terminal no longer paints
near-black code on its dark reasoning surface. The pager and clipboard copies
still follow terminal detection (#7043).
- `/balance` and the balance status chip work on the default DeepSeek route
again. The balance request now goes to the DeepSeek API root instead of the
`/beta` inference path, which failed with "Could not fetch DeepSeek account
balance". A failed balance request now names its reason, such as HTTP 401
Unauthorized (#7055).
- An `AGENTS.md` or global instruction file over 100 KiB is no longer
skipped (which could let a sibling file load in its place): Codewhale loads
its first 100 KiB, ends it with a marker giving the bytes omitted, and names
the file in `/context report`. The shared 48 KiB instruction budget still
applies after that, and the marker it leaves when it drops broader scopes
now gives the bytes dropped and the total (#6526).
- Ctrl+C on an idle composer that holds a draft now clears the draft instead
of arming exit, and Ctrl+Z restores it. The "press Ctrl+C again" exit prompt
arms only once the composer is empty, and Ctrl+C during a running turn still
cancels the turn first. Clearing a draft with Ctrl+C or Esc also resets a
pending exit prompt (#6237, #7084).
- While a background job runs, the git status probe now backs off after 30
seconds without input or engine events, as it already did in an idle
session, instead of starting about a dozen git processes every 2 seconds for
as long as the job lives (#6728).
- An idle session wakes less often. While an approval, elevation or question
prompt waits on you, while a background job runs, or while only a
once-a-second label ticks, the event loop now relaxes to its 250 ms quiet
poll instead of polling every 24 to 48 ms; animations and the prompt's
timeout still wake it on their own deadlines. The terminal input thread
polls every 200 ms instead of 50 ms after 5 seconds without input. Measured
on macOS with release builds: an approval wait went from 29 to 12 timer
wakeups per second, a background job from 32 to 16 and an idle composer
from 25 to 11 (#6728).
- Saving a session no longer copies every message body: journal entries
share their content, so the snapshot taken on each update copies ids and
pointers. Resuming a session compares a leftover crash checkpoint against
the saved document's metadata instead of parsing the whole document first,
so `--resume` and attach read the document once (#6842).
- `write` over an existing file now says when it kept that file's CRLF or LF
line endings, so the agent no longer reports a line-ending conversion that
did not happen (#7070).
- The To-do that a Plan-to-Work handoff seeds is now titled with the plan's
first non-heading line outside code fences, without list or emphasis
markdown, instead of the whole plan run together
(#7068).
- Right-click "Open in editor" on a transcript cell now also offers a bare
workspace file path with no line number, opening it at line 1 (#2342).
- Composer history is no longer rewritten from a partial read: when the
history file cannot be read, new entries are not saved over it and a
warning is logged. An empty declarative workflow is refused ("workflow must
contain at least one node") instead of succeeding with no work (#6559).
- A `SKILL.md` that is not a regular file is reported as a skill load warning
instead of being read, so a FIFO in a skills directory can no longer stall
discovery (#6554).
- `app-server`: a turn whose `response_start` could not be written to stdio
is now interrupted instead of running on with nothing able to stop it
(#6558).
## [0.10.2] - 2026-10-22
+1 -1
View File
@@ -555,7 +555,7 @@ A well-structured PR follows a consistent pattern. Recent exemplars include:
- **#386** — `/init` command: new `crates/tui/src/commands/groups/project/init.rs` module, project-type detection,
AGENTS.md generation, command registration in `commands/mod.rs`, localization strings.
- **#389** — Inline LSP diagnostics: LSP subsystem in `crates/tui/src/lsp/`, engine hooks in
- **#389** — Inline LSP diagnostics: LSP subsystem in `crates/lsp/src/`, engine hooks in
`crates/tui/src/core/engine/lsp_hooks.rs`, config toggle, test coverage.
- **#387** — Self-update: new `crates/cli/src/update.rs` module, CLI subcommand registration,
HTTP download + SHA256 verification + atomic binary replacement.
Generated
+223 -7
View File
@@ -843,10 +843,8 @@ dependencies = [
name = "codewhale-agent-contract"
version = "0.10.2"
dependencies = [
"codewhale-config",
"codewhale-execpolicy",
"codewhale-protocol",
"codewhale-workflow",
"serde",
]
@@ -903,6 +901,7 @@ dependencies = [
"codewhale-lane",
"codewhale-paths",
"codewhale-release",
"codewhale-runtime",
"codewhale-secrets",
"codewhale-state",
"codewhale-telemetry",
@@ -978,6 +977,7 @@ dependencies = [
"libc",
"ring",
"rust_decimal",
"schemars",
"semver",
"serde",
"serde_json",
@@ -986,6 +986,7 @@ dependencies = [
"toml 1.1.4+spec-1.1.0",
"toml_edit 0.25.15+spec-1.1.0",
"tracing",
"url",
"windows-sys 0.61.2",
]
@@ -1007,6 +1008,24 @@ dependencies = [
"uuid",
]
[[package]]
name = "codewhale-dependencies"
version = "0.10.2"
dependencies = [
"anyhow",
"codewhale-config",
"codewhale-paths",
"codewhale-runtime",
"codewhale-util",
"serde",
"serde_json",
"tempfile",
"tokio",
"tracing",
"uuid",
"wait-timeout",
]
[[package]]
name = "codewhale-execpolicy"
version = "0.10.2"
@@ -1015,6 +1034,7 @@ dependencies = [
"codewhale-protocol",
"regex",
"serde",
"serde_json",
"shlex 2.0.1",
"toml 1.1.4+spec-1.1.0",
]
@@ -1066,6 +1086,24 @@ dependencies = [
"unicode-width",
]
[[package]]
name = "codewhale-lsp"
version = "0.10.2"
dependencies = [
"anyhow",
"async-trait",
"codewhale-config",
"codewhale-runtime",
"codewhale-util",
"libc",
"reqwest",
"serde",
"serde_json",
"tempfile",
"tokio",
"tracing",
]
[[package]]
name = "codewhale-mcp"
version = "0.10.2"
@@ -1095,8 +1133,47 @@ version = "0.10.2"
dependencies = [
"codewhale-config",
"codewhale-core",
"codewhale-sanitize",
"serde",
"serde_json",
"thiserror 2.0.21",
]
[[package]]
name = "codewhale-net"
version = "0.10.2"
dependencies = [
"anyhow",
"chrono",
"codewhale-command-contract",
"codewhale-config",
"codewhale-release",
"codewhale-runtime",
"codewhale-tools",
"http",
"hyper-util",
"reqwest",
"serde",
"serde_json",
"tempfile",
"thiserror 2.0.21",
"tokio",
"toml 1.1.4+spec-1.1.0",
"tracing",
]
[[package]]
name = "codewhale-notify"
version = "0.10.2"
dependencies = [
"codewhale-config",
"codewhale-runtime",
"codewhale-secrets",
"dirs 7.0.0",
"regex",
"tempfile",
"tracing",
"windows",
]
[[package]]
@@ -1185,41 +1262,95 @@ dependencies = [
"webpki-roots",
]
[[package]]
name = "codewhale-repl"
version = "0.10.2"
dependencies = [
"codewhale-dependencies",
"codewhale-runtime",
"serde",
"serde_json",
"tokio",
"uuid",
]
[[package]]
name = "codewhale-runtime"
version = "0.10.2"
dependencies = [
"ahash",
"anyhow",
"aws-lc-rs",
"base64 0.23.1",
"chrono",
"codewhale-cloud-facts",
"codewhale-command-contract",
"codewhale-config",
"codewhale-core",
"codewhale-execpolicy",
"codewhale-lane",
"codewhale-localization",
"codewhale-memory",
"codewhale-models",
"codewhale-palette",
"codewhale-paths",
"codewhale-protocol",
"codewhale-release",
"codewhale-secrets",
"colored",
"dirs 7.0.0",
"fd-lock",
"futures-util",
"jsonwebtoken",
"libc",
"lru",
"parking_lot",
"portable-pty",
"regex",
"reqwest",
"ring",
"serde",
"serde_json",
"sha2 0.11.1",
"shellexpand",
"tempfile",
"thiserror 2.0.21",
"tokio",
"tokio-util",
"toml 1.1.4+spec-1.1.0",
"toml_edit 0.25.15+spec-1.1.0",
"tracing",
"uuid",
"wait-timeout",
"windows",
"windows-sys 0.61.2",
"wiremock",
]
[[package]]
name = "codewhale-sandbox"
version = "0.10.2"
dependencies = [
"anyhow",
"async-trait",
"chrono",
"codewhale-app-server",
"codewhale-execpolicy",
"codewhale-paths",
"codewhale-release",
"codewhale-runtime",
"dirs 7.0.0",
"libc",
"portable-pty",
"reqwest",
"serde",
"serde_json",
"tempfile",
"tokio",
"tracing",
"url",
"uuid",
"wait-timeout",
]
[[package]]
@@ -1250,6 +1381,46 @@ dependencies = [
"tracing",
]
[[package]]
name = "codewhale-skill-install"
version = "0.10.2"
dependencies = [
"anyhow",
"codewhale-net",
"codewhale-paths",
"codewhale-util",
"flate2",
"futures-util",
"reqwest",
"serde",
"serde_json",
"sha2 0.11.1",
"tar",
"tempfile",
"thiserror 2.0.21",
"tokio",
"yaml-rust2",
]
[[package]]
name = "codewhale-snapshot"
version = "0.10.2"
dependencies = [
"codewhale-config",
"codewhale-dependencies",
"codewhale-paths",
"codewhale-runtime",
"codewhale-util",
"fd-lock",
"ignore",
"libc",
"serde",
"sha2 0.11.1",
"tempfile",
"tracing",
"wait-timeout",
]
[[package]]
name = "codewhale-state"
version = "0.10.2"
@@ -1309,7 +1480,6 @@ dependencies = [
"arboard",
"async-stream",
"async-trait",
"aws-lc-rs",
"axum",
"base64 0.23.1",
"chrono",
@@ -1323,23 +1493,32 @@ dependencies = [
"codewhale-commands",
"codewhale-config",
"codewhale-core",
"codewhale-dependencies",
"codewhale-execpolicy",
"codewhale-hooks",
"codewhale-lane",
"codewhale-localization",
"codewhale-lsp",
"codewhale-mcp",
"codewhale-memory",
"codewhale-models",
"codewhale-net",
"codewhale-notify",
"codewhale-palette",
"codewhale-paths",
"codewhale-protocol",
"codewhale-ratatui",
"codewhale-release",
"codewhale-repl",
"codewhale-runtime",
"codewhale-sandbox",
"codewhale-sanitize",
"codewhale-secrets",
"codewhale-skill-install",
"codewhale-snapshot",
"codewhale-telemetry",
"codewhale-tools",
"codewhale-util",
"codewhale-workflow",
"codewhale-workflow-js",
"colored",
@@ -1353,11 +1532,9 @@ dependencies = [
"futures-util",
"globset",
"htmd",
"hyper-util",
"ignore",
"image",
"jsonschema",
"jsonwebtoken",
"libc",
"liblzma",
"lru",
@@ -1415,10 +1592,37 @@ dependencies = [
"yaml-rust2",
]
[[package]]
name = "codewhale-util"
version = "0.10.2"
dependencies = [
"anyhow",
"chrono",
"codewhale-config",
"codewhale-models",
"codewhale-paths",
"codewhale-protocol",
"codewhale-runtime",
"codewhale-telemetry",
"futures-util",
"ignore",
"libc",
"reqwest",
"serde_json",
"sha2 0.11.1",
"tempfile",
"tokio",
"tracing",
"uuid",
"windows",
"windows-sys 0.61.2",
]
[[package]]
name = "codewhale-workflow"
version = "0.10.2"
dependencies = [
"codewhale-protocol",
"serde",
"serde_json",
"sha2 0.11.1",
@@ -1640,6 +1844,7 @@ dependencies = [
"crossterm_winapi",
"derive_more",
"document-features",
"filedescriptor",
"mio",
"parking_lot",
"rustix",
@@ -3982,6 +4187,12 @@ dependencies = [
"windows-link",
]
[[package]]
name = "pastey"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2ee67f1008b1ba2321834326597b8e186293b049a023cdef258527550b9935b4"
[[package]]
name = "peg"
version = "0.6.3"
@@ -4772,17 +4983,20 @@ dependencies = [
[[package]]
name = "rmcp"
version = "3.5.1"
version = "3.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab5023804cbe0304cbe06197ac16e645a0a0d0ffa2953bf735c15c2fa0213350"
checksum = "ece64e19631fc38758181e637c15509a2c2709ee53bfde1e1d58f9dc991f919e"
dependencies = [
"async-trait",
"chrono",
"futures",
"indexmap",
"libc",
"oauth2",
"pastey",
"pin-project-lite",
"reqwest",
"schemars",
"serde",
"serde_json",
"thiserror 2.0.21",
@@ -4791,6 +5005,7 @@ dependencies = [
"tokio-util",
"tracing",
"url",
"uuid",
]
[[package]]
@@ -5086,6 +5301,7 @@ version = "1.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "687274d293b6cdc6e73e0fee520bf2049650090d7164f87672d212a3c530cf4a"
dependencies = [
"chrono",
"dyn-clone",
"ref-cast",
"schemars_derive",
+9
View File
@@ -17,17 +17,26 @@ members = [
"crates/mcp",
"crates/memory",
"crates/models",
"crates/net",
"crates/notify",
"crates/palette",
"crates/paths",
"crates/protocol",
"crates/release",
"crates/runtime",
"crates/sandbox",
"crates/secrets",
"crates/skill-install",
"crates/sanitize",
"crates/state",
"crates/telemetry",
"crates/tools",
"crates/tui",
"crates/util",
"crates/dependencies",
"crates/snapshot",
"crates/lsp",
"crates/repl",
"crates/workflow",
"crates/workflow-js",
"tests/portable-session",
+2 -2
View File
@@ -191,10 +191,10 @@ Connect [MCP servers](docs/MCP.md), add [skills](docs/SKILLS.md) and
[plugins](docs/PLUGINS.md), and run [hooks](docs/HOOKS.md) on session and tool
events. Code mode (experimental, on by default) lets the model write one
`execute_tools` program that calls several MCP and plugin tools; each call
still meets your approval rules and keeps its receipt. The bundled Computer
still meets your approval rules and keeps its receipt. The independently installed Computer
Use plugin (experimental, macOS only) can observe
and operate other apps. It stays off until you review its access and enable it
([guide](crates/tui/plugins/computer-use/README.md)).
([guide](https://github.com/codewhale-hq/codewhale-cu-plugin)).
## Ways to run it
+10 -5
View File
@@ -102,12 +102,17 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
```
## DeepSeek Harness utility packages (`dsh-util-values`, `dsh-tools`) — MIT
## DeepSeek Harness utility packages (`dsh-util-values`, `dsh-tools`, message compatibility) — MIT
The same bundle contains `@deepseek-ai/dsh-util-values` 0.1.7-alpha.2, and
`crates/tui/extension-host/src/dsh/dsh-tools-compat.js` contains verbatim
excerpts of `@deepseek-ai/dsh-tools` 0.1.7-alpha.2 (`lib/index.js`, lines 8-888
and 2528-2558), under the same licence text.
excerpts of `@deepseek-ai/dsh-tools` 0.2.1-rc.2 (`lib/index.js`), under the
same licence text. `crates/tui/extension-host/src/dsh/dsh-message-compat.js`
contains verbatim excerpts of `@deepseek-ai/dsh-brand` 0.2.1-rc.2,
`@deepseek-ai/dsh-llm` 0.2.1-rc.2, `@deepseek-ai/dsh-session` 0.2.1-rc.2,
`@deepseek-ai/dsh-system-prompt` 0.2.1-rc.2,
`@deepseek-ai/dsh-user-questions` 0.2.1-rc.2 and
`@deepseek-ai/dsh-util-crypto` 0.2.1-rc.2, under the same licence text.
Upstream: <https://github.com/deepseek-ai/deepseek-harness>
(`dsh-util-values` is published from `packages/util/values`)
@@ -148,7 +153,7 @@ carry the upstream licensing-transition statement and full Apache-2.0 text
reproduced below. The other packages are labeled MIT. Their exact distributed
notices are preserved.
### `@modelcontextprotocol/client` 2.3.1
### `@modelcontextprotocol/client` 2.4.0
```text
The MCP project is undergoing a licensing transition from the MIT License to the Apache License, Version 2.0 ("Apache-2.0"). All new code and specification contributions to the project are licensed under Apache-2.0. Documentation contributions (excluding specifications) are licensed under CC-BY-4.0.
@@ -369,7 +374,7 @@ CC-BY-4.0. See https://creativecommons.org/licenses/by/4.0/legalcode for
the full license text.
```
### `@modelcontextprotocol/core` 2.3.1
### `@modelcontextprotocol/core` 2.4.0
```text
The MCP project is undergoing a licensing transition from the MIT License to the Apache License, Version 2.0 ("Apache-2.0"). All new code and specification contributions to the project are licensed under Apache-2.0. Documentation contributions (excluding specifications) are licensed under CC-BY-4.0.
+7 -4
View File
@@ -5,13 +5,13 @@ inputs:
description: Exact released CLI tag (vX.Y.Z). Must support review --json.
required: true
provider:
description: codewhale for an account machine key, or an explicit BYOK provider.
default: codewhale
description: Explicit BYOK provider (orcarouter, deepseek, anthropic, openrouter, zai, modelstudio-token-plan), or codewhale for a separately verified account machine key.
required: true
model:
description: Exact model ID. Account mode requires provider/model from the account catalog.
required: true
github-token:
description: GitHub token with contents read and pull-requests write.
description: GitHub token with contents read and pull-requests read; legacy post:true also requires pull-requests write.
default: ${{ github.token }}
pr-number:
description: PR number for workflow_dispatch; pull_request events use the event snapshot.
@@ -27,7 +27,7 @@ inputs:
description: Model and publication deadline, in seconds (30–1200).
default: '600'
post:
description: Publish a COMMENT review; never approve or request changes.
description: Legacy direct COMMENT publication. Set false to export findings for a separate publisher job.
default: 'true'
outputs:
outcome:
@@ -36,6 +36,9 @@ outputs:
receipt:
description: Absolute path to the sanitized JSON execution receipt (also written on failures).
value: ${{ steps.review.outputs.receipt }}
findings:
description: Absolute path to validated findings JSON for a complete post:false review; contains repository-sensitive review text.
value: ${{ steps.review.outputs.findings }}
pr-url:
description: URL of the pull request, when resolved.
value: ${{ steps.review.outputs.pr-url }}
+31 -19
View File
@@ -168,11 +168,25 @@ sandbox_mode = "workspace-write" # read-only | workspace-write | danger-full-acc
# "muse", or "all". Env: CODEWHALE_PROJECT_INSTRUCTION_IMPORTS (comma-separated).
# project_instruction_imports = ["claude"]
#
# Everything that reaches the model as standing project instruction authority —
# the repository-root -> workspace AGENTS.md chain, the global fallback layer,
# .codewhale/rules/*.md, and any imported foreign files — shares one 48 KiB
# aggregate budget. Instructions claim it first and are trimmed from the
# broadest scope inward, so the nearest-scope file is the last thing dropped.
# Standing instructions are budgeted per scope, in estimated tokens, and every
# trim or skip is reported to the model and in /context.
# `global` covers ~/.codewhale/AGENTS.md (or ~/.agents/AGENTS.md), the global
# instructions.md fallbacks, ~/.claude/CLAUDE.md (last resort, claude_fallback
# only) and the `instructions = [...]` array. `project` covers the
# repository-root -> workspace AGENTS.md chain, co-read foreign files, and
# .codewhale/rules/*.md. A scope is capped at the smaller of its absolute token
# budget and its share of the usable window (context window minus the output
# reservation and a 10% safety reserve). Each file keeps at most its first
# 100 KiB. User config only: a cloned repository cannot change it.
# [instruction_budget]
# global_budget_tokens = 100000 # positive; absolute cap for the global scope
# project_budget_tokens = 100000 # positive; absolute cap for the project scope
# global_budget_percent = 25 # 1..=100, share of the usable window
# project_budget_percent = 25 # 1..=100, share of the usable window
# static_prefix_guard_percent = 80 # 1..=100; guard must leave >=10 points
# file_max_bytes = 8388608 # 1..=8388608; larger files are skipped
# policy = "trim" # trim | refuse
# claude_fallback = false # opt in: AGENTS.md wins; else CLAUDE.md
# prompt_suggestion = true # opt-in: show ghost-text follow-up question in composer after each turn
# Optional tab/window title shown as `[title] …` in front of the terminal
@@ -1267,20 +1281,18 @@ code_mode = true # execute_tools composes MCP/plugin/native calls; false defers
# a prompt under Full Access, under Bypass, or with a
# matching session grant, which is bound to the
# plugin's reviewed build (an update asks again).
# Sandbox: on macOS the host runs under Seatbelt, and
# on Linux under bubblewrap (/usr/bin/bwrap) when a
# launch-time probe shows it works. Either way: no
# direct network, no reads of ~/.codewhale secrets/
# credentials/config/sessions, and writes only to its
# data dir and temp dirs (plus, on macOS, the Darwin
# user cache, ~/.cargo/registry, ~/.cargo/git and the
# npm cache, which every Seatbelt-sandboxed command
# gets). If bwrap is missing or cannot start (for
# example blocked unprivileged user namespaces), the
# host runs UNSANDBOXED with your user permissions and
# /plugin, `codewhale doctor` and the start diagnostic
# say so with bwrap's own error. On Windows it always
# runs with your user permissions.
# Native code requires a verified OS sandbox: Seatbelt
# on macOS, bubblewrap on Linux, and LPAC/Job on Windows.
# An unavailable Native sandbox refuses activation.
# Unix reads are limited to the selected runtime and
# libraries, canonical host bundle, captured Core-managed
# plugin stores and Native data. Writes stay in Native
# data and private scratch; repository reads and direct
# network are denied. Native plugins share one trusted
# cohort, not separate per-plugin OS sandboxes.
# On macOS a detached child can outlive process-group
# cancellation while retaining the sandbox restrictions.
# Builtin host policy is separate and diagnosed separately.
# Toggling this changes the plugin activation policy,
# so every plugin must be re-reviewed afterwards.
-2
View File
@@ -14,8 +14,6 @@ workspace = true
test-support = []
[dependencies]
codewhale-config = { path = "../config", version = "0.10.2" }
codewhale-execpolicy = { path = "../execpolicy", version = "0.10.2" }
codewhale-protocol = { path = "../protocol", version = "0.10.2" }
codewhale-workflow = { path = "../workflow", version = "0.10.2" }
serde = { workspace = true, features = ["derive"] }
+10 -30
View File
@@ -20,8 +20,8 @@
//! ledger / executor / worker_runtime / roster / profile / identity /
//! members, and nothing from tools.
use codewhale_protocol::fleet::FleetEffectivePermissions;
use codewhale_workflow::{PermissionCeiling, ShellCeiling};
use codewhale_protocol::fleet::{FleetEffectivePermissions, migrate_legacy_role_token};
use codewhale_protocol::fleet::{PermissionCeiling, ShellCeiling};
use crate::worker_profile::{ShellPolicy, ToolScope, WorkerRuntimeProfile};
@@ -40,7 +40,7 @@ pub const FLEET_ROLE_SCHEMA_VALUES: [&str; 8] = [
"custom",
];
/// Human-readable hint listing every token [`FleetRole::from_str`] accepts,
/// Human-readable hint listing every token [`FleetRole::parse`] accepts,
/// for spawn-time error messages. Keep in sync with
/// [`migrate_legacy_role_token`] and the `from_str` match arms; those two are
/// the only role parser (#2649 was a second table in the spawn tool drifting
@@ -114,38 +114,18 @@ impl<'de> serde::Deserialize<'de> for FleetRole {
D: serde::Deserializer<'de>,
{
let raw = String::deserialize(deserializer)?;
Self::from_str(&raw)
Self::parse(&raw)
.ok_or_else(|| serde::de::Error::unknown_variant(&raw, &FLEET_ROLE_SCHEMA_VALUES))
}
}
/// Explicit boundary migration for pre-Fleet serialized role tokens.
///
/// Call this only at load / parse edges. Runtime code must use Fleet role
/// names via [`FleetRole::as_str`]. Returns `None` for tokens that are
/// already canonical or unknown — callers should prefer [`FleetRole::from_str`]
/// for full acceptance (canonical + legacy).
#[must_use]
pub fn migrate_legacy_role_token(token: &str) -> Option<&'static str> {
match token.trim().to_ascii_lowercase().as_str() {
"worker" | "general-purpose" | "general_purpose" | "default" => Some("general"),
"scout" | "exploration" | "explorer" => Some("explore"),
"plan" | "planning" | "awaiter" => Some("planner"),
"review" | "code-review" | "code_review" => Some("reviewer"),
"builder" | "implementer" | "implementation" => Some("implement"),
"verifier" | "verify" | "verification" | "validator" | "tester" => Some("test"),
"consultant" | "oracle" => Some("advisor"),
_ => None,
}
}
impl FleetRole {
/// Parse a Fleet role from user input or a serialized boundary.
///
/// Accepts Fleet role names and, at this parse boundary only, legacy
/// aliases (`scout` → explore, `plan` → planner, …).
#[must_use]
pub fn from_str(s: &str) -> Option<Self> {
pub fn parse(s: &str) -> Option<Self> {
let normalized = s.trim().to_ascii_lowercase();
// Boundary migration first, then canonical Fleet names.
let token = migrate_legacy_role_token(&normalized).unwrap_or(normalized.as_str());
@@ -222,7 +202,7 @@ impl FleetRole {
/// anything else passes through trimmed.
#[must_use]
pub fn public_role_label(token: &str) -> String {
FleetRole::from_str(token).map_or_else(
FleetRole::parse(token).map_or_else(
|| token.trim().to_string(),
|role| role.as_str().to_string(),
)
@@ -288,7 +268,7 @@ pub const NETWORK_TOOL_DENYLIST: &[&str] = &[
// contract was considered and rejected: `rlm_open` chooses its source from
// *input fields* (`file_path` / `content` / `url` / `session_object`), not
// from the action name, and the action-policy seam
// (`crate::tools::canonical_action`) resolves names, not field shapes —
// (`codewhale_execpolicy::canonical_action`) resolves names, not field shapes —
// it cannot prove a source is local before execution. So this fails closed.
// A network-denied member loses `rlm` loading and evaluation entirely,
// including the purely local `file_path` form, and keeps only the bounded
@@ -659,7 +639,7 @@ impl ChildAuthority {
/// model-facing spawn vocabulary.
///
/// Deliberately *not* folded into [`migrate_legacy_role_token`]. That table
/// feeds [`FleetRole::from_str`], which is also the closed vocabulary the
/// feeds [`FleetRole::parse`], which is also the closed vocabulary the
/// `agent` tool validates its `type` against, and — decisively — the test the
/// spawn parser uses to decide whether a `role` string is a posture alias or a
/// roster profile key. Teaching `from_str` about `manager` or `smoke-runner`
@@ -705,7 +685,7 @@ fn member_role_alias(token: &str) -> Option<FleetRole> {
/// Resolution order, and nothing else:
/// 1. an empty label means *unspecified*, which is the documented general
/// default a Fleet task without a `role` has always had;
/// 2. [`FleetRole::from_str`] — the declared closed vocabulary plus its
/// 2. [`FleetRole::parse`] — the declared closed vocabulary plus its
/// documented legacy aliases ([`VALID_ROLE_ALIASES`]);
/// 3. `member_role_alias` — the org-chart/slot spellings above;
/// 4. **fail closed.**
@@ -723,7 +703,7 @@ pub fn runtime_role_for_member(role: &str) -> FleetRole {
if token.is_empty() {
return FleetRole::Worker;
}
FleetRole::from_str(&token)
FleetRole::parse(&token)
.or_else(|| member_role_alias(&token))
.unwrap_or(FleetRole::Scout)
}
+1 -1
View File
@@ -463,7 +463,7 @@ impl WorkerRuntimeProfile {
// wins via `derive_child`, same as every other role.
reasoning_effort: matches!(role, FleetRole::Consultant).then(|| "high".to_string()),
denied_tools: Vec::new(),
max_spawn_depth: codewhale_config::DEFAULT_SPAWN_DEPTH,
max_spawn_depth: codewhale_protocol::fleet::DEFAULT_SPAWN_DEPTH,
spawn_depth: 0,
wall_time_secs: None,
wall_deadline_ms: None,
+2 -2
View File
@@ -1131,7 +1131,7 @@ mod tests {
for (provider, id) in [
(ProviderKind::Zai, "GLM-5.2"),
(ProviderKind::Stepfun, "step-3.7-flash"),
(ProviderKind::Stepfun, "step-5-preview"),
(ProviderKind::Minimax, "MiniMax-M2.1"),
(ProviderKind::MinimaxAnthropic, "MiniMax-M3"),
(ProviderKind::Openmodel, "deepseek-v4-flash"),
@@ -1307,7 +1307,7 @@ mod tests {
let stepfun = registry.resolve_ok(None, Some(ProviderKind::Stepfun));
assert_eq!(stepfun.resolved.provider, ProviderKind::Stepfun);
assert_eq!(stepfun.resolved.id, "step-3.7-flash");
assert_eq!(stepfun.resolved.id, "step-5-preview");
for (alias, expected) in [
("minimax", "MiniMax-M3"),
+19 -2
View File
@@ -2519,14 +2519,31 @@ impl RuntimeBridge {
});
}
emit_stdio_event(
if let Err(error) = emit_stdio_event(
writer,
json!({
"type": "response_start",
"response_id": response_id,
}),
)
.await?;
.await
{
let orphan = InFlightTurn {
base_url: self.base_url.clone(),
auth_token: self.auth_token.clone(),
runtime_thread_id: thread_id.to_string(),
turn_id: turn_id.clone(),
};
if let Err(interrupt) = interrupt_turn_request(&orphan).await {
tracing::warn!(
thread_id,
turn_id = %turn_id,
"failed to interrupt a turn whose response_start could not be written: {}",
interrupt.message
);
}
return Err(error);
}
// Publish the turn only for the streaming window, and take it back
// before any `?` below: a turn that has already finished must never
@@ -111,59 +111,22 @@ fn cached_build_scripts_classify_the_current_manifest_and_require_it() {
}
}
#[cfg(unix)]
#[test]
fn macos_helper_compilation_follows_the_relocated_manifest() {
use std::os::unix::fs::PermissionsExt;
fn tui_build_does_not_compile_or_embed_computer_use() {
let fixture = Fixture::new();
let original = fixture.0.path().join("original manifest");
let relocated = fixture.0.path().join("relocated manifest");
let source = Path::new("plugins/computer-use/src/backends/darwin-accessibility.m");
std::fs::create_dir_all(original.join(source).parent().unwrap()).unwrap();
std::fs::write(original.join(source), "fixture source").unwrap();
let executable = fixture.compile("tui", include_str!("../../tui/build.rs"), &original);
std::fs::rename(&original, &relocated).unwrap();
let manifest = fixture.0.path().join("manifest");
std::fs::create_dir(&manifest).unwrap();
let executable = fixture.compile("tui", include_str!("../../tui/build.rs"), &manifest);
let out = fixture.0.path().join("out");
let bin = fixture.0.path().join("bin");
std::fs::create_dir(&out).unwrap();
std::fs::create_dir(&bin).unwrap();
// Exercise the real build-script command arguments without requiring a macOS
// SDK or accessing a signing identity. A missing source fails as clang did.
for (name, script) in [
(
"xcrun",
"#!/bin/sh\nset -eu\nsource=\noutput=\nwhile [ \"$#\" -gt 0 ]; do\ncase \"$1\" in *.m) source=$1 ;; -o) shift; output=$1 ;; esac\nshift\ndone\ntest -f \"$source\"\nprintf %s \"$source\" > \"$output\"\n",
),
(
"codesign",
"#!/bin/sh\nset -eu\nfor last in \"$@\"; do :; done\ntest -s \"$last\"\n",
),
] {
let path = bin.join(name);
std::fs::write(&path, script).unwrap();
std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o755)).unwrap();
}
let mut paths = vec![bin];
paths.extend(std::env::split_paths(
&std::env::var_os("PATH").unwrap_or_default(),
));
let output = checked(
run_script(&executable, &relocated, fixture.0.path())
run_script(&executable, &manifest, fixture.0.path())
.env("CARGO_CFG_TARGET_OS", "macos")
.env("CARGO_CFG_TARGET_ARCH", "aarch64")
.env("OUT_DIR", &out)
.env("PATH", std::env::join_paths(paths).unwrap())
.env_remove("CODEWHALE_CU_SIGN_IDENTITY"),
);
assert_eq!(
std::fs::read_to_string(out.join("computer-use-accessibility")).unwrap(),
relocated.join(source).to_string_lossy()
.env("OUT_DIR", &out),
);
assert_eq!(std::fs::read_dir(out).unwrap().count(), 0);
let directives = String::from_utf8_lossy(&output.stdout);
assert!(directives.contains(&format!(
"cargo:rerun-if-changed={}\n",
relocated.join(source).display()
)));
assert!(!directives.contains(original.to_string_lossy().as_ref()));
assert!(!directives.contains("computer-use"));
assert!(!directives.contains("CODEWHALE_CU_SIGN_IDENTITY"));
}
+1
View File
@@ -24,6 +24,7 @@ name = "codewhale"
path = "src/main.rs"
[dependencies]
codewhale-runtime = { path = "../runtime", version = "0.10.2" }
anyhow.workspace = true
clap.workspace = true
clap_complete.workspace = true
+70 -11
View File
@@ -314,15 +314,14 @@ pub(super) fn run<T: CloudTransport, W: Write>(
} else {
Value::Null
};
if let Some(id) = &options.thread {
if thread["id"].as_str() != Some(id)
if let Some(id) = &options.thread
&& (thread["id"].as_str() != Some(id)
|| thread["userId"].as_str() != Some(&account.account)
|| thread["archivedAt"]
.as_str()
.is_some_and(|value| !value.is_empty())
{
bail!("This account conversation is unavailable.");
}
.is_some_and(|value| !value.is_empty()))
{
bail!("This account conversation is unavailable.");
}
let whale_id = options.whale.clone().or_else(|| {
thread["agentId"]
@@ -537,6 +536,65 @@ pub(super) fn run<T: CloudTransport, W: Write>(
"key": local.api_key.as_deref(), "expectedBaseUrl": local.base_url }),
).context("The computer could not confirm your model connection. Check that its provider endpoint matches your configured model")?;
}
let mut member_connections = Vec::new();
if use_local_model {
let lead = thread["agentId"].as_str().unwrap_or("");
let mut connected = vec![provider.to_string()];
for participant in thread["group"]["participants"]
.as_array()
.into_iter()
.flatten()
.filter(|row| row["state"] == "active" && row["agentId"].as_str() != Some(lead))
{
let id = participant["agentId"]
.as_str()
.context("A group member has no whale ID. Refresh the group.")?;
validate_resource_id(id, "Whale")?;
let name = printable(participant["name"].as_str().unwrap_or("A member"));
let agent = account.request(HttpMethod::Get, &format!("/api/agents/{id}"), None)?;
let route = &agent["agent"]["configuration"]["model"];
let (Some(member_provider), Some(member_model)) =
(route["provider"].as_str(), route["model"].as_str())
else {
bail!("{name} has no saved model. Give it a configured model in Codewhale.");
};
if agent["agent"]["id"].as_str() != Some(id) {
bail!("The account returned a different whale for {name}.");
}
validate_model_route(member_provider, member_model)?;
if connected.iter().any(|done| done == member_provider) {
continue;
}
let kind =
codewhale_config::ProviderKind::parse(member_provider).with_context(|| {
format!("Configure the {member_provider} provider in Codewhale for {name}.")
})?;
let member_local = config.config.resolve_runtime_options_with_secrets(
&codewhale_config::CliRuntimeOverrides {
provider: Some(kind),
model: Some(member_model.to_string()),
..Default::default()
},
secrets,
);
portable_model(&member_local, config)
.with_context(|| format!("{name} uses {member_provider}"))?;
account
.operation(
thread_id,
&computer,
&runtime,
"key",
json!({ "provider": member_provider,
"key": member_local.api_key.as_deref(), "expectedBaseUrl": member_local.base_url }),
)
.with_context(|| {
format!("The computer could not confirm the {member_provider} connection for {name}")
})?;
connected.push(member_provider.to_string());
member_connections.push(json!({ "agentId": id, "provider": member_provider }));
}
}
if let Some(key) = key {
validate_api_key(&key)?;
account.operation(
@@ -579,14 +637,15 @@ pub(super) fn run<T: CloudTransport, W: Write>(
}
reply
} else if options.prepare_model {
json!({ "prepared": true, "provider": provider, "model": model, "backend": "memory" })
json!({ "prepared": true, "provider": provider, "model": model, "backend": "memory",
"members": member_connections })
} else {
detail
};
if let Some(turn) = result.get("turn") {
if turn["thread_id"].as_str() != Some(&runtime) {
bail!("The computer returned a different task");
}
if let Some(turn) = result.get("turn")
&& turn["thread_id"].as_str() != Some(&runtime)
{
bail!("The computer returned a different task");
}
if options.json {
writeln!(
+53 -17
View File
@@ -465,6 +465,21 @@ The command prints the completion script to stdout; redirect it to a path your s
after_help = "GitHub Releases is the default source. Supported mirrors are explicit overrides or manifest-failure fallbacks. Checksums are required; older releases never replace a newer build.\n\nThe command prints the executable it will update. If you have multiple installs, run the intended binary by its full path.\n\nNew macOS/Linux install: curl -fsSL https://codewhale.net/install.sh | sh\nInstallation and PATH help: https://github.com/codewhale-hq/CodeWhale/blob/main/docs/INSTALL.md"
)]
Update(UpdateArgs),
#[command(
about = "Remove this receipt-owned release install, or print its package manager's removal command; user data is kept unless --purge-data"
)]
Uninstall {
#[arg(
long,
help = "Remove the preflighted files; without this flag, print the plan"
)]
yes: bool,
#[arg(
long,
help = "Also delete Codewhale's own data directory (config, secrets file, sessions); previewed unless --yes"
)]
purge_data: bool,
},
/// Export the route catalog (`providers export --json`).
Providers(ProvidersArgs),
}
@@ -2282,6 +2297,17 @@ fn run() -> Result<()> {
|| matches!(cli.command.as_ref(), Some(Commands::AppServer(args)) if args.http),
"An inherited Runtime listener requires serve --http or app-server --http"
);
if let Some(Commands::Uninstall { yes, purge_data }) = cli.command.as_ref() {
#[cfg(any(all(unix, not(target_env = "ohos")), windows))]
return update::uninstall::run(*yes, *purge_data);
#[cfg(not(any(all(unix, not(target_env = "ohos")), windows)))]
{
let _ = (yes, purge_data);
bail!(
"Automatic uninstall is supported for receipt-owned macOS, Linux and Windows release installs. Use the installer or package manager that owns this copy; user data is preserved."
);
}
}
preserve_exec_separator(&mut cli, &argv);
capture_exec_startup_options(&mut cli)?;
capture_serve_full_access(&mut cli);
@@ -2773,6 +2799,9 @@ fn run() -> Result<()> {
finish_cli_telemetry(session, &outcome, cli.verbose);
outcome
}
Some(Commands::Uninstall { .. }) => {
unreachable!("uninstall dispatches before config loading")
}
Some(Commands::Providers(args)) => run_providers_command(args),
None => {
let resolved_runtime = resolve_runtime_for_dispatch(&mut store, &runtime_overrides);
@@ -2993,7 +3022,11 @@ fn capture_serve_full_access(cli: &mut Cli) {
return;
};
let before = args.args.len();
let separator = args.args.iter().position(|arg| arg == "--").unwrap_or(before);
let separator = args
.args
.iter()
.position(|arg| arg == "--")
.unwrap_or(before);
let mut index = 0;
args.args.retain(|arg| {
index += 1;
@@ -4400,7 +4433,7 @@ fn owned_oauth_account(store: &ConfigStore, provider: ProviderKind) -> Result<Op
.oauth_credential_generation
.as_deref()
.context("no sign-in generation configured")?;
codewhale_tui::owned_oauth_account_label(provider, generation)
codewhale_runtime::oauth::owned_oauth_account_label(provider, generation)
}
#[cfg(test)]
@@ -5411,8 +5444,8 @@ fn run_config_command(
}
// A settings.toml key is answered from settings.toml, even when a
// stale config.toml copy that nothing reads is still present.
if codewhale_tui::config_keys::config_key_home(&key)
== codewhale_tui::config_keys::ConfigKeyHome::SettingsToml
if codewhale_runtime::config_keys::config_key_home(&key)
== codewhale_runtime::config_keys::ConfigKeyHome::SettingsToml
{
let value = settings_key_value(store, &key)?;
note_unread_config_copy(store, &key);
@@ -5420,7 +5453,7 @@ fn run_config_command(
return Ok(());
}
if key == "stream" || key.starts_with("stream.") {
let stream = codewhale_tui::config_keys::resolved_stream_config(&store.config)?;
let stream = codewhale_runtime::config_keys::resolved_stream_config(&store.config)?;
let value = if key == "stream" {
&stream
} else {
@@ -5462,11 +5495,11 @@ fn run_config_command(
}
// Refuse a key nothing reads, and send settings.toml keys to
// settings.toml, before config.toml is touched (#6563).
match codewhale_tui::config_keys::config_key_home(&key) {
codewhale_tui::config_keys::ConfigKeyHome::ConfigToml => {
match codewhale_runtime::config_keys::config_key_home(&key) {
codewhale_runtime::config_keys::ConfigKeyHome::ConfigToml => {
// A value typed or validated by its config.toml reader.
if let Some(typed) =
codewhale_tui::config_keys::config_toml_value(&key, &value)?
codewhale_runtime::config_keys::config_toml_value(&key, &value)?
{
store.config.extras.insert(key.trim().to_string(), typed);
store.save()?;
@@ -5474,15 +5507,17 @@ fn run_config_command(
return Ok(());
}
}
codewhale_tui::config_keys::ConfigKeyHome::SettingsToml => {
codewhale_runtime::config_keys::ConfigKeyHome::SettingsToml => {
refuse_workspace_scoped_settings_key(store, &key)?;
let path = codewhale_tui::config_keys::set_settings_value(&key, &value)?;
let path = codewhale_runtime::config_keys::set_settings_value(&key, &value)?;
println!("set {key} in {}", path.display());
note_unread_config_copy(store, &key);
return Ok(());
}
codewhale_tui::config_keys::ConfigKeyHome::Unknown => {
bail!(codewhale_tui::config_keys::unknown_config_key_message(&key));
codewhale_runtime::config_keys::ConfigKeyHome::Unknown => {
bail!(codewhale_runtime::config_keys::unknown_config_key_message(
&key
));
}
}
store.config.set_value(&key, &value)?;
@@ -5591,7 +5626,7 @@ fn run_config_command(
.context("config must be a TOML table")?
.insert(
"stream".to_string(),
codewhale_tui::config_keys::resolved_stream_config(&store.config)?,
codewhale_runtime::config_keys::resolved_stream_config(&store.config)?,
);
print!("{}", toml::to_string_pretty(&document)?);
Ok(())
@@ -5681,7 +5716,8 @@ fn refuse_workspace_scoped_settings_key(store: &ConfigStore, key: &str) -> Resul
/// never a config.toml copy that nothing reads.
fn settings_key_value(store: &ConfigStore, key: &str) -> Result<String> {
refuse_workspace_scoped_settings_key(store, key)?;
codewhale_tui::config_keys::settings_value(key)?.ok_or_else(|| anyhow!("key not found: {key}"))
codewhale_runtime::config_keys::settings_value(key)?
.ok_or_else(|| anyhow!("key not found: {key}"))
}
/// Point at a config.toml copy of a settings.toml key: nothing reads it.
@@ -5720,7 +5756,7 @@ fn apply_per_run_overrides(store: &mut ConfigStore, specs: &[String]) -> Result<
fn run_config_doctor(store: &ConfigStore) -> Result<()> {
println!("# {}", store.path().display());
let mut errors: Vec<String> = Vec::new();
let unread = codewhale_tui::config_keys::unread_config_keys(
let unread = codewhale_runtime::config_keys::unread_config_keys(
store.config.extras.keys().map(String::as_str),
);
for finding in &unread {
@@ -7221,7 +7257,7 @@ mod tests {
&overlays,
)
.unwrap();
let stream = codewhale_tui::config_keys::resolved_stream_config(&store.config).unwrap();
let stream = codewhale_runtime::config_keys::resolved_stream_config(&store.config).unwrap();
assert_eq!(stream["open_timeout_secs"].as_integer(), Some(120));
assert_eq!(stream["max_resumes"].as_integer(), Some(6));
assert_eq!(
@@ -7243,7 +7279,7 @@ mod tests {
"zzz_unknown = 1\ncalm_mode = \"false\"\nmax_subagents = 4\n",
);
let store = ConfigStore::load(Some(path)).expect("load fixture");
let unread = codewhale_tui::config_keys::unread_config_keys(
let unread = codewhale_runtime::config_keys::unread_config_keys(
store.config.extras.keys().map(String::as_str),
);
assert_eq!(unread.len(), 2, "{unread:#?}");
+35 -5
View File
@@ -29,6 +29,8 @@ use std::time::Duration;
use crate::hashing::sha256_hex;
mod compiled_host;
#[cfg(any(unix, windows))]
pub(crate) mod uninstall;
const GITHUB_LATEST_RELEASE_PAGE_URL: &str =
"https://github.com/codewhale-hq/CodeWhale/releases/latest";
@@ -62,10 +64,6 @@ pub fn run_update(beta: bool, check_only: bool, proxy_arg: Option<String>) -> Re
let channel = ReleaseChannel::from_beta_flag(beta);
let current_version = env!("CARGO_PKG_VERSION");
let build_version = env!("CODEWHALE_BUILD_VERSION");
// The build script marks an unstamped checkout with `(dev)` and nothing
// else: a release artifact carries its short SHA and a Cargo source
// package carries the plain version. Only the checkout marker means the
// installed file has no published artifact behind it.
let unreleased_checkout_build = build_version.ends_with(" (dev)");
let proxy = proxy_arg
.as_deref()
@@ -209,6 +207,10 @@ pub fn run_update(beta: bool, check_only: bool, proxy_arg: Option<String>) -> Re
// Step 4: Replace command paths only after the download and the running
// executable identity verify. The preflight happens before a colocated
// compatibility path can change, then the identity is checked just in time.
#[cfg(unix)]
let mut command_receipt = uninstall::CommandReceipt::capture(&current_exe)?;
#[cfg(windows)]
let had_receipt = uninstall::CommandReceipt::capture(&current_exe)?.is_some();
let mut host_update = compiled_host::prepare(
&download,
latest_tag,
@@ -226,13 +228,38 @@ pub fn run_update(beta: bool, check_only: bool, proxy_arg: Option<String>) -> Re
validate_update_target(target, &executable_identity)
})
})();
#[cfg(unix)]
let receipt_renewal = command_receipt
.as_mut()
.map(|receipt| receipt.renew(&bytes))
.transpose();
#[cfg(windows)]
let receipt_renewal = if had_receipt {
uninstall::CommandReceipt::capture_accepting(&current_exe, Some(&sha256_hex(&bytes)))
.and_then(|receipt| {
receipt
.context("command ownership receipt disappeared during update")?
.renew(&bytes)
})
.map(Some)
} else {
Ok(None)
};
if let Err(error) = replaced {
if let Some(host) = &mut host_update {
host.rollback()
.context("compiled-host rollback failed; retained backup path is reported below")?;
}
#[cfg(any(unix, windows))]
if let Err(renewal) = receipt_renewal {
return Err(error.context(format!(
"command ownership receipt could not be renewed after partial update: {renewal:#}"
)));
}
return Err(error);
}
#[cfg(any(unix, windows))]
receipt_renewal.context("binaries updated, but command ownership receipt renewal failed; preserved receipt/recovery paths require attention")?;
if let Some(host) = &host_update {
println!(
"Updated the qualified compiled image, notices and relink source beside this CLI; Node remains default."
@@ -1304,7 +1331,10 @@ fn parse_checksum_manifest(text: &str) -> Result<HashMap<String, String>> {
bail!("invalid SHA256 manifest line {}: {trimmed}", index + 1);
}
let (hash, rest) = trimmed.split_at(64);
let (hash, rest) = trimmed
.get(..64)
.zip(trimmed.get(64..))
.with_context(|| format!("invalid SHA256 manifest line {}: {trimmed}", index + 1))?;
if !hash.chars().all(|ch| ch.is_ascii_hexdigit())
|| rest.is_empty()
|| !rest.chars().next().is_some_and(char::is_whitespace)
+39
View File
@@ -265,6 +265,45 @@ impl Prepared {
}
}
#[cfg(any(unix, windows))]
pub(super) fn owned_for_uninstall(directory: &Path, target: &str) -> Result<Vec<GuardedFile>> {
let Some(mut receipt) = GuardedFile::open_bounded(directory, RECEIPT, 64 * 1024)? else {
return Ok(Vec::new());
};
receipt.verify_owned()?;
let catalog = parse(&receipt.read_bounded(64 * 1024)?, env!("CARGO_PKG_VERSION"))?;
let host = catalog
.hosts
.iter()
.find(|host| host.target == target)
.context("installed compiled-host receipt has no matching target")?;
let mut files = Vec::new();
let host_name = format!("codewhale-extension-host{}", std::env::consts::EXE_SUFFIX);
for (name, expected) in [
(host_name.as_str(), host.sha256.as_str()),
(
"codewhale-extension-host.LICENSES.txt",
host.notices_sha256.as_str(),
),
(
"codewhale-extension-host.relink-source.tar.gz",
host.source_sha256.as_str(),
),
] {
if let Some(file) = GuardedFile::open(directory, name)? {
if file.sha256() != expected {
bail!(
"Preserved changed compiled-host file: {}",
directory.join(name).display()
);
}
files.push(file);
}
}
files.push(receipt);
Ok(files)
}
pub(super) fn required_for(current_exe: &Path) -> Result<bool> {
let directory = current_exe
.parent()
+492
View File
@@ -0,0 +1,492 @@
use super::*;
use codewhale_tui::delivery_files::GuardedFile;
use std::collections::BTreeMap;
const RECEIPT: &str = ".codewhale-install.sha256";
const MAX_RECEIPT: usize = 512;
#[cfg(windows)]
const COMMANDS: &[&str] = &["codewhale.exe", "codew.exe", "codewhale.bat"];
#[cfg(not(windows))]
const COMMANDS: &[&str] = &["codewhale", "codew"];
pub(super) struct CommandReceipt {
directory: PathBuf,
receipt: GuardedFile,
hashes: BTreeMap<String, String>,
}
impl CommandReceipt {
pub(super) fn capture(executable: &Path) -> Result<Option<Self>> {
Self::capture_accepting(executable, None)
}
pub(super) fn capture_accepting(
executable: &Path,
updated: Option<&str>,
) -> Result<Option<Self>> {
let directory = executable.parent().context("executable has no parent")?;
#[cfg(not(windows))]
let directory = directory.canonicalize()?;
#[cfg(windows)]
let directory = directory.to_path_buf();
let Some(mut receipt) = GuardedFile::open_bounded(&directory, RECEIPT, MAX_RECEIPT)? else {
return Ok(None);
};
receipt.verify_owned()?;
let bytes = receipt.read_bounded(MAX_RECEIPT)?;
let text = std::str::from_utf8(&bytes).context("command receipt is not UTF-8")?;
let hashes = parse_checksum_manifest(text)?;
if hashes.is_empty()
|| hashes.len() > COMMANDS.len()
|| text.lines().count() != hashes.len()
|| hashes.keys().any(|name| !COMMANDS.contains(&name.as_str()))
{
bail!(
"Invalid command ownership receipt {}; no files were changed",
directory.join(RECEIPT).display()
);
}
for (name, digest) in &hashes {
if let Some(mut file) = GuardedFile::open(&directory, name)? {
file.verify_owned()?;
if file.sha256() != digest.as_str() && Some(file.sha256()) != updated {
bail!(
"Preserved modified command {}; receipt no longer matches",
directory.join(name).display()
);
}
}
}
Ok(Some(Self {
directory,
receipt,
hashes: hashes.into_iter().collect(),
}))
}
pub(super) fn renew(&mut self, verified: &[u8]) -> Result<()> {
self.receipt.verify_owned()?;
let new_hash = sha256_hex(verified);
let mut text = String::new();
for (name, old_hash) in &self.hashes {
if let Some(mut file) = GuardedFile::open(&self.directory, name)? {
if file.sha256() != old_hash.as_str() && file.sha256() != new_hash.as_str() {
bail!(
"Cannot renew ownership of modified command {}",
self.directory.join(name).display()
);
}
file.verify_owned()?;
text.push_str(&format!("{} {name}\n", file.sha256()));
}
}
if text.is_empty() {
bail!("No receipt-owned commands remain; ownership receipt was preserved");
}
if self.receipt.read_bounded(MAX_RECEIPT)? == text.as_bytes() {
return Ok(());
}
self.receipt.verify_owned()?;
let mut staged = GuardedFile::stage(&self.directory, text.as_bytes(), false)?;
staged.verify_owned()?;
let backup = GuardedFile::recovery_name();
self.receipt.move_to_vacant(&backup).with_context(|| {
format!(
"receipt retirement failed; recovery at {}",
self.directory.join(self.receipt.name()).display()
)
})?;
if let Err(error) = staged.move_to_vacant(RECEIPT) {
let restored = self.receipt.move_to_vacant(RECEIPT);
bail!(
"receipt publication failed: {error}; restoration: {restored:?}; previous receipt {}, staged receipt {}",
self.directory.join(self.receipt.name()).display(),
self.directory.join(staged.name()).display()
);
}
println!(
"Renewed command ownership receipt; previous receipt retained at {}",
self.directory.join(backup).display()
);
self.receipt = staged;
Ok(())
}
}
pub(crate) fn run(yes: bool, purge_data: bool) -> Result<()> {
let selected = std::env::current_exe().ok();
let result = remove(yes);
report_path_copies(selected.as_deref());
result?;
if purge_data {
purge(yes)
} else {
println!(
"User data was not touched. To also delete Codewhale's own data directory, add --purge-data."
);
Ok(())
}
}
fn remove(yes: bool) -> Result<()> {
let identity = update_executable_identity()?;
let method = InstallMethod::detect(&identity.path);
println!("Selected executable: {}", identity.path.display());
println!(
"Program removal preserves user state, credentials, sessions, workspaces, shell profiles and services."
);
if !method.supports_self_update() {
println!(
"This copy belongs to {}. Remove it with: {}",
method.label(),
manager_command(method, &identity.path)
);
return Ok(());
}
if let Some(uninstaller) = windows_installer_uninstaller(&identity.path) {
println!(
"This copy belongs to the Codewhale Windows installer. Remove it from Settings > Apps > CodeWhale, or run: \"{}\"",
uninstaller.display()
);
return Ok(());
}
if protected_update_path(&identity.path) {
bail!(
"System/package path was preserved. Identify its owning package manager and remove it through that manager."
);
}
let primary_name = identity
.path
.file_name()
.and_then(|name| name.to_str())
.context("executable basename is not UTF-8")?
.to_string();
if !COMMANDS.contains(&primary_name.as_str()) {
bail!(
"This command name has no installer ownership contract; preserved {}",
identity.path.display()
);
}
let ownership = CommandReceipt::capture(&identity.path)?.with_context(|| format!(
"No command ownership receipt beside {}. This legacy/manual copy is preserved. Inspect its exact path and provenance before removing it manually; reinstalling into a fresh directory creates a receipted install.", identity.path.display()))?;
if ownership.hashes.get(&primary_name) != Some(&identity.file_hash) {
bail!("The receipt does not own the selected executable; nothing was removed");
}
let directory = ownership.directory.clone();
let mut files = Vec::new();
let mut primary = None;
for name in ownership.hashes.keys() {
if let Some(file) = GuardedFile::open(&directory, name)? {
if ownership.hashes.get(name).map(String::as_str) != Some(file.sha256()) {
bail!(
"Command changed during uninstall preflight; preserved {}",
directory.join(name).display()
);
}
if name == &primary_name {
primary = Some(file);
} else {
files.push(file);
}
}
}
let target =
release_asset_stem_for_prefix("codewhale", std::env::consts::OS, std::env::consts::ARCH);
let companions =
compiled_host::owned_for_uninstall(&directory, target.trim_start_matches("codewhale-"))?;
if companions.is_empty() {
println!("Any unreceipted compiled-host files are preserved.");
}
files.extend(companions);
let mut receipt = ownership.receipt;
let receipt_bytes = receipt.read_bounded(MAX_RECEIPT)?;
files.push(receipt);
files.push(primary.context("selected executable disappeared during preflight")?);
for file in &mut files {
file.verify_owned()?;
println!(
"{} {}",
if yes { "Remove" } else { "Would remove" },
directory.join(file.name()).display()
);
}
validate_primary_update_identity(&identity)?;
if !yes {
println!(
"No files were changed. Run this executable again with uninstall --yes to remove only this plan."
);
return Ok(());
}
#[cfg(unix)]
{
let preflight = tempfile::Builder::new()
.prefix(".codewhale-uninstall-preflight-")
.tempdir_in(&directory)?
.keep();
std::fs::remove_dir(&preflight).with_context(|| {
format!(
"uninstall directory preflight failed at {}",
preflight.display()
)
})?;
}
let mut removed = Vec::new();
for file in &mut files {
let name = file.name().to_string();
let mut unlinked = false;
let outcome = validate_primary_update_identity(&identity)
.and_then(|()| file.remove_checked(&mut unlinked).map_err(Into::into));
#[cfg(windows)]
let outcome = match outcome {
Err(error) if unlinked && name == primary_name => {
let retired = directory.join(file.name());
delete_after_exit(&retired)
.map(|()| {
println!(
"The running executable was retired to {}; it is deleted after this process exits. Delete it by hand if it remains.",
retired.display()
)
})
.map_err(|schedule| {
error.context(format!(
"deferred removal of {} could not be scheduled: {schedule:#}",
retired.display()
))
})
}
other => other,
};
if unlinked {
removed.push(name.clone());
}
if let Err(error) = outcome {
if removed.iter().any(|name| name == RECEIPT)
&& !removed.iter().any(|name| name == &primary_name)
{
let restore = (|| {
let mut staged = GuardedFile::stage(&directory, &receipt_bytes, false)?;
staged.verify_owned()?;
staged.move_to_vacant(RECEIPT).with_context(|| {
format!(
"receipt recovery bytes retained at {}",
directory.join(staged.name()).display()
)
})
})();
if let Err(restore) = restore {
eprintln!("Receipt recovery needs attention: {restore:#}");
}
}
bail!(
"Uninstall stopped at {name}: {error:#}. Already unlinked: {}. Any retained recovery locations are reported above; no unknown destination is overwritten.",
removed.join(", ")
);
}
}
#[cfg(windows)]
for entry in std::fs::read_dir(&directory)?.flatten() {
let name = entry.file_name().to_string_lossy().into_owned();
if name.starts_with("codewhale.old-") || name.starts_with("codew.old-") {
println!(
"Preserved a copy left by an earlier self-update; delete it by hand: {}",
entry.path().display()
);
}
}
println!(
"Removed this receipted release installation. Data and other installations are unchanged. {}",
if cfg!(windows) {
"Remove its directory from your user PATH if you added it, then open a new terminal."
} else {
"Run hash -r in your shell."
}
);
Ok(())
}
fn quoted(path: &Path) -> String {
format!("\"{}\"", path.display())
}
fn manager_command(method: InstallMethod, executable: &Path) -> String {
let real = std::fs::canonicalize(executable).unwrap_or_else(|_| executable.to_path_buf());
let names: Vec<_> = real.components().map(|part| part.as_os_str()).collect();
let position = |marker: &str| {
names
.iter()
.position(|name| name.eq_ignore_ascii_case(marker))
};
let prefix = |count: usize| names[..count].iter().collect::<PathBuf>();
match method {
InstallMethod::Cargo => match real.parent().and_then(Path::parent) {
Some(root) => format!("cargo uninstall codewhale-cli --root {}", quoted(root)),
None => method.uninstall_command().to_string(),
},
InstallMethod::Homebrew => {
match position("Cellar").and_then(|index| names.get(index + 1)) {
Some(formula) => format!("brew uninstall {}", formula.to_string_lossy()),
None => method.uninstall_command().to_string(),
}
}
InstallMethod::Npm => match position("node_modules") {
Some(index) if index > 0 => {
let mut root = prefix(index);
if !cfg!(windows) && root.file_name().is_some_and(|name| name == "lib") {
root.pop();
}
format!("npm uninstall -g codewhale --prefix {}", quoted(&root))
}
_ => method.uninstall_command().to_string(),
},
_ => method.uninstall_command().to_string(),
}
}
fn windows_installer_uninstaller(executable: &Path) -> Option<PathBuf> {
let bin = executable.parent()?;
if !bin
.file_name()
.is_some_and(|name| name.eq_ignore_ascii_case("bin"))
{
return None;
}
let root = bin.parent()?;
let uninstaller = root.join("Uninstall.exe");
[&uninstaller, &root.join("update-user-path.ps1")]
.iter()
.all(|path| std::fs::symlink_metadata(path).is_ok_and(|meta| meta.is_file()))
.then_some(uninstaller)
}
#[cfg(windows)]
fn delete_after_exit(path: &Path) -> Result<()> {
use std::os::windows::process::CommandExt;
const DETACHED_PROCESS: u32 = 0x0000_0008;
let system = std::env::var_os("SystemRoot").context("SystemRoot is not set")?;
let powershell = Path::new(&system).join(r"System32\WindowsPowerShell\v1.0\powershell.exe");
let literal = path.display().to_string().replace('\'', "''");
std::process::Command::new(powershell)
.args(["-NoLogo", "-NoProfile", "-NonInteractive", "-Command"])
.arg(format!(
"Wait-Process -Id {} -ErrorAction SilentlyContinue; Remove-Item -LiteralPath '{literal}' -Force",
std::process::id()
))
.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.creation_flags(DETACHED_PROCESS)
.spawn()
.context("failed to start the deferred deletion helper")?;
Ok(())
}
fn purge(yes: bool) -> Result<()> {
let home = codewhale_config::codewhale_home()?;
let explicit = codewhale_config::codewhale_home_is_explicit();
let mut targets = vec![home.clone()];
if !explicit {
targets.push(codewhale_config::legacy_deepseek_home()?.join("snapshots"));
}
let user_home = dirs::home_dir().context("failed to resolve home directory")?;
println!(
"Data purge scope: Codewhale's config, plaintext secrets file, sessions, logs, skills, plugins, tasks and caches in the directories below. Nothing else is searched for."
);
let mut present = Vec::new();
for target in targets {
let metadata = match std::fs::symlink_metadata(&target) {
Ok(metadata) => metadata,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
println!("Already absent: {}", target.display());
continue;
}
Err(error) => {
return Err(error)
.with_context(|| format!("failed to inspect {}", target.display()));
}
};
if !metadata.is_dir() || user_home.starts_with(&target) || target.parent().is_none() {
bail!(
"Preserved {}: it is not a real directory, or it is your home or one of its parents; no data was deleted",
target.display()
);
}
if explicit && target == home && target.file_name().is_none_or(|name| name != ".codewhale")
{
bail!(
"Preserved explicit CODEWHALE_HOME {}: review it and delete it by hand; no data was deleted",
target.display()
);
}
#[cfg(unix)]
{
use std::os::unix::fs::MetadataExt;
if metadata.uid()
!= codewhale_config::private_directory::PrivateDirectory::current_user_id()
{
bail!(
"Preserved {}: it belongs to another user; no data was deleted",
target.display()
);
}
}
println!(
"{} {}",
if yes { "Delete" } else { "Would delete" },
target.display()
);
present.push(target);
}
println!(
"Not covered: OS keychain/keyring entries (run codewhale auth clear --provider <id> before removing the program), per-workspace .codewhale directories, shell profile lines and completion files."
);
if !yes {
println!("No data was changed. Add --yes to delete exactly these directories.");
return Ok(());
}
for target in present {
std::fs::remove_dir_all(&target).with_context(|| {
format!(
"data purge stopped at {}; inspect what remains",
target.display()
)
})?;
println!("Deleted {}", target.display());
}
Ok(())
}
fn report_path_copies(selected: Option<&Path>) {
let extensions: &[&str] = if cfg!(windows) {
&[".exe", ".cmd", ".bat", ".ps1"]
} else {
&[""]
};
let mut seen = std::collections::HashSet::new();
let mut remaining = false;
for directory in std::env::split_paths(&std::env::var_os("PATH").unwrap_or_default()) {
for name in [
"codewhale",
"codew",
"codewhale-tui",
"deepseek",
"deepseek-tui",
] {
for extension in extensions {
let path = directory.join(format!("{name}{extension}"));
if Some(path.as_path()) == selected {
continue;
}
if seen.insert(path.clone()) && std::fs::symlink_metadata(&path).is_ok() {
remaining = true;
println!(
"Another copy remains on PATH (preserved): {}. Run it with uninstall to see who owns it and how to remove it.",
path.display()
);
}
}
}
}
if !remaining {
println!("No other Codewhale command remains on PATH.");
}
}
+1 -1
View File
@@ -28,7 +28,7 @@ fn passthrough_help_leaves_configuration_and_legacy_credentials_untouched() {
.expect("seed synthetic legacy credential");
let legacy_bytes = fs::read(&legacy).expect("read legacy fixture");
for subcommand in ["exec", "init", "setup", "mcp", "review", "rc"] {
for subcommand in ["exec", "init", "setup", "mcp", "review", "rc", "uninstall"] {
for flag in ["--help", "-h"] {
let output = Command::new(env!("CARGO_BIN_EXE_codewhale"))
.env_clear()
+22 -10
View File
@@ -3,7 +3,7 @@
//! rewrite legacy child-server definitions. The active MCP client is separate.
use std::fs;
use std::io::Write;
use std::io::{BufRead, BufReader, Read, Write};
use std::path::PathBuf;
use std::process::{Command, Stdio};
@@ -67,23 +67,35 @@ impl Fixture {
.spawn()
.expect("spawn codewhale mcp-server");
{
let stdin = child.stdin.as_mut().expect("mcp-server stdin");
for request in requests {
writeln!(stdin, "{request}").expect("write request");
let mut stdin = child.stdin.take().expect("mcp-server stdin");
let mut stdout = BufReader::new(child.stdout.take().expect("mcp-server stdout"));
let mut responses = Vec::new();
for request in requests {
writeln!(stdin, "{request}").expect("write request");
stdin.flush().expect("flush request");
if let Some(id) = request.get("id") {
let mut line = String::new();
assert!(stdout.read_line(&mut line).expect("read response") > 0);
let response: Value = serde_json::from_str(&line).expect("stdout is only JSON-RPC");
assert_eq!(response.get("id"), Some(id));
responses.push(response);
}
}
drop(stdin);
let mut remainder = String::new();
stdout
.read_to_string(&mut remainder)
.expect("read remaining output");
assert!(
remainder.trim().is_empty(),
"unexpected response: {remainder}"
);
let output = child.wait_with_output().expect("mcp-server output");
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
assert!(
output.status.success(),
"native MCP server failed: {stderr}"
);
let responses = String::from_utf8_lossy(&output.stdout)
.lines()
.map(|line| serde_json::from_str::<Value>(line).expect("stdout is only JSON-RPC"))
.collect();
(responses, stderr)
}
}
@@ -187,6 +187,7 @@ pub struct ConfigStatusView {
pub cache_miss_tokens: u32,
pub cost: f64,
pub currency: CommandCurrency,
pub cost_label: Option<String>,
pub metrics: StatusMetrics,
pub ascii_safe: bool,
pub tool_outputs: StatusToolOutputs,
+3 -2
View File
@@ -48,9 +48,10 @@ impl CommandCapabilities {
/// basic session capability.
pub const SESSION_LIFECYCLE: Self = Self(1 << 13);
/// Session-control host data (FEAT-024 D3), the next non-conflicting bit
/// after `SESSION_LIFECYCLE`. Required only by the six host-dependent
/// after `SESSION_LIFECYCLE`. Required only by the seven host-dependent
/// control commands (`/relay`, `/rename`, `/resume`, `/rc`, `/remote-env`,
/// `/title`); `/remote-env` also declares `PRESENTATION`. Bit 14 fit the
/// `/title`, `/color`); `/remote-env` and `/color` also declare
/// `PRESENTATION`. Bit 14 fit the
/// original `u16` backing without speculative widening in FEAT-023.
pub const SESSION_CONTROL: Self = Self(1 << 14);
/// Session-export host data (FEAT-025 D1), the next non-conflicting bit
+6
View File
@@ -10,6 +10,10 @@ description = "Config schema and precedence model for Codewhale"
[lints]
workspace = true
[features]
schema = ["dep:schemars"]
test-support = []
[dependencies]
codewhale-protocol = { path = "../protocol", version = "0.10.2" }
anyhow.workspace = true
@@ -18,6 +22,7 @@ chrono.workspace = true
ring = "0.17.14"
rust_decimal = { version = "1.43.0", default-features = false, features = ["std"] }
semver.workspace = true
schemars = { version = "1.2.1", optional = true, features = ["derive", "preserve_order"] }
codewhale-execpolicy = { path = "../execpolicy", version = "0.10.2" }
codewhale-paths = { path = "../paths", version = "0.10.2" }
codewhale-secrets = { path = "../secrets", version = "0.10.2" }
@@ -26,6 +31,7 @@ libc = "0.2"
serde.workspace = true
serde_json.workspace = true
sha2.workspace = true
url = "2.5"
tempfile.workspace = true
toml.workspace = true
toml_edit.workspace = true
+1 -1
View File
@@ -6102,7 +6102,6 @@
"knowledge": "2026-03-01",
"release_date": "2026-05-29",
"last_updated": "2026-05-29",
"default": true,
"attachment": true,
"reasoning": true,
"reasoning_options": [
@@ -6150,6 +6149,7 @@
"description": "StepFun's next-generation flagship base model for coding and professional knowledge work, with native text, image, and video input and a 1M-token context window",
"release_date": "2026-09-16",
"last_updated": "2026-09-20",
"default": true,
"attachment": true,
"reasoning": true,
"reasoning_options": [
@@ -1133,7 +1133,7 @@
"id": "stepfun",
"label": "StepFun / StepFlash",
"base_url": "https://api.stepfun.ai/v1",
"default_model": "step-3.7-flash",
"default_model": "step-5-preview",
"env_vars": [
"STEPFUN_API_KEY",
"STEP_API_KEY"
+26 -12
View File
@@ -312,7 +312,7 @@ impl CatalogOffering {
.filter(|provider| *provider != crate::ProviderKind::Custom)
.map(|provider| provider.provider().default_base_url())
});
match (&self.source, &lower.source) {
let endpoint_facts = match (&self.source, &lower.source) {
(
CatalogSource::Live {
base_url_fingerprint: current,
@@ -323,7 +323,10 @@ impl CatalogOffering {
..
},
) if current == prior
&& base_url.is_none_or(|url| base_url_fingerprint(url) == *current) => {}
&& base_url.is_none_or(|url| base_url_fingerprint(url) == *current) =>
{
true
}
(
CatalogSource::Live {
base_url_fingerprint: current,
@@ -333,8 +336,9 @@ impl CatalogOffering {
| CatalogSource::ModelsDevLive { .. }
| CatalogSource::CloudFacts { .. }
| CatalogSource::CodewhaleBundled { .. },
) if published_base.is_some_and(|url| base_url_fingerprint(url) == *current)
&& base_url.is_none_or(|url| base_url_fingerprint(url) == *current) => {}
) if base_url.is_none_or(|url| base_url_fingerprint(url) == *current) => {
published_base.is_some_and(|url| base_url_fingerprint(url) == *current)
}
(
CatalogSource::ConfigOverride | CatalogSource::UserOverride,
CatalogSource::Bundled
@@ -345,13 +349,20 @@ impl CatalogOffering {
.zip(published_base)
.is_some_and(|(current, prior)| {
base_url_fingerprint(current) == base_url_fingerprint(prior)
}) => {}
}) =>
{
true
}
_ => return false,
}
self.complete_facts_from(lower)
};
self.complete_facts(lower, endpoint_facts)
}
pub(crate) fn complete_facts_from(&mut self, lower: &Self) -> bool {
self.complete_facts(lower, true)
}
fn complete_facts(&mut self, lower: &Self, endpoint_facts: bool) -> bool {
let mut changed = false;
macro_rules! fill {
($field:ident) => {
@@ -376,10 +387,12 @@ impl CatalogOffering {
fill!(temperature);
fill!(open_weights);
fill!(interleaved);
fill!(transport);
fill!(provider_api);
fill!(provider_npm);
fill!(status);
if endpoint_facts {
fill!(transport);
fill!(provider_api);
fill!(provider_npm);
fill!(status);
}
fill!(experimental);
fill!(license);
fill!(links);
@@ -407,7 +420,8 @@ impl CatalogOffering {
self.limit = Some(limit);
}
}
if self.cost.is_none()
if endpoint_facts
&& self.cost.is_none()
&& self.cost_source.is_none()
&& (lower.cost.is_some() || lower.cost_source.is_some())
{
+1 -1
View File
@@ -1433,7 +1433,7 @@ fn stepfun_bundled_coding_models_preserve_default_and_plan_pricing_boundary() {
.find(|row| row.default_for_provider)
.unwrap()
.wire_model_id,
"step-3.7-flash"
"step-5-preview"
);
for row in &rows {
assert_eq!(row.reasoning, Some(true));
+88
View File
@@ -0,0 +1,88 @@
use serde::Deserialize;
// === Types ===
/// `[extension_host]`: settings for the experimental TypeScript extension
/// host (`[features] extension_host`). User config only: project-scope config
/// is applied by an explicit allowlist that does not include this table.
#[derive(Debug, Clone, Default, Deserialize, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
pub struct ExtensionHostConfig {
/// Explicit MCP protocol backend. Host is the default and supports stdio,
/// HTTP and SSE independently of optional Native extension activation.
#[serde(default)]
pub mcp_backend: crate::McpBackend,
/// Which runtime runs the host: `node` (the default), `bun`, or `auto`
/// (Bun when a supported one is found and starts, else Node). `bun` and
/// `auto` are opt-ins: Bun is not the default until it is qualified on
/// every platform. An explicit `bun` or `node` never falls back to the
/// other runtime. Unset means `node`, except that a table setting only
/// `bun` means `bun` ([`Self::effective_runtime`]).
#[serde(default)]
pub runtime: Option<ExtensionHostRuntime>,
/// Path to a Node.js runtime (`^22.19 || >=24`). When set it is the only
/// Node candidate: if it does not run or is below the floor, Node
/// resolution fails with that reason instead of searching `PATH`.
/// Unset, every `node` on `PATH` is tried in order, skipping any inside
/// a `node_modules` directory or the working directory.
#[serde(default)]
pub node: Option<String>,
/// Path to a Bun runtime (>= 1.4.0). When set it is the only Bun
/// candidate, as for `node`. Unset, `bun` on `PATH` and then
/// `$BUN_INSTALL/bin` (default `~/.bun/bin`) are tried, with the same
/// skips.
#[serde(default)]
pub bun: Option<String>,
}
impl ExtensionHostConfig {
/// `runtime` as configured, else `bun` when only a Bun path is set (the
/// table names no other runtime), else `node`.
#[must_use]
pub fn effective_runtime(&self) -> ExtensionHostRuntime {
match (self.runtime, &self.node, &self.bun) {
(Some(runtime), _, _) => runtime,
(None, None, Some(_)) => ExtensionHostRuntime::Bun,
(None, _, _) => ExtensionHostRuntime::Node,
}
}
}
/// `[extension_host] runtime`. Node is the default; Bun (`bun`, or `auto`,
/// which prefers it) stays an opt-in until an explicit, recorded cutover.
#[derive(Debug, Clone, Copy, Default, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum ExtensionHostRuntime {
Auto,
Bun,
#[default]
Node,
}
impl ExtensionHostRuntime {
#[must_use]
pub fn as_str(self) -> &'static str {
match self {
Self::Auto => "auto",
Self::Bun => "bun",
Self::Node => "node",
}
}
}
/// `[plugins."<name>"]`: per-plugin settings, keyed by the plugin's manifest
/// name. User config only (like `[extension_host]`): project-scope config is
/// applied by an explicit allowlist that does not include this table, so a
/// repository cannot configure the plugins it asks you to trust.
#[derive(Debug, Clone, Default, Deserialize, PartialEq)]
#[serde(deny_unknown_fields)]
pub struct PluginSettings {
/// `[plugins."<name>".config]`: a TOML table delivered as the `config`
/// argument of the plugin's `apply(ctx, config)` when the extension host
/// activates it, validated against the plugin's own `Config` schema when it
/// declares one. A change takes effect at `/plugin reload` (the plugin is
/// re-activated). Values are not secrets storage: they are shown to the
/// plugin's code, and `/plugin show` lists their keys.
#[serde(default)]
pub config: Option<toml::Table>,
}
+580
View File
@@ -0,0 +1,580 @@
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
/// Events that can trigger hook execution
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum HookEvent {
/// Triggered when a new session starts
SessionStart,
/// Triggered when a session ends (quit, Ctrl+C)
SessionEnd,
/// Triggered before a user message is sent to the LLM
MessageSubmit,
/// Triggered before a tool is executed
ToolCallBefore,
/// Triggered after a tool completes (success or failure)
ToolCallAfter,
/// Triggered when the user changes modes (Plan, Act, Operate)
ModeChange,
/// Triggered when an error occurs
OnError,
/// Triggered after a turn completes and post-turn state has been updated
TurnEnd,
/// Triggered when a sub-agent is spawned
SubagentSpawn,
/// Triggered when a sub-agent reaches a terminal state
SubagentComplete,
/// Triggered immediately before each `exec_shell` invocation. The hook's
/// stdout is parsed as `KEY=VALUE\n` lines and merged on top of the
/// shell command's environment — useful for ephemeral credentials,
/// per-skill PATH adjustments, or short-lived tokens (#456). Hooks that
/// fail or time out are logged but do *not* abort the shell call; they
/// simply contribute no env vars.
ShellEnv,
/// Triggered when the session becomes idle after real work: a turn
/// finished (or a wait ended) and no prompt, approval, or continuation
/// is outstanding (#6004). Transient tool errors never fire this by
/// themselves; it marks "agent done, waiting for the next instruction".
SessionIdle,
/// Triggered when a turn ends in a terminal failure (#6004). Transient
/// tool failures that the agent absorbs never fire this; only a turn
/// whose final status is failed does. Hook authors that want opencode's
/// grace-period semantics should debounce inside the hook.
SessionError,
/// Triggered when the agent starts waiting on the person: an approval
/// prompt opens, a `request_user_input` question is presented, or a goal
/// continuation is parked between passes (#6004). The payload's `reason`
/// field is `approval`, `user_input`, or `goal_continuation`.
WaitingForUser,
/// Triggered when idle or waiting transitions to active work (#6004).
/// Startup and repeated observations of the same state stay silent.
SessionBusy,
}
/// Every event name the runtime actually fires, in the order `/hooks events`
/// and `docs/HOOKS.md` list them. Tests assert this is exhaustive so a new
/// variant cannot ship without a documented firing point.
#[cfg(any(test, feature = "test-support"))]
pub const ALL_HOOK_EVENTS: [HookEvent; 15] = [
HookEvent::SessionStart,
HookEvent::SessionEnd,
HookEvent::TurnEnd,
HookEvent::MessageSubmit,
HookEvent::ToolCallBefore,
HookEvent::ToolCallAfter,
HookEvent::ModeChange,
HookEvent::OnError,
HookEvent::SubagentSpawn,
HookEvent::SubagentComplete,
HookEvent::ShellEnv,
HookEvent::SessionIdle,
HookEvent::SessionError,
HookEvent::WaitingForUser,
HookEvent::SessionBusy,
];
/// How much a hook's result can change what Codewhale does next.
///
/// This is the steering allowlist. "Observer" is a statement about
/// Codewhale's control flow only — an observer hook is still an arbitrary
/// shell command and can have any external side effect it likes.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HookSteering {
/// stdout/exit code can replace or block the submitted text.
TransformsSubmittedText,
/// stdout/exit code can allow, deny, ask, rewrite input, or add context.
DecidesToolCall,
/// stdout contributes `KEY=VALUE` pairs to one `exec_shell` invocation.
ContributesShellEnv,
/// stdout is ignored and the result cannot change Codewhale's behavior.
Observer,
}
impl HookEvent {
/// Get string representation for environment variable
#[must_use]
pub fn as_str(self) -> &'static str {
match self {
HookEvent::SessionStart => "session_start",
HookEvent::SessionEnd => "session_end",
HookEvent::MessageSubmit => "message_submit",
HookEvent::ToolCallBefore => "tool_call_before",
HookEvent::ToolCallAfter => "tool_call_after",
HookEvent::ModeChange => "mode_change",
HookEvent::OnError => "on_error",
HookEvent::TurnEnd => "turn_end",
HookEvent::SubagentSpawn => "subagent_spawn",
HookEvent::SubagentComplete => "subagent_complete",
HookEvent::ShellEnv => "shell_env",
HookEvent::SessionIdle => "session_idle",
HookEvent::SessionError => "session_error",
HookEvent::WaitingForUser => "waiting_for_user",
HookEvent::SessionBusy => "session_busy",
}
}
/// The steering contract for this event, as implemented.
#[must_use]
pub fn steering(self) -> HookSteering {
match self {
HookEvent::MessageSubmit => HookSteering::TransformsSubmittedText,
HookEvent::ToolCallBefore => HookSteering::DecidesToolCall,
HookEvent::ShellEnv => HookSteering::ContributesShellEnv,
HookEvent::SessionStart
| HookEvent::SessionEnd
| HookEvent::ToolCallAfter
| HookEvent::ModeChange
| HookEvent::OnError
| HookEvent::TurnEnd
| HookEvent::SubagentSpawn
| HookEvent::SubagentComplete
| HookEvent::SessionIdle
| HookEvent::SessionError
| HookEvent::SessionBusy
| HookEvent::WaitingForUser => HookSteering::Observer,
}
}
/// Whether a hook result for this event can change Codewhale's own
/// behavior. Never read this as "side-effect free" — see [`HookSteering`].
#[must_use]
pub fn can_steer(self) -> bool {
!matches!(self.steering(), HookSteering::Observer)
}
/// Whether this event's context carries a tool name/arguments, so
/// `tool_name` / `tool_category` conditions can ever match.
///
/// `on_error` is included because the tool-failure path in
/// `tui/tool_routing.rs` fires it with the tool name, call id, and result
/// attached. An `on_error` firing that has no tool behind it (a transport
/// or capacity error) simply does not match a tool predicate — it is
/// skipped at dispatch, not rejected at load.
#[must_use]
pub fn provides_tool_identity(self) -> bool {
matches!(
self,
HookEvent::ToolCallBefore
| HookEvent::ToolCallAfter
| HookEvent::ShellEnv
| HookEvent::OnError
)
}
/// Whether this event's context can carry a real process exit code, so
/// `exit_code` conditions can ever match. `tool_call_after` observes every
/// completed tool and `on_error` observes the failing ones; in both cases
/// the code is only present when the tool actually reported one
/// (`exec_shell` and friends).
#[must_use]
pub fn provides_exit_code(self) -> bool {
matches!(self, HookEvent::ToolCallAfter | HookEvent::OnError)
}
/// Whether this event's context carries a mode label, so `mode`
/// conditions can ever match. `shell_env` fires inside the `exec_shell`
/// tool with a deliberately narrow context and has no mode.
#[must_use]
pub fn provides_mode(self) -> bool {
!matches!(self, HookEvent::ShellEnv)
}
/// Whether `background = true` is honored as actual scheduling for this
/// event. Events whose result is part of the contract are always run in
/// the foreground, so declaring them background is a config error rather
/// than a scheduling choice.
#[must_use]
pub fn honors_background(self) -> bool {
!matches!(self, HookEvent::ShellEnv)
}
}
/// Condition for when a hook should run
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
#[derive(Default)]
pub enum HookCondition {
/// Always run this hook
#[default]
Always,
/// Only run for specific tool names
ToolName {
/// Tool name to match (e.g., "`exec_shell`", "`write_file`")
name: String,
},
/// Only run for specific tool categories
ToolCategory {
/// Category: "safe", "`file_write`", "shell"
category: String,
},
/// Only run in specific modes
Mode {
/// Mode: "plan", "agent", "yolo"
mode: String,
},
/// Only run when exit code matches (for `ToolCallAfter` / `OnError`)
ExitCode {
/// Exit code to match.
///
/// `i64`, not `i32`: a Windows crash code such as `3221225477`
/// (`0xC0000005`, access violation) is a real code a shell tool
/// reports, and narrowing it would silently turn the predicate into
/// one that can never match.
code: i64,
},
/// Combine multiple conditions with AND
All { conditions: Vec<HookCondition> },
/// Combine multiple conditions with OR
Any { conditions: Vec<HookCondition> },
}
#[derive(Debug, Clone)]
pub struct NativeShellHook {
pub owner: crate::OwnerRef,
pub scope: Option<crate::EntryRef>,
pub handle: u64,
pub dialect: String,
pub point: String,
pub matcher: Option<String>,
}
/// A single hook definition
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Hook {
#[serde(skip)]
pub native_shell: Option<NativeShellHook>,
/// The event that triggers this hook
pub event: HookEvent,
/// Shell command to execute (platform shell: `sh -c` on Unix, `cmd /C` on Windows)
pub command: String,
/// Optional condition for when this hook should run
#[serde(default)]
pub condition: Option<HookCondition>,
/// Timeout in seconds (default: 30)
#[serde(default = "default_timeout")]
pub timeout_secs: u64,
/// Run in background (don't wait for completion)
#[serde(default)]
pub background: bool,
/// Continue if this hook fails (default: true)
#[serde(default = "default_continue_on_error")]
pub continue_on_error: bool,
/// Optional name for logging/debugging
#[serde(default)]
pub name: Option<String>,
/// Content- and generation-bound authority for a plugin-contributed hook.
/// Never accepted from TOML; only the reviewed staged adapter may attach
/// it after parsing immutable bytes.
#[serde(skip)]
pub plugin_authority: Option<crate::PluginAuthority>,
/// Exact-byte project approval attached by the loader; never read from TOML.
#[serde(skip)]
pub project_authority: Option<crate::ProjectHookAuthority>,
}
fn default_timeout() -> u64 {
30
}
fn default_continue_on_error() -> bool {
true
}
impl Hook {
/// Create a new hook with minimal configuration
pub fn new(event: HookEvent, command: &str) -> Self {
Self {
event,
command: command.to_string(),
condition: None,
timeout_secs: 30,
background: false,
continue_on_error: true,
name: None,
plugin_authority: None,
project_authority: None,
native_shell: None,
}
}
/// Builder: set condition
pub fn with_condition(mut self, condition: HookCondition) -> Self {
self.condition = Some(condition);
self
}
/// Builder: set timeout
pub fn with_timeout(mut self, secs: u64) -> Self {
self.timeout_secs = secs;
self
}
/// Builder: run in background
pub fn background(mut self) -> Self {
self.background = true;
self
}
/// Builder: set name
pub fn with_name(mut self, name: &str) -> Self {
self.name = Some(name.to_string());
self
}
}
/// A configured hook that can never behave the way it is written.
///
/// Reported by [`HooksConfig::validate`] and, for rejections, surfaced in
/// `/hooks list` so a broken hook is visible instead of silently inert.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct HookConfigProblem {
/// Hook `name`, or `None` for an unnamed entry.
pub name: Option<String>,
/// The event the hook is registered for, or `None` when the problem is
/// with a setting in the `[hooks]` table itself rather than with one
/// entry — `default_timeout_secs` governs every hook, so pinning its
/// rejection on an arbitrary hook would misreport the blast radius.
pub event: Option<HookEvent>,
/// What is wrong, in one line, with no paths or payload content.
pub detail: String,
/// `true` when the hook is dropped at load and will never run.
pub rejected: bool,
}
/// Configuration for hooks (loaded from config.toml)
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct HooksConfig {
/// List of hooks to execute
#[serde(default)]
pub hooks: Vec<Hook>,
/// Global enable/disable for all hooks
#[serde(default = "default_enabled")]
pub enabled: bool,
/// Global timeout override. When set this **replaces** every hook's own
/// `timeout_secs` rather than only filling in for hooks that omit one —
/// see `HookExecutor::execute_sync_inner`. Documented as-implemented in
/// `docs/HOOKS.md`; leave unset for per-hook timeouts.
#[serde(default)]
pub default_timeout_secs: Option<u64>,
/// Working directory for hook execution (default: workspace)
#[serde(default)]
pub working_dir: Option<PathBuf>,
/// Problems found by [`HooksConfig::validate`] at load time. Never read
/// from or written to `config.toml`; populated by
/// the host loader so `/hooks` can show a rejected
/// hook instead of leaving it silently inert.
#[serde(skip)]
pub problems: Vec<HookConfigProblem>,
}
fn default_enabled() -> bool {
true
}
impl HooksConfig {
/// Report every configured hook that cannot behave as written.
///
/// A condition that references context the event never carries can never
/// match, so a hook wearing one is inert — the dangerous version of that
/// is a `deny` gate the operator believes is armed. Those are reported as
/// `rejected` and dropped by the host loader rather than left
/// to fail silently at dispatch time. Problems that only affect how a
/// hook is scheduled are reported as warnings and the hook still runs.
#[must_use]
pub fn validate(&self) -> Vec<HookConfigProblem> {
self.validate_settings()
.into_iter()
.chain(self.validate_indexed().into_iter().map(|(_, p)| p))
.collect()
}
/// Problems with the `[hooks]` table itself, independent of any entry.
///
/// `default_timeout_secs = 0` is the one that matters: it *replaces* every
/// hook's own `timeout_secs`, so the per-hook `timeout_secs = 0` rejection
/// does nothing to stop one line from killing every hook in the config
/// before it can produce output — including a `tool_call_before` gate,
/// which then fails closed on every tool call.
pub fn validate_settings(&self) -> Vec<HookConfigProblem> {
let mut problems = Vec::new();
if self.default_timeout_secs == Some(0) {
problems.push(HookConfigProblem {
name: None,
event: None,
detail: "`default_timeout_secs = 0` would expire every hook immediately; \
the override is ignored and per-hook `timeout_secs` applies"
.to_string(),
rejected: true,
});
}
problems
}
/// [`Self::validate`], but each problem is paired with the index of the
/// entry that produced it.
///
/// The index is the hook's identity for rejection purposes. Keying on
/// `(name, event)` instead would make one invalid unnamed `session_start`
/// entry delete *every* unnamed `session_start` entry, and one invalid
/// `gate` delete every other hook also called `gate` — innocent hooks
/// dropped because they share a label with a broken one.
pub fn validate_indexed(&self) -> Vec<(usize, HookConfigProblem)> {
let mut problems = Vec::new();
for (index, hook) in self.hooks.iter().enumerate() {
let mut condition_rejections = Vec::new();
collect_condition_problems(hook.event, hook.condition.as_ref(), &mut |detail| {
condition_rejections.push(detail);
});
let mut push = |detail: String, rejected: bool| {
problems.push((
index,
HookConfigProblem {
name: hook.name.clone(),
event: Some(hook.event),
detail,
rejected,
},
));
};
// A condition that can never match makes the hook inert, so it is
// dropped; the rest only affect how the hook is scheduled, so the
// hook still runs and the problem is a warning.
for detail in condition_rejections {
push(detail, true);
}
if hook.background && !hook.event.honors_background() {
push(
format!(
"`background = true` is not honored for `{}`; its stdout is the \
contract, so it always runs in the foreground",
hook.event.as_str()
),
false,
);
} else if hook.background && hook.event.can_steer() {
push(
format!(
"`background = true` makes this `{}` hook observer-only — it is \
submitted and never awaited, so it cannot steer the turn",
hook.event.as_str()
),
false,
);
}
if hook.timeout_secs == 0 {
push(
"`timeout_secs = 0` expires immediately; the command is killed \
before it can produce output"
.to_string(),
true,
);
}
if hook.command.trim().is_empty() {
push("`command` is empty".to_string(), true);
}
}
problems
}
/// Get hooks for a specific event
pub fn hooks_for_event(&self, event: HookEvent) -> Vec<&Hook> {
if !self.enabled {
return Vec::new();
}
self.hooks.iter().filter(|h| h.event == event).collect()
}
/// The timeout the runtime will actually apply to `hook`.
///
/// `[hooks].default_timeout_secs` *replaces* the per-hook value when set.
/// This is the single owner of that rule: the executor enforces it and
/// `/hooks list` displays it, so the listing cannot advertise a per-hook
/// number the runtime will not use.
#[must_use]
pub fn effective_timeout_secs(&self, hook: &Hook) -> u64 {
// `filter`, not `unwrap_or`: `apply_validation` already strips a zero
// override at load, but a `HooksConfig` can also be built in code, and
// a zero here means "kill every hook before it speaks".
self.default_timeout_secs
.filter(|secs| *secs > 0)
.unwrap_or(hook.timeout_secs)
}
/// `true` when `[hooks].default_timeout_secs` is overriding per-hook
/// timeouts, so surfaces can name the provenance of the number they show.
#[must_use]
pub fn timeout_is_overridden(&self) -> bool {
// An ignored zero override is not provenance: `/hooks list` must not
// credit a number to a setting the runtime refused to apply.
self.default_timeout_secs.is_some_and(|secs| secs > 0)
}
}
/// Walk a condition tree and report every predicate the event can never
/// satisfy. `all` / `any` are walked so a nested unsupported predicate is
/// caught rather than hidden behind a combinator.
fn collect_condition_problems(
event: HookEvent,
condition: Option<&HookCondition>,
reject: &mut impl FnMut(String),
) {
let Some(condition) = condition else {
return;
};
match condition {
HookCondition::Always => {}
HookCondition::ToolName { .. } | HookCondition::ToolCategory { .. } => {
if !event.provides_tool_identity() {
reject(format!(
"`{}` never carries a tool name, so this tool condition can never match",
event.as_str()
));
}
}
HookCondition::Mode { .. } => {
if !event.provides_mode() {
reject(format!(
"`{}` runs with a narrow context that has no mode, so a `mode` \
condition can never match; scope it with `tool_name` or \
`tool_category` instead",
event.as_str()
));
}
}
HookCondition::ExitCode { .. } => {
if !event.provides_exit_code() {
reject(format!(
"`{}` has no completed process to read an exit code from; \
`exit_code` conditions are only supported on `tool_call_after` \
and `on_error`",
event.as_str()
));
}
}
HookCondition::All { conditions } | HookCondition::Any { conditions } => {
for nested in conditions {
// Reborrow rather than pass `reject` itself: `&mut F` also
// implements `FnMut`, so passing it directly would recurse in
// the type parameter and never finish monomorphizing.
collect_condition_problems(event, Some(nested), &mut *reject);
}
}
}
}
+124
View File
@@ -0,0 +1,124 @@
use serde::Deserialize;
/// Absolute per-scope default: 100k estimated tokens.
pub const DEFAULT_SCOPE_BUDGET_TOKENS: usize = 100_000;
/// Per-scope default as a share of the usable window.
pub const DEFAULT_SCOPE_BUDGET_PERCENT: u8 = 25;
/// Default ceiling for the whole static prefix, as a share of the usable window.
pub const DEFAULT_STATIC_PREFIX_GUARD_PERCENT: u8 = 80;
/// Hard memory-safety ceiling for one instruction file (8 MiB).
pub const DEFAULT_FILE_MAX_BYTES: u64 = 8 * 1024 * 1024;
const MIN_STATIC_RESIDUAL_PERCENT: u8 = 10;
/// What to do when an instruction scope does not fit.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum InstructionBudgetPolicy {
/// Trim the scope and continue with a warning (default).
#[default]
Trim,
/// Stop the session instead of trimming or running an unsatisfiable guard.
Refuse,
}
impl InstructionBudgetPolicy {
#[must_use]
pub fn as_str(self) -> &'static str {
match self {
Self::Trim => "trim",
Self::Refuse => "refuse",
}
}
}
/// The `[instruction_budget]` config table.
///
/// Every field is optional so the table only states what the user changed and
/// the defaults stay in one place. `deny_unknown_fields` makes a typo inside
/// the table fail loudly — the top-level config is deliberately tolerant of
/// unknown *tables* (#6516), so silently accepting `global_budget_token`
/// would hide the mistake forever.
#[derive(Debug, Clone, Default, PartialEq, Eq, Hash, serde::Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct InstructionBudgetConfig {
/// Absolute ceiling for the global scope, in estimated tokens.
pub global_budget_tokens: Option<usize>,
/// Absolute ceiling for the project scope, in estimated tokens.
pub project_budget_tokens: Option<usize>,
/// Global scope ceiling as a share of the usable window (1–100).
pub global_budget_percent: Option<u8>,
/// Project scope ceiling as a share of the usable window (1–100).
pub project_budget_percent: Option<u8>,
/// Ceiling for the whole static prefix as a share of the usable window.
pub static_prefix_guard_percent: Option<u8>,
/// Hard per-file ceiling in bytes; a larger file is skipped, not trimmed.
pub file_max_bytes: Option<u64>,
/// `trim` (default) or `refuse`.
pub policy: Option<InstructionBudgetPolicy>,
/// Read `CLAUDE.md` when no `AGENTS.md`/codewhale file is present.
pub claude_fallback: Option<bool>,
}
impl InstructionBudgetConfig {
/// Reject values that cannot mean anything.
///
/// Called while config is applied, so a broken table stops the launch
/// instead of silently producing a nonsensical budget.
pub fn validate(&self) -> Result<(), String> {
for (name, value) in [
("global_budget_tokens", self.global_budget_tokens),
("project_budget_tokens", self.project_budget_tokens),
] {
if value == Some(0) {
return Err(format!(
"instruction_budget.{name} must be positive (0 is not an unlimited budget)"
));
}
}
if self
.file_max_bytes
.is_some_and(|value| value == 0 || value > DEFAULT_FILE_MAX_BYTES)
{
return Err(
"instruction_budget.file_max_bytes must be between 1 and 8388608 (the hard ceiling cannot be raised)"
.to_string(),
);
}
for (name, value) in [
("global_budget_percent", self.global_budget_percent),
("project_budget_percent", self.project_budget_percent),
(
"static_prefix_guard_percent",
self.static_prefix_guard_percent,
),
] {
if let Some(value) = value
&& !(1..=100).contains(&value)
{
return Err(format!(
"instruction_budget.{name} must be between 1 and 100 (got {value})"
));
}
}
let global_percent = self
.global_budget_percent
.unwrap_or(DEFAULT_SCOPE_BUDGET_PERCENT);
let project_percent = self
.project_budget_percent
.unwrap_or(DEFAULT_SCOPE_BUDGET_PERCENT);
let guard = self
.static_prefix_guard_percent
.unwrap_or(DEFAULT_STATIC_PREFIX_GUARD_PERCENT);
if guard < global_percent.saturating_add(project_percent) {
return Err(format!(
"instruction_budget.static_prefix_guard_percent ({guard}) must be at least the sum of the scope percents ({global_percent} + {project_percent})"
));
}
if guard - global_percent.saturating_add(project_percent) < MIN_STATIC_RESIDUAL_PERCENT {
return Err(format!(
"instruction_budget.static_prefix_guard_percent ({guard}) must leave at least {MIN_STATIC_RESIDUAL_PERCENT} points above the scope percents ({global_percent} + {project_percent}) for non-instruction static content"
));
}
Ok(())
}
}
+129 -14
View File
@@ -7,7 +7,10 @@ mod config_document;
pub mod credentials;
pub mod descriptors;
pub mod device_code;
pub mod extension_host;
pub mod external_credentials;
pub mod hooks;
pub mod instruction_budget;
pub mod legacy_root;
pub mod model_reference;
pub mod models_dev;
@@ -25,6 +28,7 @@ pub mod resolve;
pub mod route;
pub mod settings_schema;
pub mod setup_state;
pub mod suggest;
pub mod user_constitution;
#[cfg(windows)]
pub mod windows_identity;
@@ -1724,16 +1728,130 @@ pub struct ToolsToml {
pub extras: BTreeMap<String, toml::Value>,
}
/// Protocol owner selected only by user configuration. Rust is the default;
/// Host explicitly selects the SDK and never falls back to Rust on refusal.
/// Existing `[workshop]` threshold configuration, retained for compatibility.
#[derive(Debug, Clone, Deserialize, Default)]
pub struct WorkshopConfig {
/// Token threshold above which results become handle-only evidence.
#[serde(default)]
pub large_output_threshold_tokens: Option<usize>,
/// Per-tool threshold overrides (tool name → token limit). A tool whose
/// name appears here uses this limit instead of
/// `large_output_threshold_tokens`.
#[serde(default)]
pub per_tool_thresholds: Option<std::collections::HashMap<String, usize>>,
/// Optional model-visible byte budget for a single `read` / `read_file`
/// result. Absent keeps the compile-time default (#5367).
#[serde(default)]
pub read_result_max_bytes: Option<usize>,
/// Optional model-visible byte budget for a generic tool result after
/// spillover. Absent keeps the compile-time default (#5367).
#[serde(default)]
pub tool_result_max_bytes: Option<usize>,
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct PluginId(pub String);
impl PluginId {
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
/// Persistable proof of the exact reviewed plugin authority attached to a
/// Skill or MCP server. Runtime contexts keep this receipt instead of a
/// pointer to mutable process-global discovery state, and revalidate it at
/// every side-effect boundary.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct PluginAuthority {
pub plugin_id: PluginId,
pub plugin_name: String,
pub workspace: std::path::PathBuf,
pub state_path: std::path::PathBuf,
#[serde(default)]
pub managed_policy_path: Option<std::path::PathBuf>,
#[serde(skip)]
pub managed_policy_source: Option<std::path::PathBuf>,
pub source_manifest: std::path::PathBuf,
pub staged_manifest: std::path::PathBuf,
pub content_hash: String,
pub capability_hash: String,
/// Monotonic generation of this plugin's persisted authority. Any trust,
/// enablement, disablement, or revocation transition invalidates receipts
/// held by another process immediately, even when hashes are unchanged.
pub state_generation: u64,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ProjectHookAuthority {
pub workspace: PathBuf,
pub digest: String,
}
/// Identity of one activation. `generation` bumps on every (re)activation;
/// `owner_token` is 128+ random bits minted by the core and handed over only
/// in `ext/activate`. It catches bugs and stale fibers; it is not a boundary
/// against a malicious plugin in the same process (see the design, §4.4).
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct OwnerRef {
pub plugin_id: String,
pub generation: u64,
pub owner_token: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct EntryRef {
pub path: String,
pub sha256: String,
}
impl std::fmt::Display for PluginId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
/// Host uses the SDK by default; explicit Rust selection remains available.
/// A Host refusal never falls back to Rust.
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum McpBackend {
#[default]
Rust,
#[default]
Host,
}
/// Return only the non-secret network authority of a configured URL.
///
/// Userinfo, path, query keys and values, and fragments are all omitted because
/// every one of those components can carry credentials. Parse failures also
/// omit the original input rather than echoing an attacker-controlled value.
#[must_use]
pub fn structural_url_authority(url: &str) -> String {
let Some(parsed) = url::Url::parse(url).ok() else {
return "unparseable (configured value omitted)".to_string();
};
let Some(host) = parsed.host_str() else {
return "unparseable (configured value omitted)".to_string();
};
// Url::host_str already includes the brackets around an IPv6 address.
let mut authority = format!("{}://{host}", parsed.scheme());
if let Some(port) = parsed.port() {
authority.push(':');
authority.push_str(&port.to_string());
}
authority
}
/// Default snapshot retention window: 7 days.
pub const DEFAULT_SNAPSHOT_MAX_AGE: std::time::Duration =
std::time::Duration::from_secs(7 * 24 * 60 * 60);
@@ -1764,7 +1882,7 @@ pub struct SnapshotsToml {
pub extras: BTreeMap<String, toml::Value>,
}
fn default_snapshots_enabled() -> bool {
pub fn default_snapshots_enabled() -> bool {
true
}
@@ -1804,9 +1922,6 @@ pub struct FleetConfigToml {
pub exec: FleetExecConfig,
}
/// Runtime delegation has no default product depth limit. Explicit owner
/// values still restrict child authority; zero disables delegation.
pub const DEFAULT_SPAWN_DEPTH: u32 = u32::MAX;
pub const DEFAULT_STREAM_CHUNK_TIMEOUT_SECS: u64 = 900;
pub const MIN_STREAM_CHUNK_TIMEOUT_SECS: u64 = 1;
pub const MAX_STREAM_CHUNK_TIMEOUT_SECS: u64 = 3600;
@@ -1883,7 +1998,7 @@ pub struct FleetExecConfig {
#[serde(default = "default_fleet_max_turns")]
pub max_turns: u32,
/// Recursive child-agent budget for headless fleet workers.
/// Defaults to [`DEFAULT_SPAWN_DEPTH`], with no product depth cap.
/// Defaults to [`codewhale_protocol::fleet::DEFAULT_SPAWN_DEPTH`], with no product depth cap.
/// Set zero to block child delegation while preserving the root worker.
#[serde(default = "default_fleet_max_spawn_depth")]
pub max_spawn_depth: u32,
@@ -1908,7 +2023,7 @@ fn default_fleet_max_turns() -> u32 {
}
fn default_fleet_max_spawn_depth() -> u32 {
DEFAULT_SPAWN_DEPTH
codewhale_protocol::fleet::DEFAULT_SPAWN_DEPTH
}
fn default_fleet_output_format() -> String {
@@ -4747,7 +4862,7 @@ fn resolve_deepseek_base_url(
}
}
fn xiaomi_mimo_base_url_for_mode(mode: &str) -> Option<&'static str> {
pub fn xiaomi_mimo_base_url_for_mode(mode: &str) -> Option<&'static str> {
let normalized = mode.trim().to_ascii_lowercase().replace(['_', ' '], "-");
if normalized.is_empty() || xiaomi_mimo_mode_uses_standard_endpoint(&normalized) {
return None;
@@ -4780,7 +4895,7 @@ fn xiaomi_mimo_base_url_for_mode(mode: &str) -> Option<&'static str> {
})
}
fn xiaomi_mimo_mode_uses_standard_endpoint(normalized_mode: &str) -> bool {
pub fn xiaomi_mimo_mode_uses_standard_endpoint(normalized_mode: &str) -> bool {
matches!(
normalized_mode,
"standard" | "default" | "payg" | "paygo" | "pay-as-you-go" | "pay-as-go"
@@ -4827,7 +4942,7 @@ fn xiaomi_mimo_env_api_key_for_runtime(
env_value(XIAOMI_MIMO_TOKEN_PLAN_ENV_VARS).or_else(|| env_value(XIAOMI_MIMO_STANDARD_ENV_VARS))
}
fn resolve_xiaomi_mimo_base_url(
pub fn resolve_xiaomi_mimo_base_url(
configured: Option<String>,
api_key: Option<&str>,
mode: Option<&str>,
@@ -4863,7 +4978,7 @@ fn resolve_xiaomi_mimo_base_url(
}
}
fn xiaomi_mimo_api_key_uses_token_plan(api_key: Option<&str>) -> bool {
pub fn xiaomi_mimo_api_key_uses_token_plan(api_key: Option<&str>) -> bool {
api_key.is_some_and(|key| key.trim_start().starts_with("tp-"))
}
@@ -5111,7 +5226,7 @@ fn base_url_uses_local_host(base_url: &str) -> bool {
.is_ok_and(|addr| addr.is_loopback() || addr.is_unspecified())
}
fn base_url_host(base_url: &str) -> Option<&str> {
pub fn base_url_host(base_url: &str) -> Option<&str> {
let without_scheme = base_url
.split_once("://")
.map_or(base_url, |(_, rest)| rest);
+1 -1
View File
@@ -800,7 +800,7 @@ fn reopen_windows_file_for_owner_security(file: &File, path: &Path) -> Result<Fi
}
#[cfg(windows)]
fn mark_windows_file_handle_for_deletion(file: &File) -> Result<()> {
pub fn mark_windows_file_handle_for_deletion(file: &File) -> Result<()> {
use std::os::windows::io::AsRawHandle as _;
use windows_sys::Win32::Foundation::{
ERROR_INVALID_FUNCTION, ERROR_INVALID_PARAMETER, ERROR_NOT_SUPPORTED,
@@ -1249,7 +1249,7 @@
"label": "StepFun / StepFlash",
"endpoint": "https://api.stepfun.ai/v1",
"wire": "chat-completions",
"defaultModel": "step-3.7-flash",
"defaultModel": "step-5-preview",
"envVars": [
"STEPFUN_API_KEY",
"STEP_API_KEY"
@@ -1271,12 +1271,12 @@
},
{
"id": "step-3.7-flash",
"canonical": "stepfun/step-3.7-flash",
"default": true
"canonical": "stepfun/step-3.7-flash"
},
{
"id": "step-5-preview",
"canonical": "stepfun/step-5-preview"
"canonical": "stepfun/step-5-preview",
"default": true
}
],
"transport": "chat-completions"
+20
View File
@@ -328,6 +328,26 @@ const COST_CURRENCY: &[SettingOption] = &[
/// and the command cannot drift. Labels stay empty: an undeclared label means
/// "show the raw value", and a capitalized literal would also have to be
/// localized in every shipped locale to satisfy the schema message-key check.
/// Canonical setting value for a reasoning-effort spelling, accepting the
/// documented aliases. Unknown values are refused with the full vocabulary.
pub fn canonical_reasoning_effort(value: &str) -> Result<&'static str, String> {
let trimmed = value.trim();
match trimmed.to_ascii_lowercase().as_str() {
"off" | "disabled" | "none" | "false" => Ok("off"),
"minimal" => Ok("minimal"),
"low" | "minimum" | "light" => Ok("low"),
"medium" | "mid" => Ok("medium"),
"high" => Ok("high"),
"xhigh" => Ok("xhigh"),
"auto" | "automatic" => Ok("auto"),
"ultra" | "ultracode" => Ok("ultra"),
"max" | "maximum" => Ok("max"),
_ => Err(format!(
"Unrecognized reasoning effort {trimmed:?}. Expected one of: auto, off, minimal, low, medium, high, xhigh, ultra, or max."
)),
}
}
const REASONING_EFFORT: &[SettingOption] = &[
SettingOption::new("auto", "", ""),
SettingOption::new("off", "", ""),
+62
View File
@@ -0,0 +1,62 @@
#[must_use]
pub fn edit_distance(a: &str, b: &str) -> usize {
if a == b {
return 0;
}
if a.is_empty() {
return b.chars().count();
}
if b.is_empty() {
return a.chars().count();
}
let b_chars: Vec<char> = b.chars().collect();
let mut previous: Vec<usize> = (0..=b_chars.len()).collect();
let mut current = vec![0usize; b_chars.len() + 1];
for (i, a_ch) in a.chars().enumerate() {
current[0] = i + 1;
for (j, b_ch) in b_chars.iter().enumerate() {
let cost = if a_ch == *b_ch { 0 } else { 1 };
let delete = previous[j + 1] + 1;
let insert = current[j] + 1;
let substitute = previous[j] + cost;
current[j + 1] = delete.min(insert).min(substitute);
}
std::mem::swap(&mut previous, &mut current);
}
previous[b_chars.len()]
}
#[must_use]
pub fn best_suggestion_score<'a>(
query: &str,
candidates: impl IntoIterator<Item = &'a str>,
) -> Option<(u8, usize)> {
let mut best: Option<(u8, usize)> = None;
for candidate in candidates {
let prefix_match = candidate.starts_with(query) || query.starts_with(candidate);
let contains_match = candidate.contains(query) || query.contains(candidate);
let distance = edit_distance(candidate, query);
let close_typo = distance <= 2;
if !(prefix_match || contains_match || close_typo) {
continue;
}
let rank = if prefix_match {
0
} else if contains_match {
1
} else {
2
};
match best {
Some((best_rank, best_distance))
if rank > best_rank || (rank == best_rank && distance >= best_distance) => {}
_ => best = Some((rank, distance)),
}
}
best
}
+1 -1
View File
@@ -8537,7 +8537,7 @@ fn fleet_exec_config_default_matches_subagent_depth() {
// the fleet default equals DEFAULT_SPAWN_DEPTH without a nesting cap.
assert_eq!(
FleetExecConfig::default().max_spawn_depth,
DEFAULT_SPAWN_DEPTH
codewhale_protocol::fleet::DEFAULT_SPAWN_DEPTH
);
assert_eq!(FleetExecConfig::default().max_spawn_depth, u32::MAX);
}
+31 -12
View File
@@ -286,17 +286,19 @@ fn is_symlink(p: &Path) -> bool {
.map(|m| m.file_type().is_symlink())
.unwrap_or(false)
}
fn read_capped(p: &Path) -> Option<String> {
let meta = std::fs::metadata(p).ok()?;
pub fn read_project_instruction_file(file: std::fs::File) -> Option<String> {
let meta = file.metadata().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);
use std::io::Read as _;
let mut buf = Vec::new();
file.take(INSTRUCTIONS_FILE_MAX_BYTES as u64 + 1)
.read_to_end(&mut buf)
.ok()?;
let observed_len = buf.len() as u64;
if meta.len().max(observed_len) > INSTRUCTIONS_FILE_MAX_BYTES as u64 {
buf.truncate(INSTRUCTIONS_FILE_MAX_BYTES);
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) {
@@ -305,11 +307,12 @@ fn read_capped(p: &Path) -> Option<String> {
text.truncate(end);
let omitted = meta
.len()
.max(observed_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 raw = String::from_utf8(buf).ok()?;
let trimmed = raw.trim();
if trimmed.is_empty() {
None
@@ -392,6 +395,7 @@ pub fn selected_project_instruction_candidate_files(
fn load_project_instruction_fragment_from_candidates(
workspace: &Path,
candidates: &[&str],
mut opener: impl FnMut(&Path) -> std::io::Result<std::fs::File>,
) -> Option<BoundedFragment> {
let files = collect_candidate_files(workspace, candidates);
if files.is_empty() {
@@ -399,7 +403,7 @@ fn load_project_instruction_fragment_from_candidates(
}
let mut sections = Vec::new();
for path in files {
if let Some(content) = read_capped(&path) {
if let Some(content) = opener(&path).ok().and_then(read_project_instruction_file) {
let rel = path
.strip_prefix(workspace)
.unwrap_or(&path)
@@ -420,7 +424,11 @@ fn load_project_instruction_fragment_from_candidates(
}
pub fn load_project_instruction_fragment(workspace: &Path) -> Option<BoundedFragment> {
load_project_instruction_fragment_from_candidates(workspace, PROJECT_INSTRUCTION_CANDIDATES)
load_project_instruction_fragment_from_candidates(
workspace,
PROJECT_INSTRUCTION_CANDIDATES,
|path| std::fs::File::open(path),
)
}
/// Load only instruction formats that the canonical TUI project-context path
@@ -434,6 +442,7 @@ pub fn load_additional_project_instruction_fragment(workspace: &Path) -> Option<
load_project_instruction_fragment_from_candidates(
workspace,
ADDITIONAL_PROJECT_INSTRUCTION_CANDIDATES,
|path| std::fs::File::open(path),
)
}
@@ -444,11 +453,21 @@ pub fn load_additional_project_instruction_fragment(workspace: &Path) -> Option<
pub fn load_selected_project_instruction_fragment(
workspace: &Path,
candidates: &[&str],
) -> Option<BoundedFragment> {
load_selected_project_instruction_fragment_with_reader(workspace, candidates, |path| {
std::fs::File::open(path)
})
}
pub fn load_selected_project_instruction_fragment_with_reader(
workspace: &Path,
candidates: &[&str],
opener: impl FnMut(&Path) -> std::io::Result<std::fs::File>,
) -> Option<BoundedFragment> {
if candidates.is_empty() {
return None;
}
load_project_instruction_fragment_from_candidates(workspace, candidates)
load_project_instruction_fragment_from_candidates(workspace, candidates, opener)
}
pub fn project_instructions_from_sources(
-5
View File
@@ -129,7 +129,6 @@ pub struct PrefixChange {
pub tools_changed: bool,
}
#[allow(dead_code)]
impl PrefixChange {
/// Returns a human-readable description of what changed.
pub fn description(&self) -> String {
@@ -316,7 +315,6 @@ impl ToolCatalogCache {
/// Drop every cached entry. Used by tool-registry mutation paths
/// (e.g. plugin hot-reload, MCP attach) when the caller cannot
/// easily prove the tool set is unchanged.
#[allow(dead_code)] // observability; called by /cache flush and tests
pub fn invalidate(&mut self) {
self.by_identity.clear();
self.insertion_order.clear();
@@ -329,7 +327,6 @@ impl ToolCatalogCache {
}
/// Returns `true` if the cache has no entries.
#[allow(dead_code)] // observability; surfaced via /status
#[must_use]
pub fn is_empty(&self) -> bool {
self.by_identity.is_empty()
@@ -337,7 +334,6 @@ impl ToolCatalogCache {
/// Returns `(current_entries, capacity)` for observability. Surfaced via
/// the `/status` chip in a follow-up; tests exercise the path.
#[allow(dead_code)] // surfaced via /status in a follow-up; tests exercise it
#[must_use]
pub fn stats(&self) -> (usize, usize) {
(self.len(), self.capacity)
@@ -423,7 +419,6 @@ fn hash_json_value<H: Hasher>(value: &serde_json::Value, state: &mut H) {
/// [`PrefixFingerprint::compute_with_tool_cache`] and pass the cache in
/// directly, both to share state and to avoid the thread-local lookup
/// on the hot path.
#[allow(dead_code)]
impl PrefixStabilityManager {
/// Create a new manager and immediately pin the first fingerprint.
pub fn new(system_text: &str, tools: Option<&[Tool]>) -> Self {
+31
View File
@@ -0,0 +1,31 @@
[package]
name = "codewhale-dependencies"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
repository.workspace = true
description = "Codewhale external tool resolution and bounded subprocess probes"
[lints]
workspace = true
[features]
test-support = []
[dependencies]
anyhow.workspace = true
codewhale-config = { path = "../config", version = "0.10.2" }
codewhale-paths = { path = "../paths", version = "0.10.2" }
codewhale-runtime = { path = "../runtime", version = "0.10.2" }
codewhale-util = { path = "../util", version = "0.10.2" }
serde.workspace = true
serde_json.workspace = true
tempfile.workspace = true
tokio.workspace = true
tracing.workspace = true
uuid.workspace = true
wait-timeout = "0.2"
[dev-dependencies]
codewhale-runtime = { path = "../runtime", version = "0.10.2", features = ["test-support"] }
@@ -25,9 +25,12 @@
//! — probing a binary involves a `Command::output` per candidate and
//! we'd rather not pay that on every model turn.
use std::io;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::process::{Command, Output};
use std::sync::OnceLock;
use std::time::Duration;
use wait_timeout::ChildExt;
/// Candidate executable names for the Python interpreter, in the
/// order we try them. On Windows the launcher convention is `py -3`,
@@ -70,7 +73,17 @@ pub fn probe_executable_with_flag(spec: &str, version_flag: &str) -> bool {
};
let mut cmd = version_probe_command(program);
cmd.args(parts).arg(version_flag);
matches!(probe_output(&mut cmd, false, VERSION_PROBE_TIMEOUT), Ok(output) if output.status.success())
probe_found(&mut cmd)
}
/// A probe that launched and was still running at its deadline found an
/// installed tool that is slow under load; only a failed launch or a failing
/// exit means the tool is absent.
fn probe_found(command: &mut Command) -> bool {
match probe_output(command, false, VERSION_PROBE_TIMEOUT) {
Ok(output) => output.status.success(),
Err(error) => error.kind() == std::io::ErrorKind::TimedOut,
}
}
/// Probe a single executable and capture its version banner in one spawn.
@@ -98,7 +111,7 @@ const VERSION_PROBE_MAX_OUTPUT: u64 = 16 * 1024;
fn version_probe_command(program: impl AsRef<std::ffi::OsStr>) -> Command {
let mut cmd = Command::new(program);
crate::utils::suppress_console_window(&mut cmd);
codewhale_util::utils::suppress_console_window(&mut cmd);
codewhale_runtime::child_env::apply_to_command(&mut cmd, std::iter::empty::<(&str, &str)>());
// A presence probe needs no bootstrap code or import paths from the
// parent. The general child allowlist retains these for normal SDK tools.
@@ -118,7 +131,7 @@ fn probe_output(
probe_output_bounded(command, capture, timeout, VERSION_PROBE_MAX_OUTPUT)
}
pub(crate) fn probe_output_bounded(
pub fn probe_output_bounded(
command: &mut Command,
capture: bool,
timeout: std::time::Duration,
@@ -126,7 +139,6 @@ pub(crate) fn probe_output_bounded(
) -> std::io::Result<std::process::Output> {
use std::io::Read;
use std::process::Stdio;
use wait_timeout::ChildExt;
command
.stdin(Stdio::null())
.stderr(Stdio::null())
@@ -244,8 +256,7 @@ fn resolve_executable_path(spec: &str, version_flag: &str) -> Option<String> {
let mut cmd = version_probe_command(&candidate);
cmd.args(&args).arg(version_flag);
if matches!(probe_output(&mut cmd, false, VERSION_PROBE_TIMEOUT), Ok(output) if output.status.success())
{
if probe_found(&mut cmd) {
return Some(candidate.to_string_lossy().into_owned());
}
}
@@ -361,8 +372,8 @@ pub fn resolve_pandoc() -> Option<String> {
/// a converter, an OCR engine) is available: `probe` decides, except that a
/// test fixture answers with the recorded host's set so results do not
/// depend on what the machine running them has installed (test builds only).
pub(crate) fn host_tool_available(tool: &str, probe: impl FnOnce() -> bool) -> bool {
#[cfg(test)]
pub fn host_tool_available(tool: &str, probe: impl FnOnce() -> bool) -> bool {
#[cfg(any(test, feature = "test-support"))]
if let Some(available) = RECORDED_HOST_TOOLS.with(|cell| {
cell.borrow()
.as_ref()
@@ -371,12 +382,12 @@ pub(crate) fn host_tool_available(tool: &str, probe: impl FnOnce() -> bool) -> b
return available;
}
// Only test fixtures read the name.
#[cfg(not(test))]
#[cfg(not(any(test, feature = "test-support")))]
let _ = tool;
probe()
}
#[cfg(test)]
#[cfg(any(test, feature = "test-support"))]
thread_local! {
static RECORDED_HOST_TOOLS: std::cell::RefCell<Option<Vec<String>>> =
const { std::cell::RefCell::new(None) };
@@ -384,16 +395,16 @@ thread_local! {
/// Pin [`host_tool_available`] on this thread to a recorded host's tools until
/// the guard drops.
#[cfg(test)]
pub(crate) fn pin_recorded_host_tools(tools: Vec<String>) -> RecordedHostToolsGuard {
#[cfg(any(test, feature = "test-support"))]
pub fn pin_recorded_host_tools(tools: Vec<String>) -> RecordedHostToolsGuard {
RECORDED_HOST_TOOLS.with(|cell| *cell.borrow_mut() = Some(tools));
RecordedHostToolsGuard
}
#[cfg(test)]
pub(crate) struct RecordedHostToolsGuard;
#[cfg(any(test, feature = "test-support"))]
pub struct RecordedHostToolsGuard;
#[cfg(test)]
#[cfg(any(test, feature = "test-support"))]
impl Drop for RecordedHostToolsGuard {
fn drop(&mut self) {
RECORDED_HOST_TOOLS.with(|cell| *cell.borrow_mut() = None);
@@ -589,7 +600,7 @@ impl HostRuntime {
/// runtime was not probed.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct HostRuntimeResolution {
pub choice: crate::config::ExtensionHostRuntime,
pub choice: codewhale_config::extension_host::ExtensionHostRuntime,
pub selected: Option<HostRuntime>,
pub bun: Option<NodeResolution>,
pub node: Option<NodeResolution>,
@@ -656,13 +667,13 @@ impl HostRuntimeResolution {
}
let (bun, node) = (HostRuntimeKind::Bun.floor(), HostRuntimeKind::Node.floor());
let wanted = match self.choice {
crate::config::ExtensionHostRuntime::Auto => {
codewhale_config::extension_host::ExtensionHostRuntime::Auto => {
format!("Bun {bun} or Node.js {node} (set `[extension_host] bun` or `node`)")
}
crate::config::ExtensionHostRuntime::Bun => {
codewhale_config::extension_host::ExtensionHostRuntime::Bun => {
format!("Bun {bun} (`runtime = \"bun\"`; set `[extension_host] bun`)")
}
crate::config::ExtensionHostRuntime::Node => {
codewhale_config::extension_host::ExtensionHostRuntime::Node => {
format!("Node.js {node} (set `[extension_host] node`)")
}
};
@@ -675,7 +686,7 @@ impl HostRuntimeResolution {
/// `SystemRoot` to load system DLLs.
fn probe_command(path: &Path) -> Command {
let mut cmd = Command::new(path);
crate::utils::suppress_console_window(&mut cmd);
codewhale_util::utils::suppress_console_window(&mut cmd);
cmd.env_clear();
#[cfg(windows)]
if let Some(root) = std::env::var_os("SystemRoot") {
@@ -686,7 +697,11 @@ fn probe_command(path: &Path) -> Command {
cmd
}
fn probe_runtime_version(kind: HostRuntimeKind, path: &Path) -> Result<(u32, u32, u32), String> {
fn probe_runtime_version(
kind: HostRuntimeKind,
path: &Path,
bundle_sha256: &str,
) -> Result<(u32, u32, u32), String> {
// Only absolute candidates are run: a relative `PATH` entry resolves
// against the current (workspace) directory, where a repository could
// plant a `node`.
@@ -706,7 +721,7 @@ fn probe_runtime_version(kind: HostRuntimeKind, path: &Path) -> Result<(u32, u32
return Err(format!("does not run (exit {})", output.status));
}
if compiled {
return parse_compiled_host_info(&output.stdout, crate::extension_host::bundle_sha256());
return parse_compiled_host_info(&output.stdout, bundle_sha256);
}
let banner = String::from_utf8_lossy(&output.stdout);
kind.parse(&banner)
@@ -849,9 +864,13 @@ fn untrusted_location(
/// runtime with its reason, rather than falling through to a search.
/// Otherwise the search candidates, minus those in an [`untrusted_location`],
/// which are recorded as rejected without being run. Blocking.
fn resolve_runtime(kind: HostRuntimeKind, override_path: Option<&Path>) -> NodeResolution {
fn resolve_runtime(
kind: HostRuntimeKind,
override_path: Option<&Path>,
bundle_sha256: &str,
) -> NodeResolution {
if let Some(path) = override_path {
return select_runtime(kind, vec![path.to_path_buf()]);
return select_runtime(kind, vec![path.to_path_buf()], bundle_sha256);
}
let cwd = std::env::current_dir().ok();
let home = codewhale_paths::user_home();
@@ -878,7 +897,7 @@ fn resolve_runtime(kind: HostRuntimeKind, override_path: Option<&Path>) -> NodeR
}
})
.collect();
let mut resolution = select_runtime(kind, candidates);
let mut resolution = select_runtime(kind, candidates, bundle_sha256);
skipped.append(&mut resolution.rejected);
resolution.rejected = skipped;
resolution
@@ -890,23 +909,24 @@ fn resolve_runtime(kind: HostRuntimeKind, override_path: Option<&Path>) -> NodeR
/// why Bun was passed over. Blocking: call from `spawn_blocking`.
#[must_use]
pub fn resolve_extension_host_runtime(
choice: crate::config::ExtensionHostRuntime,
choice: codewhale_config::extension_host::ExtensionHostRuntime,
node_override: Option<&Path>,
bun_override: Option<&Path>,
bundle_sha256: &str,
) -> HostRuntimeResolution {
select_host_runtime(
choice,
|| resolve_runtime(HostRuntimeKind::Bun, bun_override),
|| resolve_runtime(HostRuntimeKind::Node, node_override),
|| resolve_runtime(HostRuntimeKind::Bun, bun_override, bundle_sha256),
|| resolve_runtime(HostRuntimeKind::Node, node_override, bundle_sha256),
)
}
fn select_host_runtime(
choice: crate::config::ExtensionHostRuntime,
choice: codewhale_config::extension_host::ExtensionHostRuntime,
probe_bun: impl FnOnce() -> NodeResolution,
probe_node: impl FnOnce() -> NodeResolution,
) -> HostRuntimeResolution {
use crate::config::ExtensionHostRuntime as Choice;
use codewhale_config::extension_host::ExtensionHostRuntime as Choice;
let mut resolution = HostRuntimeResolution {
choice,
selected: None,
@@ -953,7 +973,11 @@ fn accepted_flags(path: &Path, flags: &[&'static str]) -> Vec<&'static str> {
.collect()
}
fn select_runtime(kind: HostRuntimeKind, candidates: Vec<PathBuf>) -> NodeResolution {
fn select_runtime(
kind: HostRuntimeKind,
candidates: Vec<PathBuf>,
bundle_sha256: &str,
) -> NodeResolution {
let mut seen = std::collections::HashSet::new();
let mut resolution = NodeResolution::default();
for candidate in candidates {
@@ -962,7 +986,7 @@ fn select_runtime(kind: HostRuntimeKind, candidates: Vec<PathBuf>) -> NodeResolu
if !seen.insert(candidate.clone()) {
continue;
}
match probe_runtime_version(kind, &candidate) {
match probe_runtime_version(kind, &candidate, bundle_sha256) {
Ok(version) if kind.supported(version) => {
resolution.selected = Some((candidate, version));
break;
@@ -1044,7 +1068,6 @@ pub trait ExternalTool {
/// Convenience: run the tool with arguments and return only the
/// exit status (discards stdout/stderr).
#[cfg_attr(not(test), expect(dead_code))]
fn status(args: &[&str], cwd: &std::path::Path) -> std::io::Result<std::process::ExitStatus> {
let mut cmd = Self::command().ok_or_else(Self::not_found_error)?;
cmd.args(args).current_dir(cwd).status()
@@ -1060,7 +1083,7 @@ pub trait ExternalTool {
let spec = Self::resolve()?;
let (program, fixed_args) = split_interpreter_spec(&spec);
let mut cmd = tokio::process::Command::new(&program);
crate::utils::suppress_tokio_console_window(&mut cmd);
codewhale_util::utils::suppress_tokio_console_window(&mut cmd);
for arg in &fixed_args {
cmd.arg(arg);
}
@@ -1072,7 +1095,7 @@ pub trait ExternalTool {
fn command_for_spec(spec: &str) -> Command {
let (program, fixed_args) = split_interpreter_spec(spec);
let mut cmd = Command::new(&program);
crate::utils::suppress_console_window(&mut cmd);
codewhale_util::utils::suppress_console_window(&mut cmd);
for arg in &fixed_args {
cmd.arg(arg);
}
@@ -1109,7 +1132,7 @@ pub struct Git;
/// This is the single definition site; [`Git::command`] and
/// [`Git::tokio_command`] apply it to every product git spawn. Call it
/// directly only for a non-git program that may shell out to git (`gh`).
pub(crate) fn apply_git_noninteractive_env(cmd: &mut Command) {
pub fn apply_git_noninteractive_env(cmd: &mut Command) {
cmd.env("GIT_TERMINAL_PROMPT", "0").env("GIT_PAGER", "");
if std::env::var_os("GIT_SSH_COMMAND").is_none() && std::env::var_os("GIT_SSH").is_none() {
cmd.env("GIT_SSH_COMMAND", "ssh -o BatchMode=yes");
@@ -1124,7 +1147,7 @@ impl Git {
/// only. A read that touches the working tree also runs the superproject's
/// clean filters, which no flag disables: build it from
/// [`Self::review_command`] too.
pub(crate) const REVIEW_DIFF_ARGS: [&'static str; 4] = [
pub const REVIEW_DIFF_ARGS: [&'static str; 4] = [
"--no-ext-diff",
"--no-textconv",
"--submodule=short",
@@ -1136,7 +1159,7 @@ impl Git {
/// Construct a read-only review command with content conversion disabled.
/// Review callers also pass [`Self::REVIEW_DIFF_ARGS`] for diffs.
/// Configured filters otherwise execute even when those flags are present.
pub(crate) fn review_command(workspace: &Path) -> anyhow::Result<Command> {
pub fn review_command(workspace: &Path) -> anyhow::Result<Command> {
let overrides = Self::review_filter_overrides(workspace)?;
let mut command = Self::review_base(workspace)?;
let mut count = 2;
@@ -1153,7 +1176,7 @@ impl Git {
/// Git with fsmonitor, hooks, lazy fetch and replace objects disabled,
/// running in `workspace`. [`Self::review_command`] adds filter overrides.
pub(crate) fn review_base(workspace: &Path) -> anyhow::Result<Command> {
pub fn review_base(workspace: &Path) -> anyhow::Result<Command> {
use anyhow::Context;
let mut command = Self::command().context("git not found on PATH")?;
@@ -1178,7 +1201,7 @@ impl Git {
.env("GIT_CONFIG_KEY_0", "codewhale.reviewConfigCapability")
.env("GIT_CONFIG_VALUE_0", &value)
.args(["config", "--get", "codewhale.reviewConfigCapability"]);
match crate::snapshot::repo::run_bounded_git_with_timeout(
match run_bounded_git_with_timeout(
&mut probe,
"config",
Self::REVIEW_CONFIG_TIMEOUT,
@@ -1229,7 +1252,7 @@ impl Git {
/// Config overrides (`key`, `value`) that neutralize every clean/process
/// filter driver configured for the repository at `workspace`. Callers
/// apply them through `GIT_CONFIG_KEY_n`/`GIT_CONFIG_VALUE_n`.
pub(crate) fn review_filter_overrides(
pub fn review_filter_overrides(
workspace: &Path,
) -> anyhow::Result<Vec<(String, &'static str)>> {
use anyhow::{Context, bail};
@@ -1242,12 +1265,9 @@ impl Git {
"--get-regexp",
r"^filter\..*\.(clean|process|required)$",
]);
let output = crate::snapshot::repo::run_bounded_git_with_timeout(
&mut command,
"config",
Self::REVIEW_CONFIG_TIMEOUT,
)
.context("Failed to inspect Git review filters")?;
let output =
run_bounded_git_with_timeout(&mut command, "config", Self::REVIEW_CONFIG_TIMEOUT)
.context("Failed to inspect Git review filters")?;
let no_filters =
output.status.code() == Some(1) && output.stdout.is_empty() && output.stderr.is_empty();
if (!output.status.success() && !no_filters)
@@ -1314,7 +1334,7 @@ impl ExternalTool for Git {
/// credentials. The guards below are applied after the scrub.
fn command() -> Option<Command> {
let mut cmd = Command::new(Self::resolve()?);
crate::utils::suppress_console_window(&mut cmd);
codewhale_util::utils::suppress_console_window(&mut cmd);
codewhale_runtime::child_env::apply_to_git_command(&mut cmd);
cmd.env("GIT_OPTIONAL_LOCKS", "0");
apply_git_noninteractive_env(&mut cmd);
@@ -1508,6 +1528,126 @@ pub fn split_interpreter_spec(spec: &str) -> (String, Vec<String>) {
(program, args)
}
/// Grace granted to the pipe readers after git has exited. A clean git
/// closes its own write ends, so EOF is already waiting; only a grandchild
/// that inherited the pipes (a post-checkout hook, a `git gc` pack worker)
/// can hold them past exit, and it must not hold the turn pipeline either.
const GIT_PIPE_DRAIN_GRACE: Duration = Duration::from_secs(2);
/// Run a pre-configured git command under `timeout` with both pipes drained
/// while the child runs (the same concurrent drain `Command::output`
/// performs). A wedged git degrades to an error instead of hanging the caller.
///
/// The timeout path kills the child and reaps it on a detached thread: on a
/// hard-wedged mount git can sit in uninterruptible kernel I/O where even
/// SIGKILL is deferred.
pub fn run_bounded_git_with_timeout(
cmd: &mut std::process::Command,
subcommand: &str,
timeout: Duration,
) -> io::Result<Output> {
cmd.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped());
let mut child = cmd.spawn()?;
// Drain both pipes while waiting: the restore path's `ls-tree -r` and
// the diff commands emit output that grows with workspace size, and a
// child blocked on a full pipe buffer never exits — it would turn every
// such call into a guaranteed timeout. Readers stream into shared
// buffers, and the completion channel lets the collect below bound how
// long a grandchild-held pipe may hold the call after git has exited.
let (done_tx, done_rx) = std::sync::mpsc::channel::<()>();
let stdout_pipe = child.stdout.take();
let stderr_pipe = child.stderr.take();
let stdout_buf = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
let stderr_buf = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
{
let buf = std::sync::Arc::clone(&stdout_buf);
let done_tx = done_tx.clone();
std::thread::spawn(move || {
if let Some(mut reader) = stdout_pipe {
read_pipe_to_buffer(&mut reader, &buf);
}
let _ = done_tx.send(());
});
}
{
let buf = std::sync::Arc::clone(&stderr_buf);
let done_tx = done_tx.clone();
std::thread::spawn(move || {
if let Some(mut reader) = stderr_pipe {
read_pipe_to_buffer(&mut reader, &buf);
}
let _ = done_tx.send(());
});
}
drop(done_tx);
let Some(status) = child.wait_timeout(timeout)? else {
let _ = child.kill();
// Reap off the pipeline thread (see the doc comment): the kernel may
// not deliver the kill until an uninterruptible syscall returns.
std::thread::spawn(move || {
let _ = child.wait();
});
return Err(io::Error::new(
io::ErrorKind::TimedOut,
format!("git {subcommand} timed out after {}s", timeout.as_secs()),
));
};
// Wait for the readers up to the grace, then return what was captured. A
// clean git already closed its write ends, so both readers deliver
// immediately; the grace only covers a pipe still held open by an
// inherited copy (a daemonizing hook). On expiry the captured output is
// returned with a note on stderr instead of silently truncating; the
// reader itself ends whenever whatever holds the pipe exits, holding
// only a pipe read — never the turn pipeline.
let mut partial = false;
let mut drained = 0;
while drained < 2 {
match done_rx.recv_timeout(GIT_PIPE_DRAIN_GRACE) {
Ok(()) => drained += 1,
Err(_) => {
partial = true;
break;
}
}
}
let stdout = stdout_buf.lock().map(|buf| buf.clone()).unwrap_or_default();
let mut stderr = stderr_buf.lock().map(|buf| buf.clone()).unwrap_or_default();
if partial {
if !stderr.is_empty() && stderr.last() != Some(&b'\n') {
stderr.push(b'\n');
}
stderr.extend_from_slice(
b"[codewhale] git output pipes did not close after git exited \
(kept open by a hook or subprocess?); captured output may be partial\n",
);
}
Ok(Output {
status,
stdout,
stderr,
})
}
/// Read a child's pipe to end into a shared buffer, tolerating interruption.
fn read_pipe_to_buffer(reader: &mut impl io::Read, buf: &std::sync::Mutex<Vec<u8>>) {
let mut chunk = [0_u8; 8192];
loop {
match reader.read(&mut chunk) {
Ok(0) => return,
Ok(read) => {
if let Ok(mut buf) = buf.lock() {
buf.extend_from_slice(&chunk[..read]);
}
}
Err(err) if err.kind() == io::ErrorKind::Interrupted => continue,
Err(_) => return,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
@@ -1555,10 +1695,13 @@ mod tests {
#[cfg(unix)]
#[test]
fn version_probes_scrub_credentials_preloads_and_close_stdin() {
let _home = crate::test_support::SealedHome::new();
let _secret =
crate::test_support::EnvVarGuard::set("DEEPSEEK_API_KEY", "synthetic-probe-secret");
let _preload = crate::test_support::EnvVarGuard::set("NODE_OPTIONS", "synthetic-preload");
let _home = codewhale_runtime::test_support::SealedHome::new();
let _secret = codewhale_runtime::test_support::EnvVarGuard::set(
"DEEPSEEK_API_KEY",
"synthetic-probe-secret",
);
let _preload =
codewhale_runtime::test_support::EnvVarGuard::set("NODE_OPTIONS", "synthetic-preload");
let mut command = version_probe_command("/bin/sh");
command.args(["-c", "[ -z \"${DEEPSEEK_API_KEY+x}\" ] && [ -z \"${NODE_OPTIONS+x}\" ] && ! read ignored && printf clean"]);
let output = probe_output(&mut command, true, VERSION_PROBE_TIMEOUT).unwrap();
@@ -1665,6 +1808,7 @@ mod tests {
good.clone(),
later,
],
"",
);
assert_eq!(resolution.selected, Some((good, (22, 20, 0))));
assert_eq!(resolution.rejected.len(), 3);
@@ -1689,7 +1833,7 @@ mod tests {
#[cfg(unix)]
#[test]
fn host_runtime_auto_prefers_bun_and_explicit_choices_never_fall_back() {
use crate::config::ExtensionHostRuntime as Choice;
use codewhale_config::extension_host::ExtensionHostRuntime as Choice;
use std::os::unix::fs::PermissionsExt;
let dir = tempfile::tempdir().unwrap();
let script = |name: &str, body: &str| {
@@ -1711,6 +1855,7 @@ mod tests {
select_runtime(
kind,
candidates.iter().map(|path| (*path).clone()).collect(),
"",
)
};
@@ -1798,7 +1943,7 @@ mod tests {
#[cfg(unix)]
#[test]
fn untrusted_search_candidates_are_skipped_and_a_failing_override_is_final() {
use crate::config::ExtensionHostRuntime as Choice;
use codewhale_config::extension_host::ExtensionHostRuntime as Choice;
use std::os::unix::fs::PermissionsExt;
let dir = tempfile::tempdir().unwrap();
let script = |name: &str, body: &str| {
@@ -1860,7 +2005,8 @@ mod tests {
// A failing override never falls through to a search.
let broken = script("broken-bun", "exit 3");
let bun_only = resolve_extension_host_runtime(Choice::Bun, None, Some(broken.as_path()));
let bun_only =
resolve_extension_host_runtime(Choice::Bun, None, Some(broken.as_path()), "");
assert!(bun_only.selected.is_none());
let failure = bun_only.failure();
assert!(
@@ -1874,6 +2020,7 @@ mod tests {
Choice::Auto,
Some(node.as_path()),
Some(broken.as_path()),
"",
);
let summary = auto.summary();
assert!(summary.starts_with("node 24.1.0 at "), "{summary}");
@@ -2218,8 +2365,8 @@ mod tests {
#[cfg(unix)]
#[test]
fn runtime_commands_do_not_inherit_parent_secret_env() {
let _env_lock = crate::test_support::lock_test_env();
let _secret = crate::test_support::EnvVarGuard::set(
let _env_lock = codewhale_runtime::test_support::lock_test_env();
let _secret = codewhale_runtime::test_support::EnvVarGuard::set(
"CODEWHALE_TEST_RUNTIME_SECRET",
"runtime-secret-value",
);
@@ -2255,9 +2402,11 @@ mod tests {
if !Git::available() {
return;
}
let _env_lock = crate::test_support::lock_test_env();
let _secret =
crate::test_support::EnvVarGuard::set("CODEWHALE_TEST_GIT_SECRET", "git-secret-value");
let _env_lock = codewhale_runtime::test_support::lock_test_env();
let _secret = codewhale_runtime::test_support::EnvVarGuard::set(
"CODEWHALE_TEST_GIT_SECRET",
"git-secret-value",
);
let repo = tempfile::tempdir().expect("repo");
let hooks = tempfile::tempdir().expect("hooks");
let marker = hooks.path().join("seen");
@@ -2357,7 +2506,7 @@ mod tests {
assert!(!repo.join("helper-seen").exists());
return;
}
let _lock = crate::test_support::lock_test_env();
let _lock = codewhale_runtime::test_support::lock_test_env();
let real_git = resolve_executable_path("git", "--version").expect("absolute native Git");
let repo = tempfile::tempdir().unwrap();
let wrapper_dir = tempfile::tempdir().unwrap();
@@ -2432,7 +2581,7 @@ mod tests {
use std::os::unix::fs::PermissionsExt;
const CHILD: &str = "CODEWHALE_TEST_GIT_REVIEW_PATH_CHANGE";
if let Some(repo) = std::env::var_os(CHILD) {
let _lock = crate::test_support::lock_test_env();
let _lock = codewhale_runtime::test_support::lock_test_env();
let repo = PathBuf::from(repo);
let initial = Git::review_command(&repo)
.unwrap()
@@ -2442,7 +2591,7 @@ mod tests {
assert!(initial.status.success());
assert_eq!(initial.stdout, b"false\n");
let changed = std::env::var_os("CODEWHALE_TEST_CHANGED_GIT_PATH").unwrap();
let _path = crate::test_support::EnvVarGuard::set("PATH", changed);
let _path = codewhale_runtime::test_support::EnvVarGuard::set("PATH", changed);
let output = Git::review_command(&repo)
.unwrap()
.args(["status", "--porcelain"])
@@ -2457,7 +2606,7 @@ mod tests {
);
return;
}
let _lock = crate::test_support::lock_test_env();
let _lock = codewhale_runtime::test_support::lock_test_env();
let real_git = resolve_executable_path("git", "--version").expect("absolute native Git");
let quoted_git = real_git.replace('\'', "'\\''");
let repo = tempfile::tempdir().unwrap();
+1
View File
@@ -15,5 +15,6 @@ anyhow.workspace = true
codewhale-protocol = { path = "../protocol", version = "0.10.2" }
regex = "1.11"
serde.workspace = true
serde_json.workspace = true
shlex = "2.0.1"
toml.workspace = true
+16
View File
@@ -17,6 +17,14 @@ pub enum ApprovalMode {
}
impl ApprovalMode {
pub fn from_auto_approve(auto_approve: bool) -> Self {
if auto_approve {
Self::Bypass
} else {
Self::Suggest
}
}
/// Shift+Tab permission cycle order (#0.8.68 M2).
pub const PERMISSION_CYCLE: [Self; 3] = [Self::Suggest, Self::Auto, Self::Bypass];
@@ -44,6 +52,14 @@ impl ApprovalMode {
}
}
#[must_use]
pub fn agent_baseline_sandbox_mode(self) -> &'static str {
match self {
Self::Bypass => "danger-full-access",
Self::Auto | Self::Suggest | Self::Never => "workspace-write",
}
}
#[must_use]
pub fn cycle_permission_next(self) -> Self {
let Some(index) = Self::PERMISSION_CYCLE.iter().position(|mode| *mode == self) else {
+176
View File
@@ -0,0 +1,176 @@
//! Semantic aliases for the model-facing action tools.
//!
//! `Bash`, `File`, `Git`, `Run`, `Web`, and `rlm` deliberately keep their
//! canonical names at the execution and audit boundaries. Presentation and
//! policy consumers, however, still understand the older per-action names.
//! Resolve that semantic name in one place so live calls and saved legacy
//! transcripts receive identical downstream behavior without rewriting the
//! original call.
//!
//! This table is not documentation — it is the **action-policy seam**. A
//! permission check that denies `fetch_url` only reaches `Web{action:"fetch"}`
//! because the pair is listed here. A family that is missing from the table is
//! a family whose actions no deny list can see, which is why `rlm` was added:
//! `rlm{action:"open", url:...}` calls `FetchUrlTool` *inside the process*,
//! under its own name, and a name-keyed deny list never sees that call.
use serde_json::Value;
pub const CANONICAL_ACTION_ALIASES: &[(&str, &str, &str)] = &[
("bash", "run", "exec_shell"),
("bash", "wait", "exec_shell_wait"),
("bash", "interact", "exec_shell_interact"),
("bash", "cancel", "exec_shell_cancel"),
("Bash", "run", "exec_shell"),
("Bash", "wait", "exec_shell_wait"),
("Bash", "interact", "exec_shell_interact"),
("Bash", "cancel", "exec_shell_cancel"),
("File", "read", "read_file"),
("File", "list", "list_dir"),
("File", "search_name", "file_search"),
("File", "search_content", "grep_files"),
("File", "write", "write_file"),
("File", "edit", "edit_file"),
("File", "patch", "apply_patch"),
("Git", "status", "git_status"),
("Git", "diff", "git_diff"),
("Git", "log", "git_log"),
("Git", "show", "git_show"),
("Git", "blame", "git_blame"),
("Git", "commit_plan", "git_commit_plan"),
("Git", "fetch", "git_fetch"),
("Git", "merge_tree", "git_merge_tree"),
("Run", "tests", "run_tests"),
("Run", "verifiers", "run_verifiers"),
("Web", "search", "web_search"),
("Web", "fetch", "fetch_url"),
("Web", "wait", "wait_for_dev_server"),
// The RLM session family. `open` reaches the network (it fetches a `url`
// through `FetchUrlTool` in-process) and `eval` runs operator-supplied
// Python against a live kernel — sockets and filesystem both. The other
// three actions are bounded local metadata. Listing every pair is what lets
// a deny list keep the local half and remove the reaching half, instead of
// having to choose between the whole family and nothing.
("rlm", "session_objects", "rlm_session_objects"),
("rlm", "open", "rlm_open"),
("rlm", "eval", "rlm_eval"),
("rlm", "configure", "rlm_configure"),
("rlm", "close", "rlm_close"),
// The durable-work families. These were absent for the same reason `rlm`
// was: they are model-visible under one canonical name (`tasks`,
// `automation`, `github`) and their per-action legacy names are registered
// as *hidden* aliases. A deny list naming `task_gate_run` therefore never
// saw `tasks{action:"gate_run"}`, and the action-enum pruner never saw the
// family at all — so an operator ceiling could not express "durable task
// bookkeeping, yes; running a gate command, no".
//
// `gate_run` runs an operator-supplied command, `automation.run` executes a
// stored automation, and the mutating `automation.*` actions schedule agent
// runs with their own cwd. All three are execution primitives spelled as
// bookkeeping, which is exactly the shape
// `execution_envelope` classifies from capabilities.
("tasks", "create", "task_create"),
("tasks", "list", "task_list"),
("tasks", "read", "task_read"),
("tasks", "cancel", "task_cancel"),
("tasks", "gate_run", "task_gate_run"),
("tasks", "pr_attempt_record", "pr_attempt_record"),
("tasks", "pr_attempt_list", "pr_attempt_list"),
("tasks", "pr_attempt_read", "pr_attempt_read"),
("tasks", "pr_attempt_preflight", "pr_attempt_preflight"),
("automation", "create", "automation_create"),
("automation", "list", "automation_list"),
("automation", "read", "automation_read"),
("automation", "update", "automation_update"),
("automation", "pause", "automation_pause"),
("automation", "resume", "automation_resume"),
("automation", "delete", "automation_delete"),
("automation", "run", "automation_run"),
("github", "issue_context", "github_issue_context"),
("github", "pr_context", "github_pr_context"),
("github", "comment", "github_comment"),
("github", "close_issue", "github_close_issue"),
("github", "close_pr", "github_close_pr"),
("github", "report_draft", "github_report_draft"),
("github", "report_read", "github_report_read"),
];
/// The conservative action label policy uses when the model omits `action`.
///
/// This is a *policy* fallback only. Execution rejects an actionless call in
/// every family (see `required_action`); approval and parallel-safety
/// predicates cannot return an error, so they still need a label, and it must
/// be the family's least dangerous action.
///
/// `None` means the family never had even a policy default — `rlm`'s contract:
/// `RlmTool::resolve_action` errors rather than guessing.
/// Policy still resolves an *explicit* action for such a family — see
/// [`canonical_action_alias`].
#[must_use]
pub fn action_family_default(tool_name: &str) -> Option<Option<&'static str>> {
match tool_name {
"bash" | "Bash" => Some(Some("run")),
"File" => Some(Some("read")),
"Git" => Some(Some("status")),
"Run" => Some(Some("tests")),
"Web" => Some(Some("search")),
// Families whose wrappers reject an actionless call rather than
// guessing. Policy still resolves an *explicit* action for them.
"rlm" | "tasks" | "automation" | "github" => Some(None),
_ => None,
}
}
/// Whether `tool_name` is a model-facing action family whose `action` enum
/// policy may prune.
///
/// Derived from [`action_family_default`] rather than spelled out at each call
/// site: a hard-coded family list that falls behind the alias table is a family
/// whose actions stay visible after policy removed them.
#[must_use]
pub fn is_action_family(tool_name: &str) -> bool {
action_family_default(tool_name).is_some()
}
/// Resolve a canonical action tool to the legacy name for that exact action.
///
/// A missing action falls back to the family's conservative default so the
/// *policy* label is never absent; execution itself refuses the call (see
/// `required_action`). Unknown actions stay canonical so policy remains
/// conservative and the eventual tool error is attributed to the call the
/// model actually made.
///
/// A family with no default (`rlm`) still resolves an **explicit** action. The
/// earlier shape returned the family name for any such call, which meant
/// `rlm{action:"eval"}` never resolved to `rlm_eval` and therefore never met a
/// deny list entry naming it.
#[must_use]
pub fn canonical_action_alias<'a>(tool_name: &'a str, input: &Value) -> &'a str {
// The new model-facing file primitives deliberately reuse the old
// semantic policy names. This keeps permissions.toml, repo law, resource
// envelopes, approval caches, audit aggregation, and saved policy state
// compatible across the presentation change.
match tool_name {
"read" => return "read_file",
"write" => return "write_file",
"edit" => return "edit_file",
_ => {}
}
let Some(default_action) = action_family_default(tool_name) else {
return tool_name;
};
let Some(action) = input
.get("action")
.and_then(Value::as_str)
.or(default_action)
else {
return tool_name;
};
CANONICAL_ACTION_ALIASES
.iter()
.find_map(|(family, candidate_action, alias)| {
(*family == tool_name && *candidate_action == action).then_some(*alias)
})
.unwrap_or(tool_name)
}
+1
View File
@@ -1,5 +1,6 @@
pub mod approval_mode;
pub mod bash_arity;
pub mod canonical_action;
pub mod command_safety;
pub mod matcher;
pub mod shell_expand;
+54 -2
View File
@@ -1,4 +1,13 @@
{
"ReasoningOnlyRetryBody": "només raonament {attempt}/{limit}; sense resposta ni crida d’eina; {action}",
"OutputLimitAnswerRetryBody": "límit de sortida {attempt}/{limit}; sense resposta ni crida d’eina; {action}",
"AnswerRetryActionBare": "tornant a demanar la resposta",
"AnswerRetryActionNudge": "tornant a demanar la resposta amb un recordatori",
"AnswerRetryCounter": "només raonament/límit de sortida",
"OrcaRouterOfferRecommended": "OrcaRouter · Recomanat",
"OrcaRouterOfferAction": "Mostra l’oferta d’OrcaRouter a /provider o /model",
"OrcaRouterOfferBody": "Aconsegueix un 30% de crèdit addicional per a DeepSeek V4.1 Flash en les recàrregues amb l’oferta Codewhale × OrcaRouter. Consulta les condicions vigents a la pàgina de l’oferta.",
"OrcaRouterOfferDisclosure": "Codewhale rep una comissió de recomanació d’OrcaRouter.",
"ShellWaitNoActive": "No s'està esperant cap ordre de l'intèrpret.",
"ShellWaitInteractive": "Les ordres interactives no poden continuar en segon pla. Cancel·la el torn o espera que acabin.",
"ShellWaitUnavailable": "Aquesta eina no pot continuar en segon pla. Cancel·la el torn o espera que acabi.",
@@ -185,6 +194,7 @@
"ComposerHintOfflineConnect": "↵ desar per més tard · /provider connecta",
"ComposerHintSendNow": "↵ enviar ara",
"ComposerHintSendIntoTurn": "↵ enviar a aquest torn",
"ComposerHintLongDraft": "{shortcut}: editor extern",
"PendingSendingIntoTurnPrefix": " ↳ S'està enviant a aquest torn: ",
"PendingCouldNotSendIntoTurnPrefix": " ↳ No s'ha pogut enviar a aquest torn: ",
"PendingEditingFollowUpPrefix": " ↳ S'està editant el seguiment: ",
@@ -464,6 +474,9 @@
"CmdImportClaudeDescription": "Planifica una importació revisable de la configuració i els servidors MCP de Claude Code",
"CmdClearDescription": "Esborra l'historial de la conversació",
"CmdColorDescription": "Defineix el color del quadre de redacció d'aquest xat",
"CmdColorSet": "Color del quadre de redacció d'aquest xat: {color}",
"CmdColorReset": "El color del quadre de redacció d'aquest xat s'ha restablert al valor per defecte del tema",
"CmdColorUnknown": "Color desconegut. Tria'n un: {colors} o off.",
"CmdCompactDescription": "Dispara la compactació del context per alliberar espai",
"CmdPurgeDescription": "Permet que l'agent podi quirúrgicament l'historial de la conversació per alliberar espai de context",
"CmdConfigDescription": "Inspecciona i canvia la configuració (claus en brut com a detall avançat)",
@@ -795,7 +808,7 @@
"KbSendDraft": "Envia quan estigui lliure; encua mentre estigui ocupat; amb el redactor buit envia ara el següent seguiment en cua",
"KbSteerCurrentTurn": "Dirigeix el torn actual (envia normalment quan està lliure)",
"KbCloseMenu": "Tanca el menú, cancel·la la petició, descarta l'esborrany o neteja l'entrada",
"KbCancelOrExit": "Cancel·la la petició, o surt quan està lliure",
"KbCancelOrExit": "Cancel·la, esborra l’esborrany o surt quan està lliure",
"KbShellControls": "Mou l'espera del shell en execució a /jobs",
"KbExitEmpty": "Surt quan l'entrada és buida",
"KbCommandPalette": "Obre la paleta de comandes",
@@ -1257,6 +1270,23 @@
"ApprovalControlsHint": "Pg↑/↓ revisa · {details} detalls · Esc atura",
"ApprovalOwnerHeader": "Agent: {agent} · {role}",
"ApprovalGoToAgent": "g ves a l'agent",
"UserInputActionRequired": "Cal una acció",
"UserInputQuestionProgress": "Pregunta {current} de {total}",
"UserInputOtherLabel": "Altres",
"UserInputOtherDescription": "Escriu una resposta personalitzada",
"UserInputConfirmLabel": "Confirma la selecció",
"UserInputConfirmDescription": "Envia la selecció ({count})",
"UserInputCustomPrompt": "Resposta personalitzada:",
"UserInputCustomPlaceholder": "(escriu la teva resposta)",
"UserInputHintSubmit": "envia",
"UserInputHintBack": "tornar",
"UserInputHintMove": "mou",
"UserInputHintToggle": "commutar",
"UserInputHintSelectConfirm": "tria/confirma",
"UserInputHintCancel": "cancel·la",
"UserInputHintQuickPick": "tria ràpida",
"UserInputHintConfirm": "confirmar",
"UserInputHintDigit": "dígit",
"ApprovalControlsHintChild": "Pg↑/↓ revisa · {details} detalls · Esc amaga",
"PendingApprovalInAgent": "Cal una aprovació a {agent} — /agents",
"ApprovalTruncationHint": " … truncat · prem {details} per a tots els detalls",
@@ -1353,6 +1383,8 @@
"ToolReceiptLinesSingular": "1 línia",
"ToolReceiptLinesPlural": "{count} línies",
"ToolDescriptionNote": "nota",
"ToolStatusWaitingForApproval": "esperant aprovació",
"ToolStatusQueued": "a la cua",
"CmdConstitutionAccountApplies": "Constitució del perfil del compte: s’aplica (sessió iniciada; el motor la carrega a cada torn)",
"CmdConstitutionAccountSignedOut": "Constitució del perfil del compte: no has iniciat sessió",
"CmdConstitutionAccountCheckFailed": "Constitució del perfil del compte: ha fallat la comprovació ({error})",
@@ -2571,6 +2603,7 @@
"PlanHandoffKeepPlanning": "Continua planificant",
"PlanHandoffKeepPlanningDetail": "Segueix a Plan. No es canvia res.",
"PlanHandoffProceed": "Endavant amb el pla.",
"PlanHandoffTodoNote": "Fes el seguiment d'aquest pla amb todo_write: mantén l'element \"{title}\" a la llista i marca'l com a completat quan el pla estigui acabat.",
"PetAppearanceLegacy": "La mascota funciona amb una versió anterior. Reinicia-la després d’actualitzar Codewhale per triar l’estil de balena i la llengua del text.",
"PluginOAuthBrowser": "Prem Retorn per iniciar sessió al navegador o Esc per tornar. No cal cap clau API.",
"PluginOAuthReady": "{provider}: sessió iniciada. Tria un model.",
@@ -2581,5 +2614,24 @@
"ExecSandboxHelp": "Política de sandbox d’aquesta execució; qualsevol --sandbox diferent de danger-full-access (o un sandbox_mode equivalent a la configuració) desactiva el mode de confiança.",
"ExecAfterHelp": "Exemples:\n codewhale exec \"explica aquesta funció\"\n codewhale exec --auto \"llista crates/ amb ls\"\n codewhale exec --auto --output-format stream-json \"corregeix la prova que falla\"\n\n`codewhale exec` sense opcions produeix una única resposta del model: un torn\ndel motor Engine amb el mateix prompt de sistema que les altres execucions i\nsense eines. Fes servir `--auto` per executar un agent amb eines sense interacció.\nNomés s’ofereixen eines amb `--auto`, `--yolo`, `--allowed-tools` o en reprendre\nuna sessió; límits com `--max-turns`, `--disallowed-tools` o `--sandbox`, i el\nformat de sortida, no afegeixen mai eines. Una resposta tallada pel límit de\nsortida del proveïdor continua en el mateix torn, amb un màxim de 8 passos\ndel model, llevat que `--max-turns` indiqui un altre valor.\n--auto fa servir Full Access per defecte: sense sandbox del shell i amb el mode de confiança activat. Fes servir --sandbox workspace-write per mantenir el sandbox del shell i les restriccions de les eines de fitxers a l’espai de treball; també s’apliquen les polítiques més restrictives configurades. Tornar a provar una operació denegada pel sandbox requereix --allow-sandbox-elevation o --sandbox danger-full-access.\nLes execucions que ofereixen eines, les execucions stream-json i les sessions represes es desen per a codewhale sessions, exec --continue i codewhale receipts <id>. Les execucions text/--json que només responen no es desen, llevat que la compactació necessiti dades de recuperació.\nQualsevol --sandbox diferent de danger-full-access (o un sandbox_mode equivalent a la configuració) desactiva el mode de confiança.\n",
"ExecPromptHelp": "Indicació que s’enviarà al model. Posa -- abans d’una indicació que comenci amb - o contingui una opció literal d’exec; fes servir --prompt-file - per llegir l’entrada estàndard.",
"ExecFlagAfterPrompt": "{flag} apareix després de la indicació i es tractaria com a text. Posa les opcions abans de la indicació o, per usar-les com a text literal, després de --."
"ExecFlagAfterPrompt": "{flag} apareix després de la indicació i es tractaria com a text. Posa les opcions abans de la indicació o, per usar-les com a text literal, després de --.",
"InstructionNoticeHeader": "S'han retallat o ometut instruccions permanents abans de començar aquesta sessió:",
"InstructionNoticeFooter": "Executeu /context report per veure el desglossament complet per font.",
"InstructionNoticeMore": "…i {count} més; la llista completa és a /context report.",
"InstructionNoticeLine": "instruccions: {scope} {label} ({origin}, {action}, {limit})",
"InstructionNoticeTrimmed": " s'han conservat {kept_bytes} de {total_bytes} bytes i ~{kept_tokens} de ~{total_tokens} tokens estimats",
"InstructionNoticeDroppedBytes": " s'han descartat {bytes} bytes",
"InstructionNoticeFallback": "; alternativa: {source}",
"InstructionNoticeStaticGuardOver": "instruccions: el prefix estàtic encara supera en ~{tokens} tokens estimats el límit del prefix estàtic després de retallar les instruccions; es continua amb la política `trim` — augmenteu static_prefix_guard_percent o reduïu la superfície d'eines.",
"InstructionNoticeStaticGuardTrimmed": "instruccions: l'àmbit {scope} s'ha retallat ~{tokens} tokens estimats per mantenir el prefix estàtic sota el límit",
"InstructionDetailDroppedBroadest": "descartat com a segment de l'àmbit més ampli",
"InstructionDetailNoRoom": "el pressupost d'àmbit de {tokens} tokens estimats no ha deixat espai per al contingut",
"InstructionDetailRulesExhausted": "les instruccions han esgotat el pressupost d'àmbit del projecte de {tokens} tokens estimats",
"InstructionDetailOverCeiling": "supera el límit de {bytes} bytes",
"InstructionDetailUnreadable": "no s'ha pogut llegir: {reason}",
"InstructionDetailAlreadyLoaded": "ja carregat des d'un àmbit més ampli",
"InstructionDetailRulesCap": "supera el límit de {count} fitxers de regles per directori",
"InstructionDetailSymlinkedRules": "es rebutja el directori de regles perquè és un enllaç simbòlic",
"InstructionBudgetRefused": "el pressupost d'instruccions ha rebutjat aquesta sol·licitud: {reason}",
"InstructionBudgetOverGuard": "el prefix estàtic de ~{measured} tokens estimats supera el límit {guard}"
}
+54 -2
View File
@@ -1,4 +1,13 @@
{
"ReasoningOnlyRetryBody": "nur Überlegungen {attempt}/{limit}; keine Antwort oder Werkzeuganfrage; {action}",
"OutputLimitAnswerRetryBody": "Ausgabelimit {attempt}/{limit}; keine Antwort oder Werkzeuganfrage; {action}",
"AnswerRetryActionBare": "Antwort wird erneut angefordert",
"AnswerRetryActionNudge": "Antwort wird mit einem Hinweis erneut angefordert",
"AnswerRetryCounter": "Überlegungen/Ausgabelimit",
"OrcaRouterOfferRecommended": "OrcaRouter · Empfohlen",
"OrcaRouterOfferAction": "OrcaRouter-Angebot in /provider oder /model anzeigen",
"OrcaRouterOfferBody": "Erhalte mit dem Angebot Codewhale × OrcaRouter bei Aufladungen 30% zusätzliches Guthaben für DeepSeek V4.1 Flash. Die aktuellen Bedingungen stehen auf der Angebotsseite.",
"OrcaRouterOfferDisclosure": "Codewhale erhält von OrcaRouter eine Vermittlungsprovision.",
"ShellWaitNoActive": "Aktuell wird auf keinen Shell-Befehl gewartet.",
"ShellWaitInteractive": "Interaktive Befehle können nicht im Hintergrund weiterlaufen. Brich den aktuellen Durchgang ab oder warte, bis der Befehl fertig ist.",
"ShellWaitUnavailable": "Dieses Werkzeug kann nicht im Hintergrund weiterlaufen. Brich den aktuellen Durchgang ab oder warte, bis es fertig ist.",
@@ -185,6 +194,7 @@
"ComposerHintOfflineConnect": "↵ für später speichern · /provider verbindet",
"ComposerHintSendNow": "↵ jetzt senden",
"ComposerHintSendIntoTurn": "↵ in diesen Zug senden",
"ComposerHintLongDraft": "{shortcut}: externer Editor",
"PendingSendingIntoTurnPrefix": " ↳ Wird in diesen Zug gesendet: ",
"PendingCouldNotSendIntoTurnPrefix": " ↳ Konnte nicht in diesen Zug: ",
"PendingEditingFollowUpPrefix": " ↳ Folgebeitrag wird bearbeitet: ",
@@ -464,6 +474,9 @@
"CmdImportClaudeDescription": "Einen prüfbaren Import der Claude-Code-Einstellungen und MCP-Server planen",
"CmdClearDescription": "Gesprächsverlauf löschen",
"CmdColorDescription": "Farbe des Eingabefelds für diesen Chat festlegen",
"CmdColorSet": "Farbe des Eingabefelds für diesen Chat: {color}",
"CmdColorReset": "Farbe des Eingabefelds für diesen Chat auf den Theme-Standard zurückgesetzt",
"CmdColorUnknown": "Unbekannte Farbe. Wähle eine von: {colors} oder off.",
"CmdCompactDescription": "Kontextkomprimierung auslösen, um Platz freizugeben",
"CmdPurgeDescription": "Den Agenten den Gesprächsverlauf gezielt kürzen lassen, um Kontextplatz freizugeben",
"CmdConfigDescription": "Einstellungen ansehen und ändern (Rohschlüssel als Detail für Fortgeschrittene)",
@@ -795,7 +808,7 @@
"KbSendDraft": "Bei Leerlauf senden; bei Arbeit einreihen; leerer Composer sendet die nächste Folgenachricht sofort",
"KbSteerCurrentTurn": "Aktuellen Zug steuern (sendet normal bei Leerlauf)",
"KbCloseMenu": "Menü schließen, Anfrage abbrechen, Entwurf verwerfen oder Eingabe leeren",
"KbCancelOrExit": "Anfrage abbrechen oder bei Leerlauf beenden",
"KbCancelOrExit": "Abbrechen, Entwurf leeren oder im Leerlauf beenden",
"KbShellControls": "Laufendes Shell-Warten nach /jobs verschieben",
"KbExitEmpty": "Beenden, wenn die Eingabe leer ist",
"KbCommandPalette": "Befehlspalette öffnen",
@@ -1257,6 +1270,23 @@
"ApprovalControlsHint": "Pg↑/↓ prüfen · {details} Details · Esc stoppen",
"ApprovalOwnerHeader": "Agent: {agent} · {role}",
"ApprovalGoToAgent": "g zum Agenten",
"UserInputActionRequired": "Aktion erforderlich",
"UserInputQuestionProgress": "Frage {current} von {total}",
"UserInputOtherLabel": "Sonstiges",
"UserInputOtherDescription": "Eigene Antwort eingeben",
"UserInputConfirmLabel": "Auswahl bestätigen",
"UserInputConfirmDescription": "Auswahl senden ({count})",
"UserInputCustomPrompt": "Eigene Antwort:",
"UserInputCustomPlaceholder": "(Antwort eingeben)",
"UserInputHintSubmit": "senden",
"UserInputHintBack": "zurück",
"UserInputHintMove": "bewegen",
"UserInputHintToggle": "an/aus",
"UserInputHintSelectConfirm": "wählen/senden",
"UserInputHintCancel": "abbrechen",
"UserInputHintQuickPick": "Schnellwahl",
"UserInputHintConfirm": "bestätigen",
"UserInputHintDigit": "Ziffer",
"ApprovalControlsHintChild": "Pg↑/↓ prüfen · {details} Details · Esc ausblenden",
"PendingApprovalInAgent": "Freigabe nötig in {agent} — /agents",
"ApprovalTruncationHint": " … gekürzt · {details} für volle Details",
@@ -1353,6 +1383,8 @@
"ToolReceiptLinesSingular": "1 Zeile",
"ToolReceiptLinesPlural": "{count} Zeilen",
"ToolDescriptionNote": "Hinweis",
"ToolStatusWaitingForApproval": "wartet auf Freigabe",
"ToolStatusQueued": "in Warteschlange",
"CmdConstitutionAccountApplies": "Kontoprofil-Verfassung: gilt (angemeldet; die Engine lädt sie bei jedem Schritt)",
"CmdConstitutionAccountSignedOut": "Kontoprofil-Verfassung: nicht angemeldet",
"CmdConstitutionAccountCheckFailed": "Kontoprofil-Verfassung: Kontoprüfung fehlgeschlagen ({error})",
@@ -2571,6 +2603,7 @@
"PlanHandoffKeepPlanning": "Weiter planen",
"PlanHandoffKeepPlanningDetail": "In Plan bleiben. Nichts wird geändert.",
"PlanHandoffProceed": "Setze den Plan jetzt um.",
"PlanHandoffTodoNote": "Verfolge diesen Plan mit todo_write: Behalte den Eintrag \"{title}\" in deiner Liste und markiere ihn als erledigt, sobald der Plan abgeschlossen ist.",
"PetAppearanceLegacy": "Das Haustier verwendet eine ältere Version. Starte es nach dem Codewhale-Update neu, um den Walstil und die Textsprache auszuwählen.",
"PluginOAuthBrowser": "Mit Eingabe im Browser anmelden, mit Esc zurück. Kein API-Schlüssel erforderlich.",
"PluginOAuthReady": "{provider}: angemeldet. Wählen Sie ein Modell.",
@@ -2581,5 +2614,24 @@
"ExecSandboxHelp": "Sandbox-Richtlinie für diesen Lauf; jedes --sandbox außer danger-full-access (oder ein entsprechender sandbox_mode in der Konfiguration) deaktiviert den Vertrauensmodus.",
"ExecAfterHelp": "Beispiele:\n codewhale exec \"erkläre diese Funktion\"\n codewhale exec --auto \"liste crates/ mit ls auf\"\n codewhale exec --auto --output-format stream-json \"behebe den fehlschlagenden Test\"\n\nEin einfaches `codewhale exec` liefert eine einmalige Modellantwort: einen\nEngine-Turn mit demselben System-Prompt wie andere Läufe und ohne Tools.\nMit `--auto` wird ein Agent mit Tools ohne Interaktion ausgeführt. Tools\nwerden nur mit `--auto`, `--yolo`, `--allowed-tools` oder beim Fortsetzen einer\nSitzung angeboten; Grenzen wie `--max-turns`, `--disallowed-tools` oder\n`--sandbox` sowie das Ausgabeformat fügen niemals Tools hinzu. Eine durch das\nAusgabelimit des Anbieters abgeschnittene Antwort wird im selben Turn\nfortgesetzt, mit höchstens 8 Modellschritten, sofern `--max-turns` nichts\nanderes vorgibt.\n--auto verwendet standardmäßig Full Access: keine Shell-Sandbox und aktiver Vertrauensmodus. Mit --sandbox workspace-write bleiben die Shell-Sandbox und die Arbeitsbereichsbeschränkungen der Datei-Tools erhalten; strengere konfigurierte Richtlinien gelten ebenfalls. Ein erneuter Versuch nach einer Sandbox-Verweigerung erfordert --allow-sandbox-elevation oder --sandbox danger-full-access.\nLäufe mit angebotenen Tools, stream-json-Läufe und fortgesetzte Sitzungen werden für codewhale sessions, exec --continue und codewhale receipts <id> gespeichert. Reine Antwortläufe mit text/--json werden nicht gespeichert, es sei denn, die Komprimierung benötigt Wiederherstellungsdaten.\nJedes --sandbox außer danger-full-access (oder ein entsprechender sandbox_mode in der Konfiguration) deaktiviert den Vertrauensmodus.\n",
"ExecPromptHelp": "Prompt für das Modell. Setze -- vor einen Prompt, der mit - beginnt oder eine exec-Option als wörtlichen Text enthält; --prompt-file - liest die Standardeingabe.",
"ExecFlagAfterPrompt": "{flag} steht nach dem Prompt und würde als Text behandelt. Setze Optionen vor den Prompt oder wörtlichen Optionstext nach --."
"ExecFlagAfterPrompt": "{flag} steht nach dem Prompt und würde als Text behandelt. Setze Optionen vor den Prompt oder wörtlichen Optionstext nach --.",
"InstructionNoticeHeader": "Dauerhafte Anweisungen wurden vor Sitzungsbeginn gekürzt oder übersprungen:",
"InstructionNoticeFooter": "Führe /context report aus, um die vollständige Aufschlüsselung pro Quelle zu sehen.",
"InstructionNoticeMore": "…und {count} weitere; die vollständige Liste steht in /context report.",
"InstructionNoticeLine": "Anweisungen: {scope} {label} ({origin}, {action}, {limit})",
"InstructionNoticeTrimmed": " {kept_bytes} von {total_bytes} Bytes und ~{kept_tokens} von ~{total_tokens} geschätzten Tokens behalten",
"InstructionNoticeDroppedBytes": " {bytes} Bytes verworfen",
"InstructionNoticeFallback": "; Ersatz: {source}",
"InstructionNoticeStaticGuardOver": "Anweisungen: Das statische Präfix liegt nach dem Kürzen der Anweisungen noch ~{tokens} geschätzte Tokens über der Grenze für das statische Präfix; es geht mit der Richtlinie `trim` weiter — erhöhe static_prefix_guard_percent oder verkleinere die Werkzeugoberfläche.",
"InstructionNoticeStaticGuardTrimmed": "Anweisungen: Bereich {scope} um ~{tokens} geschätzte Tokens gekürzt, damit das statische Präfix unter der Grenze bleibt",
"InstructionDetailDroppedBroadest": "als Segment des breitesten Bereichs verworfen",
"InstructionDetailNoRoom": "das Bereichsbudget von {tokens} geschätzten Tokens ließ keinen Platz für Inhalt",
"InstructionDetailRulesExhausted": "das Projektbudget von {tokens} geschätzten Tokens ist durch Anweisungen aufgebraucht",
"InstructionDetailOverCeiling": "größer als die Obergrenze von {bytes} Bytes",
"InstructionDetailUnreadable": "konnte nicht gelesen werden: {reason}",
"InstructionDetailAlreadyLoaded": "bereits aus einem breiteren Bereich geladen",
"InstructionDetailRulesCap": "über der Grenze von {count} Regeldateien pro Verzeichnis",
"InstructionDetailSymlinkedRules": "Regelverzeichnis als Symlink abgelehnt",
"InstructionBudgetRefused": "Das Anweisungsbudget hat diese Anfrage abgelehnt: {reason}",
"InstructionBudgetOverGuard": "statisches Präfix von ~{measured} geschätzten Tokens überschreitet die Grenze {guard}"
}
+55 -3
View File
@@ -1,4 +1,13 @@
{
"ReasoningOnlyRetryBody": "reasoning-only {attempt}/{limit}; no answer or tool call; {action}",
"OutputLimitAnswerRetryBody": "output-limit {attempt}/{limit}; no answer or tool call; {action}",
"AnswerRetryActionBare": "re-requesting the answer",
"AnswerRetryActionNudge": "re-requesting the answer with a nudge",
"AnswerRetryCounter": "reasoning-only/output-limit",
"OrcaRouterOfferRecommended": "OrcaRouter · Recommended",
"OrcaRouterOfferAction": "View OrcaRouter offer in /provider or /model",
"OrcaRouterOfferBody": "Get a 30% top-up match for DeepSeek V4.1 Flash with the Codewhale × OrcaRouter offer. Check the offer page for current terms.",
"OrcaRouterOfferDisclosure": "Codewhale earns a referral fee from OrcaRouter.",
"ShellWaitNoActive": "No shell wait is active.",
"ShellWaitInteractive": "Interactive commands cannot continue in the background. Cancel the turn, or wait for completion.",
"ShellWaitUnavailable": "This tool cannot continue in the background. Cancel the turn, or wait for completion.",
@@ -185,6 +194,7 @@
"ComposerHintOfflineConnect": "↵ save for later · /provider to connect",
"ComposerHintSendNow": "↵ send now",
"ComposerHintSendIntoTurn": "↵ send into this turn",
"ComposerHintLongDraft": "{shortcut}: external editor",
"PendingSendingIntoTurnPrefix": " ↳ Sending into this turn: ",
"PendingCouldNotSendIntoTurnPrefix": " ↳ Held for next turn: ",
"PendingEditingFollowUpPrefix": " ↳ Editing follow-up: ",
@@ -467,6 +477,9 @@
"CmdImportClaudeDescription": "Plan a reviewable import of Claude Code settings and servers",
"CmdClearDescription": "Clear conversation history",
"CmdColorDescription": "Set this chat's composer box color",
"CmdColorSet": "Composer color for this chat: {color}",
"CmdColorReset": "Composer color for this chat reset to the theme default",
"CmdColorUnknown": "Unknown color. Choose one of: {colors}, or off.",
"CmdCompactDescription": "Shrink the context to free space",
"CmdPurgeDescription": "Let the agent prune or offload history to free context",
"CmdConfigDescription": "Inspect and change settings — raw keys under advanced detail",
@@ -812,7 +825,7 @@
"KbSendDraft": "Send when idle; queue when busy; empty input sends the next queued item",
"KbSteerCurrentTurn": "Steer this turn (sends normally when idle)",
"KbCloseMenu": "Close menus, cancel, discard the draft, or clear input",
"KbCancelOrExit": "Cancel, or exit when idle",
"KbCancelOrExit": "Cancel, clear draft, or exit when idle",
"KbShellControls": "Move the running shell wait to /jobs",
"KbExitEmpty": "Exit when input is empty",
"KbCommandPalette": "Open the command palette",
@@ -1280,6 +1293,23 @@
"ApprovalControlsHint": "Pg↑/↓ review · {details} details · Esc stop",
"ApprovalOwnerHeader": "Agent: {agent} · {role}",
"ApprovalGoToAgent": "g go to agent",
"UserInputActionRequired": "Action required",
"UserInputQuestionProgress": "Question {current} of {total}",
"UserInputOtherLabel": "Other",
"UserInputOtherDescription": "Type a custom response",
"UserInputConfirmLabel": "Confirm selection",
"UserInputConfirmDescription": "Submit {count} selected",
"UserInputCustomPrompt": "Custom response:",
"UserInputCustomPlaceholder": "(type your response)",
"UserInputHintSubmit": "submit",
"UserInputHintBack": "back",
"UserInputHintMove": "move",
"UserInputHintToggle": "toggle",
"UserInputHintSelectConfirm": "select/confirm",
"UserInputHintCancel": "cancel",
"UserInputHintQuickPick": "quick pick",
"UserInputHintConfirm": "confirm",
"UserInputHintDigit": "digit",
"ApprovalControlsHintChild": "Pg↑/↓ review · {details} details · Esc hide",
"PendingApprovalInAgent": "Approval needed in {agent} — /agents",
"ApprovalTruncationHint": " … truncated · press {details} for full details",
@@ -1376,6 +1406,8 @@
"ToolReceiptLinesSingular": "1 line",
"ToolReceiptLinesPlural": "{count} lines",
"ToolDescriptionNote": "note",
"ToolStatusWaitingForApproval": "waiting for approval",
"ToolStatusQueued": "queued",
"CmdConstitutionAccountApplies": "Account profile constitution: applies (signed in; loaded by the Engine each turn)",
"CmdConstitutionAccountSignedOut": "Account profile constitution: not signed in",
"CmdConstitutionAccountCheckFailed": "Account profile constitution: account check failed ({error})",
@@ -2339,7 +2371,7 @@
"ConfigHintManagedApprovalPolicy": "a project, profile, environment, managed config, or organization requirement controls this value",
"ConfigHintManagedAllowShell": "a project, profile, environment, or managed config controls shell access",
"ConfigHintAllowShell": "on exposes shell tools in Work mode; permission rules still apply",
"ConfigHintComposerMultilineMode": "off: Enter sends, Shift+Enter adds a line; on: Enter adds a line, Shift+Enter sends",
"ConfigHintComposerMultilineMode": "off: Enter sends, Alt+Enter / Shift+Enter adds a line; on: Enter adds a line, Alt+Enter / Shift+Enter sends",
"ConfigHintBooleanValues": "on/off, true/false, yes/no, 1/0",
"ConfigHintDensity": "compact | comfortable | spacious",
"ConfigHintInlineDiffs": "full | summary | off; full change stays in Alt/Option+V",
@@ -2571,6 +2603,7 @@
"PlanHandoffKeepPlanning": "Keep planning",
"PlanHandoffKeepPlanningDetail": "Stay in Plan. Nothing is changed.",
"PlanHandoffProceed": "Go ahead with the plan.",
"PlanHandoffTodoNote": "Track this plan with todo_write: keep the item \"{title}\" in your list and mark it completed when the plan is done.",
"PetAppearanceLegacy": "The pet companion is running an older version. Restart it after updating Codewhale to use whale styles and text languages.",
"PluginOAuthBrowser": "Press Enter to connect in your browser. Esc returns. No API key is needed.",
"PluginOAuthReady": "{provider}: connected. Choose a model.",
@@ -2581,5 +2614,24 @@
"ExecSandboxHelp": "Sandbox policy for this run; any --sandbox other than danger-full-access (or an equivalent sandbox_mode in config) disables trust mode.",
"ExecAfterHelp": "Examples:\n codewhale exec \"explain this function\"\n codewhale exec --auto \"list crates/ with ls\"\n codewhale exec --auto --output-format stream-json \"fix the failing test\"\n\nPlain `codewhale exec` is a one-shot model response: one Engine turn with the\nsame system prompt as every other run, and no tools. Use `--auto` for\nnon-interactive agent-with-tools execution. Tools are offered only with\n`--auto`, `--yolo`, `--allowed-tools`, or when resuming a session; limits such\nas `--max-turns`, `--disallowed-tools` or `--sandbox`, and the output format,\nnever add tools. A reply cut off at the provider's output limit is continued\nin the same turn, at most 8 model steps unless `--max-turns` says otherwise.\n--auto defaults to Full Access: no shell sandbox and trust mode on. Use --sandbox workspace-write to keep the shell sandbox and file-tool workspace restrictions; stricter configured policies also apply. A denied sandbox retry requires --allow-sandbox-elevation or --sandbox danger-full-access.\nRuns that offer tools, stream-json runs, and resumed sessions are saved for codewhale sessions, exec --continue, and codewhale receipts <id>. Plain answer-only text/--json runs are not saved unless compaction needs recovery.\nAny --sandbox other than danger-full-access (or an equivalent sandbox_mode in config) disables trust mode.\n",
"ExecPromptHelp": "Prompt to send to the model. Put -- before a prompt starting with - or containing a literal exec flag; use --prompt-file - to read stdin.",
"ExecFlagAfterPrompt": "{flag} appears after the prompt and would be treated as text. Put flags before the prompt, or put literal flag text after --."
"ExecFlagAfterPrompt": "{flag} appears after the prompt and would be treated as text. Put flags before the prompt, or put literal flag text after --.",
"InstructionNoticeHeader": "Standing instructions were trimmed or skipped before this session started:",
"InstructionNoticeFooter": "Run /context report for the full per-source breakdown.",
"InstructionNoticeMore": "…and {count} more; the full list is in /context report.",
"InstructionNoticeLine": "instructions: {scope} {label} ({origin}, {action}, {limit})",
"InstructionNoticeTrimmed": " kept {kept_bytes} of {total_bytes} bytes and ~{kept_tokens} of ~{total_tokens} estimated tokens",
"InstructionNoticeDroppedBytes": " dropped {bytes} bytes",
"InstructionNoticeFallback": "; fallback: {source}",
"InstructionNoticeStaticGuardOver": "instructions: the static prefix is still ~{tokens} estimated tokens over the static-prefix guard after instruction trimming; continuing under policy `trim` — raise static_prefix_guard_percent or narrow the tool surface.",
"InstructionNoticeStaticGuardTrimmed": "instructions: {scope} scope trimmed by ~{tokens} estimated tokens to keep the static prefix under the guard",
"InstructionDetailDroppedBroadest": "dropped as a broadest-scope segment",
"InstructionDetailNoRoom": "scope budget of {tokens} estimated tokens left no room for content",
"InstructionDetailRulesExhausted": "project scope budget of {tokens} estimated tokens exhausted by instructions",
"InstructionDetailOverCeiling": "larger than the {bytes} byte ceiling",
"InstructionDetailUnreadable": "could not be read: {reason}",
"InstructionDetailAlreadyLoaded": "already loaded from a wider scope",
"InstructionDetailRulesCap": "beyond the per-directory rules cap of {count} files",
"InstructionDetailSymlinkedRules": "refusing symlinked rules directory",
"InstructionBudgetRefused": "instruction budget refused this request: {reason}",
"InstructionBudgetOverGuard": "static prefix ~{measured} estimated tokens exceeds guard {guard}"
}
+55 -3
View File
@@ -1,4 +1,13 @@
{
"ReasoningOnlyRetryBody": "solo razonamiento {attempt}/{limit}; sin respuesta ni llamada a una herramienta; {action}",
"OutputLimitAnswerRetryBody": "límite de salida {attempt}/{limit}; sin respuesta ni llamada a una herramienta; {action}",
"AnswerRetryActionBare": "solicitando otra vez la respuesta",
"AnswerRetryActionNudge": "solicitando otra vez la respuesta con un recordatorio",
"AnswerRetryCounter": "solo razonamiento/límite de salida",
"OrcaRouterOfferRecommended": "OrcaRouter · Recomendado",
"OrcaRouterOfferAction": "Ver oferta de OrcaRouter en /provider o /model",
"OrcaRouterOfferBody": "Obtén un 30% de crédito extra para DeepSeek V4.1 Flash en tus recargas con la oferta Codewhale × OrcaRouter. Consulta las condiciones vigentes en la página de la oferta.",
"OrcaRouterOfferDisclosure": "Codewhale recibe una comisión por recomendación de OrcaRouter.",
"ShellWaitNoActive": "No hay ninguna espera de shell activa.",
"ShellWaitInteractive": "Los comandos interactivos no pueden continuar en segundo plano. Cancela el turno o espera a que terminen.",
"ShellWaitUnavailable": "Esta herramienta no puede continuar en segundo plano. Cancela el turno o espera a que termine.",
@@ -185,6 +194,7 @@
"ComposerHintOfflineConnect": "↵ guardar para después · /provider conecta",
"ComposerHintSendNow": "↵ enviar ahora",
"ComposerHintSendIntoTurn": "↵ enviar a este turno",
"ComposerHintLongDraft": "{shortcut}: editor externo",
"PendingSendingIntoTurnPrefix": " ↳ Enviando a este turno: ",
"PendingCouldNotSendIntoTurnPrefix": " ↳ No se pudo enviar a este turno: ",
"PendingEditingFollowUpPrefix": " ↳ Editando el seguimiento: ",
@@ -467,6 +477,9 @@
"CmdImportClaudeDescription": "Planificar una importación revisable de la configuración y los servidores MCP de Claude Code",
"CmdClearDescription": "Limpiar el historial de la conversación",
"CmdColorDescription": "Definir el color del cuadro de mensaje de este chat",
"CmdColorSet": "Color del cuadro de mensaje de este chat: {color}",
"CmdColorReset": "Color del cuadro de mensaje de este chat restablecido al predeterminado del tema",
"CmdColorUnknown": "Color desconocido. Elige uno de: {colors} u off.",
"CmdCompactDescription": "Compactar el contexto para liberar espacio",
"CmdPurgeDescription": "Permite al agente eliminar quirúrgicamente historial innecesario para liberar espacio de contexto",
"CmdConfigDescription": "Inspeccionar y cambiar ajustes (claves crudas en detalle avanzado)",
@@ -812,7 +825,7 @@
"KbSendDraft": "Enviar si está inactivo; encolar si está ocupado; con el compositor vacío, enviar ahora el siguiente seguimiento en cola",
"KbSteerCurrentTurn": "Guiar el turno actual (envía normalmente si está inactivo)",
"KbCloseMenu": "Cerrar menú, cancelar solicitud, descartar borrador o limpiar entrada",
"KbCancelOrExit": "Cancelar solicitud o salir cuando está inactivo",
"KbCancelOrExit": "Cancelar, borrar el borrador o salir cuando está inactivo",
"KbShellControls": "Mover la espera del shell a /jobs",
"KbExitEmpty": "Salir cuando la entrada está vacía",
"KbCommandPalette": "Abrir paleta de comandos",
@@ -1278,6 +1291,23 @@
"ApprovalControlsHint": "Pg↑/↓ revisar · {details} detalles · Esc detener",
"ApprovalOwnerHeader": "Agente: {agent} · {role}",
"ApprovalGoToAgent": "g ir al agente",
"UserInputActionRequired": "Se requiere acción",
"UserInputQuestionProgress": "Pregunta {current} de {total}",
"UserInputOtherLabel": "Otro",
"UserInputOtherDescription": "Escribe una respuesta personalizada",
"UserInputConfirmLabel": "Confirmar selección",
"UserInputConfirmDescription": "Enviar selección ({count})",
"UserInputCustomPrompt": "Respuesta personalizada:",
"UserInputCustomPlaceholder": "(escribe tu respuesta)",
"UserInputHintSubmit": "enviar",
"UserInputHintBack": "volver",
"UserInputHintMove": "mover",
"UserInputHintToggle": "alternar",
"UserInputHintSelectConfirm": "elegir/confirmar",
"UserInputHintCancel": "cancelar",
"UserInputHintQuickPick": "selección rápida",
"UserInputHintConfirm": "confirmar",
"UserInputHintDigit": "núm.",
"ApprovalControlsHintChild": "Pg↑/↓ revisar · {details} detalles · Esc ocultar",
"PendingApprovalInAgent": "Se necesita aprobación en {agent} — /agents",
"ApprovalTruncationHint": " … truncado · presiona {details} para ver todos los detalles",
@@ -1374,6 +1404,8 @@
"ToolReceiptLinesSingular": "1 línea",
"ToolReceiptLinesPlural": "{count} líneas",
"ToolDescriptionNote": "nota",
"ToolStatusWaitingForApproval": "esperando aprobación",
"ToolStatusQueued": "en cola",
"CmdConstitutionAccountApplies": "Constitución del perfil de cuenta: se aplica (sesión iniciada; el motor la carga en cada turno)",
"CmdConstitutionAccountSignedOut": "Constitución del perfil de cuenta: sesión no iniciada",
"CmdConstitutionAccountCheckFailed": "Constitución del perfil de cuenta: falló la comprobación ({error})",
@@ -2339,7 +2371,7 @@
"ConfigHintManagedApprovalPolicy": "un proyecto, perfil, entorno, config administrado o requisito de la organización controla este valor",
"ConfigHintManagedAllowShell": "un proyecto, perfil, entorno o config administrado controla el acceso al shell",
"ConfigHintAllowShell": "activado expone herramientas de shell en modo Agente; las reglas de permiso siguen aplicando",
"ConfigHintComposerMultilineMode": "desactivado: Enter envía, Shift+Enter agrega línea; activado: Enter agrega línea, Shift+Enter envía",
"ConfigHintComposerMultilineMode": "desactivado: Enter envía, Alt+Enter / Shift+Enter agrega línea; activado: Enter agrega línea, Alt+Enter / Shift+Enter envía",
"ConfigHintBooleanValues": "on/off, true/false, yes/no, 1/0",
"ConfigHintDensity": "compact | comfortable | spacious",
"ConfigHintInlineDiffs": "full | summary | off; el cambio exacto permanece en los detalles de Alt/Option+V",
@@ -2571,6 +2603,7 @@
"PlanHandoffKeepPlanning": "Seguir planificando",
"PlanHandoffKeepPlanningDetail": "Sigue en Plan. No se cambia nada.",
"PlanHandoffProceed": "Adelante con el plan.",
"PlanHandoffTodoNote": "Haz seguimiento de este plan con todo_write: mantén el elemento \"{title}\" en tu lista y márcalo como completado cuando el plan esté terminado.",
"PetAppearanceLegacy": "La mascota usa una versión anterior. Reiníciala después de actualizar Codewhale para elegir el estilo de ballena y el idioma del texto.",
"PluginOAuthBrowser": "Presiona Intro para iniciar sesión en el navegador o Esc para volver. No se necesita una clave API.",
"PluginOAuthReady": "{provider}: sesión iniciada. Elige un modelo.",
@@ -2581,5 +2614,24 @@
"ExecSandboxHelp": "Política de sandbox para esta ejecución; cualquier --sandbox distinto de danger-full-access (o un sandbox_mode equivalente en la configuración) desactiva el modo de confianza.",
"ExecAfterHelp": "Ejemplos:\n codewhale exec \"explica esta función\"\n codewhale exec --auto \"lista crates/ con ls\"\n codewhale exec --auto --output-format stream-json \"corrige la prueba que falla\"\n\n`codewhale exec` sin opciones genera una única respuesta del modelo: un turno\ndel Engine con el mismo prompt del sistema que las demás ejecuciones y sin\nherramientas. Usa `--auto` para ejecutar un agente con herramientas sin interacción.\nSolo se ofrecen herramientas con `--auto`, `--yolo`, `--allowed-tools` o al\nreanudar una sesión; límites como `--max-turns`, `--disallowed-tools` o\n`--sandbox`, y el formato de salida, nunca agregan herramientas. Una respuesta\ncortada por el límite de salida del proveedor continúa en el mismo turno,\ncon un máximo de 8 pasos del modelo salvo que `--max-turns` indique otro valor.\n--auto usa Full Access de forma predeterminada: sin sandbox del shell y con el modo de confianza activado. Usa --sandbox workspace-write para mantener el sandbox del shell y las restricciones de las herramientas de archivos al espacio de trabajo; también se aplican las políticas más restrictivas configuradas. Reintentar una operación denegada por el sandbox requiere --allow-sandbox-elevation o --sandbox danger-full-access.\nLas ejecuciones que ofrecen herramientas, las ejecuciones stream-json y las sesiones reanudadas se guardan para codewhale sessions, exec --continue y codewhale receipts <id>. Las ejecuciones text/--json que solo responden no se guardan, a menos que la compactación necesite datos de recuperación.\nCualquier --sandbox distinto de danger-full-access (o un sandbox_mode equivalente en la configuración) desactiva el modo de confianza.\n",
"ExecPromptHelp": "Prompt que se enviará al modelo. Coloca -- antes de un prompt que empiece con - o contenga una opción literal de exec; usa --prompt-file - para leer la entrada estándar.",
"ExecFlagAfterPrompt": "{flag} aparece después del prompt y se trataría como texto. Coloca las opciones antes del prompt o, para usarlas como texto literal, después de --."
"ExecFlagAfterPrompt": "{flag} aparece después del prompt y se trataría como texto. Coloca las opciones antes del prompt o, para usarlas como texto literal, después de --.",
"InstructionNoticeHeader": "Se recortaron u omitieron instrucciones permanentes antes de que empezara esta sesión:",
"InstructionNoticeFooter": "Ejecuta /context report para ver el desglose completo por fuente.",
"InstructionNoticeMore": "…y {count} más; la lista completa está en /context report.",
"InstructionNoticeLine": "instrucciones: {scope} {label} ({origin}, {action}, {limit})",
"InstructionNoticeTrimmed": " se conservaron {kept_bytes} de {total_bytes} bytes y ~{kept_tokens} de ~{total_tokens} tokens estimados",
"InstructionNoticeDroppedBytes": " se descartaron {bytes} bytes",
"InstructionNoticeFallback": "; alternativa: {source}",
"InstructionNoticeStaticGuardOver": "instrucciones: el prefijo estático sigue ~{tokens} tokens estimados por encima del límite del prefijo estático tras recortar las instrucciones; se continúa con la política `trim` — aumenta static_prefix_guard_percent o reduce la superficie de herramientas.",
"InstructionNoticeStaticGuardTrimmed": "instrucciones: el ámbito {scope} se recortó ~{tokens} tokens estimados para mantener el prefijo estático bajo el límite",
"InstructionDetailDroppedBroadest": "descartado como segmento del ámbito más amplio",
"InstructionDetailNoRoom": "el presupuesto del ámbito de {tokens} tokens estimados no dejó espacio para contenido",
"InstructionDetailRulesExhausted": "las instrucciones agotaron el presupuesto del ámbito del proyecto de {tokens} tokens estimados",
"InstructionDetailOverCeiling": "supera el límite de {bytes} bytes",
"InstructionDetailUnreadable": "no se pudo leer: {reason}",
"InstructionDetailAlreadyLoaded": "ya se cargó desde un ámbito más amplio",
"InstructionDetailRulesCap": "excede el tope de {count} archivos de reglas por directorio",
"InstructionDetailSymlinkedRules": "se rechaza el directorio de reglas por ser un enlace simbólico",
"InstructionBudgetRefused": "el presupuesto de instrucciones rechazó esta solicitud: {reason}",
"InstructionBudgetOverGuard": "el prefijo estático de ~{measured} tokens estimados supera el límite {guard}"
}
+54 -2
View File
@@ -1,4 +1,13 @@
{
"ReasoningOnlyRetryBody": "réflexion seule {attempt}/{limit} ; aucune réponse ni appel d’outil ; {action}",
"OutputLimitAnswerRetryBody": "limite de sortie {attempt}/{limit} ; aucune réponse ni appel d’outil ; {action}",
"AnswerRetryActionBare": "nouvelle demande de réponse",
"AnswerRetryActionNudge": "nouvelle demande de réponse avec un rappel",
"AnswerRetryCounter": "réflexion seule/limite de sortie",
"OrcaRouterOfferRecommended": "OrcaRouter · Recommandé",
"OrcaRouterOfferAction": "Voir l’offre OrcaRouter dans /provider ou /model",
"OrcaRouterOfferBody": "Obtenez 30% de crédit supplémentaire pour DeepSeek V4.1 Flash sur vos recharges avec l’offre Codewhale × OrcaRouter. Consultez les conditions en vigueur sur la page de l’offre.",
"OrcaRouterOfferDisclosure": "Codewhale reçoit une commission de recommandation d’OrcaRouter.",
"ShellWaitNoActive": "Aucune attente de commande shell en cours.",
"ShellWaitInteractive": "Les commandes interactives ne peuvent pas continuer en arrière-plan. Annulez le tour ou attendez la fin de la commande.",
"ShellWaitUnavailable": "Cet outil ne peut pas continuer en arrière-plan. Annulez le tour ou attendez la fin de son exécution.",
@@ -185,6 +194,7 @@
"ComposerHintOfflineConnect": "↵ enregistrer pour plus tard · /provider connecte",
"ComposerHintSendNow": "↵ envoyer maintenant",
"ComposerHintSendIntoTurn": "↵ envoyer dans ce tour",
"ComposerHintLongDraft": "{shortcut} : éditeur externe",
"PendingSendingIntoTurnPrefix": " ↳ Envoi dans ce tour : ",
"PendingCouldNotSendIntoTurnPrefix": " ↳ Impossible d'envoyer dans ce tour : ",
"PendingEditingFollowUpPrefix": " ↳ Modification du suivi : ",
@@ -464,6 +474,9 @@
"CmdImportClaudeDescription": "Planifier une importation vérifiable des réglages et serveurs MCP de Claude Code",
"CmdClearDescription": "Effacer l'historique de la conversation",
"CmdColorDescription": "Définir la couleur de la zone de saisie de cette conversation",
"CmdColorSet": "Couleur de la zone de saisie de cette conversation : {color}",
"CmdColorReset": "Couleur de la zone de saisie de cette conversation rétablie à la valeur par défaut du thème",
"CmdColorUnknown": "Couleur inconnue. Choisissez parmi : {colors} ou off.",
"CmdCompactDescription": "Déclencher la compaction du contexte pour libérer de l'espace",
"CmdPurgeDescription": "Laisser l'agent élaguer chirurgicalement l'historique pour libérer du contexte",
"CmdConfigDescription": "Inspecter et modifier les paramètres (clés brutes en détail avancé)",
@@ -795,7 +808,7 @@
"KbSendDraft": "Envoyer si inactif ; mettre en file si occupé ; composer vide envoie le prochain suivi en file",
"KbSteerCurrentTurn": "Orienter le tour en cours (envoie normalement si inactif)",
"KbCloseMenu": "Fermer le menu, annuler la requête, abandonner le brouillon ou vider la saisie",
"KbCancelOrExit": "Annuler la requête, ou quitter si inactif",
"KbCancelOrExit": "Annuler, effacer le brouillon ou quitter si inactif",
"KbShellControls": "Déplacer l'attente du shell en cours vers /jobs",
"KbExitEmpty": "Quitter quand la saisie est vide",
"KbCommandPalette": "Ouvrir la palette de commandes",
@@ -1257,6 +1270,23 @@
"ApprovalControlsHint": "Pg↑/↓ réviser · {details} détails · Esc arrêter",
"ApprovalOwnerHeader": "Agent : {agent} · {role}",
"ApprovalGoToAgent": "g aller à l'agent",
"UserInputActionRequired": "Action requise",
"UserInputQuestionProgress": "Question {current} sur {total}",
"UserInputOtherLabel": "Autre",
"UserInputOtherDescription": "Saisir une réponse personnalisée",
"UserInputConfirmLabel": "Confirmer la sélection",
"UserInputConfirmDescription": "Envoyer la sélection ({count})",
"UserInputCustomPrompt": "Réponse personnalisée :",
"UserInputCustomPlaceholder": "(saisissez votre réponse)",
"UserInputHintSubmit": "envoyer",
"UserInputHintBack": "retour",
"UserInputHintMove": "déplacer",
"UserInputHintToggle": "basculer",
"UserInputHintSelectConfirm": "choisir/valider",
"UserInputHintCancel": "annuler",
"UserInputHintQuickPick": "choix rapide",
"UserInputHintConfirm": "confirmer",
"UserInputHintDigit": "n°",
"ApprovalControlsHintChild": "Pg↑/↓ réviser · {details} détails · Esc masquer",
"PendingApprovalInAgent": "Approbation requise dans {agent} — /agents",
"ApprovalTruncationHint": " … tronqué · appuyez sur {details} pour tous les détails",
@@ -1353,6 +1383,8 @@
"ToolReceiptLinesSingular": "1 ligne",
"ToolReceiptLinesPlural": "{count} lignes",
"ToolDescriptionNote": "note",
"ToolStatusWaitingForApproval": "en attente d'approbation",
"ToolStatusQueued": "en file",
"CmdConstitutionAccountApplies": "Constitution du profil du compte : appliquée (connecté ; chargée par le moteur à chaque tour)",
"CmdConstitutionAccountSignedOut": "Constitution du profil du compte : non connecté",
"CmdConstitutionAccountCheckFailed": "Constitution du profil du compte : vérification échouée ({error})",
@@ -2571,6 +2603,7 @@
"PlanHandoffKeepPlanning": "Continuer à planifier",
"PlanHandoffKeepPlanningDetail": "Rester en Plan. Rien n'est modifié.",
"PlanHandoffProceed": "Allez-y, appliquez le plan.",
"PlanHandoffTodoNote": "Suivez ce plan avec todo_write : gardez l'élément \"{title}\" dans votre liste et marquez-le comme terminé quand le plan est achevé.",
"PetAppearanceLegacy": "Le compagnon utilise une ancienne version. Redémarrez-le après la mise à jour de Codewhale pour choisir le style de baleine et la langue du texte.",
"PluginOAuthBrowser": "Appuyez sur Entrée pour vous connecter dans le navigateur, ou Échap pour revenir. Aucune clé API nécessaire.",
"PluginOAuthReady": "{provider} : connexion établie. Choisissez un modèle.",
@@ -2581,5 +2614,24 @@
"ExecSandboxHelp": "Politique de sandbox pour cette exécution ; tout --sandbox autre que danger-full-access (ou un sandbox_mode équivalent dans la configuration) désactive le mode de confiance.",
"ExecAfterHelp": "Exemples :\n codewhale exec \"explique cette fonction\"\n codewhale exec --auto \"liste crates/ avec ls\"\n codewhale exec --auto --output-format stream-json \"corrige le test en échec\"\n\n`codewhale exec` seul produit une réponse unique du modèle : un tour Engine\navec la même instruction système que les autres exécutions, sans outils.\nUtilisez `--auto` pour exécuter un agent avec outils sans interaction.\nLes outils sont proposés uniquement avec `--auto`, `--yolo`, `--allowed-tools`\nou lors de la reprise d’une session ; les limites comme `--max-turns`,\n`--disallowed-tools` ou `--sandbox`, ainsi que le format de sortie, n’ajoutent\njamais d’outils. Une réponse interrompue par la limite de sortie du fournisseur\nse poursuit dans le même tour, avec au plus 8 étapes du modèle, sauf indication\ncontraire de `--max-turns`.\n--auto utilise Full Access par défaut : aucun bac à sable du shell et mode de confiance activé. Utilisez --sandbox workspace-write pour conserver le bac à sable du shell et les restrictions des outils de fichiers à l’espace de travail ; les politiques configurées plus strictes s’appliquent aussi. Réessayer une opération refusée par le bac à sable nécessite --allow-sandbox-elevation ou --sandbox danger-full-access.\nLes exécutions proposant des outils, les exécutions stream-json et les sessions reprises sont enregistrées pour codewhale sessions, exec --continue et codewhale receipts <id>. Les exécutions text/--json qui ne font que répondre ne sont pas enregistrées, sauf si la compaction nécessite des données de récupération.\nTout --sandbox autre que danger-full-access (ou un sandbox_mode équivalent dans la configuration) désactive le mode de confiance.\n",
"ExecPromptHelp": "Prompt à envoyer au modèle. Placez -- avant un prompt qui commence par - ou contient une option exec comme texte littéral ; utilisez --prompt-file - pour lire l’entrée standard.",
"ExecFlagAfterPrompt": "{flag} apparaît après le prompt et serait traité comme du texte. Placez les options avant le prompt, ou le texte littéral d’une option après --."
"ExecFlagAfterPrompt": "{flag} apparaît après le prompt et serait traité comme du texte. Placez les options avant le prompt, ou le texte littéral d’une option après --.",
"InstructionNoticeHeader": "Des instructions permanentes ont été tronquées ou ignorées avant le début de cette session :",
"InstructionNoticeFooter": "Exécutez /context report pour le détail complet par source.",
"InstructionNoticeMore": "…et {count} de plus ; la liste complète figure dans /context report.",
"InstructionNoticeLine": "instructions : {scope} {label} ({origin}, {action}, {limit})",
"InstructionNoticeTrimmed": " {kept_bytes} sur {total_bytes} octets et ~{kept_tokens} sur ~{total_tokens} tokens estimés conservés",
"InstructionNoticeDroppedBytes": " {bytes} octets supprimés",
"InstructionNoticeFallback": " ; repli : {source}",
"InstructionNoticeStaticGuardOver": "instructions : le préfixe statique dépasse encore de ~{tokens} tokens estimés la limite du préfixe statique après réduction des instructions ; poursuite sous la politique `trim` — augmentez static_prefix_guard_percent ou réduisez la surface d'outils.",
"InstructionNoticeStaticGuardTrimmed": "instructions : portée {scope} réduite de ~{tokens} tokens estimés pour garder le préfixe statique sous la limite",
"InstructionDetailDroppedBroadest": "supprimé comme segment de la portée la plus large",
"InstructionDetailNoRoom": "le budget de portée de {tokens} tokens estimés n'a laissé aucune place au contenu",
"InstructionDetailRulesExhausted": "le budget de portée du projet de {tokens} tokens estimés est épuisé par les instructions",
"InstructionDetailOverCeiling": "plus grand que le plafond de {bytes} octets",
"InstructionDetailUnreadable": "lecture impossible : {reason}",
"InstructionDetailAlreadyLoaded": "déjà chargé depuis une portée plus large",
"InstructionDetailRulesCap": "au-delà du plafond de {count} fichiers de règles par répertoire",
"InstructionDetailSymlinkedRules": "refus : le répertoire de règles est un lien symbolique",
"InstructionBudgetRefused": "le budget d'instructions a refusé cette requête : {reason}",
"InstructionBudgetOverGuard": "le préfixe statique de ~{measured} tokens estimés dépasse la limite {guard}"
}
+55 -3
View File
@@ -1,4 +1,13 @@
{
"ReasoningOnlyRetryBody": "केवल तर्क {attempt}/{limit}; कोई उत्तर या टूल कॉल नहीं; {action}",
"OutputLimitAnswerRetryBody": "आउटपुट सीमा {attempt}/{limit}; कोई उत्तर या टूल कॉल नहीं; {action}",
"AnswerRetryActionBare": "उत्तर दोबारा माँगा जा रहा है",
"AnswerRetryActionNudge": "संकेत के साथ उत्तर दोबारा माँगा जा रहा है",
"AnswerRetryCounter": "केवल तर्क/आउटपुट सीमा",
"OrcaRouterOfferRecommended": "OrcaRouter · सुझाया गया",
"OrcaRouterOfferAction": "/provider या /model में OrcaRouter का ऑफ़र देखें",
"OrcaRouterOfferBody": "Codewhale × OrcaRouter ऑफ़र के साथ टॉप-अप पर DeepSeek V4.1 Flash के लिए 30% अतिरिक्त क्रेडिट पाएँ। मौजूदा शर्तों के लिए ऑफ़र पेज देखें।",
"OrcaRouterOfferDisclosure": "Codewhale को OrcaRouter से रेफ़रल शुल्क मिलता है।",
"ShellWaitNoActive": "अभी किसी शेल कमांड के पूरा होने का इंतज़ार नहीं हो रहा है।",
"ShellWaitInteractive": "इंटरैक्टिव कमांड बैकग्राउंड में जारी नहीं रह सकतीं। इस बारी को रद्द करें या कमांड पूरी होने की प्रतीक्षा करें।",
"ShellWaitUnavailable": "यह टूल बैकग्राउंड में जारी नहीं रह सकता। इस बारी को रद्द करें या इसके पूरा होने की प्रतीक्षा करें।",
@@ -185,6 +194,7 @@
"ComposerHintOfflineConnect": "↵ बाद के लिए सहेजें · /provider जोड़ता है",
"ComposerHintSendNow": "↵ अभी भेजें",
"ComposerHintSendIntoTurn": "↵ इस टर्न में भेजें",
"ComposerHintLongDraft": "{shortcut}: बाहरी संपादक",
"PendingSendingIntoTurnPrefix": " ↳ इस टर्न में भेजा जा रहा है: ",
"PendingCouldNotSendIntoTurnPrefix": " ↳ इस टर्न में नहीं भेजा जा सका: ",
"PendingEditingFollowUpPrefix": " ↳ फॉलो-अप संपादित हो रहा है: ",
@@ -464,6 +474,9 @@
"CmdImportClaudeDescription": "Claude Code की सेटिंग्स और MCP सर्वरों के लिए समीक्षा-योग्य आयात योजना बनाएं",
"CmdClearDescription": "वार्तालाप इतिहास साफ़ करें",
"CmdColorDescription": "इस चैट के इनपुट बॉक्स का रंग सेट करें",
"CmdColorSet": "इस चैट के इनपुट बॉक्स का रंग: {color}",
"CmdColorReset": "इस चैट के इनपुट बॉक्स का रंग थीम के डिफ़ॉल्ट पर रीसेट किया गया",
"CmdColorUnknown": "अज्ञात रंग। इनमें से चुनें: {colors}, या off।",
"CmdCompactDescription": "जगह खाली करने के लिए संदर्भ कॉम्पैक्शन चलाएँ",
"CmdPurgeDescription": "एजेंट को संदर्भ जगह खाली करने के लिए वार्तालाप इतिहास चुनिंदा काटने दें",
"CmdConfigDescription": "सेटिंग्स देखें और बदलें (उन्नत विवरण के रूप में कच्ची कुंजियाँ)",
@@ -795,7 +808,7 @@
"KbSendDraft": "खाली होने पर भेजें; व्यस्त होने पर कतार करें; खाली कम्पोज़र अगला कतारबद्ध फ़ॉलो-अप तुरंत भेजता है",
"KbSteerCurrentTurn": "वर्तमान टर्न को दिशा दें (खाली समय में सामान्य भेजता है)",
"KbCloseMenu": "मेनू बंद करें, अनुरोध रद्द करें, मसौदा छोड़ें, या इनपुट साफ़ करें",
"KbCancelOrExit": "अनुरोध रद्द करें, या खाली समय में बाहर निकलें",
"KbCancelOrExit": "रद्द करें, मसौदा साफ़ करें या निष्क्रिय होने पर बाहर निकलें",
"KbShellControls": "चलती शेल प्रतीक्षा को /jobs में ले जाएँ",
"KbExitEmpty": "इनपुट खाली होने पर बाहर निकलें",
"KbCommandPalette": "कमांड पैलेट खोलें",
@@ -1257,6 +1270,23 @@
"ApprovalControlsHint": "Pg↑/↓ समीक्षा · {details} विवरण · Esc रोकें",
"ApprovalOwnerHeader": "एजेंट: {agent} · {role}",
"ApprovalGoToAgent": "g एजेंट पर जाएँ",
"UserInputActionRequired": "कार्रवाई आवश्यक",
"UserInputQuestionProgress": "प्रश्न {current} / {total}",
"UserInputOtherLabel": "अन्य",
"UserInputOtherDescription": "अपना उत्तर लिखें",
"UserInputConfirmLabel": "चयन की पुष्टि करें",
"UserInputConfirmDescription": "चयन जमा करें ({count})",
"UserInputCustomPrompt": "अपना उत्तर:",
"UserInputCustomPlaceholder": "(अपना उत्तर लिखें)",
"UserInputHintSubmit": "जमा करें",
"UserInputHintBack": "वापस",
"UserInputHintMove": "घुमाएँ",
"UserInputHintToggle": "टॉगल",
"UserInputHintSelectConfirm": "चुनें/पुष्टि",
"UserInputHintCancel": "रद्द करें",
"UserInputHintQuickPick": "त्वरित चयन",
"UserInputHintConfirm": "पुष्टि",
"UserInputHintDigit": "अंक",
"ApprovalControlsHintChild": "Pg↑/↓ समीक्षा · {details} विवरण · Esc छिपाएँ",
"PendingApprovalInAgent": "{agent} में स्वीकृति चाहिए — /agents",
"ApprovalTruncationHint": " … छाँटा गया · पूरा विवरण के लिए {details} दबाएँ",
@@ -1353,6 +1383,8 @@
"ToolReceiptLinesSingular": "1 पंक्ति",
"ToolReceiptLinesPlural": "{count} पंक्तियाँ",
"ToolDescriptionNote": "टिप्पणी",
"ToolStatusWaitingForApproval": "अनुमति की प्रतीक्षा",
"ToolStatusQueued": "कतार में",
"CmdConstitutionAccountApplies": "खाता प्रोफ़ाइल संविधान: लागू है (साइन इन; इंजन हर चरण में लोड करता है)",
"CmdConstitutionAccountSignedOut": "खाता प्रोफ़ाइल संविधान: साइन इन नहीं है",
"CmdConstitutionAccountCheckFailed": "खाता प्रोफ़ाइल संविधान: खाते की जाँच विफल ({error})",
@@ -2339,7 +2371,7 @@
"ConfigHintManagedApprovalPolicy": "कोई प्रोजेक्ट, प्रोफ़ाइल, परिवेश, प्रबंधित config या संगठन की आवश्यकता इस मान को नियंत्रित करती है",
"ConfigHintManagedAllowShell": "कोई प्रोजेक्ट, प्रोफ़ाइल, परिवेश या प्रबंधित config shell पहुँच को नियंत्रित करता है",
"ConfigHintAllowShell": "चालू होने पर एजेंट मोड में shell टूल उपलब्ध होते हैं; अनुमति नियम फिर भी लागू रहते हैं",
"ConfigHintComposerMultilineMode": "बंद: Enter भेजता है, Shift+Enter पंक्ति जोड़ता है; चालू: Enter पंक्ति जोड़ता है, Shift+Enter भेजता है",
"ConfigHintComposerMultilineMode": "बंद: Enter भेजता है, Alt+Enter / Shift+Enter पंक्ति जोड़ता है; चालू: Enter पंक्ति जोड़ता है, Alt+Enter / Shift+Enter भेजता है",
"ConfigHintBooleanValues": "on/off, true/false, yes/no, 1/0",
"ConfigHintDensity": "compact | comfortable | spacious",
"ConfigHintInlineDiffs": "full | summary | off; सटीक बदलाव Alt/Option+V विवरण में बना रहता है",
@@ -2571,6 +2603,7 @@
"PlanHandoffKeepPlanning": "योजना बनाना जारी रखें",
"PlanHandoffKeepPlanningDetail": "Plan में बने रहें। कुछ भी नहीं बदलता।",
"PlanHandoffProceed": "योजना के अनुसार काम शुरू करें।",
"PlanHandoffTodoNote": "इस योजना को todo_write से ट्रैक करें: \"{title}\" आइटम को अपनी सूची में रखें और योजना पूरी होने पर उसे पूरा हुआ चिह्नित करें।",
"PetAppearanceLegacy": "पेट पुराने संस्करण पर चल रहा है। व्हेल का रूप और टेक्स्ट की भाषा चुनने के लिए Codewhale अपडेट करने के बाद पेट को फिर से शुरू करें।",
"PluginOAuthBrowser": "ब्राउज़र से साइन इन करने के लिए Enter दबाएँ। वापस जाने के लिए Esc दबाएँ। API कुंजी की ज़रूरत नहीं है।",
"PluginOAuthReady": "{provider}: साइन इन हो गया। मॉडल चुनें।",
@@ -2581,5 +2614,24 @@
"ExecSandboxHelp": "इस रन की sandbox नीति; danger-full-access के अलावा कोई भी --sandbox (या कॉन्फ़िगरेशन में समान sandbox_mode) ट्रस्ट मोड बंद करता है।",
"ExecAfterHelp": "उदाहरण:\n codewhale exec \"इस फ़ंक्शन को समझाएँ\"\n codewhale exec --auto \"ls से crates/ की सूची दिखाएँ\"\n codewhale exec --auto --output-format stream-json \"विफल हो रहा टेस्ट ठीक करें\"\n\nसामान्य `codewhale exec` मॉडल से एक बार जवाब लेता है: अन्य रन वाले ही सिस्टम\nप्रॉम्प्ट के साथ Engine का एक टर्न, बिना टूल के। बिना इंटरैक्शन के टूल वाले\nएजेंट को चलाने के लिए `--auto` इस्तेमाल करें। टूल केवल `--auto`, `--yolo`,\n`--allowed-tools` के साथ या सत्र फिर शुरू करने पर उपलब्ध होते हैं;\n`--max-turns`, `--disallowed-tools` या `--sandbox` जैसी सीमाएँ और आउटपुट\nफ़ॉर्मैट कभी टूल नहीं जोड़ते। प्रदाता की आउटपुट सीमा से कटे जवाब को उसी टर्न\nमें आगे जारी रखा जाता है। `--max-turns` से अन्य सीमा न देने पर मॉडल के\nअधिकतम 8 चरण होते हैं।\n--auto डिफ़ॉल्ट रूप से Full Access इस्तेमाल करता है: शेल सैंडबॉक्स नहीं और भरोसा मोड चालू। शेल सैंडबॉक्स और फ़ाइल टूल की कार्यक्षेत्र संबंधी पाबंदियाँ बनाए रखने के लिए --sandbox workspace-write इस्तेमाल करें; कॉन्फ़िगर की गई अधिक सख्त नीतियाँ भी लागू होती हैं। सैंडबॉक्स द्वारा रोकी गई कार्रवाई दोबारा आज़माने के लिए --allow-sandbox-elevation या --sandbox danger-full-access ज़रूरी है।\nटूल उपलब्ध कराने वाले रन, stream-json रन और फिर शुरू किए गए सत्र सहेजे जाते हैं, ताकि codewhale sessions, exec --continue और codewhale receipts <id> में उनका उपयोग हो सके। केवल जवाब देने वाले सामान्य text/--json रन नहीं सहेजे जाते, जब तक संदर्भ संपीड़न के लिए पुनर्प्राप्ति डेटा की ज़रूरत न हो।\ndanger-full-access के अलावा कोई भी --sandbox (या कॉन्फ़िगरेशन में समान sandbox_mode) ट्रस्ट मोड बंद करता है।\n",
"ExecPromptHelp": "मॉडल को भेजने वाला प्रॉम्प्ट। - से शुरू होने वाले या exec फ़्लैग को सामान्य टेक्स्ट के रूप में रखने वाले प्रॉम्प्ट से पहले -- लगाएँ; मानक इनपुट पढ़ने के लिए --prompt-file - का उपयोग करें।",
"ExecFlagAfterPrompt": "{flag} प्रॉम्प्ट के बाद है और टेक्स्ट माना जाएगा। फ़्लैग प्रॉम्प्ट से पहले रखें, या फ़्लैग को सामान्य टेक्स्ट के रूप में भेजने के लिए उसे -- के बाद रखें।"
"ExecFlagAfterPrompt": "{flag} प्रॉम्प्ट के बाद है और टेक्स्ट माना जाएगा। फ़्लैग प्रॉम्प्ट से पहले रखें, या फ़्लैग को सामान्य टेक्स्ट के रूप में भेजने के लिए उसे -- के बाद रखें।",
"InstructionNoticeHeader": "स्थायी निर्देश इस सत्र के शुरू होने से पहले छाँट दिए गए या छोड़ दिए गए:",
"InstructionNoticeFooter": "स्रोत-वार पूरा ब्योरा देखने के लिए /context report चलाएँ।",
"InstructionNoticeMore": "…और {count} अन्य; पूरी सूची /context report में है।",
"InstructionNoticeLine": "निर्देश: {scope} {label} ({origin}, {action}, {limit})",
"InstructionNoticeTrimmed": " {total_bytes} में से {kept_bytes} बाइट और ~{total_tokens} में से ~{kept_tokens} अनुमानित टोकन रखे गए",
"InstructionNoticeDroppedBytes": " {bytes} बाइट हटाए गए",
"InstructionNoticeFallback": "; विकल्प: {source}",
"InstructionNoticeStaticGuardOver": "निर्देश: निर्देश छाँटने के बाद भी स्थिर प्रीफ़िक्स, स्थिर प्रीफ़िक्स सीमा से ~{tokens} अनुमानित टोकन ऊपर है; नीति `trim` के साथ जारी है — static_prefix_guard_percent बढ़ाएँ या टूल का दायरा घटाएँ।",
"InstructionNoticeStaticGuardTrimmed": "निर्देश: स्थिर प्रीफ़िक्स को सीमा के भीतर रखने के लिए {scope} दायरा ~{tokens} अनुमानित टोकन छाँटा गया",
"InstructionDetailDroppedBroadest": "सबसे व्यापक दायरे के खंड के रूप में हटाया गया",
"InstructionDetailNoRoom": "{tokens} अनुमानित टोकन के दायरा बजट में सामग्री के लिए जगह नहीं बची",
"InstructionDetailRulesExhausted": "{tokens} अनुमानित टोकन का प्रोजेक्ट दायरा बजट निर्देशों से समाप्त हो गया",
"InstructionDetailOverCeiling": "{bytes} बाइट की सीमा से बड़ा",
"InstructionDetailUnreadable": "पढ़ा नहीं जा सका: {reason}",
"InstructionDetailAlreadyLoaded": "अधिक व्यापक दायरे से पहले ही लोड हो चुका है",
"InstructionDetailRulesCap": "प्रति निर्देशिका {count} नियम फ़ाइलों की सीमा से अधिक",
"InstructionDetailSymlinkedRules": "सिमलिंक वाली नियम निर्देशिका अस्वीकार की गई",
"InstructionBudgetRefused": "निर्देश बजट ने इस अनुरोध को अस्वीकार कर दिया: {reason}",
"InstructionBudgetOverGuard": "स्थिर प्रीफ़िक्स ~{measured} अनुमानित टोकन सीमा {guard} से अधिक है"
}
+55 -3
View File
@@ -1,4 +1,13 @@
{
"ReasoningOnlyRetryBody": "hanya penalaran {attempt}/{limit}; tanpa jawaban atau panggilan alat; {action}",
"OutputLimitAnswerRetryBody": "batas keluaran {attempt}/{limit}; tanpa jawaban atau panggilan alat; {action}",
"AnswerRetryActionBare": "meminta jawaban kembali",
"AnswerRetryActionNudge": "meminta jawaban kembali dengan pengingat",
"AnswerRetryCounter": "hanya penalaran/batas keluaran",
"OrcaRouterOfferRecommended": "OrcaRouter · Direkomendasikan",
"OrcaRouterOfferAction": "Lihat penawaran OrcaRouter di /provider atau /model",
"OrcaRouterOfferBody": "Dapatkan tambahan kredit DeepSeek V4.1 Flash sebesar 30% dari isi ulang melalui penawaran Codewhale × OrcaRouter. Lihat ketentuan terbaru di halaman penawaran.",
"OrcaRouterOfferDisclosure": "Codewhale menerima komisi rujukan dari OrcaRouter.",
"ShellWaitNoActive": "Tidak ada perintah shell yang sedang ditunggu.",
"ShellWaitInteractive": "Perintah interaktif tidak dapat berjalan di latar belakang. Batalkan giliran atau tunggu hingga selesai.",
"ShellWaitUnavailable": "Alat ini tidak dapat berjalan di latar belakang. Batalkan giliran atau tunggu hingga selesai.",
@@ -185,6 +194,7 @@
"ComposerHintOfflineConnect": "↵ simpan untuk nanti · /provider menghubungkan",
"ComposerHintSendNow": "↵ kirim sekarang",
"ComposerHintSendIntoTurn": "↵ kirim ke giliran ini",
"ComposerHintLongDraft": "{shortcut}: editor eksternal",
"PendingSendingIntoTurnPrefix": " ↳ Mengirim ke giliran ini: ",
"PendingCouldNotSendIntoTurnPrefix": " ↳ Tidak bisa mengirim ke giliran ini: ",
"PendingEditingFollowUpPrefix": " ↳ Menyunting tindak lanjut: ",
@@ -464,6 +474,9 @@
"CmdImportClaudeDescription": "Rencanakan impor yang dapat ditinjau untuk pengaturan dan server MCP Claude Code",
"CmdClearDescription": "Hapus riwayat percakapan",
"CmdColorDescription": "Atur warna kotak tulis untuk obrolan ini",
"CmdColorSet": "Warna kotak tulis untuk obrolan ini: {color}",
"CmdColorReset": "Warna kotak tulis untuk obrolan ini dikembalikan ke bawaan tema",
"CmdColorUnknown": "Warna tidak dikenal. Pilih salah satu: {colors}, atau off.",
"CmdCompactDescription": "Picu pemadatan konteks untuk mengosongkan ruang",
"CmdPurgeDescription": "Biarkan agen memangkas riwayat percakapan secara selektif untuk mengosongkan konteks",
"CmdConfigDescription": "Periksa dan ubah pengaturan (kunci mentah sebagai detail lanjutan)",
@@ -795,7 +808,7 @@
"KbSendDraft": "Kirim saat idle; antrekan saat sibuk; komposer kosong mengirim tindak lanjut berikutnya dalam antrean sekarang",
"KbSteerCurrentTurn": "Arahkan giliran saat ini (terkirim normal saat idle)",
"KbCloseMenu": "Tutup menu, batalkan permintaan, buang draf, atau bersihkan input",
"KbCancelOrExit": "Batalkan permintaan, atau keluar saat idle",
"KbCancelOrExit": "Batalkan, hapus draf, atau keluar saat idle",
"KbShellControls": "Pindahkan penantian shell yang berjalan ke /jobs",
"KbExitEmpty": "Keluar saat input kosong",
"KbCommandPalette": "Buka palet perintah",
@@ -1257,6 +1270,23 @@
"ApprovalControlsHint": "Pg↑/↓ tinjau · {details} detail · Esc hentikan",
"ApprovalOwnerHeader": "Agen: {agent} · {role}",
"ApprovalGoToAgent": "g buka agen",
"UserInputActionRequired": "Tindakan diperlukan",
"UserInputQuestionProgress": "Pertanyaan {current} dari {total}",
"UserInputOtherLabel": "Lainnya",
"UserInputOtherDescription": "Ketik jawaban kustom",
"UserInputConfirmLabel": "Konfirmasi pilihan",
"UserInputConfirmDescription": "Kirim pilihan ({count})",
"UserInputCustomPrompt": "Jawaban kustom:",
"UserInputCustomPlaceholder": "(ketik jawaban Anda)",
"UserInputHintSubmit": "kirim",
"UserInputHintBack": "kembali",
"UserInputHintMove": "pindah",
"UserInputHintToggle": "alihkan",
"UserInputHintSelectConfirm": "pilih/konfirmasi",
"UserInputHintCancel": "batal",
"UserInputHintQuickPick": "pilih cepat",
"UserInputHintConfirm": "konfirmasi",
"UserInputHintDigit": "angka",
"ApprovalControlsHintChild": "Pg↑/↓ tinjau · {details} detail · Esc sembunyikan",
"PendingApprovalInAgent": "Persetujuan diperlukan di {agent} — /agents",
"ApprovalTruncationHint": " … terpotong · tekan {details} untuk detail lengkap",
@@ -1353,6 +1383,8 @@
"ToolReceiptLinesSingular": "1 baris",
"ToolReceiptLinesPlural": "{count} baris",
"ToolDescriptionNote": "catatan",
"ToolStatusWaitingForApproval": "menunggu persetujuan",
"ToolStatusQueued": "mengantre",
"CmdConstitutionAccountApplies": "Konstitusi profil akun: berlaku (masuk; dimuat Engine setiap giliran)",
"CmdConstitutionAccountSignedOut": "Konstitusi profil akun: belum masuk",
"CmdConstitutionAccountCheckFailed": "Konstitusi profil akun: pemeriksaan akun gagal ({error})",
@@ -2339,7 +2371,7 @@
"ConfigHintManagedApprovalPolicy": "proyek, profil, lingkungan, config terkelola, atau persyaratan organisasi mengendalikan nilai ini",
"ConfigHintManagedAllowShell": "proyek, profil, lingkungan, atau config terkelola mengendalikan akses shell",
"ConfigHintAllowShell": "aktif memunculkan alat shell di mode Agen; aturan izin tetap berlaku",
"ConfigHintComposerMultilineMode": "nonaktif: Enter mengirim, Shift+Enter menambah baris; aktif: Enter menambah baris, Shift+Enter mengirim",
"ConfigHintComposerMultilineMode": "nonaktif: Enter mengirim, Alt+Enter / Shift+Enter menambah baris; aktif: Enter menambah baris, Alt+Enter / Shift+Enter mengirim",
"ConfigHintBooleanValues": "on/off, true/false, yes/no, 1/0",
"ConfigHintDensity": "compact | comfortable | spacious",
"ConfigHintInlineDiffs": "full | summary | off; perubahan persis tetap ada di detail Alt/Option+V",
@@ -2571,6 +2603,7 @@
"PlanHandoffKeepPlanning": "Lanjutkan merencanakan",
"PlanHandoffKeepPlanningDetail": "Tetap di Plan. Tidak ada yang diubah.",
"PlanHandoffProceed": "Silakan jalankan rencananya.",
"PlanHandoffTodoNote": "Lacak rencana ini dengan todo_write: pertahankan item \"{title}\" di daftar Anda dan tandai selesai setelah rencana rampung.",
"PetAppearanceLegacy": "Pet masih memakai versi lama. Mulai ulang pet setelah memperbarui Codewhale untuk memilih gaya paus dan bahasa teks.",
"PluginOAuthBrowser": "Tekan Enter untuk masuk melalui peramban atau Esc untuk kembali. Kunci API tidak diperlukan.",
"PluginOAuthReady": "{provider}: berhasil masuk. Pilih model.",
@@ -2581,5 +2614,24 @@
"ExecSandboxHelp": "Kebijakan sandbox untuk proses ini; setiap --sandbox selain danger-full-access (atau sandbox_mode yang setara dalam konfigurasi) menonaktifkan mode kepercayaan.",
"ExecAfterHelp": "Contoh:\n codewhale exec \"jelaskan fungsi ini\"\n codewhale exec --auto \"tampilkan isi crates/ dengan ls\"\n codewhale exec --auto --output-format stream-json \"perbaiki pengujian yang gagal\"\n\n`codewhale exec` biasa menghasilkan satu respons model: satu giliran Engine\ndengan prompt sistem yang sama seperti eksekusi lain, tanpa alat. Gunakan\n`--auto` untuk menjalankan agen dengan alat secara noninteraktif. Alat hanya\ndisediakan dengan `--auto`, `--yolo`, `--allowed-tools`, atau saat melanjutkan\nsesi; batas seperti `--max-turns`, `--disallowed-tools`, atau `--sandbox`, dan\nformat keluaran, tidak pernah menambahkan alat. Respons yang terpotong karena\nbatas keluaran penyedia dilanjutkan dalam giliran yang sama, hingga 8 langkah\nmodel kecuali `--max-turns` menentukan batas lain.\n--auto menggunakan Full Access secara bawaan: tanpa sandbox shell dan dengan mode kepercayaan aktif. Gunakan --sandbox workspace-write untuk mempertahankan sandbox shell dan pembatasan alat berkas pada ruang kerja; kebijakan lebih ketat yang dikonfigurasi juga berlaku. Mencoba ulang operasi yang ditolak sandbox memerlukan --allow-sandbox-elevation atau --sandbox danger-full-access.\nEksekusi yang menyediakan alat, eksekusi stream-json, dan sesi yang dilanjutkan disimpan untuk codewhale sessions, exec --continue, dan codewhale receipts <id>. Eksekusi text/--json biasa yang hanya memberikan jawaban tidak disimpan, kecuali pemadatan memerlukan data pemulihan.\nSetiap --sandbox selain danger-full-access (atau sandbox_mode yang setara dalam konfigurasi) menonaktifkan mode kepercayaan.\n",
"ExecPromptHelp": "Prompt untuk dikirim ke model. Letakkan -- sebelum prompt yang diawali - atau memuat flag exec sebagai teks literal; gunakan --prompt-file - untuk membaca input standar.",
"ExecFlagAfterPrompt": "{flag} muncul setelah prompt dan akan dianggap sebagai teks. Letakkan flag sebelum prompt, atau letakkan flag yang dimaksudkan sebagai teks literal setelah --."
"ExecFlagAfterPrompt": "{flag} muncul setelah prompt dan akan dianggap sebagai teks. Letakkan flag sebelum prompt, atau letakkan flag yang dimaksudkan sebagai teks literal setelah --.",
"InstructionNoticeHeader": "Instruksi tetap dipangkas atau dilewati sebelum sesi ini dimulai:",
"InstructionNoticeFooter": "Jalankan /context report untuk rincian lengkap per sumber.",
"InstructionNoticeMore": "…dan {count} lagi; daftar lengkap ada di /context report.",
"InstructionNoticeLine": "instruksi: {scope} {label} ({origin}, {action}, {limit})",
"InstructionNoticeTrimmed": " dipertahankan {kept_bytes} dari {total_bytes} byte dan ~{kept_tokens} dari ~{total_tokens} token perkiraan",
"InstructionNoticeDroppedBytes": " {bytes} byte dibuang",
"InstructionNoticeFallback": "; cadangan: {source}",
"InstructionNoticeStaticGuardOver": "instruksi: prefiks statis masih ~{tokens} token perkiraan di atas batas prefiks statis setelah instruksi dipangkas; dilanjutkan dengan kebijakan `trim` — naikkan static_prefix_guard_percent atau persempit cakupan alat.",
"InstructionNoticeStaticGuardTrimmed": "instruksi: cakupan {scope} dipangkas ~{tokens} token perkiraan agar prefiks statis tetap di bawah batas",
"InstructionDetailDroppedBroadest": "dibuang sebagai segmen cakupan terluas",
"InstructionDetailNoRoom": "anggaran cakupan sebesar {tokens} token perkiraan tidak menyisakan ruang untuk konten",
"InstructionDetailRulesExhausted": "anggaran cakupan proyek sebesar {tokens} token perkiraan habis oleh instruksi",
"InstructionDetailOverCeiling": "melebihi batas {bytes} byte",
"InstructionDetailUnreadable": "tidak dapat dibaca: {reason}",
"InstructionDetailAlreadyLoaded": "sudah dimuat dari cakupan yang lebih luas",
"InstructionDetailRulesCap": "melebihi batas {count} berkas aturan per direktori",
"InstructionDetailSymlinkedRules": "menolak direktori aturan berupa symlink",
"InstructionBudgetRefused": "anggaran instruksi menolak permintaan ini: {reason}",
"InstructionBudgetOverGuard": "prefiks statis ~{measured} token perkiraan melebihi batas {guard}"
}
+55 -3
View File
@@ -1,4 +1,13 @@
{
"ReasoningOnlyRetryBody": "推論のみ {attempt}/{limit}、回答もツール呼び出しもありません。{action}",
"OutputLimitAnswerRetryBody": "出力上限 {attempt}/{limit}、回答もツール呼び出しもありません。{action}",
"AnswerRetryActionBare": "回答を再要求します",
"AnswerRetryActionNudge": "補足指示を添えて回答を再要求します",
"AnswerRetryCounter": "推論のみ/出力上限",
"OrcaRouterOfferRecommended": "OrcaRouter · おすすめ",
"OrcaRouterOfferAction": "/provider または /model で OrcaRouter の特典を表示",
"OrcaRouterOfferBody": "Codewhale × OrcaRouter の特典で、DeepSeek V4.1 Flash 向けチャージ額の30%が追加されます。最新の条件は特典ページでご確認ください。",
"OrcaRouterOfferDisclosure": "Codewhale は OrcaRouter から紹介料を受け取ります。",
"ShellWaitNoActive": "待機中のシェルコマンドはありません。",
"ShellWaitInteractive": "対話型コマンドはバックグラウンドで続行できません。ターンをキャンセルするか、完了をお待ちください。",
"ShellWaitUnavailable": "このツールはバックグラウンドで続行できません。ターンをキャンセルするか、完了をお待ちください。",
@@ -185,6 +194,7 @@
"ComposerHintOfflineConnect": "↵ あとで送るために保存 · /provider で接続",
"ComposerHintSendNow": "↵ すぐ送信",
"ComposerHintSendIntoTurn": "↵ このターンに送る",
"ComposerHintLongDraft": "{shortcut}: 外部エディター",
"PendingSendingIntoTurnPrefix": " ↳ このターンに送信中: ",
"PendingCouldNotSendIntoTurnPrefix": " ↳ このターンに送れませんでした: ",
"PendingEditingFollowUpPrefix": " ↳ フォローアップを編集中: ",
@@ -467,6 +477,9 @@
"CmdImportClaudeDescription": "Claude Code の設定と MCP サーバーの確認可能なインポート計画を作成",
"CmdClearDescription": "会話履歴をクリア",
"CmdColorDescription": "このチャットの入力欄の色を設定",
"CmdColorSet": "このチャットの入力欄の色: {color}",
"CmdColorReset": "このチャットの入力欄の色をテーマのデフォルトに戻しました",
"CmdColorUnknown": "不明な色です。次から選択してください: {colors}、または off。",
"CmdCompactDescription": "コンテキスト圧縮で容量を確保",
"CmdPurgeDescription": "エージェントに会話履歴を分析させ、不要なメッセージを削除・要約",
"CmdConfigDescription": "設定を確認・変更(生キーは詳細/高度)",
@@ -812,7 +825,7 @@
"KbSendDraft": "待機中は送信、実行中はキュー。入力欄が空なら次のキューを今すぐ送信",
"KbSteerCurrentTurn": "現在のターンをステア(待機中は通常送信)",
"KbCloseMenu": "メニューを閉じる、リクエストをキャンセル、下書きを破棄、または入力をクリア",
"KbCancelOrExit": "リクエストをキャンセル、またはアイドル時に終了",
"KbCancelOrExit": "キャンセル、下書きを消去、またはアイドル時に終了",
"KbShellControls": "実行中のシェル待機を /jobs へ移動",
"KbExitEmpty": "入力が空の時に終了",
"KbCommandPalette": "コマンドパレットを開く",
@@ -1278,6 +1291,23 @@
"ApprovalControlsHint": "Pg↑/↓ 履歴 · {details} 詳細 · Esc 停止",
"ApprovalOwnerHeader": "エージェント: {agent} · {role}",
"ApprovalGoToAgent": "g エージェントへ移動",
"UserInputActionRequired": "対応が必要です",
"UserInputQuestionProgress": "質問 {current}/{total}",
"UserInputOtherLabel": "その他",
"UserInputOtherDescription": "自由入力で回答する",
"UserInputConfirmLabel": "選択を確定",
"UserInputConfirmDescription": "選択した {count} 件を送信",
"UserInputCustomPrompt": "自由入力:",
"UserInputCustomPlaceholder": "(回答を入力)",
"UserInputHintSubmit": "送信",
"UserInputHintBack": "戻る",
"UserInputHintMove": "移動",
"UserInputHintToggle": "切替",
"UserInputHintSelectConfirm": "選択/確定",
"UserInputHintCancel": "キャンセル",
"UserInputHintQuickPick": "クイック選択",
"UserInputHintConfirm": "確定",
"UserInputHintDigit": "数字",
"ApprovalControlsHintChild": "Pg↑/↓ 履歴 · {details} 詳細 · Esc 隠す",
"PendingApprovalInAgent": "{agent} で承認が必要です — /agents",
"ApprovalTruncationHint": " … 省略 · {details} で詳細を表示",
@@ -1374,6 +1404,8 @@
"ToolReceiptLinesSingular": "1 行",
"ToolReceiptLinesPlural": "{count} 行",
"ToolDescriptionNote": "説明",
"ToolStatusWaitingForApproval": "承認待ち",
"ToolStatusQueued": "待機中",
"CmdConstitutionAccountApplies": "アカウントの憲章: 適用(ログイン済み。各ターンで Engine が読み込み)",
"CmdConstitutionAccountSignedOut": "アカウントの憲章: 未ログイン",
"CmdConstitutionAccountCheckFailed": "アカウントの憲章: アカウント確認に失敗({error})",
@@ -2339,7 +2371,7 @@
"ConfigHintManagedApprovalPolicy": "プロジェクト・プロファイル・環境・管理された設定・組織の要件がこの値を制御します",
"ConfigHintManagedAllowShell": "プロジェクト・プロファイル・環境・管理された設定がシェルアクセスを制御します",
"ConfigHintAllowShell": "オンにするとエージェントモードでシェルツールを公開します。権限ルールは引き続き適用されます",
"ConfigHintComposerMultilineMode": "オフ: Enter で送信、Shift+Enter で改行。オン: Enter で改行、Shift+Enter で送信",
"ConfigHintComposerMultilineMode": "オフ: Enter で送信、Alt+Enter / Shift+Enter で改行。オン: Enter で改行、Alt+Enter / Shift+Enter で送信",
"ConfigHintBooleanValues": "on/off, true/false, yes/no, 1/0",
"ConfigHintDensity": "compact | comfortable | spacious",
"ConfigHintInlineDiffs": "full | summary | off。正確な変更は Alt/Option+V の詳細に残ります",
@@ -2571,6 +2603,7 @@
"PlanHandoffKeepPlanning": "計画を続ける",
"PlanHandoffKeepPlanningDetail": "Plan のままにします。何も変更されません。",
"PlanHandoffProceed": "計画どおりに進めてください。",
"PlanHandoffTodoNote": "この計画を todo_write で追跡してください。リストに項目「{title}」を残し、計画が終わったら完了にしてください。",
"PetAppearanceLegacy": "ペットは以前のバージョンで動作しています。Codewhale を更新してペットを再起動すると、クジラのスタイルと文字の言語を選べます。",
"PluginOAuthBrowser": "Enter でブラウザーからログインします。Esc で戻ります。API キーは不要です。",
"PluginOAuthReady": "{provider}:ログインしました。モデルを選択してください。",
@@ -2581,5 +2614,24 @@
"ExecSandboxHelp": "この実行のサンドボックスポリシー。danger-full-access 以外の --sandbox(または設定内の同等の sandbox_mode)は信頼モードを無効にします。",
"ExecAfterHelp": "使用例:\n codewhale exec \"この関数を説明して\"\n codewhale exec --auto \"ls で crates/ の内容を表示して\"\n codewhale exec --auto --output-format stream-json \"失敗しているテストを修正して\"\n\n通常の `codewhale exec` はモデルの単発応答です。他の実行と同じシステム\nプロンプトを使い、ツールなしで Engine を 1 ターン実行します。対話なしで\nツールを使うエージェントを実行するには `--auto` を指定してください。\nツールが提供されるのは `--auto`、`--yolo`、`--allowed-tools` の指定時、\nまたはセッションの再開時のみです。`--max-turns`、`--disallowed-tools`、\n`--sandbox` などの制限や出力形式を指定してもツールは追加されません。\nプロバイダーの出力上限で途切れた応答は同じターン内で続行されます。\n`--max-turns` で変更しない限り、モデルのステップ数は最大 8 回です。\n--auto は既定で Full Access です。シェルのサンドボックスはなく、信頼モードが有効です。シェルのサンドボックスと、ファイルツールのワークスペース制限を維持するには --sandbox workspace-write を指定してください。設定済みのより厳しいポリシーも適用されます。サンドボックスで拒否された操作の再試行には --allow-sandbox-elevation または --sandbox danger-full-access が必要です。\nツールを提供する実行、stream-json の実行、再開したセッションは保存され、codewhale sessions、exec --continue、codewhale receipts <id> で利用できます。応答のみの text/--json 実行は、圧縮時に復旧用の保存が必要な場合を除いて保存されません。\ndanger-full-access 以外の --sandbox(または設定内の同等の sandbox_mode)は信頼モードを無効にします。\n",
"ExecPromptHelp": "モデルに送信するプロンプト。- で始まる場合や exec のフラグを文字列として含む場合は、プロンプトの前に -- を置いてください。標準入力を読むには --prompt-file - を使用してください。",
"ExecFlagAfterPrompt": "{flag} がプロンプトの後にあり、文字列として扱われます。フラグはプロンプトの前に置くか、文字列として渡す場合は -- の後に置いてください。"
"ExecFlagAfterPrompt": "{flag} がプロンプトの後にあり、文字列として扱われます。フラグはプロンプトの前に置くか、文字列として渡す場合は -- の後に置いてください。",
"InstructionNoticeHeader": "常設の指示は、このセッションの開始前に切り詰められたか、スキップされました:",
"InstructionNoticeFooter": "ソースごとの詳細は /context report を実行してください。",
"InstructionNoticeMore": "…ほか {count} 件。全一覧は /context report にあります。",
"InstructionNoticeLine": "指示: {scope} {label} ({origin}, {action}, {limit})",
"InstructionNoticeTrimmed": " {total_bytes} バイト中 {kept_bytes} バイト、推定 ~{total_tokens} トークン中 ~{kept_tokens} トークンを保持",
"InstructionNoticeDroppedBytes": " {bytes} バイトを破棄",
"InstructionNoticeFallback": "; 代替: {source}",
"InstructionNoticeStaticGuardOver": "指示: 指示を切り詰めても、静的プレフィックスはなお推定 ~{tokens} トークン、静的プレフィックスの上限を超えています。ポリシー `trim` のまま続行します — static_prefix_guard_percent を上げるか、ツールの範囲を絞ってください。",
"InstructionNoticeStaticGuardTrimmed": "指示: 静的プレフィックスを上限内に収めるため、{scope} スコープを推定 ~{tokens} トークン切り詰めました",
"InstructionDetailDroppedBroadest": "最も広いスコープのセグメントとして破棄",
"InstructionDetailNoRoom": "推定 {tokens} トークンのスコープ予算では内容を置く余地がありませんでした",
"InstructionDetailRulesExhausted": "推定 {tokens} トークンのプロジェクトスコープ予算を指示が使い切りました",
"InstructionDetailOverCeiling": "上限 {bytes} バイトを超えています",
"InstructionDetailUnreadable": "読み取れませんでした: {reason}",
"InstructionDetailAlreadyLoaded": "より広いスコープから読み込み済み",
"InstructionDetailRulesCap": "ディレクトリごとのルール上限 {count} ファイルを超えています",
"InstructionDetailSymlinkedRules": "シンボリックリンクのルールディレクトリは拒否します",
"InstructionBudgetRefused": "指示の予算により、このリクエストは拒否されました: {reason}",
"InstructionBudgetOverGuard": "静的プレフィックス ~{measured} 推定トークンが上限 {guard} を超えています"
}
+55 -3
View File
@@ -1,4 +1,13 @@
{
"ReasoningOnlyRetryBody": "추론만 반환 {attempt}/{limit}; 답변이나 도구 호출 없음; {action}",
"OutputLimitAnswerRetryBody": "출력 한도 {attempt}/{limit}; 답변이나 도구 호출 없음; {action}",
"AnswerRetryActionBare": "답변을 다시 요청합니다",
"AnswerRetryActionNudge": "안내를 덧붙여 답변을 다시 요청합니다",
"AnswerRetryCounter": "추론만 반환/출력 한도",
"OrcaRouterOfferRecommended": "OrcaRouter · 추천",
"OrcaRouterOfferAction": "/provider 또는 /model에서 OrcaRouter 혜택 보기",
"OrcaRouterOfferBody": "Codewhale × OrcaRouter 혜택으로 충전 금액의 30%를 DeepSeek V4.1 Flash 크레딧으로 추가 적립하세요. 최신 조건은 혜택 페이지에서 확인하세요.",
"OrcaRouterOfferDisclosure": "Codewhale은 OrcaRouter로부터 추천 수수료를 받습니다.",
"ShellWaitNoActive": "현재 완료를 기다리는 셸 명령이 없습니다.",
"ShellWaitInteractive": "대화형 명령은 백그라운드에서 계속 실행할 수 없습니다. 턴을 취소하거나 명령이 완료될 때까지 기다리세요.",
"ShellWaitUnavailable": "이 도구는 백그라운드에서 계속 실행할 수 없습니다. 턴을 취소하거나 완료될 때까지 기다리세요.",
@@ -185,6 +194,7 @@
"ComposerHintOfflineConnect": "↵ 나중에 보내도록 저장 · /provider로 연결",
"ComposerHintSendNow": "↵ 지금 보내기",
"ComposerHintSendIntoTurn": "↵ 이 턴으로 보내기",
"ComposerHintLongDraft": "{shortcut}: 외부 편집기",
"PendingSendingIntoTurnPrefix": " ↳ 이 턴으로 보내는 중: ",
"PendingCouldNotSendIntoTurnPrefix": " ↳ 이 턴으로 보내지 못함: ",
"PendingEditingFollowUpPrefix": " ↳ 후속 메시지 편집 중: ",
@@ -467,6 +477,9 @@
"CmdImportClaudeDescription": "Claude Code 설정과 MCP 서버의 검토 가능한 가져오기 계획을 생성합니다",
"CmdClearDescription": "대화 기록을 지웁니다",
"CmdColorDescription": "이 채팅의 입력창 색상을 설정",
"CmdColorSet": "이 채팅의 입력창 색상: {color}",
"CmdColorReset": "이 채팅의 입력창 색상을 테마 기본값으로 되돌렸습니다",
"CmdColorUnknown": "알 수 없는 색상입니다. 다음 중 선택하세요: {colors} 또는 off.",
"CmdCompactDescription": "컨텍스트 압축을 실행해 공간을 확보합니다",
"CmdPurgeDescription": "에이전트가 대화 기록을 선별적으로 정리해 컨텍스트 공간을 확보하게 합니다",
"CmdConfigDescription": "설정을 확인하고 변경합니다 (고급 정보로 원시 키 제공)",
@@ -812,7 +825,7 @@
"KbSendDraft": "대기 중이면 전송, 실행 중이면 대기열에 추가; 입력창이 비어 있으면 다음 후속 메시지를 즉시 전송",
"KbSteerCurrentTurn": "현재 턴 조정 (대기 중에는 일반 전송)",
"KbCloseMenu": "메뉴 닫기, 요청 취소, 초안 버리기, 또는 입력 지우기",
"KbCancelOrExit": "요청 취소, 유휴 상태면 종료",
"KbCancelOrExit": "취소, 초안 지우기 또는 유휴 상태에서 종료",
"KbShellControls": "실행 중인 셸 대기를 /jobs로 이동",
"KbExitEmpty": "입력창이 비어 있으면 종료",
"KbCommandPalette": "명령 팔레트 열기",
@@ -1280,6 +1293,23 @@
"ApprovalControlsHint": "Pg↑/↓ 기록 · {details} 상세 · Esc 중지",
"ApprovalOwnerHeader": "에이전트: {agent} · {role}",
"ApprovalGoToAgent": "g 에이전트로 이동",
"UserInputActionRequired": "조치 필요",
"UserInputQuestionProgress": "질문 {current}/{total}",
"UserInputOtherLabel": "기타",
"UserInputOtherDescription": "직접 입력해서 답변하기",
"UserInputConfirmLabel": "선택 확인",
"UserInputConfirmDescription": "선택한 {count}개 제출",
"UserInputCustomPrompt": "직접 입력:",
"UserInputCustomPlaceholder": "(답변을 입력하세요)",
"UserInputHintSubmit": "제출",
"UserInputHintBack": "뒤로",
"UserInputHintMove": "이동",
"UserInputHintToggle": "토글",
"UserInputHintSelectConfirm": "선택/확인",
"UserInputHintCancel": "취소",
"UserInputHintQuickPick": "빠른 선택",
"UserInputHintConfirm": "확인",
"UserInputHintDigit": "숫자",
"ApprovalControlsHintChild": "Pg↑/↓ 기록 · {details} 상세 · Esc 숨기기",
"PendingApprovalInAgent": "{agent}에서 승인이 필요합니다 — /agents",
"ApprovalTruncationHint": " … 잘림 · 전체 상세는 {details}",
@@ -1376,6 +1406,8 @@
"ToolReceiptLinesSingular": "1줄",
"ToolReceiptLinesPlural": "{count}줄",
"ToolDescriptionNote": "설명",
"ToolStatusWaitingForApproval": "승인 대기 중",
"ToolStatusQueued": "대기 중",
"CmdConstitutionAccountApplies": "계정 프로필 규약: 적용됨 (로그인 상태, Engine이 매 턴 로드)",
"CmdConstitutionAccountSignedOut": "계정 프로필 규약: 로그인되지 않음",
"CmdConstitutionAccountCheckFailed": "계정 프로필 규약: 계정 확인 실패 ({error})",
@@ -2339,7 +2371,7 @@
"ConfigHintManagedApprovalPolicy": "프로젝트, 프로필, 환경, 관리 설정 또는 조직 요구 사항이 이 값을 제어",
"ConfigHintManagedAllowShell": "프로젝트, 프로필, 환경 또는 관리 설정이 shell 접근을 제어",
"ConfigHintAllowShell": "켜면 에이전트 모드에서 shell 도구를 노출. 권한 규칙은 계속 적용",
"ConfigHintComposerMultilineMode": "끔: Enter로 전송, Shift+Enter로 줄 추가. 켬: Enter로 줄 추가, Shift+Enter로 전송",
"ConfigHintComposerMultilineMode": "끔: Enter로 전송, Alt+Enter / Shift+Enter로 줄 추가. 켬: Enter로 줄 추가, Alt+Enter / Shift+Enter로 전송",
"ConfigHintBooleanValues": "on/off, true/false, yes/no, 1/0",
"ConfigHintDensity": "compact | comfortable | spacious",
"ConfigHintInlineDiffs": "full | summary | off. 정확한 변경은 Alt/Option+V 세부 정보에 남음",
@@ -2571,6 +2603,7 @@
"PlanHandoffKeepPlanning": "계속 계획하기",
"PlanHandoffKeepPlanningDetail": "Plan 에 머뭅니다. 아무것도 변경되지 않습니다.",
"PlanHandoffProceed": "계획대로 진행해 주세요.",
"PlanHandoffTodoNote": "이 계획을 todo_write로 추적하세요. 목록에 \"{title}\" 항목을 유지하고 계획이 끝나면 완료로 표시하세요.",
"PetAppearanceLegacy": "펫이 이전 버전으로 실행 중입니다. Codewhale을 업데이트한 뒤 펫을 다시 시작하면 고래 스타일과 텍스트 언어를 선택할 수 있습니다.",
"PluginOAuthBrowser": "Enter를 눌러 브라우저에서 로그인하세요. Esc를 누르면 돌아갑니다. API 키는 필요하지 않습니다.",
"PluginOAuthReady": "{provider}: 로그인했습니다. 모델을 선택하세요.",
@@ -2581,5 +2614,24 @@
"ExecSandboxHelp": "이번 실행의 샌드박스 정책입니다. danger-full-access 이외의 --sandbox(또는 설정의 동등한 sandbox_mode)는 신뢰 모드를 끕니다.",
"ExecAfterHelp": "예시:\n codewhale exec \"이 함수를 설명해 줘\"\n codewhale exec --auto \"ls로 crates/ 목록을 보여 줘\"\n codewhale exec --auto --output-format stream-json \"실패한 테스트를 수정해 줘\"\n\n일반 `codewhale exec`는 모델의 일회성 응답입니다. 다른 실행과 같은 시스템\n프롬프트로 도구 없이 Engine 턴을 한 번 실행합니다. 대화형 입력 없이\n도구를 쓰는 에이전트를 실행하려면 `--auto`를 사용하세요. 도구는 `--auto`,\n`--yolo`, `--allowed-tools`를 지정하거나 세션을 재개할 때만 제공됩니다.\n`--max-turns`, `--disallowed-tools`, `--sandbox` 같은 제한이나 출력 형식은\n도구를 추가하지 않습니다. 공급자의 출력 한도로 잘린 응답은 같은 턴에서\n이어지며, `--max-turns`로 달리 지정하지 않으면 모델 단계는 최대 8회입니다.\n--auto의 기본값은 Full Access입니다. 셸 샌드박스가 없고 신뢰 모드가 켜집니다. 셸 샌드박스와 파일 도구의 워크스페이스 제한을 유지하려면 --sandbox workspace-write를 사용하세요. 설정된 더 엄격한 정책도 적용됩니다. 샌드박스에서 거부된 작업을 재시도하려면 --allow-sandbox-elevation 또는 --sandbox danger-full-access가 필요합니다.\n도구를 제공하는 실행, stream-json 실행, 재개한 세션은 저장되며 codewhale sessions, exec --continue, codewhale receipts <id>에서 사용할 수 있습니다. 응답만 제공하는 일반 text/--json 실행은 압축 시 복구 데이터가 필요한 경우를 제외하고 저장되지 않습니다.\ndanger-full-access 이외의 --sandbox(또는 설정의 동등한 sandbox_mode)는 신뢰 모드를 끕니다.\n",
"ExecPromptHelp": "모델에 보낼 프롬프트입니다. -로 시작하거나 exec 플래그를 일반 텍스트로 포함하는 프롬프트 앞에는 --를 넣으세요. 표준 입력을 읽으려면 --prompt-file -를 사용하세요.",
"ExecFlagAfterPrompt": "{flag}가 프롬프트 뒤에 있어 텍스트로 처리됩니다. 플래그는 프롬프트 앞에 넣고, 일반 텍스트로 사용할 플래그는 -- 뒤에 넣으세요."
"ExecFlagAfterPrompt": "{flag}가 프롬프트 뒤에 있어 텍스트로 처리됩니다. 플래그는 프롬프트 앞에 넣고, 일반 텍스트로 사용할 플래그는 -- 뒤에 넣으세요.",
"InstructionNoticeHeader": "상시 지침이 이 세션이 시작되기 전에 잘리거나 건너뛰어졌습니다:",
"InstructionNoticeFooter": "소스별 전체 내역은 /context report를 실행하세요.",
"InstructionNoticeMore": "…외 {count}건. 전체 목록은 /context report에 있습니다.",
"InstructionNoticeLine": "지침: {scope} {label} ({origin}, {action}, {limit})",
"InstructionNoticeTrimmed": " {total_bytes}바이트 중 {kept_bytes}바이트, 추정 ~{total_tokens}토큰 중 ~{kept_tokens}토큰 유지",
"InstructionNoticeDroppedBytes": " {bytes}바이트 제외",
"InstructionNoticeFallback": "; 대체: {source}",
"InstructionNoticeStaticGuardOver": "지침: 지침을 잘라낸 뒤에도 정적 접두부가 정적 접두부 한도를 추정 ~{tokens}토큰 초과합니다. `trim` 정책으로 계속합니다 — static_prefix_guard_percent를 높이거나 도구 범위를 줄이세요.",
"InstructionNoticeStaticGuardTrimmed": "지침: 정적 접두부를 한도 아래로 유지하려고 {scope} 범위를 추정 ~{tokens}토큰 잘랐습니다",
"InstructionDetailDroppedBroadest": "가장 넓은 범위의 세그먼트로 제외됨",
"InstructionDetailNoRoom": "추정 {tokens}토큰의 범위 예산으로는 내용을 담을 여유가 없었습니다",
"InstructionDetailRulesExhausted": "추정 {tokens}토큰의 프로젝트 범위 예산을 지침이 모두 사용했습니다",
"InstructionDetailOverCeiling": "{bytes}바이트 한도를 초과합니다",
"InstructionDetailUnreadable": "읽을 수 없음: {reason}",
"InstructionDetailAlreadyLoaded": "더 넓은 범위에서 이미 로드됨",
"InstructionDetailRulesCap": "디렉터리당 규칙 파일 한도 {count}개를 초과합니다",
"InstructionDetailSymlinkedRules": "심볼릭 링크 규칙 디렉터리는 거부합니다",
"InstructionBudgetRefused": "지침 예산이 이 요청을 거부했습니다: {reason}",
"InstructionBudgetOverGuard": "정적 접두부 ~{measured} 추정 토큰이 한도 {guard}를 초과합니다"
}
+55 -3
View File
@@ -1,4 +1,13 @@
{
"ReasoningOnlyRetryBody": "somente raciocínio {attempt}/{limit}; sem resposta nem chamada de ferramenta; {action}",
"OutputLimitAnswerRetryBody": "limite de saída {attempt}/{limit}; sem resposta nem chamada de ferramenta; {action}",
"AnswerRetryActionBare": "solicitando a resposta novamente",
"AnswerRetryActionNudge": "solicitando a resposta novamente com um lembrete",
"AnswerRetryCounter": "somente raciocínio/limite de saída",
"OrcaRouterOfferRecommended": "OrcaRouter · Recomendado",
"OrcaRouterOfferAction": "Ver oferta da OrcaRouter em /provider ou /model",
"OrcaRouterOfferBody": "Receba 30% de crédito extra para DeepSeek V4.1 Flash nas recargas com a oferta Codewhale × OrcaRouter. Confira os termos atuais na página da oferta.",
"OrcaRouterOfferDisclosure": "A Codewhale recebe uma comissão por indicação da OrcaRouter.",
"ShellWaitNoActive": "Não há espera ativa por um comando do shell.",
"ShellWaitInteractive": "Comandos interativos não podem continuar em segundo plano. Cancele o turno ou aguarde a conclusão.",
"ShellWaitUnavailable": "Esta ferramenta não pode continuar em segundo plano. Cancele o turno ou aguarde a conclusão.",
@@ -185,6 +194,7 @@
"ComposerHintOfflineConnect": "↵ salvar para depois · /provider conecta",
"ComposerHintSendNow": "↵ enviar agora",
"ComposerHintSendIntoTurn": "↵ enviar nesta rodada",
"ComposerHintLongDraft": "{shortcut}: editor externo",
"PendingSendingIntoTurnPrefix": " ↳ Enviando nesta rodada: ",
"PendingCouldNotSendIntoTurnPrefix": " ↳ Não foi possível enviar nesta rodada: ",
"PendingEditingFollowUpPrefix": " ↳ Editando o acompanhamento: ",
@@ -467,6 +477,9 @@
"CmdImportClaudeDescription": "Planejar uma importação revisável das configurações e servidores MCP do Claude Code",
"CmdClearDescription": "Limpar o histórico da conversa",
"CmdColorDescription": "Definir a cor da caixa de mensagem desta conversa",
"CmdColorSet": "Cor da caixa de mensagem desta conversa: {color}",
"CmdColorReset": "Cor da caixa de mensagem desta conversa redefinida para o padrão do tema",
"CmdColorUnknown": "Cor desconhecida. Escolha uma de: {colors} ou off.",
"CmdCompactDescription": "Compactar o contexto para liberar espaço",
"CmdPurgeDescription": "Deixe o agente podar cirurgicamente o histórico para liberar espaço de contexto",
"CmdConfigDescription": "Inspecionar e alterar configurações (chaves brutas no detalhe avançado)",
@@ -812,7 +825,7 @@
"KbSendDraft": "Enviar quando ocioso; enfileirar quando ocupado; com o compositor vazio, enviar agora o próximo follow-up",
"KbSteerCurrentTurn": "Direcionar o turno atual (envia normalmente quando ocioso)",
"KbCloseMenu": "Fechar menu, cancelar requisição, descartar rascunho ou limpar entrada",
"KbCancelOrExit": "Cancelar requisição ou sair quando ocioso",
"KbCancelOrExit": "Cancelar, limpar rascunho ou sair quando ocioso",
"KbShellControls": "Mover a espera do shell para /jobs",
"KbExitEmpty": "Sair quando entrada vazia",
"KbCommandPalette": "Abrir paleta de comandos",
@@ -1278,6 +1291,23 @@
"ApprovalControlsHint": "Pg↑/↓ revisar · {details} detalhes · Esc parar",
"ApprovalOwnerHeader": "Agente: {agent} · {role}",
"ApprovalGoToAgent": "g ir para o agente",
"UserInputActionRequired": "Ação necessária",
"UserInputQuestionProgress": "Pergunta {current} de {total}",
"UserInputOtherLabel": "Outro",
"UserInputOtherDescription": "Digite uma resposta personalizada",
"UserInputConfirmLabel": "Confirmar seleção",
"UserInputConfirmDescription": "Enviar seleção ({count})",
"UserInputCustomPrompt": "Resposta personalizada:",
"UserInputCustomPlaceholder": "(digite sua resposta)",
"UserInputHintSubmit": "enviar",
"UserInputHintBack": "voltar",
"UserInputHintMove": "mover",
"UserInputHintToggle": "alternar",
"UserInputHintSelectConfirm": "marcar/confirmar",
"UserInputHintCancel": "cancelar",
"UserInputHintQuickPick": "seleção rápida",
"UserInputHintConfirm": "confirmar",
"UserInputHintDigit": "nº",
"ApprovalControlsHintChild": "Pg↑/↓ revisar · {details} detalhes · Esc ocultar",
"PendingApprovalInAgent": "Aprovação necessária em {agent} — /agents",
"ApprovalTruncationHint": " … truncado · pressione {details} para ver todos os detalhes",
@@ -1374,6 +1404,8 @@
"ToolReceiptLinesSingular": "1 linha",
"ToolReceiptLinesPlural": "{count} linhas",
"ToolDescriptionNote": "nota",
"ToolStatusWaitingForApproval": "aguardando aprovação",
"ToolStatusQueued": "na fila",
"CmdConstitutionAccountApplies": "Constituição do perfil da conta: aplicada (conectado; carregada pelo Engine a cada turno)",
"CmdConstitutionAccountSignedOut": "Constituição do perfil da conta: não conectado",
"CmdConstitutionAccountCheckFailed": "Constituição do perfil da conta: falha na verificação ({error})",
@@ -2339,7 +2371,7 @@
"ConfigHintManagedApprovalPolicy": "um projeto, perfil, ambiente, config gerenciado ou exigência da organização controla este valor",
"ConfigHintManagedAllowShell": "um projeto, perfil, ambiente ou config gerenciado controla o acesso ao shell",
"ConfigHintAllowShell": "ligado expõe ferramentas de shell no modo Agente; as regras de permissão continuam valendo",
"ConfigHintComposerMultilineMode": "desligado: Enter envia, Shift+Enter adiciona linha; ligado: Enter adiciona linha, Shift+Enter envia",
"ConfigHintComposerMultilineMode": "desligado: Enter envia, Alt+Enter / Shift+Enter adiciona linha; ligado: Enter adiciona linha, Alt+Enter / Shift+Enter envia",
"ConfigHintBooleanValues": "on/off, true/false, yes/no, 1/0",
"ConfigHintDensity": "compact | comfortable | spacious",
"ConfigHintInlineDiffs": "full | summary | off; a mudança exata permanece nos detalhes de Alt/Option+V",
@@ -2571,6 +2603,7 @@
"PlanHandoffKeepPlanning": "Continuar planejando",
"PlanHandoffKeepPlanningDetail": "Permanecer em Plan. Nada é alterado.",
"PlanHandoffProceed": "Pode seguir com o plano.",
"PlanHandoffTodoNote": "Acompanhe este plano com todo_write: mantenha o item \"{title}\" na sua lista e marque-o como concluído quando o plano terminar.",
"PetAppearanceLegacy": "O pet está usando uma versão anterior. Reinicie-o após atualizar o Codewhale para escolher o estilo da baleia e o idioma do texto.",
"PluginOAuthBrowser": "Pressione Enter para entrar pelo navegador ou Esc para voltar. Não é necessária uma chave de API.",
"PluginOAuthReady": "{provider}: sessão iniciada. Escolha um modelo.",
@@ -2581,5 +2614,24 @@
"ExecSandboxHelp": "Política de sandbox desta execução; qualquer --sandbox diferente de danger-full-access (ou sandbox_mode equivalente na configuração) desativa o modo de confiança.",
"ExecAfterHelp": "Exemplos:\n codewhale exec \"explique esta função\"\n codewhale exec --auto \"liste crates/ com ls\"\n codewhale exec --auto --output-format stream-json \"corrija o teste que falha\"\n\n`codewhale exec` sem opções produz uma única resposta do modelo: um turno do\nEngine com o mesmo prompt de sistema das demais execuções e sem ferramentas.\nUse `--auto` para executar um agente com ferramentas sem interação.\nAs ferramentas só são oferecidas com `--auto`, `--yolo`, `--allowed-tools`\nou ao retomar uma sessão; limites como `--max-turns`, `--disallowed-tools`\nou `--sandbox`, assim como o formato de saída, nunca adicionam ferramentas.\nUma resposta cortada pelo limite de saída do provedor continua no mesmo turno,\ncom no máximo 8 etapas do modelo, salvo indicação diferente de `--max-turns`.\n--auto usa Full Access por padrão: sem sandbox do shell e com modo de confiança ativado. Use --sandbox workspace-write para manter o sandbox do shell e as restrições das ferramentas de arquivos ao workspace; políticas mais restritivas configuradas também se aplicam. Repetir uma operação negada pelo sandbox exige --allow-sandbox-elevation ou --sandbox danger-full-access.\nExecuções que oferecem ferramentas, execuções stream-json e sessões retomadas são salvas para codewhale sessions, exec --continue e codewhale receipts <id>. Execuções text/--json que apenas respondem não são salvas, a menos que a compactação precise de dados de recuperação.\nQualquer --sandbox diferente de danger-full-access (ou sandbox_mode equivalente na configuração) desativa o modo de confiança.\n",
"ExecPromptHelp": "Prompt enviado ao modelo. Coloque -- antes de um prompt que comece com - ou contenha uma opção literal de exec; use --prompt-file - para ler a entrada padrão.",
"ExecFlagAfterPrompt": "{flag} aparece após o prompt e seria tratado como texto. Coloque as opções antes do prompt ou, para usá-las como texto literal, após --."
"ExecFlagAfterPrompt": "{flag} aparece após o prompt e seria tratado como texto. Coloque as opções antes do prompt ou, para usá-las como texto literal, após --.",
"InstructionNoticeHeader": "Instruções permanentes foram cortadas ou ignoradas antes do início desta sessão:",
"InstructionNoticeFooter": "Execute /context report para ver o detalhamento completo por fonte.",
"InstructionNoticeMore": "…e mais {count}; a lista completa está em /context report.",
"InstructionNoticeLine": "instruções: {scope} {label} ({origin}, {action}, {limit})",
"InstructionNoticeTrimmed": " mantidos {kept_bytes} de {total_bytes} bytes e ~{kept_tokens} de ~{total_tokens} tokens estimados",
"InstructionNoticeDroppedBytes": " {bytes} bytes descartados",
"InstructionNoticeFallback": "; alternativa: {source}",
"InstructionNoticeStaticGuardOver": "instruções: o prefixo estático ainda está ~{tokens} tokens estimados acima do limite do prefixo estático após o corte das instruções; continuando com a política `trim` — aumente static_prefix_guard_percent ou reduza a superfície de ferramentas.",
"InstructionNoticeStaticGuardTrimmed": "instruções: o escopo {scope} foi cortado em ~{tokens} tokens estimados para manter o prefixo estático abaixo do limite",
"InstructionDetailDroppedBroadest": "descartado como segmento do escopo mais amplo",
"InstructionDetailNoRoom": "o orçamento do escopo de {tokens} tokens estimados não deixou espaço para conteúdo",
"InstructionDetailRulesExhausted": "o orçamento do escopo do projeto de {tokens} tokens estimados foi esgotado pelas instruções",
"InstructionDetailOverCeiling": "maior que o limite de {bytes} bytes",
"InstructionDetailUnreadable": "não foi possível ler: {reason}",
"InstructionDetailAlreadyLoaded": "já carregado de um escopo mais amplo",
"InstructionDetailRulesCap": "acima do limite de {count} arquivos de regras por diretório",
"InstructionDetailSymlinkedRules": "recusando diretório de regras que é um link simbólico",
"InstructionBudgetRefused": "o orçamento de instruções recusou esta solicitação: {reason}",
"InstructionBudgetOverGuard": "o prefixo estático de ~{measured} tokens estimados excede o limite {guard}"
}
+55 -3
View File
@@ -1,4 +1,13 @@
{
"ReasoningOnlyRetryBody": "только рассуждение {attempt}/{limit}; нет ответа или вызова инструмента; {action}",
"OutputLimitAnswerRetryBody": "лимит вывода {attempt}/{limit}; нет ответа или вызова инструмента; {action}",
"AnswerRetryActionBare": "повторно запрашивается ответ",
"AnswerRetryActionNudge": "повторно запрашивается ответ с подсказкой",
"AnswerRetryCounter": "только рассуждение/лимит вывода",
"OrcaRouterOfferRecommended": "OrcaRouter · Рекомендуем",
"OrcaRouterOfferAction": "Посмотреть предложение OrcaRouter в /provider или /model",
"OrcaRouterOfferBody": "Получите 30% дополнительного кредита для DeepSeek V4.1 Flash при пополнении по предложению Codewhale × OrcaRouter. Актуальные условия указаны на странице предложения.",
"OrcaRouterOfferDisclosure": "Codewhale получает от OrcaRouter комиссию за рекомендации.",
"ShellWaitNoActive": "Сейчас нет ожидания команды оболочки.",
"ShellWaitInteractive": "Интерактивные команды не могут продолжать работу в фоне. Отмените ход или дождитесь завершения команды.",
"ShellWaitUnavailable": "Этот инструмент не может продолжать работу в фоне. Отмените ход или дождитесь его завершения.",
@@ -185,6 +194,7 @@
"ComposerHintOfflineConnect": "↵ сохранить на потом · /provider подключает",
"ComposerHintSendNow": "↵ отправить сейчас",
"ComposerHintSendIntoTurn": "↵ отправить в этот ход",
"ComposerHintLongDraft": "{shortcut}: внешний редактор",
"PendingSendingIntoTurnPrefix": " ↳ Отправка в этот ход: ",
"PendingCouldNotSendIntoTurnPrefix": " ↳ Не удалось отправить в этот ход: ",
"PendingEditingFollowUpPrefix": " ↳ Редактируется следующее сообщение: ",
@@ -464,6 +474,9 @@
"CmdImportClaudeDescription": "Спланировать проверяемый импорт настроек Claude Code и серверов MCP",
"CmdClearDescription": "Очистить историю диалога",
"CmdColorDescription": "Задать цвет поля ввода для этого чата",
"CmdColorSet": "Цвет поля ввода для этого чата: {color}",
"CmdColorReset": "Цвет поля ввода для этого чата сброшен до значения темы по умолчанию",
"CmdColorUnknown": "Неизвестный цвет. Выберите один из: {colors} или off.",
"CmdCompactDescription": "Запустить сжатие контекста, чтобы освободить место",
"CmdPurgeDescription": "Позволить агенту точечно сократить историю диалога и освободить контекст",
"CmdConfigDescription": "Просмотр и изменение настроек (сырые ключи — для продвинутых)",
@@ -795,7 +808,7 @@
"KbSendDraft": "Отправить, когда свободно; в очередь, когда занято; пустое поле ввода отправляет следующее сообщение из очереди сразу",
"KbSteerCurrentTurn": "Направить текущий ход (в простое отправляет как обычно)",
"KbCloseMenu": "Закрыть меню, отменить запрос, отбросить черновик или очистить ввод",
"KbCancelOrExit": "Отменить запрос или выйти в простое",
"KbCancelOrExit": "Отменить, очистить черновик или выйти в простое",
"KbShellControls": "Перенести ожидание запущенного шелла в /jobs",
"KbExitEmpty": "Выйти при пустом вводе",
"KbCommandPalette": "Открыть палитру команд",
@@ -1257,6 +1270,23 @@
"ApprovalControlsHint": "Pg↑/↓ просмотр · {details} детали · Esc стоп",
"ApprovalOwnerHeader": "Агент: {agent} · {role}",
"ApprovalGoToAgent": "g перейти к агенту",
"UserInputActionRequired": "Требуется действие",
"UserInputQuestionProgress": "Вопрос {current} из {total}",
"UserInputOtherLabel": "Другое",
"UserInputOtherDescription": "Введите свой вариант ответа",
"UserInputConfirmLabel": "Подтвердить выбор",
"UserInputConfirmDescription": "Отправить выбранное ({count})",
"UserInputCustomPrompt": "Свой ответ:",
"UserInputCustomPlaceholder": "(введите ответ)",
"UserInputHintSubmit": "отправить",
"UserInputHintBack": "назад",
"UserInputHintMove": "перейти",
"UserInputHintToggle": "отметить",
"UserInputHintSelectConfirm": "выбрать/отправить",
"UserInputHintCancel": "отмена",
"UserInputHintQuickPick": "быстрый выбор",
"UserInputHintConfirm": "подтвердить",
"UserInputHintDigit": "№",
"ApprovalControlsHintChild": "Pg↑/↓ просмотр · {details} детали · Esc скрыть",
"PendingApprovalInAgent": "Нужно одобрение в {agent} — /agents",
"ApprovalTruncationHint": " … обрезано · нажмите {details} для полных деталей",
@@ -1353,6 +1383,8 @@
"ToolReceiptLinesSingular": "1 строка",
"ToolReceiptLinesPlural": "{count} строк",
"ToolDescriptionNote": "примечание",
"ToolStatusWaitingForApproval": "ожидает одобрения",
"ToolStatusQueued": "в очереди",
"CmdConstitutionAccountApplies": "Конституция профиля аккаунта: применяется (вход выполнен; Engine загружает её на каждом ходе)",
"CmdConstitutionAccountSignedOut": "Конституция профиля аккаунта: вход не выполнен",
"CmdConstitutionAccountCheckFailed": "Конституция профиля аккаунта: проверка аккаунта не удалась ({error})",
@@ -2339,7 +2371,7 @@
"ConfigHintManagedApprovalPolicy": "это значение задаёт проект, профиль, окружение, управляемый config или требование организации",
"ConfigHintManagedAllowShell": "доступ к shell задаёт проект, профиль, окружение или управляемый config",
"ConfigHintAllowShell": "вкл открывает инструменты shell в режиме агента; правила разрешений по-прежнему действуют",
"ConfigHintComposerMultilineMode": "выкл: Enter отправляет, Shift+Enter добавляет строку; вкл: Enter добавляет строку, Shift+Enter отправляет",
"ConfigHintComposerMultilineMode": "выкл: Enter отправляет, Alt+Enter / Shift+Enter добавляет строку; вкл: Enter добавляет строку, Alt+Enter / Shift+Enter отправляет",
"ConfigHintBooleanValues": "on/off, true/false, yes/no, 1/0",
"ConfigHintDensity": "compact | comfortable | spacious",
"ConfigHintInlineDiffs": "full | summary | off; точное изменение остаётся в подробностях по Alt/Option+V",
@@ -2571,6 +2603,7 @@
"PlanHandoffKeepPlanning": "Продолжить планирование",
"PlanHandoffKeepPlanningDetail": "Остаться в Plan. Ничего не меняется.",
"PlanHandoffProceed": "Приступайте к плану.",
"PlanHandoffTodoNote": "Отслеживайте этот план через todo_write: оставьте в списке пункт \"{title}\" и отметьте его как выполненный, когда план будет завершён.",
"PetAppearanceLegacy": "Питомец работает на старой версии. После обновления Codewhale перезапустите его, чтобы выбрать стиль кита и язык текста.",
"PluginOAuthBrowser": "Нажмите Enter для входа через браузер или Esc для возврата. Ключ API не нужен.",
"PluginOAuthReady": "{provider}: вход выполнен. Выберите модель.",
@@ -2581,5 +2614,24 @@
"ExecSandboxHelp": "Политика песочницы для этого запуска; любое значение --sandbox, кроме danger-full-access (или соответствующее значение sandbox_mode в конфигурации), отключает режим доверия.",
"ExecAfterHelp": "Примеры:\n codewhale exec \"объясни эту функцию\"\n codewhale exec --auto \"покажи содержимое crates/ с помощью ls\"\n codewhale exec --auto --output-format stream-json \"исправь падающий тест\"\n\nОбычный `codewhale exec` выдаёт один ответ модели: один ход Engine с тем же\nсистемным промптом, что и в других запусках, без инструментов. Для запуска\nагента с инструментами без взаимодействия используйте `--auto`. Инструменты\nдоступны только с `--auto`, `--yolo`, `--allowed-tools` или при продолжении\nсессии; ограничения вроде `--max-turns`, `--disallowed-tools`, `--sandbox` и\nформат вывода никогда не добавляют инструменты. Ответ, прерванный лимитом\nвывода провайдера, продолжается в том же ходе, не более 8 шагов модели,\nесли `--max-turns` не задаёт другой предел.\nПо умолчанию --auto использует Full Access: без песочницы оболочки, с включённым режимом доверия. Используйте --sandbox workspace-write, чтобы сохранить песочницу оболочки и ограничения файловых инструментов рабочей областью; более строгие настроенные политики также действуют. Повтор операции, отклонённой песочницей, требует --allow-sandbox-elevation или --sandbox danger-full-access.\nЗапуски с доступными инструментами, запуски stream-json и возобновлённые сессии сохраняются для codewhale sessions, exec --continue и codewhale receipts <id>. Обычные запуски text/--json только с ответом не сохраняются, если сжатие контекста не требует данных для восстановления.\nЛюбое значение --sandbox, кроме danger-full-access (или соответствующее значение sandbox_mode в конфигурации), отключает режим доверия.\n",
"ExecPromptHelp": "Запрос для модели. Поставьте -- перед запросом, который начинается с - или содержит флаг exec как обычный текст; --prompt-file - читает стандартный ввод.",
"ExecFlagAfterPrompt": "{flag} указан после запроса и будет воспринят как текст. Укажите флаги перед запросом, а если флаг нужен как обычный текст — после --."
"ExecFlagAfterPrompt": "{flag} указан после запроса и будет воспринят как текст. Укажите флаги перед запросом, а если флаг нужен как обычный текст — после --.",
"InstructionNoticeHeader": "Постоянные инструкции были урезаны или пропущены до начала этой сессии:",
"InstructionNoticeFooter": "Выполните /context report, чтобы увидеть полную разбивку по источникам.",
"InstructionNoticeMore": "…и ещё {count}; полный список в /context report.",
"InstructionNoticeLine": "инструкции: {scope} {label} ({origin}, {action}, {limit})",
"InstructionNoticeTrimmed": " сохранено {kept_bytes} из {total_bytes} байт и ~{kept_tokens} из ~{total_tokens} оценочных токенов",
"InstructionNoticeDroppedBytes": " отброшено {bytes} байт",
"InstructionNoticeFallback": "; замена: {source}",
"InstructionNoticeStaticGuardOver": "инструкции: статический префикс всё ещё превышает его предел на ~{tokens} оценочных токенов после сокращения инструкций; работа продолжается с политикой `trim` — увеличьте static_prefix_guard_percent или сократите набор инструментов.",
"InstructionNoticeStaticGuardTrimmed": "инструкции: область {scope} сокращена на ~{tokens} оценочных токенов, чтобы статический префикс уложился в предел",
"InstructionDetailDroppedBroadest": "отброшено как сегмент самой широкой области",
"InstructionDetailNoRoom": "бюджет области в {tokens} оценочных токенов не оставил места для содержимого",
"InstructionDetailRulesExhausted": "бюджет области проекта в {tokens} оценочных токенов исчерпан инструкциями",
"InstructionDetailOverCeiling": "больше предела в {bytes} байт",
"InstructionDetailUnreadable": "не удалось прочитать: {reason}",
"InstructionDetailAlreadyLoaded": "уже загружено из более широкой области",
"InstructionDetailRulesCap": "сверх предела в {count} файлов правил на каталог",
"InstructionDetailSymlinkedRules": "отклонено: каталог правил является символической ссылкой",
"InstructionBudgetRefused": "бюджет инструкций отклонил этот запрос: {reason}",
"InstructionBudgetOverGuard": "статический префикс ~{measured} оценочных токенов превышает предел {guard}"
}
+55 -3
View File
@@ -1,4 +1,13 @@
{
"ReasoningOnlyRetryBody": "лише міркування {attempt}/{limit}; немає відповіді чи виклику інструмента; {action}",
"OutputLimitAnswerRetryBody": "ліміт виводу {attempt}/{limit}; немає відповіді чи виклику інструмента; {action}",
"AnswerRetryActionBare": "повторно запитується відповідь",
"AnswerRetryActionNudge": "повторно запитується відповідь із підказкою",
"AnswerRetryCounter": "лише міркування/ліміт виводу",
"OrcaRouterOfferRecommended": "OrcaRouter · Рекомендуємо",
"OrcaRouterOfferAction": "Переглянути пропозицію OrcaRouter у /provider або /model",
"OrcaRouterOfferBody": "Отримайте 30% додаткового кредиту для DeepSeek V4.1 Flash за поповнення з пропозицією Codewhale × OrcaRouter. Актуальні умови наведено на сторінці пропозиції.",
"OrcaRouterOfferDisclosure": "Codewhale отримує від OrcaRouter комісію за рекомендації.",
"ShellWaitNoActive": "Зараз немає очікування команди оболонки.",
"ShellWaitInteractive": "Інтерактивні команди не можуть продовжувати роботу у фоні. Скасуйте хід або дочекайтеся завершення команди.",
"ShellWaitUnavailable": "Цей інструмент не може продовжувати роботу у фоні. Скасуйте хід або дочекайтеся його завершення.",
@@ -185,6 +194,7 @@
"ComposerHintOfflineConnect": "↵ зберегти на потім · /provider підключає",
"ComposerHintSendNow": "↵ надіслати зараз",
"ComposerHintSendIntoTurn": "↵ надіслати в цей хід",
"ComposerHintLongDraft": "{shortcut}: зовнішній редактор",
"PendingSendingIntoTurnPrefix": " ↳ Надсилання в цей хід: ",
"PendingCouldNotSendIntoTurnPrefix": " ↳ Не вдалося надіслати в цей хід: ",
"PendingEditingFollowUpPrefix": " ↳ Редагується наступне повідомлення: ",
@@ -464,6 +474,9 @@
"CmdImportClaudeDescription": "Спланувати перевірний імпорт налаштувань Claude Code і серверів MCP",
"CmdClearDescription": "Очистити історію розмови",
"CmdColorDescription": "Встановити колір поля введення для цього чату",
"CmdColorSet": "Колір поля введення для цього чату: {color}",
"CmdColorReset": "Колір поля введення для цього чату скинуто до типового для теми",
"CmdColorUnknown": "Невідомий колір. Виберіть один із: {colors} або off.",
"CmdCompactDescription": "Запустити стиснення контексту, щоб звільнити місце",
"CmdPurgeDescription": "Дозволити агенту вибірково скоротити історію розмови, щоб звільнити контекст",
"CmdConfigDescription": "Переглянути й змінити налаштування (сирі ключі — як додаткова деталь)",
@@ -795,7 +808,7 @@
"KbSendDraft": "Надіслати, коли вільно; у чергу, коли зайнято; порожній композер одразу надсилає наступне повідомлення з черги",
"KbSteerCurrentTurn": "Скерувати поточний крок (коли вільно — надсилає як зазвичай)",
"KbCloseMenu": "Закрити меню, скасувати запит, відкинути чернетку або очистити ввід",
"KbCancelOrExit": "Скасувати запит або вийти, коли вільно",
"KbCancelOrExit": "Скасувати, очистити чернетку або вийти, коли вільно",
"KbShellControls": "Перенести очікування запущеної оболонки до /jobs",
"KbExitEmpty": "Вийти, коли ввід порожній",
"KbCommandPalette": "Відкрити палітру команд",
@@ -1257,6 +1270,23 @@
"ApprovalControlsHint": "Pg↑/↓ перегляд · {details} деталі · Esc зупинити",
"ApprovalOwnerHeader": "Агент: {agent} · {role}",
"ApprovalGoToAgent": "g перейти до агента",
"UserInputActionRequired": "Потрібна дія",
"UserInputQuestionProgress": "Питання {current} з {total}",
"UserInputOtherLabel": "Інше",
"UserInputOtherDescription": "Введіть власну відповідь",
"UserInputConfirmLabel": "Підтвердити вибір",
"UserInputConfirmDescription": "Надіслати вибране ({count})",
"UserInputCustomPrompt": "Власна відповідь:",
"UserInputCustomPlaceholder": "(введіть відповідь)",
"UserInputHintSubmit": "надіслати",
"UserInputHintBack": "назад",
"UserInputHintMove": "рухатися",
"UserInputHintToggle": "позначити",
"UserInputHintSelectConfirm": "вибрати/підтв.",
"UserInputHintCancel": "скасувати",
"UserInputHintQuickPick": "швидкий вибір",
"UserInputHintConfirm": "підтвердити",
"UserInputHintDigit": "№",
"ApprovalControlsHintChild": "Pg↑/↓ перегляд · {details} деталі · Esc сховати",
"PendingApprovalInAgent": "Потрібне схвалення в {agent} — /agents",
"ApprovalTruncationHint": " … обрізано · натисніть {details} для повних деталей",
@@ -1353,6 +1383,8 @@
"ToolReceiptLinesSingular": "1 рядок",
"ToolReceiptLinesPlural": "{count} рядків",
"ToolDescriptionNote": "примітка",
"ToolStatusWaitingForApproval": "очікує схвалення",
"ToolStatusQueued": "у черзі",
"CmdConstitutionAccountApplies": "Конституція профілю облікового запису: застосовується (вхід виконано; Engine завантажує її на кожному кроці)",
"CmdConstitutionAccountSignedOut": "Конституція профілю облікового запису: вхід не виконано",
"CmdConstitutionAccountCheckFailed": "Конституція профілю облікового запису: перевірка не вдалася ({error})",
@@ -2339,7 +2371,7 @@
"ConfigHintManagedApprovalPolicy": "це значення задає проєкт, профіль, середовище, керований config або вимога організації",
"ConfigHintManagedAllowShell": "доступ до shell задає проєкт, профіль, середовище або керований config",
"ConfigHintAllowShell": "увімк відкриває інструменти shell у режимі агента; правила дозволів і далі діють",
"ConfigHintComposerMultilineMode": "вимк: Enter надсилає, Shift+Enter додає рядок; увімк: Enter додає рядок, Shift+Enter надсилає",
"ConfigHintComposerMultilineMode": "вимк: Enter надсилає, Alt+Enter / Shift+Enter додає рядок; увімк: Enter додає рядок, Alt+Enter / Shift+Enter надсилає",
"ConfigHintBooleanValues": "on/off, true/false, yes/no, 1/0",
"ConfigHintDensity": "compact | comfortable | spacious",
"ConfigHintInlineDiffs": "full | summary | off; точна зміна залишається в подробицях за Alt/Option+V",
@@ -2571,6 +2603,7 @@
"PlanHandoffKeepPlanning": "Планувати далі",
"PlanHandoffKeepPlanningDetail": "Залишитися в Plan. Нічого не змінюється.",
"PlanHandoffProceed": "Беріться до плану.",
"PlanHandoffTodoNote": "Відстежуйте цей план через todo_write: залиште в списку пункт \"{title}\" і позначте його як виконаний, коли план буде завершено.",
"PetAppearanceLegacy": "Улюбленець працює на старій версії. Після оновлення Codewhale перезапустіть його, щоб вибрати стиль кита й мову тексту.",
"PluginOAuthBrowser": "Натисніть Enter для входу через браузер або Esc для повернення. Ключ API не потрібен.",
"PluginOAuthReady": "{provider}: вхід виконано. Виберіть модель.",
@@ -2581,5 +2614,24 @@
"ExecSandboxHelp": "Політика пісочниці для цього запуску; будь-яке значення --sandbox, крім danger-full-access (або відповідне значення sandbox_mode у конфігурації), вимикає режим довіри.",
"ExecAfterHelp": "Приклади:\n codewhale exec \"поясни цю функцію\"\n codewhale exec --auto \"покажи вміст crates/ за допомогою ls\"\n codewhale exec --auto --output-format stream-json \"виправ тест, що не проходить\"\n\nЗвичайний `codewhale exec` дає одну відповідь моделі: один хід Engine з тією\nсамою системною підказкою, що й інші запуски, без інструментів. Використовуйте\n`--auto`, щоб запускати агента з інструментами без взаємодії. Інструменти\nдоступні лише з `--auto`, `--yolo`, `--allowed-tools` або під час поновлення\nсесії; обмеження на кшталт `--max-turns`, `--disallowed-tools`, `--sandbox` і\nформат виводу ніколи не додають інструментів. Відповідь, перервана лімітом\nвиводу провайдера, продовжується в тому самому ході, щонайбільше 8 кроків\nмоделі, якщо `--max-turns` не задає іншу межу.\nТипово --auto використовує Full Access: без пісочниці оболонки, з увімкненим режимом довіри. Використовуйте --sandbox workspace-write, щоб зберегти пісочницю оболонки та обмеження файлових інструментів робочою областю; суворіші налаштовані політики також діють. Повтор операції, відхиленої пісочницею, потребує --allow-sandbox-elevation або --sandbox danger-full-access.\nЗапуски з доступними інструментами, запуски stream-json і поновлені сесії зберігаються для codewhale sessions, exec --continue і codewhale receipts <id>. Звичайні запуски text/--json лише з відповіддю не зберігаються, якщо стиснення контексту не потребує даних для відновлення.\nБудь-яке значення --sandbox, крім danger-full-access (або відповідне значення sandbox_mode у конфігурації), вимикає режим довіри.\n",
"ExecPromptHelp": "Запит для моделі. Поставте -- перед запитом, який починається з - або містить прапорець exec як звичайний текст; --prompt-file - читає стандартний ввід.",
"ExecFlagAfterPrompt": "{flag} вказано після запиту, тож його буде сприйнято як текст. Указуйте прапорці перед запитом, а якщо прапорець потрібен як звичайний текст — після --."
"ExecFlagAfterPrompt": "{flag} вказано після запиту, тож його буде сприйнято як текст. Указуйте прапорці перед запитом, а якщо прапорець потрібен як звичайний текст — після --.",
"InstructionNoticeHeader": "Постійні інструкції було скорочено або пропущено до початку цього сеансу:",
"InstructionNoticeFooter": "Виконайте /context report, щоб побачити повну розбивку за джерелами.",
"InstructionNoticeMore": "…і ще {count}; повний список у /context report.",
"InstructionNoticeLine": "інструкції: {scope} {label} ({origin}, {action}, {limit})",
"InstructionNoticeTrimmed": " збережено {kept_bytes} із {total_bytes} байтів і ~{kept_tokens} із ~{total_tokens} оціночних токенів",
"InstructionNoticeDroppedBytes": " відкинуто {bytes} байтів",
"InstructionNoticeFallback": "; заміна: {source}",
"InstructionNoticeStaticGuardOver": "інструкції: статичний префікс усе ще перевищує свій ліміт на ~{tokens} оціночних токенів після скорочення інструкцій; роботу продовжено з політикою `trim` — збільште static_prefix_guard_percent або скоротіть набір інструментів.",
"InstructionNoticeStaticGuardTrimmed": "інструкції: область {scope} скорочено на ~{tokens} оціночних токенів, щоб статичний префікс вклався в ліміт",
"InstructionDetailDroppedBroadest": "відкинуто як сегмент найширшої області",
"InstructionDetailNoRoom": "бюджет області в {tokens} оціночних токенів не залишив місця для вмісту",
"InstructionDetailRulesExhausted": "бюджет області проєкту в {tokens} оціночних токенів вичерпано інструкціями",
"InstructionDetailOverCeiling": "більше за ліміт у {bytes} байтів",
"InstructionDetailUnreadable": "не вдалося прочитати: {reason}",
"InstructionDetailAlreadyLoaded": "уже завантажено з ширшої області",
"InstructionDetailRulesCap": "понад ліміт у {count} файлів правил на каталог",
"InstructionDetailSymlinkedRules": "відхилено: каталог правил є символічним посиланням",
"InstructionBudgetRefused": "бюджет інструкцій відхилив цей запит: {reason}",
"InstructionBudgetOverGuard": "статичний префікс ~{measured} оціночних токенів перевищує ліміт {guard}"
}
+55 -3
View File
@@ -1,4 +1,13 @@
{
"ReasoningOnlyRetryBody": "chỉ có suy luận {attempt}/{limit}; không có câu trả lời hay lệnh gọi công cụ; {action}",
"OutputLimitAnswerRetryBody": "giới hạn đầu ra {attempt}/{limit}; không có câu trả lời hay lệnh gọi công cụ; {action}",
"AnswerRetryActionBare": "đang yêu cầu trả lời lại",
"AnswerRetryActionNudge": "đang yêu cầu trả lời lại kèm lời nhắc",
"AnswerRetryCounter": "chỉ suy luận/giới hạn đầu ra",
"OrcaRouterOfferRecommended": "OrcaRouter · Đề xuất",
"OrcaRouterOfferAction": "Xem ưu đãi OrcaRouter trong /provider hoặc /model",
"OrcaRouterOfferBody": "Nhận thêm 30% tín dụng DeepSeek V4.1 Flash khi nạp tiền với ưu đãi Codewhale × OrcaRouter. Xem điều kiện hiện hành trên trang ưu đãi.",
"OrcaRouterOfferDisclosure": "Codewhale nhận phí giới thiệu từ OrcaRouter.",
"ShellWaitNoActive": "Không có lệnh shell nào đang được chờ.",
"ShellWaitInteractive": "Lệnh tương tác không thể tiếp tục chạy nền. Hãy hủy lượt hoặc chờ lệnh hoàn tất.",
"ShellWaitUnavailable": "Công cụ này không thể tiếp tục chạy nền. Hãy hủy lượt hoặc chờ công cụ hoàn tất.",
@@ -185,6 +194,7 @@
"ComposerHintOfflineConnect": "↵ lưu để gửi sau · /provider kết nối",
"ComposerHintSendNow": "↵ gửi ngay",
"ComposerHintSendIntoTurn": "↵ gửi vào lượt này",
"ComposerHintLongDraft": "{shortcut}: trình sửa ngoài",
"PendingSendingIntoTurnPrefix": " ↳ Đang gửi vào lượt này: ",
"PendingCouldNotSendIntoTurnPrefix": " ↳ Không gửi được vào lượt này: ",
"PendingEditingFollowUpPrefix": " ↳ Đang sửa tin theo sau: ",
@@ -467,6 +477,9 @@
"CmdImportClaudeDescription": "Lập kế hoạch nhập có thể xem lại cho cài đặt và máy chủ MCP của Claude Code",
"CmdClearDescription": "Xóa lịch sử trò chuyện",
"CmdColorDescription": "Đặt màu ô soạn tin cho cuộc trò chuyện này",
"CmdColorSet": "Màu ô soạn tin của cuộc trò chuyện này: {color}",
"CmdColorReset": "Đã đặt lại màu ô soạn tin của cuộc trò chuyện này về mặc định của chủ đề",
"CmdColorUnknown": "Màu không hợp lệ. Chọn một trong: {colors}, hoặc off.",
"CmdCompactDescription": "Kích hoạt nén ngữ cảnh để giải phóng không gian",
"CmdPurgeDescription": "Cho agent cắt gọn lịch sử trò chuyện để giải phóng ngữ cảnh",
"CmdConfigDescription": "Xem và đổi cài đặt (khóa thô ở chi tiết nâng cao)",
@@ -812,7 +825,7 @@
"KbSendDraft": "Gửi khi rảnh; xếp hàng khi bận; khung soạn thảo trống sẽ gửi ngay follow-up tiếp theo trong hàng đợi",
"KbSteerCurrentTurn": "Điều hướng lượt hiện tại (gửi bình thường khi rảnh)",
"KbCloseMenu": "Đóng menu, hủy yêu cầu, hủy bản nháp hoặc xóa sạch đầu vào",
"KbCancelOrExit": "Hủy yêu cầu, hoặc thoát khi rảnh",
"KbCancelOrExit": "Hủy, xóa bản nháp hoặc thoát khi rảnh",
"KbShellControls": "Chuyển phiên chờ shell sang /jobs",
"KbExitEmpty": "Thoát khi khung nhập trống",
"KbCommandPalette": "Mở bảng lệnh (command palette)",
@@ -1278,6 +1291,23 @@
"ApprovalControlsHint": "Pg↑/↓ xem lại · {details} chi tiết · Esc dừng",
"ApprovalOwnerHeader": "Agent: {agent} · {role}",
"ApprovalGoToAgent": "g tới agent",
"UserInputActionRequired": "Cần bạn thao tác",
"UserInputQuestionProgress": "Câu hỏi {current}/{total}",
"UserInputOtherLabel": "Khác",
"UserInputOtherDescription": "Nhập câu trả lời tùy chỉnh",
"UserInputConfirmLabel": "Xác nhận lựa chọn",
"UserInputConfirmDescription": "Gửi lựa chọn ({count})",
"UserInputCustomPrompt": "Câu trả lời tùy chỉnh:",
"UserInputCustomPlaceholder": "(nhập câu trả lời của bạn)",
"UserInputHintSubmit": "gửi",
"UserInputHintBack": "quay lại",
"UserInputHintMove": "di chuyển",
"UserInputHintToggle": "bật/tắt",
"UserInputHintSelectConfirm": "chọn/xác nhận",
"UserInputHintCancel": "hủy",
"UserInputHintQuickPick": "chọn nhanh",
"UserInputHintConfirm": "xác nhận",
"UserInputHintDigit": "chữ số",
"ApprovalControlsHintChild": "Pg↑/↓ xem lại · {details} chi tiết · Esc ẩn",
"PendingApprovalInAgent": "Cần phê duyệt trong {agent} — /agents",
"ApprovalTruncationHint": " … đã rút gọn · nhấn {details} để xem đầy đủ",
@@ -1374,6 +1404,8 @@
"ToolReceiptLinesSingular": "1 dòng",
"ToolReceiptLinesPlural": "{count} dòng",
"ToolDescriptionNote": "ghi chú",
"ToolStatusWaitingForApproval": "đang chờ phê duyệt",
"ToolStatusQueued": "đang xếp hàng",
"CmdConstitutionAccountApplies": "Hiến chương hồ sơ tài khoản: áp dụng (đã đăng nhập; Engine tải mỗi lượt)",
"CmdConstitutionAccountSignedOut": "Hiến chương hồ sơ tài khoản: chưa đăng nhập",
"CmdConstitutionAccountCheckFailed": "Hiến chương hồ sơ tài khoản: kiểm tra tài khoản thất bại ({error})",
@@ -2339,7 +2371,7 @@
"ConfigHintManagedApprovalPolicy": "một dự án, hồ sơ, môi trường, config được quản lý hoặc yêu cầu của tổ chức kiểm soát giá trị này",
"ConfigHintManagedAllowShell": "một dự án, hồ sơ, môi trường hoặc config được quản lý kiểm soát quyền truy cập shell",
"ConfigHintAllowShell": "bật sẽ cung cấp công cụ shell ở chế độ Tác nhân; quy tắc quyền vẫn áp dụng",
"ConfigHintComposerMultilineMode": "tắt: Enter gửi, Shift+Enter thêm dòng; bật: Enter thêm dòng, Shift+Enter gửi",
"ConfigHintComposerMultilineMode": "tắt: Enter gửi, Alt+Enter / Shift+Enter thêm dòng; bật: Enter thêm dòng, Alt+Enter / Shift+Enter gửi",
"ConfigHintBooleanValues": "on/off, true/false, yes/no, 1/0",
"ConfigHintDensity": "compact | comfortable | spacious",
"ConfigHintInlineDiffs": "full | summary | off; thay đổi chính xác vẫn ở chi tiết Alt/Option+V",
@@ -2571,6 +2603,7 @@
"PlanHandoffKeepPlanning": "Tiếp tục lập kế hoạch",
"PlanHandoffKeepPlanningDetail": "Ở lại Plan. Không có gì thay đổi.",
"PlanHandoffProceed": "Hãy làm theo kế hoạch.",
"PlanHandoffTodoNote": "Theo dõi kế hoạch này bằng todo_write: giữ mục \"{title}\" trong danh sách của bạn và đánh dấu đã hoàn tất khi kế hoạch xong.",
"PetAppearanceLegacy": "Thú cưng đang dùng phiên bản cũ. Khởi động lại sau khi cập nhật Codewhale để chọn kiểu cá voi và ngôn ngữ văn bản.",
"PluginOAuthBrowser": "Nhấn Enter để đăng nhập bằng trình duyệt hoặc Esc để quay lại. Không cần khóa API.",
"PluginOAuthReady": "{provider}: đã đăng nhập. Hãy chọn mô hình.",
@@ -2581,5 +2614,24 @@
"ExecSandboxHelp": "Chính sách sandbox cho lần chạy này; mọi --sandbox khác danger-full-access (hoặc sandbox_mode tương đương trong cấu hình) đều tắt chế độ tin cậy.",
"ExecAfterHelp": "Ví dụ:\n codewhale exec \"giải thích hàm này\"\n codewhale exec --auto \"liệt kê crates/ bằng ls\"\n codewhale exec --auto --output-format stream-json \"sửa bài kiểm thử đang lỗi\"\n\n`codewhale exec` thông thường tạo một câu trả lời của mô hình: một lượt Engine\nvới cùng lời nhắc hệ thống như các lần chạy khác và không có công cụ. Dùng\n`--auto` để chạy tác nhân có công cụ mà không cần tương tác. Công cụ chỉ được\ncung cấp khi có `--auto`, `--yolo`, `--allowed-tools` hoặc khi tiếp tục phiên;\ncác giới hạn như `--max-turns`, `--disallowed-tools`, `--sandbox` và định dạng\nđầu ra không bao giờ thêm công cụ. Câu trả lời bị ngắt do giới hạn đầu ra của\nnhà cung cấp được tiếp tục trong cùng lượt, tối đa 8 bước mô hình trừ khi\n`--max-turns` quy định khác.\n--auto mặc định dùng Full Access: không có sandbox shell và bật chế độ tin cậy. Dùng --sandbox workspace-write để duy trì sandbox shell và giới hạn công cụ tệp trong không gian làm việc; các chính sách chặt chẽ hơn đã cấu hình cũng được áp dụng. Thử lại thao tác bị sandbox từ chối cần --allow-sandbox-elevation hoặc --sandbox danger-full-access.\nCác lần chạy có cung cấp công cụ, lần chạy stream-json và phiên được tiếp tục đều được lưu để dùng với codewhale sessions, exec --continue và codewhale receipts <id>. Các lần chạy text/--json thông thường chỉ trả lời sẽ không được lưu, trừ khi việc nén ngữ cảnh cần dữ liệu khôi phục.\nMọi --sandbox khác danger-full-access (hoặc sandbox_mode tương đương trong cấu hình) đều tắt chế độ tin cậy.\n",
"ExecPromptHelp": "Lời nhắc gửi đến mô hình. Đặt -- trước lời nhắc bắt đầu bằng - hoặc chứa cờ exec dưới dạng văn bản; dùng --prompt-file - để đọc đầu vào chuẩn.",
"ExecFlagAfterPrompt": "{flag} nằm sau lời nhắc và sẽ bị xem là văn bản. Hãy đặt cờ trước lời nhắc hoặc đặt cờ cần dùng dưới dạng văn bản sau --."
"ExecFlagAfterPrompt": "{flag} nằm sau lời nhắc và sẽ bị xem là văn bản. Hãy đặt cờ trước lời nhắc hoặc đặt cờ cần dùng dưới dạng văn bản sau --.",
"InstructionNoticeHeader": "Các chỉ dẫn thường trực đã bị cắt bớt hoặc bỏ qua trước khi phiên này bắt đầu:",
"InstructionNoticeFooter": "Chạy /context report để xem chi tiết đầy đủ theo từng nguồn.",
"InstructionNoticeMore": "…và {count} mục nữa; danh sách đầy đủ nằm trong /context report.",
"InstructionNoticeLine": "chỉ dẫn: {scope} {label} ({origin}, {action}, {limit})",
"InstructionNoticeTrimmed": " giữ {kept_bytes}/{total_bytes} byte và ~{kept_tokens}/~{total_tokens} token ước tính",
"InstructionNoticeDroppedBytes": " đã bỏ {bytes} byte",
"InstructionNoticeFallback": "; thay thế: {source}",
"InstructionNoticeStaticGuardOver": "chỉ dẫn: tiền tố tĩnh vẫn vượt giới hạn tiền tố tĩnh ~{tokens} token ước tính sau khi cắt chỉ dẫn; tiếp tục theo chính sách `trim` — hãy tăng static_prefix_guard_percent hoặc thu hẹp bề mặt công cụ.",
"InstructionNoticeStaticGuardTrimmed": "chỉ dẫn: phạm vi {scope} bị cắt ~{tokens} token ước tính để giữ tiền tố tĩnh dưới giới hạn",
"InstructionDetailDroppedBroadest": "bị bỏ vì là đoạn thuộc phạm vi rộng nhất",
"InstructionDetailNoRoom": "ngân sách phạm vi {tokens} token ước tính không còn chỗ cho nội dung",
"InstructionDetailRulesExhausted": "ngân sách phạm vi dự án {tokens} token ước tính đã bị chỉ dẫn dùng hết",
"InstructionDetailOverCeiling": "lớn hơn mức tối đa {bytes} byte",
"InstructionDetailUnreadable": "không đọc được: {reason}",
"InstructionDetailAlreadyLoaded": "đã được nạp từ phạm vi rộng hơn",
"InstructionDetailRulesCap": "vượt giới hạn {count} tệp quy tắc mỗi thư mục",
"InstructionDetailSymlinkedRules": "từ chối thư mục quy tắc là liên kết tượng trưng",
"InstructionBudgetRefused": "ngân sách chỉ dẫn đã từ chối yêu cầu này: {reason}",
"InstructionBudgetOverGuard": "tiền tố tĩnh ~{measured} token ước tính vượt giới hạn {guard}"
}
+55 -3
View File
@@ -1,4 +1,13 @@
{
"ReasoningOnlyRetryBody": "仅有推理 {attempt}/{limit};没有回答或工具调用;{action}",
"OutputLimitAnswerRetryBody": "达到输出上限 {attempt}/{limit};没有回答或工具调用;{action}",
"AnswerRetryActionBare": "重新请求回答",
"AnswerRetryActionNudge": "附带提示后重新请求回答",
"AnswerRetryCounter": "仅有推理/输出上限",
"OrcaRouterOfferRecommended": "OrcaRouter · 推荐",
"OrcaRouterOfferAction": "在 /provider 或 /model 中查看 OrcaRouter 优惠",
"OrcaRouterOfferBody": "通过 Codewhale × OrcaRouter 优惠,充值可额外获得 30% 的 DeepSeek V4.1 Flash 额度。最新条款请查看优惠页面。",
"OrcaRouterOfferDisclosure": "Codewhale 会从 OrcaRouter 获得推荐佣金。",
"ShellWaitNoActive": "当前没有正在等待的 shell 命令。",
"ShellWaitInteractive": "交互式命令无法在后台继续运行。请取消当前回合,或等待命令完成。",
"ShellWaitUnavailable": "此工具无法在后台继续运行。请取消当前回合,或等待它完成。",
@@ -185,6 +194,7 @@
"ComposerHintOfflineConnect": "↵ 先保存 · /provider 连接",
"ComposerHintSendNow": "↵ 立即发送",
"ComposerHintSendIntoTurn": "↵ 发进本回合",
"ComposerHintLongDraft": "{shortcut}:外部编辑器",
"PendingSendingIntoTurnPrefix": " ↳ 正在发进本回合: ",
"PendingCouldNotSendIntoTurnPrefix": " ↳ 留待下一回合: ",
"PendingEditingFollowUpPrefix": " ↳ 正在编辑后续消息: ",
@@ -467,6 +477,9 @@
"CmdImportClaudeDescription": "生成可审阅的 Claude Code 设置与 MCP 服务器导入计划",
"CmdClearDescription": "清除对话历史",
"CmdColorDescription": "设置此对话输入框的颜色",
"CmdColorSet": "此对话输入框的颜色:{color}",
"CmdColorReset": "此对话输入框的颜色已重置为主题默认值",
"CmdColorUnknown": "未知颜色。可选:{colors},或 off。",
"CmdCompactDescription": "触发上下文压缩以释放空间",
"CmdPurgeDescription": "让 Agent 分析对话历史,精确保留有用信息并移除冗余内容",
"CmdConfigDescription": "查看与修改设置(原始键在高级详情)",
@@ -812,7 +825,7 @@
"KbSendDraft": "空闲时发送,忙碌时排队;输入框为空时立即发送下一条排队消息",
"KbSteerCurrentTurn": "调整当前回合(空闲时正常发送)",
"KbCloseMenu": "关闭菜单、取消请求、丢弃草稿或清空输入",
"KbCancelOrExit": "取消请求,或空闲时退出",
"KbCancelOrExit": "取消请求、清空草稿,或空闲时退出",
"KbShellControls": "将正在等待的 shell 移到 /jobs",
"KbExitEmpty": "输入框为空时退出",
"KbCommandPalette": "打开命令面板",
@@ -1280,6 +1293,23 @@
"ApprovalControlsHint": "Pg↑/↓ 回看 · {details} 详情 · Esc 停止",
"ApprovalOwnerHeader": "智能体:{agent} · {role}",
"ApprovalGoToAgent": "g 前往智能体",
"UserInputActionRequired": "需要你操作",
"UserInputQuestionProgress": "问题 {current}/{total}",
"UserInputOtherLabel": "其他",
"UserInputOtherDescription": "输入自定义回复",
"UserInputConfirmLabel": "确认所选",
"UserInputConfirmDescription": "提交已选 {count} 项",
"UserInputCustomPrompt": "自定义回复:",
"UserInputCustomPlaceholder": "(输入你的回复)",
"UserInputHintSubmit": "提交",
"UserInputHintBack": "返回",
"UserInputHintMove": "移动",
"UserInputHintToggle": "切换",
"UserInputHintSelectConfirm": "选择/确认",
"UserInputHintCancel": "取消",
"UserInputHintQuickPick": "快速选择",
"UserInputHintConfirm": "确认",
"UserInputHintDigit": "数字",
"ApprovalControlsHintChild": "Pg↑/↓ 回看 · {details} 详情 · Esc 隐藏",
"PendingApprovalInAgent": "{agent} 需要批准 — /agents",
"ApprovalTruncationHint": " … 已截断 · 按 {details} 查看完整内容",
@@ -1376,6 +1406,8 @@
"ToolReceiptLinesSingular": "1 行",
"ToolReceiptLinesPlural": "{count} 行",
"ToolDescriptionNote": "说明",
"ToolStatusWaitingForApproval": "等待审批",
"ToolStatusQueued": "已排队",
"CmdConstitutionAccountApplies": "账户资料宪章:生效(已登录;Engine 在每轮加载)",
"CmdConstitutionAccountSignedOut": "账户资料宪章:未登录",
"CmdConstitutionAccountCheckFailed": "账户资料宪章:账户检查失败({error})",
@@ -2339,7 +2371,7 @@
"ConfigHintManagedApprovalPolicy": "项目、配置文件、环境、托管配置或组织要求控制此值",
"ConfigHintManagedAllowShell": "项目、配置文件、环境或托管配置控制 shell 访问",
"ConfigHintAllowShell": "开启后在 Work 模式中提供 shell 工具;权限规则仍然适用",
"ConfigHintComposerMultilineMode": "关:Enter 发送,Shift+Enter 换行;开:Enter 换行,Shift+Enter 发送",
"ConfigHintComposerMultilineMode": "关:Enter 发送,Alt+Enter / Shift+Enter 换行;开:Enter 换行,Alt+Enter / Shift+Enter 发送",
"ConfigHintBooleanValues": "on/off, true/false, yes/no, 1/0",
"ConfigHintDensity": "compact | comfortable | spacious",
"ConfigHintInlineDiffs": "full | summary | off;精确更改仍在 Alt/Option+V 详情中",
@@ -2571,6 +2603,7 @@
"PlanHandoffKeepPlanning": "继续规划",
"PlanHandoffKeepPlanningDetail": "留在 Plan。不做任何更改。",
"PlanHandoffProceed": "按计划开始执行。",
"PlanHandoffTodoNote": "使用 todo_write 跟踪此计划:在列表中保留条目「{title}」,并在计划完成后将其标记为已完成。",
"PetAppearanceLegacy": "宠物仍在使用旧版本。更新 Codewhale 后重启宠物,即可选择鲸鱼样式和文字语言。",
"PluginOAuthBrowser": "按 Enter 打开浏览器登录,按 Esc 返回。不需要 API Key。",
"PluginOAuthReady": "{provider}:已登录,请选择模型。",
@@ -2581,5 +2614,24 @@
"ExecSandboxHelp": "本次运行的沙盒策略;danger-full-access 以外的任何 --sandbox(或配置中等效的 sandbox_mode)都会关闭信任模式。",
"ExecAfterHelp": "示例:\n codewhale exec \"解释这个函数\"\n codewhale exec --auto \"用 ls 列出 crates/ 的内容\"\n codewhale exec --auto --output-format stream-json \"修复失败的测试\"\n\n普通的 `codewhale exec` 仅获取一次模型回答:使用与其他运行相同的系统提示词,\n执行一轮 Engine,不提供工具。使用 `--auto` 可运行非交互式工具代理。\n只有指定 `--auto`、`--yolo`、`--allowed-tools` 或恢复会话时才会提供工具;\n`--max-turns`、`--disallowed-tools`、`--sandbox` 等限制以及输出格式\n都不会添加工具。因提供商输出上限而中断的回答会在同一轮中继续,\n除非通过 `--max-turns` 另行指定,否则最多进行 8 个模型步骤。\n--auto 默认使用 Full Access:无 shell 沙盒,开启信任模式。使用 --sandbox workspace-write 可保留 shell 沙盒和文件工具的工作区限制;已配置的更严格策略也会生效。重试被沙盒拒绝的操作需要 --allow-sandbox-elevation 或 --sandbox danger-full-access。\n提供工具的运行、stream-json 运行以及恢复的会话都会保存,可通过 codewhale sessions、exec --continue 和 codewhale receipts <id> 使用。仅回答的普通 text/--json 运行不会保存,除非上下文压缩需要保留恢复数据。\ndanger-full-access 以外的任何 --sandbox(或配置中等效的 sandbox_mode)都会关闭信任模式。\n",
"ExecPromptHelp": "发送给模型的提示词。若以 - 开头或包含作为文本的 exec 标志,请在提示词前加 --;使用 --prompt-file - 读取标准输入。",
"ExecFlagAfterPrompt": "{flag} 位于提示词之后,会被当作文本。请将标志放在提示词之前,或将作为文本的标志放在 -- 之后。"
"ExecFlagAfterPrompt": "{flag} 位于提示词之后,会被当作文本。请将标志放在提示词之前,或将作为文本的标志放在 -- 之后。",
"InstructionNoticeHeader": "常驻指令在本会话开始前已被截断或跳过:",
"InstructionNoticeFooter": "运行 /context report 查看按来源划分的完整明细。",
"InstructionNoticeMore": "…另有 {count} 项;完整列表见 /context report。",
"InstructionNoticeLine": "指令:{scope} {label}({origin}, {action}, {limit})",
"InstructionNoticeTrimmed": " 保留 {total_bytes} 字节中的 {kept_bytes} 字节,以及约 {total_tokens} 估算 token 中的约 {kept_tokens}",
"InstructionNoticeDroppedBytes": " 丢弃 {bytes} 字节",
"InstructionNoticeFallback": ";替代:{source}",
"InstructionNoticeStaticGuardOver": "指令:截断指令后,静态前缀仍超出静态前缀上限约 {tokens} 估算 token;按策略 `trim` 继续——请调高 static_prefix_guard_percent 或收窄工具范围。",
"InstructionNoticeStaticGuardTrimmed": "指令:{scope} 范围已截断约 {tokens} 估算 token,以使静态前缀保持在上限内",
"InstructionDetailDroppedBroadest": "作为最宽范围的片段被丢弃",
"InstructionDetailNoRoom": "{tokens} 估算 token 的范围预算已无空间容纳内容",
"InstructionDetailRulesExhausted": "{tokens} 估算 token 的项目范围预算已被指令用尽",
"InstructionDetailOverCeiling": "超过 {bytes} 字节上限",
"InstructionDetailUnreadable": "无法读取:{reason}",
"InstructionDetailAlreadyLoaded": "已从更宽范围加载",
"InstructionDetailRulesCap": "超过每个目录 {count} 个规则文件的上限",
"InstructionDetailSymlinkedRules": "拒绝符号链接的规则目录",
"InstructionBudgetRefused": "指令预算拒绝了此请求:{reason}",
"InstructionBudgetOverGuard": "静态前缀约 {measured} 估算 token 超出上限 {guard}"
}
+55 -3
View File
@@ -1,4 +1,13 @@
{
"ReasoningOnlyRetryBody": "僅有推理 {attempt}/{limit};沒有回答或工具呼叫;{action}",
"OutputLimitAnswerRetryBody": "達到輸出上限 {attempt}/{limit};沒有回答或工具呼叫;{action}",
"AnswerRetryActionBare": "重新請求回答",
"AnswerRetryActionNudge": "附加提示後重新請求回答",
"AnswerRetryCounter": "僅有推理/輸出上限",
"OrcaRouterOfferRecommended": "OrcaRouter · 推薦",
"OrcaRouterOfferAction": "在 /provider 或 /model 中查看 OrcaRouter 優惠",
"OrcaRouterOfferBody": "透過 Codewhale × OrcaRouter 優惠,儲值可額外獲得 30% 的 DeepSeek V4.1 Flash 額度。最新條款請查看優惠頁面。",
"OrcaRouterOfferDisclosure": "Codewhale 會從 OrcaRouter 獲得推薦佣金。",
"ShellWaitNoActive": "目前沒有正在等待的 shell 命令。",
"ShellWaitInteractive": "互動式命令無法在背景繼續執行。請取消目前回合,或等待命令完成。",
"ShellWaitUnavailable": "此工具無法在背景繼續執行。請取消目前回合,或等待它完成。",
@@ -185,6 +194,7 @@
"ComposerHintOfflineConnect": "↵ 先儲存 · /provider 連線",
"ComposerHintSendNow": "↵ 立即傳送",
"ComposerHintSendIntoTurn": "↵ 送進本回合",
"ComposerHintLongDraft": "{shortcut}:外部編輯器",
"PendingSendingIntoTurnPrefix": " ↳ 正在送進本回合: ",
"PendingCouldNotSendIntoTurnPrefix": " ↳ 留待下一回合: ",
"PendingEditingFollowUpPrefix": " ↳ 正在編輯後續訊息: ",
@@ -465,6 +475,9 @@
"CmdImportClaudeDescription": "產生可審閱的 Claude Code 設定與 MCP 伺服器匯入計畫",
"CmdClearDescription": "清除對話歷史",
"CmdColorDescription": "設定此對話輸入框的顏色",
"CmdColorSet": "此對話輸入框的顏色:{color}",
"CmdColorReset": "此對話輸入框的顏色已重設為主題預設值",
"CmdColorUnknown": "未知顏色。可選:{colors},或 off。",
"CmdCompactDescription": "觸發上下文壓縮以釋放空間",
"CmdPurgeDescription": "讓 Agent 分析對話歷史,精確保留有用資訊並移除冗餘內容",
"CmdConfigDescription": "檢視與修改設定(原始鍵在高級詳情)",
@@ -810,7 +823,7 @@
"KbSendDraft": "閒置時傳送,忙碌時排隊;輸入框為空時立即傳送下一條排隊訊息",
"KbSteerCurrentTurn": "調整目前回合(閒置時正常傳送)",
"KbCloseMenu": "關閉選單、取消請求、丟棄草稿或清空輸入",
"KbCancelOrExit": "取消請求,或閒置時退出",
"KbCancelOrExit": "取消請求、清空草稿,或閒置時退出",
"KbShellControls": "將正在等待的 shell 移到 /jobs",
"KbExitEmpty": "輸入框為空時退出",
"KbCommandPalette": "開啟指令面板",
@@ -1278,6 +1291,23 @@
"ApprovalControlsHint": "Pg↑/↓ 回看 · {details} 詳情 · Esc 停止",
"ApprovalOwnerHeader": "代理:{agent} · {role}",
"ApprovalGoToAgent": "g 前往代理",
"UserInputActionRequired": "需要你操作",
"UserInputQuestionProgress": "問題 {current}/{total}",
"UserInputOtherLabel": "其他",
"UserInputOtherDescription": "輸入自訂回覆",
"UserInputConfirmLabel": "確認所選",
"UserInputConfirmDescription": "提交已選 {count} 項",
"UserInputCustomPrompt": "自訂回覆:",
"UserInputCustomPlaceholder": "(輸入你的回覆)",
"UserInputHintSubmit": "提交",
"UserInputHintBack": "返回",
"UserInputHintMove": "移動",
"UserInputHintToggle": "切換",
"UserInputHintSelectConfirm": "選擇/確認",
"UserInputHintCancel": "取消",
"UserInputHintQuickPick": "快速選擇",
"UserInputHintConfirm": "確認",
"UserInputHintDigit": "數字",
"ApprovalControlsHintChild": "Pg↑/↓ 回看 · {details} 詳情 · Esc 隱藏",
"PendingApprovalInAgent": "{agent} 需要核准 — /agents",
"ApprovalTruncationHint": " … 已截斷 · 按 {details} 查看完整內容",
@@ -1374,6 +1404,8 @@
"ToolReceiptLinesSingular": "1 行",
"ToolReceiptLinesPlural": "{count} 行",
"ToolDescriptionNote": "說明",
"ToolStatusWaitingForApproval": "等待核准",
"ToolStatusQueued": "已排隊",
"CmdConstitutionAccountApplies": "帳戶資料憲章:生效(已登入;Engine 在每輪載入)",
"CmdConstitutionAccountSignedOut": "帳戶資料憲章:未登入",
"CmdConstitutionAccountCheckFailed": "帳戶資料憲章:帳戶檢查失敗({error})",
@@ -2337,7 +2369,7 @@
"ConfigHintManagedApprovalPolicy": "專案、設定檔、環境、受管設定或組織要求控制此值",
"ConfigHintManagedAllowShell": "專案、設定檔、環境或受管設定控制 shell 存取",
"ConfigHintAllowShell": "開啟後在 Work 模式中提供 shell 工具;權限規則仍然適用",
"ConfigHintComposerMultilineMode": "關:Enter 送出,Shift+Enter 換行;開:Enter 換行,Shift+Enter 送出",
"ConfigHintComposerMultilineMode": "關:Enter 送出,Alt+Enter / Shift+Enter 換行;開:Enter 換行,Alt+Enter / Shift+Enter 送出",
"ConfigHintBooleanValues": "on/off, true/false, yes/no, 1/0",
"ConfigHintDensity": "compact | comfortable | spacious",
"ConfigHintInlineDiffs": "full | summary | off;精確變更仍在 Alt/Option+V 詳情中",
@@ -2571,6 +2603,7 @@
"PlanHandoffKeepPlanning": "繼續規劃",
"PlanHandoffKeepPlanningDetail": "留在 Plan。不做任何變更。",
"PlanHandoffProceed": "依計畫開始執行。",
"PlanHandoffTodoNote": "使用 todo_write 追蹤此計畫:在清單中保留項目「{title}」,並在計畫完成後將其標記為已完成。",
"PetAppearanceLegacy": "寵物仍在使用舊版本。更新 Codewhale 後重新啟動寵物,即可選擇鯨魚樣式和文字語言。",
"PluginOAuthBrowser": "按 Enter 開啟瀏覽器登入,按 Esc 返回。不需要 API Key。",
"PluginOAuthReady": "{provider}:已登入,請選擇模型。",
@@ -2581,5 +2614,24 @@
"ExecSandboxHelp": "本次執行的沙盒原則;danger-full-access 以外的任何 --sandbox(或設定中等效的 sandbox_mode)都會關閉信任模式。",
"ExecAfterHelp": "範例:\n codewhale exec \"解釋這個函式\"\n codewhale exec --auto \"用 ls 列出 crates/ 的內容\"\n codewhale exec --auto --output-format stream-json \"修正失敗的測試\"\n\n一般的 `codewhale exec` 僅取得一次模型回覆:使用與其他執行相同的系統提示詞,\n執行一輪 Engine,不提供工具。使用 `--auto` 可執行非互動式工具代理。\n只有指定 `--auto`、`--yolo`、`--allowed-tools` 或還原工作階段時才會提供工具;\n`--max-turns`、`--disallowed-tools`、`--sandbox` 等限制及輸出格式\n都不會新增工具。因供應商輸出上限而中斷的回覆會在同一輪中繼續,\n除非透過 `--max-turns` 另行指定,否則最多進行 8 個模型步驟。\n--auto 預設使用 Full Access:無 shell 沙盒,開啟信任模式。使用 --sandbox workspace-write 可保留 shell 沙盒與檔案工具的工作區限制;已設定的更嚴格政策也會生效。重試遭沙盒拒絕的操作需要 --allow-sandbox-elevation 或 --sandbox danger-full-access。\n提供工具的執行、stream-json 執行及還原的工作階段都會儲存,可透過 codewhale sessions、exec --continue 與 codewhale receipts <id> 使用。僅回覆的一般 text/--json 執行不會儲存,除非內容壓縮需要保留復原資料。\ndanger-full-access 以外的任何 --sandbox(或設定中等效的 sandbox_mode)都會關閉信任模式。\n",
"ExecPromptHelp": "傳送給模型的提示詞。若以 - 開頭或包含作為文字的 exec 旗標,請在提示詞前加 --;使用 --prompt-file - 讀取標準輸入。",
"ExecFlagAfterPrompt": "{flag} 位於提示詞之後,會被視為文字。請將旗標放在提示詞之前,或將作為文字的旗標放在 -- 之後。"
"ExecFlagAfterPrompt": "{flag} 位於提示詞之後,會被視為文字。請將旗標放在提示詞之前,或將作為文字的旗標放在 -- 之後。",
"InstructionNoticeHeader": "常駐指令在此工作階段開始前已被截斷或略過:",
"InstructionNoticeFooter": "執行 /context report 以查看依來源劃分的完整明細。",
"InstructionNoticeMore": "…另有 {count} 項;完整清單見 /context report。",
"InstructionNoticeLine": "指令:{scope} {label}({origin}, {action}, {limit})",
"InstructionNoticeTrimmed": " 保留 {total_bytes} 位元組中的 {kept_bytes} 位元組,以及約 {total_tokens} 估算 token 中的約 {kept_tokens}",
"InstructionNoticeDroppedBytes": " 捨棄 {bytes} 位元組",
"InstructionNoticeFallback": ";替代:{source}",
"InstructionNoticeStaticGuardOver": "指令:截斷指令後,靜態前綴仍超出靜態前綴上限約 {tokens} 估算 token;依政策 `trim` 繼續——請調高 static_prefix_guard_percent 或縮小工具範圍。",
"InstructionNoticeStaticGuardTrimmed": "指令:{scope} 範圍已截斷約 {tokens} 估算 token,以使靜態前綴維持在上限內",
"InstructionDetailDroppedBroadest": "作為最廣範圍的片段被捨棄",
"InstructionDetailNoRoom": "{tokens} 估算 token 的範圍預算已無空間容納內容",
"InstructionDetailRulesExhausted": "{tokens} 估算 token 的專案範圍預算已被指令用盡",
"InstructionDetailOverCeiling": "超過 {bytes} 位元組上限",
"InstructionDetailUnreadable": "無法讀取:{reason}",
"InstructionDetailAlreadyLoaded": "已從更廣範圍載入",
"InstructionDetailRulesCap": "超過每個目錄 {count} 個規則檔案的上限",
"InstructionDetailSymlinkedRules": "拒絕符號連結的規則目錄",
"InstructionBudgetRefused": "指令預算拒絕了此請求:{reason}",
"InstructionBudgetOverGuard": "靜態前綴約 {measured} 估算 token 超出上限 {guard}"
}
+106 -2
View File
@@ -120,6 +120,17 @@ impl Locale {
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MessageId {
ReasoningOnlyRetryBody,
OutputLimitAnswerRetryBody,
AnswerRetryActionBare,
AnswerRetryActionNudge,
AnswerRetryCounter,
OrcaRouterOfferRecommended,
OrcaRouterOfferAction,
OrcaRouterOfferBody,
OrcaRouterOfferDisclosure,
MobileStreamReplayFailed,
MobileStreamCatchUpFailed,
MobileStreamRuntimeShutdown,
@@ -474,6 +485,9 @@ pub enum MessageId {
CmdImportClaudeDescription,
CmdClearDescription,
CmdColorDescription,
CmdColorSet,
CmdColorReset,
CmdColorUnknown,
CmdCompactDescription,
CmdPurgeDescription,
CmdConfigDescription,
@@ -1528,6 +1542,23 @@ pub enum MessageId {
ApprovalControlsHint,
ApprovalOwnerHeader,
ApprovalGoToAgent,
UserInputActionRequired,
UserInputQuestionProgress,
UserInputOtherLabel,
UserInputOtherDescription,
UserInputConfirmLabel,
UserInputConfirmDescription,
UserInputCustomPrompt,
UserInputCustomPlaceholder,
UserInputHintSubmit,
UserInputHintBack,
UserInputHintMove,
UserInputHintToggle,
UserInputHintSelectConfirm,
UserInputHintCancel,
UserInputHintQuickPick,
UserInputHintConfirm,
UserInputHintDigit,
ApprovalControlsHintChild,
PendingApprovalInAgent,
ApprovalTruncationHint,
@@ -1629,6 +1660,8 @@ pub enum MessageId {
ToolReceiptLinesSingular,
ToolReceiptLinesPlural,
ToolDescriptionNote,
ToolStatusWaitingForApproval,
ToolStatusQueued,
CmdConstitutionAccountApplies,
CmdConstitutionAccountSignedOut,
CmdConstitutionAccountCheckFailed,
@@ -2590,6 +2623,7 @@ pub enum MessageId {
ComposerHintOfflineConnect,
ComposerHintSendNow,
ComposerHintSendIntoTurn,
ComposerHintLongDraft,
PendingSendingIntoTurnPrefix,
PendingCouldNotSendIntoTurnPrefix,
PendingEditingFollowUpPrefix,
@@ -2816,6 +2850,26 @@ pub enum MessageId {
PluginOAuthReady,
PluginOAuthLocalLogout,
PlanHandoffProceed,
PlanHandoffTodoNote,
InstructionNoticeHeader,
InstructionNoticeFooter,
InstructionNoticeMore,
InstructionNoticeLine,
InstructionNoticeTrimmed,
InstructionNoticeDroppedBytes,
InstructionNoticeFallback,
InstructionNoticeStaticGuardOver,
InstructionNoticeStaticGuardTrimmed,
InstructionDetailDroppedBroadest,
InstructionDetailNoRoom,
InstructionDetailRulesExhausted,
InstructionDetailOverCeiling,
InstructionDetailUnreadable,
InstructionDetailAlreadyLoaded,
InstructionDetailRulesCap,
InstructionDetailSymlinkedRules,
InstructionBudgetRefused,
InstructionBudgetOverGuard,
ExecAutoHelp,
ExecSandboxHelp,
ExecAfterHelp,
@@ -2823,8 +2877,16 @@ pub enum MessageId {
ExecFlagAfterPrompt,
}
#[allow(dead_code)]
pub const ALL_MESSAGE_IDS: &[MessageId] = &[
MessageId::ReasoningOnlyRetryBody,
MessageId::OutputLimitAnswerRetryBody,
MessageId::AnswerRetryActionBare,
MessageId::AnswerRetryActionNudge,
MessageId::AnswerRetryCounter,
MessageId::OrcaRouterOfferRecommended,
MessageId::OrcaRouterOfferAction,
MessageId::OrcaRouterOfferBody,
MessageId::OrcaRouterOfferDisclosure,
MessageId::MobileStreamReplayFailed,
MessageId::MobileStreamCatchUpFailed,
MessageId::MobileStreamRuntimeShutdown,
@@ -3171,6 +3233,9 @@ pub const ALL_MESSAGE_IDS: &[MessageId] = &[
MessageId::CmdTurnInspectDescription,
MessageId::CmdClearDescription,
MessageId::CmdColorDescription,
MessageId::CmdColorSet,
MessageId::CmdColorReset,
MessageId::CmdColorUnknown,
MessageId::CmdCompactDescription,
MessageId::CmdPurgeDescription,
MessageId::CmdConfigDescription,
@@ -4192,6 +4257,23 @@ pub const ALL_MESSAGE_IDS: &[MessageId] = &[
MessageId::ApprovalControlsHint,
MessageId::ApprovalOwnerHeader,
MessageId::ApprovalGoToAgent,
MessageId::UserInputActionRequired,
MessageId::UserInputQuestionProgress,
MessageId::UserInputOtherLabel,
MessageId::UserInputOtherDescription,
MessageId::UserInputConfirmLabel,
MessageId::UserInputConfirmDescription,
MessageId::UserInputCustomPrompt,
MessageId::UserInputCustomPlaceholder,
MessageId::UserInputHintSubmit,
MessageId::UserInputHintBack,
MessageId::UserInputHintMove,
MessageId::UserInputHintToggle,
MessageId::UserInputHintSelectConfirm,
MessageId::UserInputHintCancel,
MessageId::UserInputHintQuickPick,
MessageId::UserInputHintConfirm,
MessageId::UserInputHintDigit,
MessageId::ApprovalControlsHintChild,
MessageId::PendingApprovalInAgent,
MessageId::ApprovalTruncationHint,
@@ -4288,6 +4370,8 @@ pub const ALL_MESSAGE_IDS: &[MessageId] = &[
MessageId::ToolReceiptLinesSingular,
MessageId::ToolReceiptLinesPlural,
MessageId::ToolDescriptionNote,
MessageId::ToolStatusWaitingForApproval,
MessageId::ToolStatusQueued,
MessageId::CmdConstitutionAccountApplies,
MessageId::CmdConstitutionAccountSignedOut,
MessageId::CmdConstitutionAccountCheckFailed,
@@ -5185,6 +5269,7 @@ pub const ALL_MESSAGE_IDS: &[MessageId] = &[
MessageId::ComposerHintOfflineConnect,
MessageId::ComposerHintSendNow,
MessageId::ComposerHintSendIntoTurn,
MessageId::ComposerHintLongDraft,
MessageId::PendingSendingIntoTurnPrefix,
MessageId::PendingCouldNotSendIntoTurnPrefix,
MessageId::PendingEditingFollowUpPrefix,
@@ -5404,6 +5489,26 @@ pub const ALL_MESSAGE_IDS: &[MessageId] = &[
MessageId::PluginOAuthReady,
MessageId::PluginOAuthLocalLogout,
MessageId::PlanHandoffProceed,
MessageId::PlanHandoffTodoNote,
MessageId::InstructionNoticeHeader,
MessageId::InstructionNoticeFooter,
MessageId::InstructionNoticeMore,
MessageId::InstructionNoticeLine,
MessageId::InstructionNoticeTrimmed,
MessageId::InstructionNoticeDroppedBytes,
MessageId::InstructionNoticeFallback,
MessageId::InstructionNoticeStaticGuardOver,
MessageId::InstructionNoticeStaticGuardTrimmed,
MessageId::InstructionDetailDroppedBroadest,
MessageId::InstructionDetailNoRoom,
MessageId::InstructionDetailRulesExhausted,
MessageId::InstructionDetailOverCeiling,
MessageId::InstructionDetailUnreadable,
MessageId::InstructionDetailAlreadyLoaded,
MessageId::InstructionDetailRulesCap,
MessageId::InstructionDetailSymlinkedRules,
MessageId::InstructionBudgetRefused,
MessageId::InstructionBudgetOverGuard,
MessageId::ExecAutoHelp,
MessageId::ExecSandboxHelp,
MessageId::ExecAfterHelp,
@@ -5585,7 +5690,6 @@ where
Locale::En
}
#[allow(dead_code)]
pub fn truncate_to_width(text: &str, max_width: usize) -> String {
if max_width == 0 {
return String::new();
+33
View File
@@ -0,0 +1,33 @@
[package]
name = "codewhale-lsp"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
repository.workspace = true
description = "Codewhale language server client and diagnostics"
[lints]
workspace = true
[features]
test-support = ["codewhale-runtime/test-support"]
[dependencies]
anyhow.workspace = true
async-trait.workspace = true
codewhale-config = { path = "../config", version = "0.10.2" }
codewhale-runtime = { path = "../runtime", version = "0.10.2" }
codewhale-util = { path = "../util", version = "0.10.2" }
reqwest.workspace = true
serde.workspace = true
serde_json.workspace = true
tempfile.workspace = true
tokio.workspace = true
tracing.workspace = true
[dev-dependencies]
codewhale-runtime = { path = "../runtime", version = "0.10.2", features = ["test-support"] }
[target.'cfg(unix)'.dependencies]
libc.workspace = true
@@ -39,7 +39,7 @@ use tokio::sync::{mpsc, oneshot};
use tokio::time::timeout;
use super::diagnostics::{Diagnostic, Severity};
use crate::utils::spawn_supervised;
use codewhale_util::utils::spawn_supervised;
const INITIALIZE_TIMEOUT: Duration = Duration::from_secs(5);
const MAX_LSP_HEADER_BYTES: usize = 8 * 1024;
@@ -692,7 +692,7 @@ fn parse_publish_diagnostics(value: &Value) -> Option<DiagnosticMessage> {
}
/// Encode an absolute filesystem path without following its links.
pub(crate) fn uri_from_path(path: &Path) -> String {
pub fn uri_from_path(path: &Path) -> String {
reqwest::Url::from_file_path(path)
.map(|url| url.to_string())
.unwrap_or_default()
@@ -715,7 +715,8 @@ pub(super) fn path_from_uri(uri: &str) -> Option<PathBuf> {
url.to_file_path().ok()
}
#[cfg(test)]
#[cfg(any(test, feature = "test-support"))]
#[cfg_attr(not(test), allow(dead_code, unused_imports))]
pub(super) mod tests {
use super::*;
@@ -968,7 +969,7 @@ pub(super) mod tests {
assert!(parse_publish_diagnostics(&payload).is_none());
}
#[cfg(unix)]
pub(crate) const STDIO_FIXTURE: &str = r#"
pub const STDIO_FIXTURE: &str = r#"
import json, os, select, sys, time
mode, pid_path = sys.argv[1:]
input_stream = os.fdopen(0, 'rb', buffering=0)
@@ -1128,7 +1129,7 @@ while True:
#[cfg(unix)]
#[tokio::test]
async fn language_server_does_not_inherit_parent_secret_env() {
use crate::test_support::{EnvVarGuard, lock_test_env};
use codewhale_runtime::test_support::{EnvVarGuard, lock_test_env};
let _env_lock = lock_test_env();
let _secret = EnvVarGuard::set("CODEWHALE_TEST_LSP_SECRET", "lsp-secret-value");
let root = tempfile::tempdir().unwrap();
File renamed without changes.
@@ -112,7 +112,7 @@ impl Default for LspConfig {
impl LspConfig {
/// Resolve `(command, args)` for `lang`. User-supplied overrides take
/// precedence over the built-in registry.
pub(crate) fn resolve_command(&self, lang: Language) -> Option<(String, Vec<String>)> {
pub fn resolve_command(&self, lang: Language) -> Option<(String, Vec<String>)> {
if let Some(parts) = self.servers.get(lang.as_key())
&& let Some((first, rest)) = parts.split_first()
{
@@ -153,32 +153,32 @@ pub struct LspManager {
/// best-effort post-edit hook, this preserves the distinction between an
/// honest empty result, a server/read error, and a timeout.
#[derive(Debug)]
pub(crate) struct LintReadResult {
pub(crate) file: PathBuf,
pub(crate) status: LintReadStatus,
pub(crate) items: Vec<Diagnostic>,
pub(crate) freshness: DiagnosticFreshness,
pub struct LintReadResult {
pub file: PathBuf,
pub status: LintReadStatus,
pub items: Vec<Diagnostic>,
pub freshness: DiagnosticFreshness,
/// Number of diagnostics after severity selection but before the
/// configured per-file cap was applied. Unknown when the request did not
/// complete successfully.
pub(crate) total_diagnostic_count: Option<usize>,
pub(crate) truncated: bool,
pub total_diagnostic_count: Option<usize>,
pub truncated: bool,
}
#[derive(Debug)]
pub(crate) enum LintReadStatus {
pub enum LintReadStatus {
Success,
Error(String),
Timeout { wait_ms: u64 },
}
#[derive(Debug, Default, serde::Serialize)]
pub(crate) struct DiagnosticFreshness {
pub(crate) source_revision: Option<String>,
pub(crate) document_version: Option<i64>,
pub(crate) diagnostic_version: Option<i64>,
pub struct DiagnosticFreshness {
pub source_revision: Option<String>,
pub document_version: Option<i64>,
pub diagnostic_version: Option<i64>,
/// Only an exact publication version match proves the text was checked.
pub(crate) freshness: Option<&'static str>,
pub freshness: Option<&'static str>,
}
struct DiagnosticPollSuccess {
@@ -238,7 +238,8 @@ impl LspManager {
// Match the existing workspace file serving ceiling. Decode only
// bounded UTF-8 bytes from the same no-follow workspace opener.
const MAX_DOCUMENT_BYTES: u64 = 16 * 1024 * 1024;
let file = crate::fleet::files::WorkspaceFile::open(&canonical_root, relative, false)?;
let file =
codewhale_util::files::WorkspaceFile::open(&canonical_root, relative, false)?;
let mut bytes = Vec::new();
file.open_file()?
.take(MAX_DOCUMENT_BYTES + 1)
@@ -258,7 +259,7 @@ impl LspManager {
/// Inject a fake transport for a language. Used by tests so we never
/// fork a real LSP server in CI.
#[cfg(test)]
#[cfg(any(test, feature = "test-support"))]
pub async fn install_test_transport(&self, lang: Language, transport: Arc<dyn LspTransport>) {
self.test_transports.lock().await.insert(lang, transport);
}
@@ -748,7 +749,7 @@ impl LspManager {
};
let relative = match target.strip_prefix(&self.workspace).or_else(|_| target.strip_prefix(&root)) {
Ok(relative) if !relative.as_os_str().is_empty()
&& relative.components().all(|component| matches!(component, std::path::Component::Normal(part) if !crate::snapshot::is_git_metadata_name(part))) => relative,
&& relative.components().all(|component| matches!(component, std::path::Component::Normal(part) if !codewhale_util::git_metadata::is_git_metadata_name(part))) => relative,
_ => { omitted += 1; continue; }
};
let Some(path) = semantic_relative_path(relative) else {
@@ -799,7 +800,7 @@ impl LspManager {
/// transport pool. Every attempted file gets an explicit outcome, and the
/// same configured severity policy as post-edit diagnostics is applied:
/// errors are always retained and warnings depend on `include_warnings`.
pub(crate) async fn diagnostics_for_paths(
pub async fn diagnostics_for_paths(
&self,
files: &[PathBuf],
) -> Result<Vec<LintReadResult>, String> {
@@ -906,7 +907,9 @@ fn semantic_relative_path(path: &Path) -> Option<String> {
let parts = path
.components()
.map(|component| match component {
std::path::Component::Normal(part) if !crate::snapshot::is_git_metadata_name(part) => {
std::path::Component::Normal(part)
if !codewhale_util::git_metadata::is_git_metadata_name(part) =>
{
part.to_str().filter(|part| !part.contains('\\'))
}
_ => None,
@@ -1021,28 +1024,29 @@ impl LspManager {
}
}
#[cfg(test)]
pub(crate) mod tests {
#[cfg(any(test, feature = "test-support"))]
#[cfg_attr(not(test), allow(dead_code, unused_imports))]
pub mod tests {
use super::*;
use async_trait::async_trait;
use std::sync::atomic::{AtomicUsize, Ordering};
/// Fake transport: returns a fixed list of diagnostics. Used by
/// integration tests so we never spawn a real LSP server in CI.
pub(crate) struct FakeTransport {
pub struct FakeTransport {
items: Vec<Diagnostic>,
calls: AtomicUsize,
}
impl FakeTransport {
pub(crate) fn new(items: Vec<Diagnostic>) -> Self {
pub fn new(items: Vec<Diagnostic>) -> Self {
Self {
items,
calls: AtomicUsize::new(0),
}
}
pub(crate) fn call_count(&self) -> usize {
pub fn call_count(&self) -> usize {
self.calls.load(Ordering::Relaxed)
}
}
@@ -1054,7 +1058,7 @@ pub(crate) mod tests {
_path: &Path,
_text: &str,
_wait: Duration,
) -> anyhow::Result<crate::lsp::client::DiagnosticPublication> {
) -> anyhow::Result<crate::client::DiagnosticPublication> {
self.calls.fetch_add(1, Ordering::Relaxed);
Ok(self.items.clone().into())
}
File renamed without changes.
+2
View File
@@ -17,5 +17,7 @@ workspace = true
[dependencies]
codewhale-config = { path = "../config", version = "0.10.2" }
codewhale-core = { path = "../core", version = "0.10.2" }
codewhale-sanitize = { path = "../sanitize", version = "0.10.2" }
serde.workspace = true
serde_json = { workspace = true, features = ["preserve_order"] }
thiserror.workspace = true
+28
View File
@@ -0,0 +1,28 @@
use std::collections::BTreeMap;
use serde::{Deserialize, Serialize};
/// What a decision model answered and what it cost. Probabilities are basis
/// points (0..=10000) so the receipt stays `Eq` and never re-renders floats.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct AutoRouteDecisionEvidence {
/// The tier the decision model chose (`fast` | `strong`).
pub choice: String,
pub probabilities_bp: BTreeMap<String, u16>,
pub confidence_bp: u16,
pub min_confidence_bp: u16,
/// `[auto] cost_saving` kept the fast tier because the strong tier's
/// probability was below the cost-saving floor.
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub cost_saving_kept_fast: bool,
/// The reasoning effort the decision applied, if any.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub thinking: Option<String>,
/// `usage.cost` exactly as the provider reported it (USD decimal).
#[serde(default, skip_serializing_if = "Option::is_none")]
pub provider_reported_cost_usd: Option<String>,
pub latency_ms: u64,
/// The dated model snapshot the provider echoed, when present.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub response_model: Option<String>,
}
+134
View File
@@ -0,0 +1,134 @@
use crate::{ContentBlock, Message};
/// Split a `data:<media-type>;base64,<payload>` URL.
///
/// Anthropic's Messages API models an image as a tagged `source` rather than a
/// URL, so the native route has to take the data URL back apart. Returns
/// `None` for `http(s)` URLs and for anything malformed, which the caller
/// renders as a remote source or a visible degradation respectively.
#[must_use]
pub fn parse_data_url(url: &str) -> Option<(&str, &str)> {
let rest = url.strip_prefix("data:")?;
let (header, payload) = rest.split_once(',')?;
let media_type = header.strip_suffix(";base64")?;
if media_type.is_empty() || payload.is_empty() {
return None;
}
Some((media_type, payload))
}
/// Whether a URL is one a provider can fetch for itself.
#[must_use]
pub fn is_remote_image_url(url: &str) -> bool {
url.starts_with("https://") || url.starts_with("http://")
}
pub const ELIDED_IMAGE_NOTE: &str = "[Image not attached to this request: this model accepts only a limited number of images per request, so the oldest are left out. It remains in the session; do not guess what it showed.]";
pub const ELIDED_TOOL_IMAGE_NOTE: &str = "[Tool image not attached to this request: this model accepts only a limited number of images per request, so the oldest are left out; do not guess what it showed. Take a fresh observation (for example a new screenshot) if you need the current state; do not repeat an action to recover its image.]";
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct ImageWindow {
pub total: usize,
pub elided: usize,
pub cap: usize,
}
fn block_carries_image(block: &ContentBlock) -> bool {
match block {
ContentBlock::ImageUrl { .. } => true,
ContentBlock::ToolResult { content_blocks, .. } => {
content_blocks.as_ref().is_some_and(|blocks| {
blocks.iter().any(|value| {
value.get("type").and_then(serde_json::Value::as_str) == Some("image")
})
})
}
_ => false,
}
}
#[must_use]
pub fn count_request_images(messages: &[Message]) -> usize {
messages
.iter()
.flat_map(|message| &message.content)
.filter(|block| block_carries_image(block))
.count()
}
#[must_use]
pub fn images_to_elide(total: usize, cap: usize) -> usize {
if total <= cap {
return 0;
}
let chunk = (cap / 4).max(1);
((total - cap).div_ceil(chunk) * chunk).min(total)
}
pub fn cap_request_images(messages: &mut [Message], cap: usize) -> ImageWindow {
let total = count_request_images(messages);
let mut window = cap_request_images_with_notes(
messages,
total - images_to_elide(total, cap),
ELIDED_IMAGE_NOTE,
ELIDED_TOOL_IMAGE_NOTE,
);
window.cap = cap;
window
}
pub fn cap_request_images_with_notes(
messages: &mut [Message],
cap: usize,
image_note: &str,
tool_note: &str,
) -> ImageWindow {
let total = count_request_images(messages);
let elided = total.saturating_sub(cap);
let mut remaining = elided;
for block in messages.iter_mut().flat_map(|message| &mut message.content) {
if remaining == 0 {
break;
}
if !block_carries_image(block) {
continue;
}
match block {
ContentBlock::ImageUrl { .. } => {
*block = ContentBlock::Text {
text: image_note.to_string(),
cache_control: None,
};
}
ContentBlock::ToolResult {
content,
content_blocks,
..
} => {
if let Some(blocks) = content_blocks.as_mut() {
blocks.retain(|value| {
value.get("type").and_then(serde_json::Value::as_str) != Some("image")
});
if blocks.is_empty() {
*content_blocks = None;
}
}
*content = format!("{content}\n{tool_note}");
}
_ => continue,
}
remaining -= 1;
}
ImageWindow { total, elided, cap }
}
#[must_use]
pub fn tool_result_text_with_omission(content: &str, omitted: usize) -> String {
if omitted == 0 {
return content.to_string();
}
format!(
"{content}\n[{omitted} tool-result image block(s) omitted: invalid, unsupported, oversized, or additional.]"
)
}
+8 -3
View File
@@ -2,6 +2,14 @@
use serde::{Deserialize, Serialize};
pub mod auto_route_evidence;
pub mod image_window;
pub mod reasoning_effort_tier;
pub mod schema_canonicalize;
pub mod schema_sanitize;
pub mod tool_projection;
pub mod turn_classify;
/// Context window used only for legacy DeepSeek model IDs that do not name a
/// newer V4 alias and do not carry an explicit `*k` suffix.
pub const LEGACY_DEEPSEEK_CONTEXT_WINDOW_TOKENS: u32 = 128_000;
@@ -484,7 +492,6 @@ where
Ok(Option::<String>::deserialize(deserializer)?.unwrap_or_default())
}
#[allow(dead_code)]
#[derive(Debug, Deserialize, Clone)]
#[serde(tag = "type")]
/// Streaming event types for SSE responses.
@@ -522,7 +529,6 @@ pub enum StreamEvent {
Error { error: serde_json::Value },
}
#[allow(dead_code)]
#[derive(Debug, Deserialize, Clone)]
#[serde(tag = "type")]
/// Content block types used in streaming starts.
@@ -590,7 +596,6 @@ pub enum Delta {
ReasoningStateDelta { state: OpaqueReasoningState },
}
#[allow(dead_code)]
#[derive(Debug, Deserialize, Clone)]
/// Delta payload for message-level updates.
pub struct MessageDelta {
View File
Whitespace-only changes.
@@ -0,0 +1,25 @@
use serde::{Deserialize, Serialize};
/// Canonical reasoning-effort tiers recorded as configuration facts. This is
/// deliberately an enum rather than free-form text so Work Graph activity can
/// never become a side channel for model reasoning.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ReasoningEffortTier {
Off,
Minimal,
Low,
Medium,
High,
#[serde(rename = "xhigh")]
XHigh,
Ultra,
Auto,
Max,
/// Thinking is enabled, but the provider route exposes no supported
/// effort tiers. This is an effective receipt, never a requested setting.
ThinkingEnabledGranularityUnavailable,
/// The configured route exposes no verified reasoning-control contract.
/// This is an effective receipt, never a requested setting.
Unavailable,
}
@@ -15,8 +15,6 @@ use std::collections::HashSet;
use serde_json::{Map, Value};
use codewhale_models::Tool;
/// Sanitize a JSON Schema in-place for DeepSeek strict-tool compatibility.
///
/// Applies a sequence of normalisations chosen to be semantics-preserving:
@@ -42,25 +40,6 @@ pub fn sanitize(schema: &mut Value) {
}
}
/// Prepare a complete active tool set for DeepSeek strict function-calling.
///
/// Each tool is evaluated independently: compatible schemas are sanitized and
/// marked strict, while incompatible schemas remain unchanged and non-strict.
/// Returns `true` only when every tool in the set can use strict mode.
pub fn prepare_tools_for_strict_mode(tools: &mut [Tool]) -> bool {
let mut all_strict = true;
for tool in tools {
if strict_schema_supported(&tool.input_schema) {
sanitize_for_strict(&mut tool.input_schema);
tool.strict = Some(true);
} else {
tool.strict = None;
all_strict = false;
}
}
all_strict
}
/// Sanitize a schema for DeepSeek strict function-calling.
///
/// This extends the general sanitizer with the official strict-mode object
@@ -123,7 +102,7 @@ pub fn sanitize_for_responses(schema: &mut Value) -> Option<String> {
combine_constraint_notes(constraint_note, dependent_note)
}
fn strict_schema_supported(schema: &Value) -> bool {
pub fn strict_schema_supported(schema: &Value) -> bool {
let mut normalized = schema.clone();
sanitize(&mut normalized);
!has_strict_incompatible_composition(&normalized, true)
@@ -472,20 +451,6 @@ mod tests {
use super::*;
use serde_json::json;
fn test_tool(name: &str, input_schema: Value) -> Tool {
Tool {
tool_type: None,
name: name.to_string(),
description: name.to_string(),
input_schema,
allowed_callers: None,
defer_loading: None,
input_examples: None,
strict: None,
cache_control: None,
}
}
#[test]
fn collapses_nullable_anyof() {
let mut schema = json!({
@@ -839,139 +804,6 @@ mod tests {
assert_eq!(score["maximum"], 5);
}
#[test]
fn strict_mode_applies_per_tool_in_mixed_catalog() {
let mut tools = vec![
test_tool(
"lookup",
json!({
"type": "object",
"properties": {
"query": {"type": "string"}
},
"required": []
}),
),
test_tool(
"either",
json!({
"type": "object",
"properties": {
"a": {"type": "string"},
"b": {"type": "string"}
},
"anyOf": [
{"required": ["a"]},
{"required": ["b"]}
]
}),
),
test_tool(
"nested",
json!({
"type": "object",
"properties": {
"value": {
"oneOf": [
{"type": "string"},
{"type": "integer"}
]
}
}
}),
),
];
assert!(!prepare_tools_for_strict_mode(&mut tools));
assert_eq!(tools[0].strict, Some(true));
assert_eq!(tools[0].input_schema["required"], json!(["query"]));
assert_eq!(tools[0].input_schema["additionalProperties"], false);
assert_eq!(tools[1].strict, None);
assert!(tools[1].input_schema.get("anyOf").is_some());
assert_eq!(tools[2].strict, None);
assert!(
tools[2].input_schema["properties"]["value"]
.get("oneOf")
.is_some()
);
}
#[test]
fn strict_mode_rejects_nested_unsupported_composition() {
let mut tools = vec![Tool {
tool_type: None,
name: "nested".to_string(),
description: "Nested oneOf".to_string(),
input_schema: json!({
"type": "object",
"properties": {
"value": {
"oneOf": [
{"type": "string"},
{"type": "integer"}
]
}
}
}),
allowed_callers: None,
defer_loading: None,
input_examples: None,
strict: None,
cache_control: None,
}];
assert!(!prepare_tools_for_strict_mode(&mut tools));
assert_eq!(tools[0].strict, None);
}
#[test]
fn strict_mode_leaves_dependent_schema_tools_non_strict() {
let schema = json!({
"type": "object",
"properties": {
"action": {"type": "string"},
"message": {"type": "string"}
},
"dependentSchemas": {
"action": {
"properties": {"message": {}},
"required": ["message"]
}
}
});
let mut tools = vec![test_tool("agent", schema.clone())];
assert!(!prepare_tools_for_strict_mode(&mut tools));
assert_eq!(tools[0].strict, None);
assert_eq!(tools[0].input_schema, schema);
}
#[test]
fn strict_mode_marks_compatible_tools_strict() {
let mut tools = vec![Tool {
tool_type: None,
name: "lookup".to_string(),
description: "Lookup".to_string(),
input_schema: json!({
"type": "object",
"properties": {
"query": {"type": "string"}
},
"required": []
}),
allowed_callers: None,
defer_loading: None,
input_examples: None,
strict: None,
cache_control: None,
}];
assert!(prepare_tools_for_strict_mode(&mut tools));
assert_eq!(tools[0].strict, Some(true));
assert_eq!(tools[0].input_schema["required"], json!(["query"]));
assert_eq!(tools[0].input_schema["additionalProperties"], false);
}
#[test]
fn responses_sanitize_removes_root_composition_from_apply_patch_shape() {
let mut schema = json!({
@@ -1084,30 +916,6 @@ mod tests {
assert_eq!(note, None);
}
#[test]
fn xai_sanitize_flattens_apply_patch_root_one_of() {
// The exact shape that produced the live 400:
// "apply_patch: tool parameter root must be an object type (root
// schema is an anyOf/oneOf union with a non-object branch)".
use crate::tools::spec::ToolSpec as _;
let mut schema = crate::tools::apply_patch::ApplyPatchTool.input_schema();
assert!(schema.get("oneOf").is_some(), "fixture must match the tool");
let note = sanitize_for_xai_parameters(&mut schema);
assert_eq!(schema["type"], "object");
assert!(schema.get("oneOf").is_none());
assert!(schema.get("anyOf").is_none());
assert!(schema["properties"].get("patch").is_some());
assert!(schema["properties"].get("changes").is_some());
assert_eq!(
note.as_deref(),
Some(
"Exactly one of these parameter groups must be provided: `changes` | `patch` | `replace`."
)
);
}
#[test]
fn responses_sanitize_plain_object_has_no_constraint_note() {
let mut schema = json!({
@@ -2157,8 +1965,6 @@ fn validate_mfjs_expanded_depth(
#[cfg(test)]
mod kimi_tests {
use super::*;
use crate::tools::apply_patch::ApplyPatchTool;
use crate::tools::spec::ToolSpec;
use serde_json::json;
#[test]
@@ -2424,27 +2230,6 @@ mod kimi_tests {
assert_eq!(schema["properties"]["name"]["type"], "string");
}
#[test]
fn kimi_parameters_flattens_actual_apply_patch_root_and_returns_constraint_note() {
let mut schema = ApplyPatchTool.input_schema();
let note = sanitize_for_kimi_parameters(&mut schema).unwrap();
assert_eq!(schema["type"], "object");
assert!(schema.get("oneOf").is_none());
assert!(schema.get("anyOf").is_none());
assert!(schema.get("allOf").is_none());
assert_eq!(schema["properties"]["patch"]["type"], "string");
assert_eq!(schema["properties"]["replace"]["type"], "array");
assert_eq!(schema["properties"]["changes"]["type"], "array");
assert_eq!(
note.as_deref(),
Some(
"Exactly one of these parameter groups must be provided: `changes` | `patch` | `replace`."
)
);
}
#[test]
fn kimi_parameters_preserves_nested_anyof_branches() {
let mut schema = json!({
+36
View File
@@ -0,0 +1,36 @@
pub const TOOL_PROJECTION_WARNING_MAX_NAMES: usize = 8;
pub const TOOL_PROJECTION_WARNING_MAX_NAME_CHARS: usize = 64;
/// Return a privacy-safe, display-bounded sample of omitted wire tool names.
///
/// MCP catalogs may contain thousands of tools and their names are supplied by
/// external servers. Keep the exact count separately, but never let one route
/// diagnostic flood a transcript, toast, or runtime record.
#[must_use]
pub fn bounded_tool_projection_warning_names(omitted_tool_names: &[String]) -> Vec<String> {
omitted_tool_names
.iter()
.take(TOOL_PROJECTION_WARNING_MAX_NAMES)
.map(|name| {
let cleaned: String = name
.chars()
.filter(|c| !c.is_control() && !codewhale_sanitize::sanitize::is_bidi_control(*c))
.collect();
let collapsed = cleaned.split_whitespace().collect::<Vec<_>>().join(" ");
let collapsed = if collapsed.is_empty() {
"<unnamed>".to_string()
} else {
collapsed
};
if collapsed.chars().count() <= TOOL_PROJECTION_WARNING_MAX_NAME_CHARS {
return collapsed;
}
let mut out: String = collapsed
.chars()
.take(TOOL_PROJECTION_WARNING_MAX_NAME_CHARS - 1)
.collect();
out.push('…');
out
})
.collect()
}
+208
View File
@@ -0,0 +1,208 @@
use crate::{ContentBlock, Message, Role};
/// Detection marker for a new-format checkpoint: the opening words of
/// [`SUMMARY_HEADER`]. A new checkpoint is recognised only when its first
/// block starts with this marker and its second block is the provenance
/// block, so a user message that merely quotes the phrase stays a user
/// message.
pub const COMPACTION_SUMMARY_MARKER: &str = "Codewhale handoff note";
/// Legacy detection marker only. Checkpoints written before the handoff note
/// opened with this sentence. Saved sessions that still carry it must be
/// recognised so the next pass replaces that checkpoint instead of stacking a
/// second one.
pub const LEGACY_V2_COMPACTION_SUMMARY_MARKER: &str =
"Another language model started to solve this problem";
pub const COMPACTION_CHECKPOINT_PROVENANCE: &str = "<!-- codewhale.compaction-checkpoint.v1 -->";
/// Request-time recognition: exactly the header text block plus the
/// provenance block. User text merely quoting a marker keeps its original
/// wire position. Checkpoints saved before the handoff note still pass, so
/// their position survives restore and the next pass replaces them.
pub fn is_wire_compaction_checkpoint_message(message: &Message) -> bool {
let [
ContentBlock::Text {
text,
cache_control: None,
},
ContentBlock::Text {
text: provenance,
cache_control: None,
},
] = message.content.as_slice()
else {
return false;
};
message.role == Role::User
&& (text.starts_with(COMPACTION_SUMMARY_MARKER)
|| text.starts_with(LEGACY_V2_COMPACTION_SUMMARY_MARKER))
&& provenance == COMPACTION_CHECKPOINT_PROVENANCE
}
pub const RESTORED_CHECKPOINT_TURN_META: &str = concat!(
"<turn_meta>\n",
"Input provenance: subagent_handoff (non-authoritative)\n",
"Restore projection: subagent_checkpoint_v1\n",
"</turn_meta>",
);
pub const RESTORED_COMPLETION_HEADER: &str = "[Codewhale restored sub-agent checkpoint]";
pub const RESTORED_COMPLETIONS_HEADER: &str = "[Codewhale restored sub-agent checkpoints]";
pub const RESTORED_RUNNING_HEADER: &str = "[Codewhale restored sub-agent runtime checkpoint]";
pub const RESTORED_TOPOLOGY_HEADER: &str = "[Codewhale restored Agent topology checkpoint]";
/// Return the user-safe display body for an already projected checkpoint.
/// The exact metadata marker keeps arbitrary user-authored text on the normal
/// conversation path.
pub fn restored_subagent_checkpoint_display(message: &Message) -> Option<&str> {
if message.role != "user" {
return None;
}
let [
ContentBlock::Text {
text,
cache_control: first_cache,
},
ContentBlock::Text {
text: turn_meta,
cache_control: meta_cache,
},
] = message.content.as_slice()
else {
return None;
};
if first_cache.is_some()
|| meta_cache.is_some()
|| turn_meta != RESTORED_CHECKPOINT_TURN_META
|| ![
RESTORED_COMPLETION_HEADER,
RESTORED_COMPLETIONS_HEADER,
RESTORED_RUNNING_HEADER,
RESTORED_TOPOLOGY_HEADER,
]
.iter()
.any(|header| text.starts_with(header))
{
return None;
}
Some(text)
}
/// Classification used when locating a user-authored turn in the session log.
///
/// Runtime and tool messages are skipped because their provider-compatible
/// `role = "user"` is not user authority. Unsupported user content is a real
/// turn boundary, however, so callers must not skip it and edit an older turn.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum UserTurnPromptKind {
/// Assistant messages, tool results, and runtime-owned control messages.
NotPrompt,
/// A genuine user turn containing editable text.
Editable,
/// A genuine user turn without editable text, such as an image-only turn.
Unsupported,
}
/// Classify `message` for edit-last-turn and admitted-display handling.
#[must_use]
pub fn classify_user_turn_prompt(message: &Message) -> UserTurnPromptKind {
if message.role != Role::User {
return UserTurnPromptKind::NotPrompt;
}
if message.content.iter().any(|block| {
matches!(
block,
ContentBlock::ToolResult { .. }
| ContentBlock::ToolSearchToolResult { .. }
| ContentBlock::CodeExecutionToolResult { .. }
)
}) {
return UserTurnPromptKind::NotPrompt;
}
if is_runtime_owned_user_message(message) || is_wire_compaction_checkpoint_message(message) {
return UserTurnPromptKind::NotPrompt;
}
let turn_metadata_index = turn_metadata_text(message).map(|(index, _)| index);
if message.content.iter().enumerate().any(|(index, block)| {
Some(index) != turn_metadata_index && matches!(block, ContentBlock::Text { .. })
}) {
UserTurnPromptKind::Editable
} else {
UserTurnPromptKind::Unsupported
}
}
/// True when a `role = "user"` message is runtime-owned rather than
/// user-authored. Runtime authority is accepted only from the engine-owned
/// structural `<turn_meta>` block, never from arbitrary user text that happens
/// to resemble a runtime envelope or metadata marker.
pub fn is_runtime_owned_user_message(message: &Message) -> bool {
restored_subagent_checkpoint_display(message).is_some()
|| has_non_authoritative_turn_provenance(message)
}
/// Return engine-owned metadata in either the current trailing shape or the
/// historical leading shape. Requiring a separate prompt block prevents a
/// user who submits `<turn_meta>…</turn_meta>` as ordinary text from minting
/// authority.
pub fn turn_metadata_text(message: &Message) -> Option<(usize, &str)> {
if message.content.len() < 2 {
return None;
}
if let Some(ContentBlock::Text {
text,
cache_control: None,
}) = message.content.last()
{
let trimmed = text.trim();
if is_complete_turn_metadata(trimmed) {
return Some((message.content.len() - 1, trimmed));
}
}
// Sessions written before the metadata-tail migration used
// `[turn_meta, prompt, ...]`. Match the same conservative legacy shape as
// the transcript renderer: a metadata envelope first and ordinary text
// last. A single user-authored metadata example is never hidden.
let ContentBlock::Text {
text,
cache_control: None,
} = message.content.first()?
else {
return None;
};
let trimmed = text.trim();
let trailing_text_is_ordinary = matches!(
message.content.last(),
Some(ContentBlock::Text { text, .. }) if !is_complete_turn_metadata(text.trim())
);
(is_complete_turn_metadata(trimmed) && trailing_text_is_ordinary).then_some((0, trimmed))
}
fn is_complete_turn_metadata(text: &str) -> bool {
text.starts_with("<turn_meta>") && text.ends_with("</turn_meta>")
}
/// Recognize both the current condensed provenance line and the legacy
/// provenance/authority pair. Any explicitly non-authoritative provenance is
/// runtime-owned; this stays correct as new provenance variants are added.
fn has_non_authoritative_turn_provenance(message: &Message) -> bool {
let Some((_, metadata)) = turn_metadata_text(message) else {
return false;
};
let mut has_provenance = false;
let mut condensed_non_authoritative = false;
let mut legacy_non_authoritative = false;
for line in metadata.lines().map(str::trim) {
if let Some(value) = line.strip_prefix("Input provenance: ") {
has_provenance = true;
condensed_non_authoritative |= value.ends_with(" (non-authoritative)");
}
legacy_non_authoritative |= line == "Input authority: non_authoritative";
}
condensed_non_authoritative || (has_provenance && legacy_non_authoritative)
}
+32
View File
@@ -0,0 +1,32 @@
[package]
name = "codewhale-net"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
repository.workspace = true
description = "Codewhale outbound network policy, guarded destinations and proxy resolution"
[lints]
workspace = true
[dependencies]
anyhow.workspace = true
chrono.workspace = true
codewhale-command-contract = { path = "../command-contract", version = "0.10.2" }
codewhale-config = { path = "../config", version = "0.10.2" }
codewhale-release = { path = "../release", version = "0.10.2" }
codewhale-runtime = { path = "../runtime", version = "0.10.2" }
codewhale-tools = { path = "../tools", version = "0.10.2" }
http = "1"
hyper-util = { version = "=0.1.21", features = ["client-proxy"] }
reqwest.workspace = true
serde.workspace = true
serde_json.workspace = true
thiserror.workspace = true
tokio.workspace = true
toml.workspace = true
tracing.workspace = true
[dev-dependencies]
tempfile.workspace = true
+630
View File
@@ -0,0 +1,630 @@
use crate::network_policy::NetworkPolicyDecider;
use codewhale_tools::ToolError;
use std::net::IpAddr;
use std::time::Duration;
/// Wall-clock bound for one pre-flight DNS resolution. The resolution runs
/// before the guarded request (and once per redirect), so a hung resolver
/// would otherwise stall the tool far beyond the documented request timeout
/// envelope of the calling fetch tool.
pub const DNS_PREFLIGHT_TIMEOUT: Duration = Duration::from_secs(10);
/// DNS pin returned when a hostname was resolved to a validated public IP.
/// Callers should pass this to `reqwest::ClientBuilder::resolve` so the
/// connection uses the pre-validated address instead of re-resolving.
pub type DnsPin = Option<(String, IpAddr)>;
/// Build the transport used after a destination has passed SSRF validation.
/// Ambient HTTP(S)/SOCKS proxies are deliberately disabled: a proxy would
/// receive the original hostname, resolve it again outside this process, and
/// bypass the validated DNS pin.
pub fn guarded_reqwest_client_builder() -> reqwest::ClientBuilder {
codewhale_release::tls::reqwest_client_builder().no_proxy()
}
/// Check if an IP address is loopback, private, link-local, cloud-metadata,
/// multicast, or reserved — all addresses that should not be reachable via
/// an LLM-initiated fetch request (SSRF prevention).
pub fn is_restricted_ip(ip: &IpAddr) -> bool {
match ip {
IpAddr::V4(v4) => {
v4.is_loopback()
|| v4.is_private()
|| v4.is_link_local()
|| v4.is_multicast()
|| v4.is_broadcast()
|| v4.is_unspecified()
// 100.64.0.0/10 — Carrier-grade NAT (CGNAT / shared address space)
|| matches!(v4.octets(), [100, 64..=127, ..])
// 169.254.169.254 — cloud metadata (AWS/GCP/Azure)
|| *ip == IpAddr::V4(std::net::Ipv4Addr::new(169, 254, 169, 254))
// 198.18.0.0/15 — IETF benchmark testing
|| matches!(v4.octets(), [198, 18..=19, ..])
// 240.0.0.0/4 — reserved (former Class E)
|| v4.octets()[0] >= 240
}
IpAddr::V6(v6) => {
// IPv4-mapped IPv6 addresses (::ffff:a.b.c.d) — unwrap and check as IPv4
// to prevent bypass via ::ffff:127.0.0.1 etc.
if v6.is_unspecified()
|| matches!(v6.octets(), [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff, ..])
{
return true;
}
if let Some(v4) = v6.to_ipv4_mapped() {
return is_restricted_ip(&IpAddr::V4(v4));
}
// A NAT64 gateway reaches the embedded IPv4 target, so the prefix
// must not hide a restricted address.
if let Some(v4) = nat64_ipv4(*v6)
&& is_restricted_ip(&IpAddr::V4(v4))
{
return true;
}
v6.is_loopback()
|| v6.is_multicast()
|| matches!(v6.segments(), [0xfc00..=0xfdff, ..]) // ULA fc00::/7
|| matches!(v6.segments(), [0xfe80..=0xfebf, ..]) // Link-local fe80::/10
|| matches!(v6.segments(), [0xfec0..=0xfeff, ..]) // Site-local fec0::/10
|| matches!(v6.segments(), [0x2002, ..]) // 6to4 2002::/16
|| matches!(v6.segments(), [0x2001, 0x0000, ..]) // Teredo 2001::/32
|| matches!(v6.segments(), [0, 0, 0, 0, 0, 0, ..]) // IPv4-compatible ::a.b.c.d
}
}
}
/// The IPv4 target of a well-known-prefix NAT64 address (`64:ff9b::/96`).
fn nat64_ipv4(v6: std::net::Ipv6Addr) -> Option<std::net::Ipv4Addr> {
let segments = v6.segments();
if segments[0] == 0x0064 && segments[1] == 0xff9b && segments[2..6].iter().all(|s| *s == 0) {
let octets = v6.octets();
return Some(std::net::Ipv4Addr::new(
octets[12], octets[13], octets[14], octets[15],
));
}
None
}
pub fn fetch_target_host(url: &reqwest::Url) -> Result<String, ToolError> {
if url.scheme() != "http" && url.scheme() != "https" {
return Err(ToolError::invalid_input(
"only http:// and https:// URLs are supported",
));
}
url.host_str()
.map(str::to_ascii_lowercase)
.ok_or_else(|| ToolError::invalid_input("URL must include a host"))
}
pub async fn validate_fetch_destination(
host: String,
decider: Option<&NetworkPolicyDecider>,
tool: &str,
resolve: bool,
) -> Result<DnsPin, ToolError> {
// SSRF protection: reject localhost names and private/link-local/loopback
// IP literals; when `resolve` is set, also resolve the hostname and reject
// restricted answers. Prevents LLM-prompted requests to cloud metadata
// (169.254.169.254), localhost services, and internal networks.
let bare = host.strip_suffix('.').unwrap_or(&host);
if bare == "localhost" || bare == "localhost.localdomain" || bare.ends_with(".localhost") {
return Err(ToolError::permission_denied(
"requests to localhost are not allowed",
));
}
// Normalize bracketed IPv6 literals before the literal-IP check so they
// route through the same restricted-IP policy as unbracketed forms
// (GHSA-88gh-2526-gfrr).
let ip_candidate = host
.strip_prefix('[')
.and_then(|s| s.strip_suffix(']'))
.unwrap_or(host.as_str());
if let Ok(ip) = ip_candidate.parse::<IpAddr>() {
if is_restricted_ip(&ip) {
return Err(ToolError::permission_denied(format!(
"IP {ip} is a restricted address (private/loopback/link-local)"
)));
}
return Ok(None);
}
if !resolve {
return Ok(None);
}
let addrs = tokio::time::timeout(
DNS_PREFLIGHT_TIMEOUT,
tokio::net::lookup_host((host.as_str(), 0u16)),
)
.await
.map_err(|_| {
ToolError::permission_denied(format!("timed out resolving host before {tool} request"))
})?
.map_err(|e| {
ToolError::permission_denied(format!("could not resolve host before {tool} request: {e}"))
})?;
let mut first_valid: Option<IpAddr> = None;
for addr in addrs {
validate_dns_resolved_ip(&host, &addr.ip(), decider, tool)?;
if first_valid.is_none() {
first_valid = Some(addr.ip());
}
}
let Some(validated_ip) = first_valid else {
return Err(ToolError::permission_denied(format!(
"host resolved to no addresses before {tool} request"
)));
};
Ok(Some((host, validated_ip)))
}
pub fn validate_dns_resolved_ip(
host: &str,
ip: &IpAddr,
decider: Option<&NetworkPolicyDecider>,
tool: &str,
) -> Result<(), ToolError> {
if !is_restricted_ip(ip) {
return Ok(());
}
// A fake-IP exception requires both an explicitly trusted hostname and an
// explicitly trusted placeholder CIDR. The CIDR parser admits only subnets
// inside 198.18.0.0/15, so real private/loopback/link-local/metadata/ULA
// addresses remain blocked even when the hostname is trusted.
if let Some(decider) = decider
&& decider.is_trusted_fakeip_addr(ip)
&& decider.trusts_proxy_fakeip_host(host)
{
decider.record_trusted_proxy_fakeip_allow(host, tool);
return Ok(());
}
Err(ToolError::permission_denied(format!(
"resolved IP {ip} is a restricted address (private/loopback/link-local)"
)))
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(not(windows))]
use std::io::{Read, Write};
#[cfg(not(windows))]
use std::net::{Ipv4Addr, SocketAddr, TcpListener};
#[cfg(not(windows))]
use std::process::Command;
#[cfg(not(windows))]
use std::sync::Arc;
#[cfg(not(windows))]
use std::sync::atomic::{AtomicBool, Ordering};
#[cfg(not(windows))]
use std::time::{Duration, Instant};
#[cfg(not(windows))]
#[derive(Clone, Copy, Debug)]
enum AmbientProxyKind {
Http,
HttpsConnect,
SocksRemoteDns,
}
#[cfg(not(windows))]
fn spawn_accept_probe(
stop: Arc<AtomicBool>,
respond_http: bool,
drain_http_headers: bool,
) -> (u16, std::thread::JoinHandle<bool>) {
let listener = TcpListener::bind((Ipv4Addr::LOCALHOST, 0)).expect("bind probe listener");
let port = listener.local_addr().expect("probe address").port();
listener
.set_nonblocking(true)
.expect("nonblocking probe listener");
let handle = std::thread::spawn(move || {
let deadline = Instant::now() + Duration::from_secs(5);
loop {
match listener.accept() {
Ok((mut stream, _)) => {
// Accepted sockets inherit nonblocking from the listener
// on several Unixes. Restore blocking mode before timed
// header reads so a not-yet-ready socket is not treated
// as a hard failure (WouldBlock / EAGAIN).
stream
.set_nonblocking(false)
.expect("blocking probe stream");
if drain_http_headers {
stream
.set_read_timeout(Some(Duration::from_secs(2)))
.expect("probe read timeout");
let mut request = Vec::new();
let mut chunk = [0_u8; 1024];
while !request.windows(4).any(|window| window == b"\r\n\r\n") {
let read = match stream.read(&mut chunk) {
Ok(n) => n,
Err(err)
if matches!(
err.kind(),
std::io::ErrorKind::WouldBlock
| std::io::ErrorKind::TimedOut
| std::io::ErrorKind::Interrupted
) =>
{
continue;
}
Err(err) => panic!("read probe request: {err}"),
};
if read == 0 {
break;
}
request.extend_from_slice(&chunk[..read]);
if Instant::now() >= deadline {
break;
}
}
}
if respond_http {
let _ = stream.write_all(
b"HTTP/1.1 200 OK\r\nContent-Length: 2\r\nConnection: close\r\n\r\nok",
);
}
return true;
}
Err(err) if err.kind() == std::io::ErrorKind::WouldBlock => {}
Err(err) => panic!("probe accept failed: {err}"),
}
if stop.load(Ordering::SeqCst) || Instant::now() >= deadline {
return false;
}
std::thread::sleep(Duration::from_millis(10));
}
});
(port, handle)
}
#[cfg(not(windows))]
fn run_ambient_proxy_probe(kind: AmbientProxyKind, guarded: bool) -> (bool, bool) {
let stop = Arc::new(AtomicBool::new(false));
let (target_port, target_handle) = spawn_accept_probe(
Arc::clone(&stop),
matches!(
kind,
AmbientProxyKind::Http | AmbientProxyKind::SocksRemoteDns
),
matches!(
kind,
AmbientProxyKind::Http | AmbientProxyKind::SocksRemoteDns
),
);
let (proxy_port, proxy_handle) = spawn_accept_probe(Arc::clone(&stop), true, false);
let mut command = Command::new(std::env::current_exe().expect("current test executable"));
command.args([
"--exact",
"guard::tests::guarded_transport_proxy_probe_child",
"--ignored",
"--nocapture",
]);
for key in [
"HTTP_PROXY",
"http_proxy",
"HTTPS_PROXY",
"https_proxy",
"ALL_PROXY",
"all_proxy",
"NO_PROXY",
"no_proxy",
"REQUEST_METHOD",
] {
command.env_remove(key);
}
command
.env("CODEWHALE_PROXY_PROBE_CHILD", "1")
.env(
"CODEWHALE_PROXY_PROBE_GUARDED",
if guarded { "1" } else { "0" },
)
.env("CODEWHALE_PROXY_PROBE_TARGET_PORT", target_port.to_string());
match kind {
AmbientProxyKind::Http => {
let proxy = format!("http://127.0.0.1:{proxy_port}");
command
.env("CODEWHALE_PROXY_PROBE_SCHEME", "http")
.env("HTTP_PROXY", &proxy)
.env("http_proxy", proxy);
}
AmbientProxyKind::HttpsConnect => {
let proxy = format!("http://127.0.0.1:{proxy_port}");
command
.env("CODEWHALE_PROXY_PROBE_SCHEME", "https")
.env("HTTPS_PROXY", &proxy)
.env("https_proxy", proxy);
}
AmbientProxyKind::SocksRemoteDns => {
let proxy = format!("socks5h://127.0.0.1:{proxy_port}");
command
.env("CODEWHALE_PROXY_PROBE_SCHEME", "http")
.env("ALL_PROXY", &proxy)
.env("all_proxy", proxy);
}
}
let output = command.output().expect("run proxy probe child");
stop.store(true, Ordering::SeqCst);
let target_hit = target_handle.join().expect("join target probe");
let proxy_hit = proxy_handle.join().expect("join proxy probe");
assert!(
output.status.success(),
"proxy probe child failed for {kind:?} guarded={guarded}:\nstdout:\n{}\nstderr:\n{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr),
);
(target_hit, proxy_hit)
}
#[cfg(not(windows))]
#[tokio::test]
#[ignore = "subprocess helper for ambient proxy regression test"]
async fn guarded_transport_proxy_probe_child() {
if std::env::var_os("CODEWHALE_PROXY_PROBE_CHILD").is_none() {
return;
}
let guarded = std::env::var("CODEWHALE_PROXY_PROBE_GUARDED").as_deref() == Ok("1");
let scheme = std::env::var("CODEWHALE_PROXY_PROBE_SCHEME").expect("probe scheme");
let target_port = std::env::var("CODEWHALE_PROXY_PROBE_TARGET_PORT")
.expect("probe target port")
.parse::<u16>()
.expect("numeric target port");
let host = "guarded-proxy-probe.example.invalid";
let builder = if guarded {
guarded_reqwest_client_builder()
} else {
codewhale_release::tls::reqwest_client_builder()
};
let client = builder
.timeout(Duration::from_secs(2))
.resolve(
host,
SocketAddr::new(Ipv4Addr::LOCALHOST.into(), target_port),
)
.build()
.expect("build proxy probe client");
let result = client
.get(format!("{scheme}://{host}:{target_port}/"))
.send()
.await;
if guarded && scheme == "http" {
assert!(
result.is_ok(),
"guarded HTTP target should answer: {result:?}"
);
}
}
#[cfg(not(windows))]
#[test]
fn guarded_transport_bypasses_ambient_http_https_and_remote_dns_socks_proxies() {
for kind in [
AmbientProxyKind::Http,
AmbientProxyKind::HttpsConnect,
AmbientProxyKind::SocksRemoteDns,
] {
let (unguarded_target, unguarded_proxy) = run_ambient_proxy_probe(kind, false);
assert!(
unguarded_proxy && !unguarded_target,
"control client must demonstrate ambient {kind:?} proxy interception"
);
let (guarded_target, guarded_proxy) = run_ambient_proxy_probe(kind, true);
assert!(
guarded_target && !guarded_proxy,
"guarded client must preserve its DNS pin and bypass ambient {kind:?} proxy"
);
}
}
#[test]
fn rejects_private_localhost_literal() {
assert!(is_restricted_ip(&"127.0.0.1".parse().unwrap()));
assert!(is_restricted_ip(&"::1".parse().unwrap()));
}
#[test]
fn rejects_private_rfc1918() {
assert!(is_restricted_ip(&"10.0.0.1".parse().unwrap()));
assert!(is_restricted_ip(&"172.16.0.1".parse().unwrap()));
assert!(is_restricted_ip(&"192.168.1.1".parse().unwrap()));
}
#[test]
fn rejects_cloud_metadata() {
assert!(is_restricted_ip(&"169.254.169.254".parse().unwrap()));
}
#[test]
fn rejects_link_local() {
assert!(is_restricted_ip(&"169.254.1.1".parse().unwrap()));
}
#[test]
fn rejects_cgnat() {
assert!(is_restricted_ip(&"100.64.0.1".parse().unwrap()));
assert!(!is_restricted_ip(&"100.63.0.1".parse().unwrap()));
assert!(!is_restricted_ip(&"100.128.0.1".parse().unwrap()));
}
#[test]
fn rejects_ipv6_ula() {
assert!(is_restricted_ip(&"fc00::1".parse().unwrap()));
assert!(is_restricted_ip(&"fd12:3456::1".parse().unwrap()));
}
#[test]
fn rejects_ipv4_mapped_ipv6() {
// ::ffff:127.0.0.1 — IPv4-mapped IPv6 loopback bypass
assert!(is_restricted_ip(&"::ffff:127.0.0.1".parse().unwrap()));
assert!(is_restricted_ip(&"::ffff:10.0.0.1".parse().unwrap()));
assert!(is_restricted_ip(&"::ffff:169.254.169.254".parse().unwrap()));
assert!(is_restricted_ip(&"::ffff:192.168.1.1".parse().unwrap()));
// :: (unspecified)
assert!(is_restricted_ip(&"::".parse().unwrap()));
}
#[test]
fn allows_public_ips() {
assert!(!is_restricted_ip(&"8.8.8.8".parse().unwrap()));
assert!(!is_restricted_ip(&"1.1.1.1".parse().unwrap()));
assert!(!is_restricted_ip(&"93.184.216.34".parse().unwrap()));
assert!(!is_restricted_ip(&"2606:4700::1".parse().unwrap()));
}
#[test]
fn restricted_dns_result_is_denied_without_proxy_opt_in() {
let ip = "198.18.0.1".parse().unwrap();
let err = validate_dns_resolved_ip("github.com", &ip, None, "fetch_url")
.expect_err("fake-IP DNS result must be denied by default");
assert!(format!("{err}").contains("resolved IP 198.18.0.1 is a restricted address"));
}
#[test]
fn proxy_host_and_fakeip_cidr_allow_matching_placeholder() {
use crate::network_policy::{Decision, NetworkPolicy, NetworkPolicyDecider};
let policy = NetworkPolicy {
default: Decision::Allow.into(),
allow: Vec::new(),
deny: Vec::new(),
proxy: vec!["github.com".to_string()],
proxy_fake_ip_cidrs: vec!["198.18.0.0/15".to_string()],
audit: false,
};
let decider = NetworkPolicyDecider::new(policy, None);
let ip = "198.18.0.1".parse().unwrap();
validate_dns_resolved_ip("github.com", &ip, Some(&decider), "fetch_url")
.expect("matching host and fake-IP CIDR should allow the placeholder");
}
#[test]
fn proxy_host_without_fakeip_cidr_does_not_allow_restricted_dns() {
use crate::network_policy::{Decision, NetworkPolicy, NetworkPolicyDecider};
let policy = NetworkPolicy {
default: Decision::Allow.into(),
allow: Vec::new(),
deny: Vec::new(),
proxy: vec!["github.com".to_string()],
proxy_fake_ip_cidrs: Vec::new(),
audit: false,
};
let decider = NetworkPolicyDecider::new(policy, None);
let ip = "198.18.0.1".parse().unwrap();
validate_dns_resolved_ip("github.com", &ip, Some(&decider), "fetch_url")
.expect_err("hostname trust alone must not allow a restricted address");
}
#[test]
fn fakeip_cidr_without_proxy_host_does_not_allow_restricted_dns() {
use crate::network_policy::{Decision, NetworkPolicy, NetworkPolicyDecider};
let policy = NetworkPolicy {
default: Decision::Allow.into(),
allow: Vec::new(),
deny: Vec::new(),
proxy: Vec::new(),
proxy_fake_ip_cidrs: vec!["198.18.0.0/15".to_string()],
audit: false,
};
let decider = NetworkPolicyDecider::new(policy, None);
let ip = "198.18.0.1".parse().unwrap();
validate_dns_resolved_ip("github.com", &ip, Some(&decider), "fetch_url")
.expect_err("fake-IP CIDR alone must not allow an untrusted hostname");
}
#[test]
fn proxy_host_never_exempts_real_private_or_local_addresses() {
use crate::network_policy::{Decision, NetworkPolicy, NetworkPolicyDecider};
let policy = NetworkPolicy {
default: Decision::Allow.into(),
allow: Vec::new(),
deny: Vec::new(),
proxy: vec!["github.com".to_string()],
proxy_fake_ip_cidrs: vec![
"198.18.0.0/15".to_string(),
"127.0.0.0/8".to_string(),
"10.0.0.0/8".to_string(),
"169.254.0.0/16".to_string(),
],
audit: false,
};
let decider = NetworkPolicyDecider::new(policy, None);
for ip in [
"127.0.0.1",
"10.0.0.1",
"192.168.1.1",
"169.254.169.254",
"fc00::1",
] {
let ip = ip.parse().unwrap();
assert!(
validate_dns_resolved_ip("github.com", &ip, Some(&decider), "fetch_url").is_err(),
"{ip} must remain restricted"
);
}
}
#[test]
fn proxy_opt_in_does_not_allow_unlisted_host() {
use crate::network_policy::{Decision, NetworkPolicy, NetworkPolicyDecider};
let policy = NetworkPolicy {
default: Decision::Allow.into(),
allow: Vec::new(),
deny: Vec::new(),
proxy: vec!["github.com".to_string()],
proxy_fake_ip_cidrs: vec!["198.18.0.0/15".to_string()],
audit: false,
};
let decider = NetworkPolicyDecider::new(policy, None);
let ip = "198.18.0.1".parse().unwrap();
let err = validate_dns_resolved_ip("example.com", &ip, Some(&decider), "fetch_url")
.expect_err("proxy opt-in must be scoped to configured hosts");
assert!(format!("{err}").contains("resolved IP 198.18.0.1 is a restricted address"));
}
#[test]
fn proxy_dns_allow_is_audited() {
use crate::network_policy::{
Decision, NetworkAuditor, NetworkPolicy, NetworkPolicyDecider,
};
use tempfile::tempdir;
let dir = tempdir().expect("tempdir");
let auditor = NetworkAuditor::new(dir.path().join("audit.log"), true);
let policy = NetworkPolicy {
default: Decision::Allow.into(),
allow: Vec::new(),
deny: Vec::new(),
proxy: vec!["github.com".to_string()],
proxy_fake_ip_cidrs: vec!["198.18.0.0/15".to_string()],
audit: true,
};
let decider = NetworkPolicyDecider::new(policy, Some(auditor));
let ip = "198.18.0.1".parse().unwrap();
validate_dns_resolved_ip("github.com", &ip, Some(&decider), "fetch_url")
.expect("proxy DNS allow");
let body = std::fs::read_to_string(dir.path().join("audit.log")).expect("audit log");
assert!(body.contains("github.com"));
assert!(body.contains("TrustedProxyFakeIp-Allow"));
}
}
@@ -26,7 +26,7 @@ use std::path::{Path, PathBuf};
/// Maximum size accepted for any single Claude source file. Generous for
/// real `~/.claude.json` files with many projects; bounded so a pathological
/// file cannot exhaust memory. Matches the bundle reader's order of magnitude.
pub(crate) const MAX_SOURCE_BYTES: u64 = 5 * 1024 * 1024;
pub const MAX_SOURCE_BYTES: u64 = 5 * 1024 * 1024;
/// Env keys safe to propose for the portable bundle (lowercase). Deliberately
/// a short allowlist: timeouts, output caps, and editor/telemetry choices.
@@ -66,7 +66,7 @@ fn read_bounded_json(path: &Path) -> Result<Value, CommandSourceStatus> {
}
/// Read the Claude sources under `home` (usually `~`).
pub(crate) fn read_sources(
pub fn read_sources(
home: &Path,
) -> (
Vec<(PathBuf, CommandSourceStatus)>,
@@ -109,7 +109,7 @@ fn safe_env_value(value: &Value) -> Option<String> {
/// Build the plan from already-read sources plus MCP candidates discovered by
/// the existing external-import machinery (so server parsing stays single-sourced).
pub(crate) fn build_plan(
pub fn build_plan(
sources: Vec<(PathBuf, CommandSourceStatus)>,
claude: Option<Value>,
settings: Option<Value>,
+4
View File
@@ -0,0 +1,4 @@
pub mod guard;
pub mod import_claude;
pub mod network_policy;
pub mod proxy;
@@ -1,8 +1,3 @@
// Several public helpers in this module are exposed for future slash-command
// wiring (`/network allow <host>`, `/network deny <host>`) and for the
// approval-modal hook that v0.7.x adds incrementally. Dead-code warnings
// would otherwise be noisy until those call sites land.
#![allow(dead_code)]
// Audit-write failure must route through `tracing::*`, not raw stderr —
// see `runtime_log` for the scroll-demon rationale.
#![deny(clippy::print_stdout)]
@@ -443,7 +438,7 @@ impl NetworkAuditor {
))
}
/// Append one line. Best-effort: errors are logged via `eprintln!` but
/// Append one line. Best-effort: errors are logged via `tracing` but
/// never bubble back to the caller.
pub fn record(&self, host: &str, tool: &str, decision_label: &str) {
if !self.enabled {
+190
View File
@@ -0,0 +1,190 @@
use anyhow::Result;
/// Redact the userinfo segment (`username[:password]@…` portion) from
/// a proxy URL so it can be safely included in `tracing::warn!` output
/// without leaking the
/// password into the on-disk log. URLs without userinfo are returned
/// unchanged. Garbage input (no `://` scheme separator) is also returned
/// unchanged — the malformed-URL warning path is the only caller, so an
/// unparseable input is already the failure case.
fn redact_proxy_userinfo(proxy_url: &str) -> String {
let Some(scheme_end) = proxy_url.find("://") else {
return proxy_url.to_string();
};
let after_scheme = scheme_end + 3;
// The userinfo segment ends at the next `@`, but only if that `@`
// comes before the next `/`, `?`, or `#` (otherwise the `@` is in a
// path / query and the URL has no userinfo at all).
let rest = &proxy_url[after_scheme..];
let at_idx = rest.find('@');
let path_idx = rest.find(['/', '?', '#']);
let userinfo_end = match (at_idx, path_idx) {
(Some(a), Some(p)) if a < p => Some(a),
(Some(a), None) => Some(a),
_ => None,
};
if let Some(end) = userinfo_end {
let mut out = String::with_capacity(proxy_url.len());
out.push_str(&proxy_url[..after_scheme]);
out.push_str("***@");
out.push_str(&rest[end + 1..]);
out
} else {
proxy_url.to_string()
}
}
/// Resolve the operator's proxy route for this exact request using the same
/// matcher as reqwest. A NO_PROXY match returns None: direct requests must keep
/// their public DNS validation and pins. Model and reviewed-plugin requests
/// return before even reading proxy credentials.
pub fn configured_mcp_proxy<F>(
url: &reqwest::Url,
disallow_ambient_proxy: bool,
mut read_environment: F,
) -> Result<Option<reqwest::Proxy>>
where
F: FnMut(&str) -> std::result::Result<String, std::env::VarError>,
{
if disallow_ambient_proxy {
return Ok(None);
}
let proxy_url = ["HTTPS_PROXY", "https_proxy", "HTTP_PROXY", "http_proxy"]
.into_iter()
.find_map(|key| {
read_environment(key)
.ok()
.filter(|value| !value.trim().is_empty())
});
let Some(proxy_url) = proxy_url else {
return Ok(None);
};
// Normalize userinfo and Unicode with the URL parser before passing the
// URL to reqwest's own underlying matcher. Keep its missing-scheme support.
let normalized = reqwest::Url::parse(&proxy_url)
.ok()
.filter(|url| url.has_host())
.or_else(|| reqwest::Url::parse(&format!("http://{proxy_url}")).ok());
let Some(normalized) = normalized else {
tracing::warn!(target: "mcp", proxy = %redact_proxy_userinfo(&proxy_url), "ignoring malformed HTTP(S)_PROXY URL");
return Ok(None);
};
let no_proxy = read_environment("NO_PROXY")
.or_else(|_| read_environment("no_proxy"))
.unwrap_or_default()
.replace(char::is_whitespace, ",");
if no_proxy.split(',').any(|entry| entry == "*") {
return Ok(None);
}
let matcher = hyper_util::client::proxy::matcher::Matcher::builder()
.all(normalized.as_str())
.no(no_proxy)
.build();
let destination: http::Uri = url.as_str().parse()?;
let Some(route) = matcher.intercept(&destination) else {
return Ok(None);
};
// Build the actual proxy from the matched route itself so the decision
// that grants delegated DNS authority cannot diverge from the transport.
let route_uri = route.uri().to_string();
let route_uri = match route.uri().scheme_str() {
Some("socks5") => route_uri.replacen("socks5://", "socks5h://", 1),
Some("socks4") => route_uri.replacen("socks4://", "socks4a://", 1),
_ => route_uri,
};
let mut proxy = reqwest::Proxy::all(route_uri)?;
if let Some(auth) = route.basic_auth() {
proxy = proxy.custom_http_auth(auth.clone());
}
if let Some((user, password)) = route.raw_auth() {
proxy = proxy.basic_auth(user, password);
}
Ok(Some(proxy))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn reviewed_plugin_remote_proxy_policy_never_reads_ambient_environment() {
let reads = std::cell::Cell::new(0_u32);
let proxy = configured_mcp_proxy(
&reqwest::Url::parse("https://example.com/mcp").unwrap(),
true,
|_| {
reads.set(reads.get() + 1);
Ok("http://127.0.0.1:9999".to_string())
},
);
assert_eq!(
reads.get(),
0,
"reviewed remotes must not read proxy values"
);
assert!(proxy.unwrap().is_none());
}
#[test]
fn user_authored_mcp_proxy_policy_keeps_environment_support() {
let requested = std::cell::RefCell::new(Vec::new());
let proxy = configured_mcp_proxy(
&reqwest::Url::parse("https://example.com/mcp").unwrap(),
false,
|name| {
requested.borrow_mut().push(name.to_string());
match name {
"HTTPS_PROXY" => Ok("http://127.0.0.1:8080".to_string()),
_ => Err(std::env::VarError::NotPresent),
}
},
);
assert_eq!(
requested.into_inner(),
vec![
"HTTPS_PROXY".to_string(),
"NO_PROXY".to_string(),
"no_proxy".to_string(),
]
);
assert!(proxy.unwrap().is_some());
}
#[test]
fn redact_proxy_userinfo_strips_password() {
// Corporate-style proxy URL with embedded creds — the
// password must never reach the on-disk log file. URL strings
// are assembled from placeholder constants via `format!` so the
// literal source never contains a scheme-prefixed username +
// password pair (colon-separated, `@`-terminated) that
// GitGuardian's "Basic Auth String" detector would flag as a
// committed credential.
let (placeholder_user, placeholder_pass) = ("PLACEHOLDER_USER", "PLACEHOLDER_PASS");
let with_creds = format!("http://{placeholder_user}:{placeholder_pass}@proxy.example/");
let redacted = redact_proxy_userinfo(&with_creds);
assert_eq!(redacted, "http://***@proxy.example/");
assert!(!redacted.contains(placeholder_pass));
assert!(!redacted.contains(placeholder_user));
// User only (no password) — still redacted.
let with_user_only = format!("https://{placeholder_user}@proxy.example:8080");
let redacted = redact_proxy_userinfo(&with_user_only);
assert_eq!(redacted, "https://***@proxy.example:8080");
// No userinfo segment — pass through.
let redacted = redact_proxy_userinfo("http://proxy.example:3128/");
assert_eq!(redacted, "http://proxy.example:3128/");
// `@` appears only in the path, not as userinfo separator —
// must not be mistaken for credentials.
let redacted = redact_proxy_userinfo("http://proxy.example/path@thing");
assert_eq!(redacted, "http://proxy.example/path@thing");
// Garbage input (no `://`) returned unchanged — the
// surrounding warning log is the only caller and is already
// handling the malformed-URL case.
assert_eq!(redact_proxy_userinfo("not-a-url"), "not-a-url");
}
}
+29
View File
@@ -0,0 +1,29 @@
[package]
name = "codewhale-notify"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
repository.workspace = true
description = "Shared Codewhale notification policy and local audio dispatch"
[lints]
workspace = true
[features]
test-support = []
test-thread-scoped-state = ["test-support"]
[dependencies]
codewhale-config = { path = "../config", version = "0.10.2" }
codewhale-secrets = { path = "../secrets", version = "0.10.2" }
regex = "1.11"
tempfile.workspace = true
tracing.workspace = true
[dev-dependencies]
codewhale-runtime = { path = "../runtime", version = "0.10.2", features = ["test-support"] }
[target.'cfg(windows)'.dependencies]
dirs.workspace = true
windows = { version = "0.62", features = ["Win32_Media_Audio", "Win32_System_Diagnostics_Debug", "Win32_UI_WindowsAndMessaging"] }
@@ -27,7 +27,7 @@ pub fn emit_terminal(cue: &SoundCue, out: &mut dyn Write) -> AudioOutcome {
}
}
pub const WHALE_WAV: &[u8] = include_bytes!("../../assets/audio/codewhale-whale-call.wav");
pub const WHALE_WAV: &[u8] = include_bytes!("../assets/audio/codewhale-whale-call.wav");
/// Resolve an external system helper (the audio players `aplay` and the
/// pet's `ffplay`, the Linux browser launcher `xdg-open`) from fixed install
@@ -39,7 +39,7 @@ pub const WHALE_WAV: &[u8] = include_bytes!("../../assets/audio/codewhale-whale-
/// custom `~/bin`, a version manager shim) is refused and the caller reports
/// the feature as unavailable. That is deliberate — there is no `PATH`
/// fallback.
pub(crate) fn trusted_system_executable(name: &str) -> io::Result<PathBuf> {
pub fn trusted_system_executable(name: &str) -> io::Result<PathBuf> {
trusted_player_in(name, &trusted_player_dirs())
}
@@ -110,7 +110,7 @@ fn is_executable_file(path: &Path) -> bool {
}
}
#[cfg(not(test))]
#[cfg(not(any(test, all(feature = "test-thread-scoped-state", debug_assertions))))]
pub fn dispatch(cue: &SoundCue, out: &mut dyn Write) -> AudioOutcome {
#[cfg(target_os = "windows")]
if matches!(cue, SoundCue::Bell | SoundCue::Beep | SoundCue::DoubleBell) {
@@ -130,16 +130,16 @@ pub fn dispatch(cue: &SoundCue, out: &mut dyn Write) -> AudioOutcome {
dispatch_wav(cue)
}
#[cfg(test)]
#[cfg(any(test, all(feature = "test-thread-scoped-state", debug_assertions)))]
pub fn dispatch(_cue: &SoundCue, _out: &mut dyn Write) -> AudioOutcome {
// Production entry points are also fail-closed in library tests.
// Production entry points are also fail-closed in library and scoped debug tests.
AudioOutcome::Unsupported
}
#[cfg(not(test))]
#[cfg(not(any(test, all(feature = "test-thread-scoped-state", debug_assertions))))]
static PLAYING: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
#[cfg(not(test))]
#[cfg(not(any(test, all(feature = "test-thread-scoped-state", debug_assertions))))]
fn dispatch_wav(cue: &SoundCue) -> AudioOutcome {
use std::sync::atomic::Ordering;
if !cfg!(any(
@@ -179,7 +179,7 @@ fn dispatch_wav(cue: &SoundCue) -> AudioOutcome {
}
}
#[cfg(not(test))]
#[cfg(not(any(test, all(feature = "test-thread-scoped-state", debug_assertions))))]
fn play_wav(cue: &SoundCue) -> io::Result<()> {
let mut bundled = None;
let path = match cue {
@@ -203,7 +203,10 @@ fn play_wav(cue: &SoundCue) -> io::Result<()> {
result
}
#[cfg(all(not(test), target_os = "windows"))]
#[cfg(all(
not(any(test, all(feature = "test-thread-scoped-state", debug_assertions))),
target_os = "windows"
))]
fn play_file(path: &std::path::Path) -> io::Result<()> {
use std::os::windows::ffi::OsStrExt;
use windows::Win32::Media::Audio::{PlaySoundW, SND_FILENAME, SND_NODEFAULT};
@@ -217,7 +220,10 @@ fn play_file(path: &std::path::Path) -> io::Result<()> {
}
}
#[cfg(all(not(test), any(target_os = "macos", target_os = "linux")))]
#[cfg(all(
not(any(test, all(feature = "test-thread-scoped-state", debug_assertions))),
any(target_os = "macos", target_os = "linux")
))]
fn play_file(path: &std::path::Path) -> io::Result<()> {
#[cfg(target_os = "macos")]
let player = PathBuf::from("/usr/bin/afplay");
@@ -237,7 +243,7 @@ fn play_file(path: &std::path::Path) -> io::Result<()> {
}
#[cfg(all(
not(test),
not(any(test, all(feature = "test-thread-scoped-state", debug_assertions))),
not(any(target_os = "windows", target_os = "macos", target_os = "linux"))
))]
fn play_file(_path: &std::path::Path) -> io::Result<()> {
@@ -16,7 +16,7 @@ use super::{DeliveryOutcome, Method, NotificationGate};
/// - `$TERM` contains `kitty` → `Kitty`
/// - Unknown terminal → `Off` (never invent an audible fallback)
#[must_use]
pub(crate) fn resolve_method() -> Method {
pub fn resolve_method() -> Method {
let term_program = std::env::var("TERM_PROGRAM").unwrap_or_default();
match term_program.as_str() {
"iTerm.app" | "WezTerm" | "Cmux" => return Method::Osc9,
@@ -108,7 +108,7 @@ fn build_escape(method: Method, in_tmux: bool, msg: &str) -> Vec<u8> {
/// All side effects sit behind injected sinks. A disallowed event reaches none.
#[allow(clippy::too_many_arguments)]
pub(crate) fn notify_with_sinks(
pub fn notify_with_sinks(
method: Method,
in_tmux: bool,
payload: &NotificationPayload,
@@ -117,18 +117,15 @@ pub(crate) fn notify_with_sinks(
gate: NotificationGate,
attention_allowed: bool,
sink: &mut dyn Write,
decide_sound: &mut dyn FnMut(
NotificationKind,
bool,
) -> crate::notify::sound_policy::SoundDecision,
decide_sound: &mut dyn FnMut(NotificationKind, bool) -> crate::sound_policy::SoundDecision,
audio: &mut dyn FnMut(
&crate::notify::sound_policy::SoundCue,
&crate::sound_policy::SoundCue,
&mut dyn Write,
) -> crate::notify::audio::AudioOutcome,
) -> crate::audio::AudioOutcome,
native: &mut dyn FnMut(&NotificationPayload) -> DeliveryOutcome,
) -> DeliveryOutcome {
use crate::notify::audio::AudioOutcome;
use crate::notify::sound_policy::SoundDecision;
use crate::audio::AudioOutcome;
use crate::sound_policy::SoundDecision;
if !attention_allowed {
return DeliveryOutcome::SuppressedByAttention;
}
@@ -2,7 +2,7 @@
//! delivery-method and category gate, attention (focus) rules, the
//! per-event sound policy and audio cues.
//!
//! Runtime code owns *what* may notify and *which* sound it selects. The
//! This crate owns *what* may notify and *which* sound it selects. The
//! terminal UI owns the *terminal*: `tui::notifications` is the only code that
//! hands a real terminal to [`delivery::notify_with_sinks`], writes the taskbar
//! progress sequence or the window title. Runtime callers that need a delivery
@@ -21,7 +21,9 @@ pub mod sound_policy;
use std::time::Duration;
use crate::config::{NotificationCondition, NotificationMethod, NotificationsConfig};
use codewhale_config::notifications::{
NotificationCondition, NotificationMethod, NotificationsConfig,
};
use payload::NotificationKind;
/// Notification delivery method.
@@ -197,7 +199,7 @@ impl NotificationGate {
const ELEVATION_NEEDED_BIT: u8 = 1 << 5;
const MODEL_NOTIFY_BIT: u8 = 1 << 6;
pub(crate) const fn to_bits(self) -> u8 {
pub const fn to_bits(self) -> u8 {
(self.quiet as u8 * Self::QUIET_BIT)
| (self.turn_complete as u8 * Self::TURN_COMPLETE_BIT)
| (self.subagent_terminal as u8 * Self::SUBAGENT_TERMINAL_BIT)
@@ -207,7 +209,7 @@ impl NotificationGate {
| (self.model_notify as u8 * Self::MODEL_NOTIFY_BIT)
}
pub(crate) const fn from_bits(bits: u8) -> Self {
pub const fn from_bits(bits: u8) -> Self {
Self {
quiet: bits & Self::QUIET_BIT != 0,
turn_complete: bits & Self::TURN_COMPLETE_BIT != 0,
@@ -226,15 +228,15 @@ impl NotificationGate {
/// until the terminal explicitly reports `FocusLost`, so the safe startup
/// behavior is silence rather than an unexpected banner or bell.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum AttentionCondition {
pub enum AttentionCondition {
Always = 0,
Unfocused = 1,
Never = 2,
}
pub(crate) const DEFAULT_UNFOCUSED_GRACE: Duration = Duration::from_secs(2);
pub const DEFAULT_UNFOCUSED_GRACE: Duration = Duration::from_secs(2);
#[must_use]
pub(crate) fn attention_delivery_allowed_at(
pub fn attention_delivery_allowed_at(
condition: AttentionCondition,
focused: bool,
unfocused_since_ms: u64,
@@ -254,7 +256,7 @@ pub(crate) fn attention_delivery_allowed_at(
/// Native hosts provide focus observations; the same grace/condition rule
/// applies before either native sound or banner preparation.
pub(crate) fn native_attention_allowed(
pub fn native_attention_allowed(
config: &NotificationsConfig,
focused: bool,
unfocused_for: Duration,
@@ -214,14 +214,6 @@ impl NotificationPayload {
}
/// Bounded, sanitized headline. Never empty.
///
/// Only the macOS path reads this today — `display notification` is the
/// one backend that takes a separate subtitle, while the escape-sequence
/// backends send a single string via [`Self::body`]. Tests exercise it on
/// every platform, but `#[cfg(test)]` uses do not keep it alive in a
/// non-macOS release build, so the allow is scoped to exactly that case
/// rather than blanket-silencing dead_code on the accessor.
#[cfg_attr(not(target_os = "macos"), allow(dead_code))]
#[must_use]
pub fn headline(&self) -> &str {
&self.headline
@@ -3,8 +3,8 @@
//! one cue and preserves per-category repeat history across settings updates.
use super::payload::NotificationKind;
use crate::config::{CompletionSound, NotificationsConfig};
pub use codewhale_config::notifications::NotificationEvent as SoundEvent;
use codewhale_config::notifications::{CompletionSound, NotificationsConfig};
use std::path::PathBuf;
use std::sync::{OnceLock, RwLock};
@@ -199,7 +199,7 @@ pub fn decide_configured(
.unwrap_or(SoundDecision::Suppress(SuppressReason::Disabled))
}
#[cfg(test)]
#[cfg(any(test, feature = "test-support"))]
pub fn configure(policy: EventSoundPolicy) {
*policy_cell().write().unwrap() = policy;
}
@@ -207,7 +207,7 @@ pub fn configure(policy: EventSoundPolicy) {
#[cfg(test)]
mod tests {
use super::*;
use crate::config::EventSoundConfig;
use codewhale_config::notifications::EventSoundConfig;
#[test]
fn canonical_whale_controls_all_six_categories_over_legacy_controls() {
@@ -314,7 +314,7 @@ mod tests {
#[test]
fn settings_refresh_preserves_repeat_history() {
let _lock = crate::test_support::lock_test_env();
let _lock = codewhale_runtime::test_support::lock_test_env();
let config = NotificationsConfig {
sound: Some(CompletionSound::Whale),
..Default::default()
@@ -334,7 +334,7 @@ mod tests {
#[test]
fn configured_off_decision_ignores_intervening_installed_whale_policy() {
let _lock = crate::test_support::lock_test_env();
let _lock = codewhale_runtime::test_support::lock_test_env();
let off = NotificationsConfig {
sound: Some(CompletionSound::Off),
..Default::default()
@@ -367,7 +367,7 @@ mod tests {
#[test]
fn configured_sound_keeps_installed_defaults_and_suppression_keeps_history() {
let _lock = crate::test_support::lock_test_env();
let _lock = codewhale_runtime::test_support::lock_test_env();
let off = NotificationsConfig {
sound: Some(CompletionSound::Off),
..Default::default()
@@ -412,7 +412,7 @@ mod tests {
#[test]
fn concurrent_configured_decisions_share_history_with_settings_refresh() {
let _lock = crate::test_support::lock_test_env();
let _lock = codewhale_runtime::test_support::lock_test_env();
configure(EventSoundPolicy::default());
let whale = NotificationsConfig {
sound: Some(CompletionSound::Whale),
+36
View File
@@ -264,9 +264,20 @@ pub struct McpManagerSnapshot {
/// Structured clarification request (`request_user_input`).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct UserInputRequest {
#[serde(default)]
pub require_typed_answers: bool,
pub questions: Vec<UserInputQuestionEvent>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum UserInputClosedOutcome {
Answered,
Cancelled,
TimedOut,
Unavailable,
}
/// Which permission gate produced a `ToolGateDecision`.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
@@ -743,6 +754,12 @@ pub enum EventMsg {
id: String,
request: UserInputRequest,
},
UserInputClosed {
thread_id: ThreadId,
session_id: SessionId,
id: String,
outcome: UserInputClosedOutcome,
},
/// Authoritative API conversation state (`Message`s / `SystemPrompt`
/// serialized).
SessionUpdated {
@@ -752,6 +769,8 @@ pub enum EventMsg {
messages: Vec<Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
system_prompt: Option<Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
instruction_assembly: Option<Value>,
model: String,
workspace: PathBuf,
},
@@ -871,6 +890,7 @@ pub const EVENT_KINDS: &[&str] = &[
"approval_required",
"approval_withdrawn",
"user_input_required",
"user_input_closed",
"session_updated",
"elevation_required",
"lsp_repair_update",
@@ -932,6 +952,7 @@ impl EventMsg {
Self::ApprovalRequired { .. } => "approval_required",
Self::ApprovalWithdrawn { .. } => "approval_withdrawn",
Self::UserInputRequired { .. } => "user_input_required",
Self::UserInputClosed { .. } => "user_input_closed",
Self::SessionUpdated { .. } => "session_updated",
Self::ElevationRequired { .. } => "elevation_required",
Self::LspRepairUpdate { .. } => "lsp_repair_update",
@@ -993,6 +1014,7 @@ impl EventMsg {
| Self::ApprovalRequired { thread_id, .. }
| Self::ApprovalWithdrawn { thread_id, .. }
| Self::UserInputRequired { thread_id, .. }
| Self::UserInputClosed { thread_id, .. }
| Self::SessionUpdated { thread_id, .. }
| Self::ElevationRequired { thread_id, .. }
| Self::LspRepairUpdate { thread_id, .. }
@@ -1054,6 +1076,7 @@ impl EventMsg {
| Self::ApprovalRequired { session_id, .. }
| Self::ApprovalWithdrawn { session_id, .. }
| Self::UserInputRequired { session_id, .. }
| Self::UserInputClosed { session_id, .. }
| Self::SessionUpdated { session_id, .. }
| Self::ElevationRequired { session_id, .. }
| Self::LspRepairUpdate { session_id, .. }
@@ -1480,22 +1503,35 @@ mod tests {
session_id: s.clone(),
id: "c2".into(),
request: UserInputRequest {
require_typed_answers: true,
questions: vec![UserInputQuestionEvent {
header: "h".into(),
id: "q1".into(),
question: "?".into(),
detail: Some("Exact detail".into()),
intent: Some(crate::UserInputIntentEvent::PlanReview {
approve: "Proceed".into(),
call_id: Some("plan-1".into()),
}),
options: vec![],
allow_free_text: true,
multi_select: false,
}],
},
},
EventMsg::UserInputClosed {
thread_id: t.clone(),
session_id: s.clone(),
id: "c2".into(),
outcome: UserInputClosedOutcome::TimedOut,
},
EventMsg::SessionUpdated {
thread_id: t.clone(),
session_id: s.clone(),
engine_session_id: "sess".into(),
messages: vec![json!({"role": "user", "content": []})],
system_prompt: Some(json!("sys")),
instruction_assembly: None,
model: "m".into(),
workspace: PathBuf::from("/ws"),
},
+137
View File
@@ -17,6 +17,26 @@ use serde_json::Value;
use super::Status;
/// Explicit boundary migration for pre-Fleet serialized role tokens.
///
/// Call this only at load / parse edges. Runtime code must use Fleet role
/// names via `FleetRole::as_str`. Returns `None` for tokens that are
/// already canonical or unknown — callers should prefer `FleetRole::parse`
/// for full acceptance (canonical + legacy).
#[must_use]
pub fn migrate_legacy_role_token(token: &str) -> Option<&'static str> {
match token.trim().to_ascii_lowercase().as_str() {
"worker" | "general-purpose" | "general_purpose" | "default" => Some("general"),
"scout" | "exploration" | "explorer" => Some("explore"),
"plan" | "planning" | "awaiter" => Some("planner"),
"review" | "code-review" | "code_review" => Some("reviewer"),
"builder" | "implementer" | "implementation" => Some("implement"),
"verifier" | "verify" | "verification" | "validator" | "tester" => Some("test"),
"consultant" | "oracle" => Some("advisor"),
_ => None,
}
}
pub const FLEET_PROTOCOL_VERSION: &str = "0.1.0";
/// Globally unique identifier for a fleet run.
@@ -1872,3 +1892,120 @@ mod tests {
}
}
}
/// Runtime delegation has no default product depth limit. Explicit owner
/// values still restrict child authority; zero disables delegation.
pub const DEFAULT_SPAWN_DEPTH: u32 = u32::MAX;
/// Shell posture, ordered most → least restrictive so `min_with` is the safe
/// side of a clamp.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ShellCeiling {
None,
ReadOnly,
Full,
}
impl ShellCeiling {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::None => "none",
Self::ReadOnly => "read_only",
Self::Full => "full",
}
}
#[must_use]
pub fn min_with(self, other: Self) -> Self {
if self <= other { self } else { other }
}
}
/// A Runtime child-authority envelope.
///
/// This type is intentionally not part of `ExactMember`. Exact Fleet files
/// choose identity, route, and reasoning; Runtime intersects its role posture
/// with the live parent after member selection. Keeping the envelope as a
/// separate type lets receipts describe the authority that Runtime actually
/// installed without turning a saved Fleet into a trust boundary.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct PermissionCeiling {
pub write: bool,
/// Whether the Runtime child may be handed a **model-visible network tool**
/// (fetch, browse, HTTP).
///
/// This is deliberately *not* a statement about transport. Host-owned
/// provider inference — the ordinary API call CodeWhale makes on the
/// member's behalf — always happens over the network and is not governed
/// by this field. A child with `network_tool = false` still runs on a
/// remote model; it simply has no tool with which to reach the network
/// itself. Receipts disclose that distinction rather than implying an
/// air-gap.
#[serde(alias = "network")]
pub network_tool: bool,
pub shell: ShellCeiling,
/// Nested-delegation budget the Runtime child may consume.
pub delegation_depth: u32,
/// Whether the Runtime child may be handed tools at all.
pub tools: bool,
}
impl PermissionCeiling {
/// The Router's fixed posture: no tools (so no network tool), no shell, no
/// writes, no delegation. Not configurable — see `RouterMember`.
///
/// The Router itself is still *inferred* by its configured provider over
/// the network; that is host-owned transport, disclosed on the receipt.
pub const ROUTER: Self = Self {
write: false,
network_tool: false,
shell: ShellCeiling::None,
delegation_depth: 0,
tools: false,
};
/// Legacy named presets retained for Runtime compatibility and tests.
///
/// The exact Fleet parser accepts a historic `permissions` key only as
/// ignored input; no preset selected here is projected onto a member.
pub fn preset(name: &str) -> Option<Self> {
let base = |write, network_tool, shell, delegation_depth| Self {
write,
network_tool,
shell,
delegation_depth,
tools: true,
};
match name.trim().to_ascii_lowercase().as_str() {
"none" => Some(Self::ROUTER),
"analyst" => Some(base(false, false, ShellCeiling::None, 0)),
"read_only" | "readonly" => Some(base(false, false, ShellCeiling::ReadOnly, 0)),
"tester" | "verifier" => Some(base(false, false, ShellCeiling::Full, 0)),
"read_write" | "readwrite" => Some(base(true, false, ShellCeiling::Full, 0)),
"full" => Some(base(true, true, ShellCeiling::Full, 1)),
_ => None,
}
}
/// Narrow this ceiling against the active session posture. Every field
/// takes the more restrictive side, so the result can never grant more
/// than either input.
#[must_use]
pub fn clamp_to(self, session: Self) -> Self {
Self {
write: self.write && session.write,
network_tool: self.network_tool && session.network_tool,
shell: self.shell.min_with(session.shell),
delegation_depth: self.delegation_depth.min(session.delegation_depth),
tools: self.tools && session.tools,
}
}
}
impl Default for PermissionCeiling {
fn default() -> Self {
Self::preset("read_only").expect("read_only is a known preset")
}
}
Loaded 100 of 1128 files, more files were not shown because too many files have changed in this diff. Show more