Compare commits

..
Author SHA1 Message Date
CodeWhale BotandClaude Sonnet 5.5 c0c80ffa07 fix(web): Plugins page names what each permission mode does with an agent-written plugin and the real skill command (CW-8087 PL-04 repair 1)
Auto-Review's reviewer model judges and Plan refuses, per the recorded ruling, so the
page no longer says approval is always asked outside Full Access. /skill install is
the command that adds a skill. The coming lead says the rest of the page is the
released version, and the meta description no longer carries a sentence that outlives
the 0.10.3 release.

Checks: vitest 92 files / 1303 tests passed, tsc exit 0, eslint exit 0, check:facts exit 0,
next build exit 0; built /en and /zh plugins pages fetched and show the new text.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-12 01:55:43 -08:00
CodeWhale BotandClaude Sonnet 5.5 66987f4fb5 feat(web): the Plugins page says plugin for what a person adds and labels what 0.10.3 brings as coming (CW-8087 PL-04)
The page described "everything is a plugin" as if the released terminal
already listed skills, connectors and hooks as plugins. It does not: a plugin
bundle carries them, and /skills, /mcp and /hooks add them one at a time. The
title, lead and meta now say that, and the heading "One format for every
extension" becomes "What a plugin can add", since one format is not released.

What 0.10.3 adds goes in a new section, "Coming in 0.10.3, not in the current
release": one Plugins place with kind labels, Standard and Creator mode per
conversation, the approval rule for a plugin the agent wrote, and the old
Plan/Work control renamed Permissions. The section renders only while the
published release is older than 0.10.3 (pluginsComingApplies) and is in the
site search under the same condition. English and Simplified Chinese carry the
text; the other 16 languages show the English page body through the existing
fallback, and no dictionary key was added or removed, so the 18-language
parity checks are unchanged.

Also: the authoring button opens the site's own plugin-authoring page (written
from the released tag) instead of the main-branch document; the DeepSeek
Harness compatibility link says it describes current source, because that
document is not in the v0.10.2 tag; the desktop app page names Plugins as one
noun with skills, connectors and hooks inside it; docs map descriptions say a
connector is an MCP server, a hook is added, and a skill can come inside a
plugin; the unrendered product copy says a skill is shared inside a plugin.

Checks run in web/: tsc --noEmit, eslint, check:facts, check:locales,
check:docs, vitest (91 files, 1301 tests plus 2 new), next build; the built
/en, /zh and /de plugins pages were fetched and the coming section checked.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-11-12 01:55:42 -08:00
CodeWhale BotandClaude Sonnet 5.5 bd1d61630a docs(plugins): repair round 2 for review of CW-8071
plugin-creator step 7 says to write the Full Access plugin in an ordinary
project folder, because install refuses a name already used by the bundle
step 1 creates. English RUNTIME_API gets the same after-0.10.2 label as zh.
Agent docs regenerated; preflight --check OK.

At landing: the skills and plugins rows in docs/features.toml are
shortened to the registry's 120-character summary limit (they were 122
and 153, which failed feature_registry_matches_code); features JSON
regenerated.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-12 01:55:40 -08:00
CodeWhale BotandClaude Sonnet 5.5 45794614da docs(plugins): repair round 1 for review of CW-8071
Docs no longer claim a single Plugins list exists: hand-made skills, servers
and hooks are plugins of a kind, still managed in Extensions and Customize,
and the one list is stated as planned for 0.10.3. plugin-creator step 7 now
gives the install, trust with review token and enable steps that the Full
Access exception accepts. Drop the generation 22 comment. zh RUNTIME_API says
ACP plugin and MCP tools are main after 0.10.2.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-11-12 01:55:38 -08:00
CodeWhale BotandClaude Sonnet 5.5 3dbb084a9e docs(plugins): the docs say plugin, in English and Simplified Chinese (CW-8071)
PLUGINS, SKILLS, MCP and HOOKS open by saying everything a person adds is a plugin;
a skill, an MCP connector and a hook are each a plugin or part of one and keep their
kind as a label, with their files, trust rules and commands unchanged. Connection
stays the word for a model provider or another agent and nothing else. PLUGINS no
longer says DSH plugin UI is unsupported. features.toml names the three kinds as
plugin kinds (ids unchanged); agent docs and feature data regenerated.

Checks: scripts/preflight.sh --check (see the review packet).

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-11-12 01:55:37 -08:00
CodeWhale BotandClaude Sonnet 5.5 50b4d5b45d docs(plugins): correct the stale creator texts of ruling R8 (CW-8071)
plugin-creator no longer tells an agent never to automate trust: in Full Access an
agent may trust and enable a plugin it wrote and installed from its own project,
through the session's approval, recorded as the agent's (section 142.4), and code or
screen plugins point at cordis-plugin-development instead of a second format. The
0.10.2 body (generation 19) is retained and listed by hash so untouched copies
upgrade; the bundled generation is 22. DSH_PLUGIN_COMPAT now says imported client
UIs are served on the admitted seats instead of unsupported. The Simplified Chinese
EXTENSIONS and RUNTIME_API say ACP sessions get the extension host, plugin tools and
configured MCP servers, as the English does. The vocabulary script says /pod answers
Unknown command and codewhale pod is prompt text.

Checks: codewhale-skills nextest 188 passed 0 skipped; clippy -D warnings clean;
scripts/check-tui-product-vocabulary.sh exit 0.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-11-12 01:55:36 -08:00
CodeWhale BotandClaude Sonnet 5.5 7b60b85ac3 fix(extension-host): cordisInspect commands.register states the alias and refusal rules the registry enforces (CR-06a, CW-8073)
A built-in clash gets plugin-<plugin-slug>-<command> and is refused if invalid or taken; a name another plugin registered is refused. Catalog regenerated; --check exit 0; inspect tests 17/17 on Node and Bun; typecheck clean.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-11-12 01:55:34 -08:00
CodeWhale BotandClaude Sonnet 5.5 f94b43012a fix(extension-host): cordisInspect catalog states the persona section and activation result contracts the host enforces (CR-06a, CW-8073)
systemPrompt.section is named deployment:persona-prefix with the order getSectionOrder returns; subagents result is a getter resolving to stopReason and output blocks. Command, avatar and hook limits corrected; non-JSON provider output is refused as DSH does. The method-existence test now checks each method on the class that serves it; a renamed shim method fails it.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-11-12 01:55:33 -08:00
CodeWhale BotandClaude Sonnet 5.5 34b0631492 feat(extension-host): cordisInspect data that describes this host (CR-06a, CW-8073)
A dependency-light cordisInspect service with list() and query() in the
shape DSH's cordis_inspect_list/query tools call. Host providers: Service,
Event, Config (Tool only when a source is supplied); client providers:
Slots and Theme. The catalog is generated from compatibility.json, the
host's refused-event list in root.ts, the shell-hook event map and the
Engine seat table, plus one hand-kept method table; manifests pass DSH's
own supported-schema check and inputs its validator. Not wired to the live
host (CR-06b); dist/ is not regenerated.

Checks: node --test test/inspect.test.mjs 15 pass 0 fail; bun test 15 pass
0 fail; npm run typecheck clean; generator --check clean; preflight OK.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-11-12 01:55:32 -08:00
CodeWhale BotandClaude Sonnet 5.5 3d4158cf07 test(tui): real-terminal thinking stream, stop, restore and sign-in notice at four widths (CW-7876)
Adds a PTY scenario to the existing cucumber harness, compiled only on unix
with long-running-tests: stream thinking, stop, and restore at four terminal
widths, the one-time sign-in notice with Esc dismissal, and a saved choice to
hide thinking surviving the new default.

Checks: farm run 20261112T073916Z-ENG-TUI-rebased-pty4-b3fc94, 3 tests run,
3 passed (1 slow), 60 skipped, exit 0.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-12 01:55:30 -08:00
CodeWhale BotandClaude Sonnet 5.5 4bd3e15373 fix(tui): a paused stream shows the thinking it already received (CW-7876)
When the provider paused mid-stream, reasoning already received stayed
unrendered until the next token arrived. The owed revision now renders on a
later display beat (render_owed_revision), so the transcript shows what has
arrived. Covered by a_paused_stream_renders_the_reasoning_it_already_received
and the PTY scenario.

Checks: farm tests run 20261112T061929Z-ENG-TUI-rebased-tests-d67557, 7557
passed / 10 skipped, exit 0; clippy run 20261112T061950Z-ENG-TUI-rebased-clippy-50d460
clean, exit 0; PTY run 20261112T073916Z-ENG-TUI-rebased-pty4-b3fc94, 3 of 3
passed.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-11-12 01:55:29 -08:00
CodeWhale BotandClaude Sonnet 5.5 192a7306ed fix(tui): the footer clock says thinking, like the transcript (CW-7876)
The footer phase word and clock said reasoning while the transcript header
said thinking. Both now say thinking, in the 12 locale packs that carried the
old word and the localization library; uk and ru keep their own wording. The
ui tests assert the new clock text.

Checks: farm tests run 20261112T061929Z-ENG-TUI-rebased-tests-d67557, 7557
passed / 10 skipped, exit 0; locale parity PASS; vocabulary check exit 0.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-11-12 01:55:28 -08:00
CodeWhale BotandClaude Sonnet 5.5 4ec3c45b47 feat(settings): model thinking shows by default; a saved choice is kept (CW-7876)
Thinking traces are first-class: show_thinking now defaults to true in the
runtime Settings and the config schema, while thinking_default_expanded stays
false (live while streaming, collapsed once the answer starts). Docs, the
features.toml row, the zh-Hans docs, the config panel goldens and the derived
web json follow. A show_thinking value already present in settings.toml is
read as an explicit choice and kept. Note that earlier saves wrote the old
default into that file, so only installs without the key get the new default;
whether to migrate those is a founder decision recorded in the review packet.

Checks: farm tests run 20261112T061929Z-ENG-TUI-rebased-tests-d67557, 7557
passed / 10 skipped, exit 0, including feature_registry (2 passed); clippy run
20261112T061950Z-ENG-TUI-rebased-clippy-50d460 clean, exit 0; check-facts OK.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-11-12 01:55:26 -08:00
CodeWhale BotandClaude Sonnet 5.5 cf6e4c702e fix(tui): the sign-in notice is a one-time footer notice that Esc dismisses (CW-7876)
The "account preferences not loaded" notice was pushed into the transcript
on every refresh, so it repeated and scrolled away with the conversation.
It is now a standing footer notice shown once per sign-in
(account_preferences_notice_shown), and Esc dismisses it while idle. The
message names the remedy within 40 columns and carries meaning in words, not
colour. New string added to all 15 locale packs and the settings schema.

Checks: farm tests run 20261112T061929Z-ENG-TUI-rebased-tests-d67557, 7557
passed / 10 skipped, exit 0; clippy run 20261112T061950Z-ENG-TUI-rebased-clippy-50d460
clean, exit 0; PTY run 20261112T073916Z-ENG-TUI-rebased-pty4-b3fc94, 3 of 3
passed; locale parity PASS; vocabulary check exit 0.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-11-12 01:55:24 -08:00
CodeWhale BotandClaude Opus 5.5 d82eeaf592 docs(sandbox): Fleet workers keep the managed variable; named files fail closed; serve --mcp refuses shell (CW-8052)
CHANGELOG: the Sandbox bullet's managed-configuration sentence now says what
the four CW-8052 commits do.

Checks: round 0 nextest 987/987 passed and clippy clean on runtime, engine,
app-server and TUI; repair round on the rebased tip nextest 1058/1058 passed
(runtime lib 922, engine fleet::host 26, TUI config_stay 110); preflight OK.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-12 01:55:23 -08:00
CodeWhale BotandClaude Opus 5.5 5aed666083 fix(mcp): serve --mcp does not expose shell tools under a tool launcher (CW-8052)
The stdio MCP server builds its own ToolContext, whose shell manager has
no launcher, deny list or prefer_bwrap. When the loaded config carries a
managed tool_launcher, run_mcp_server now removes every expose_tools entry
that maps to exec_shell (shell, exec_shell) before the registry is built,
so no shell tool is registered, and names each one on stderr. Wiring the
launcher would also need the deny list and read guard; refusing is the
closed form the review allowed.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-12 01:55:22 -08:00
CodeWhale BotandClaude Opus 5.5 1396698607 fix(fleet): local workers keep the parent's managed config variable (CW-8052)
A Fleet local worker is spawned with a cleaned environment rebuilt from
the child allow-list, which has no CODEWHALE_* names, so it lost
CODEWHALE_MANAGED_CONFIG_PATH and read the user config's
managed_config_path instead. base_env_from now carries both managed-config
names from the parent for every local worker (not only when a sessions
directory is set), and worker_env drops them from the request so an
allow-listed request cannot replace or add them.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-12 01:55:20 -08:00
CodeWhale BotandClaude Opus 5.5 72e5bcdb39 fix(config): a named managed file that cannot be read is a load error (CW-8052)
One helper, managed_config_file, now resolves the managed path for the
loader and for the three "who controls this" readers: the variable first
(an empty CODEWHALE_MANAGED_CONFIG_PATH defers to the legacy alias instead
of hiding it), then the config file's managed_config_path, then the
default path. The variable is read through codewhale_env_var, under the
test environment lock.

A file named by the variable or by managed_config_path that is missing,
a dangling link, a directory or unreadable used to load with no managed
layer; it now fails the load. Only a missing file at the default path
means no managed layer, and a config file's managed_config_path can no
longer point away from an existing default-path file. The readers report
Ambiguous where the loader would fail.

A provider that only a plugin declares used to fail under a managed file
with the saved-session message; it now says that plugin providers are not
usable under a managed configuration and what to do.

Tests that isolated themselves with an absent managed file now name an
empty one (no_managed_config writes it). New test covers missing,
directory, config-key and blank-variable cases; the late-provider test
now pins the error instead of passing on any Err.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-12 01:55:19 -08:00
CodeWhale BotandClaude Sonnet 5.5 e6183ad966 fix(usage-reporting): installers force the kill switch for their own run; doctor names a saved opt-out as config (CW-8033)
The installers (web/public/install.sh, scripts/release/install.sh,
scripts/release/install.ps1) now run their closing `doctor --installs --brief`
with CODEWHALE_TELEMETRY=0 and CODEWHALE_NO_UPDATE_CHECK=1 for that command
only, and print one line saying usage reporting is on by default and how to
turn it off. v0.10.2 already honours both variables, so nothing needs the next
release. `doctor` is classed read-only and does not arm reporting at v0.10.2 or
main, so this is a guard that does not depend on that classification.

The dispatcher hands the child CODEWHALE_TELEMETRY with the resolved value, so
a saved opt-out read as an environment kill switch and doctor printed "(env)".
The dispatcher now also passes CODEWHALE_TELEMETRY_SOURCE; the child uses it
only to label the source, only next to the dispatcher's floor statement, and
never to change the decision. A hand-set CODEWHALE_TELEMETRY=0 still reads
"(env)".

Wording: the two clap help strings and the English TUI strings say "usage
reporting".

Checks: node install-safety.test.js 74 passed; farm nextest of the four new or
touched tests 4 passed, and a wider telemetry sweep (config, cli, tui,
localization) 206 passed, 1 failed, the failure being a test expectation fixed
in this commit.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-12 01:55:17 -08:00
CodeWhale BotandClaude Sonnet 5.5 7ed8f92649 fix(engine): withhold tools whose encoded name exceeds the route limit; make decode exact (CW-8041)
to_api_tool_name expands every character outside [A-Za-z0-9_] (a dot is
nine characters, a hyphen two), so a long qualified MCP or plugin tool name
could encode past the provider limit and fail every retry with a 400 on
tools.N.name. A shortened name could not be decoded back, so no shortening
is attempted: the encoder stays injective and nothing is truncated.

Policy, applied in the one encoder module (client/wire.rs) and used by the
chat, responses and Anthropic builders:
- A tool whose encoded name exceeds the limit is withheld through the
  existing omitted-tools path. Forcing it with tool_choice fails with an
  error giving the tool name, its encoded length, the limit and what to
  change.
- A tool name already present in message history that exceeds a documented
  limit (Anthropic, OpenAI and Codex, DeepSeek) refuses the request with
  its own message: the tool was called earlier in the session, so switch to
  a route with a larger limit or start a new session. Routes with no
  documented limit send history unchanged. History is never rewritten.
- The omitted-tools warning (core/events.rs, one message) names the tools
  and states the limits and what to do. The exact encoded length is not in
  it: the warning carries names already cut to 64 characters; adding it
  needs a new field on the warning event.

Limit rule, in tool_name_limit(provider, wire): where a provider documents
its own limit it is used; otherwise the default is the limit of the wire
the route speaks, 128 on the Anthropic Messages wire and 64 on the OpenAI
chat and responses wires, except Ollama, Ollama Cloud, vLLM, SGLang and
custom routes, which have no name limit and use 128.
- Documented, read 2026-11-10: Anthropic 128 (define-tools page,
  ^[a-zA-Z0-9_-]{1,128}$); DeepSeek 128 on its chat and Anthropic wires
  (chat-completion API reference); OpenAI 64 (OpenAPI spec, function name
  description).
- Wire default, no documented limit found: MiniMax and ModelStudio on the
  Anthropic wire (128; their Anthropic-compatible references, read
  2026-11-10, state no tool-name limit), every other Anthropic-wire route
  (128), and every other chat or responses provider including Gemini (64;
  a wire default, not a documented provider limit).

Decode: from_api_tool_name ran a lenient second pass that rewrote bare
x00002E-style text, so a tool literally named ax00002Eb decoded to a.b, a
different tool. A name that is the canonical encoding of its own exact
decode now returns that decode and skips the lenient pass, so a legal tool
name can no longer be turned into another. The cost: a model that strips
both dashes from an encoded name is no longer recovered; the old
foo_x00002Ebar expectation is changed accordingly.

Tests: encoding collision and round trip over hostile names, limit counting,
limit per wire and provider, a 100-character name offered on the
MiniMax Anthropic wire and withheld on a chat wire, over-long MCP-style
names withheld per provider in the Anthropic, chat and responses builders,
forced-tool and history refusals, and a 65 to 128 character history name sent unchanged on Ollama, Custom and MiniMax chat routes, and the warning wording. Farm: nextest -p codewhale-engine client:: and core::events:: 664 run, 664 passed;
workspace clippy (ci.yml flags) clean.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-12 01:55:16 -08:00
CodeWhale BotandClaude Sonnet 5.5 a937eaccbc fix(engine): encode the tool name in a chat function-object tool_choice (CW-8040)
map_tool_choice_for_chat encoded only the internal {"type":"tool","name"}
shape. A choice already in chat-completions form,
{"type":"function","function":{"name":"web.run"}}, fell into the
passthrough arm and named a tool the request never declared, because the
declaration is sent as web-x00002E-run. The same held for the flat
{"type":"function","name":...} form and for a bare tool-name string, both
of which forced_tool_name already treats as a forced tool.

All of these now go out as {"type":"function","function":{"name":
to_api_tool_name(name)}}. Reserved strings (auto, none, required) and a
function choice with no name are unchanged.

Shape per OpenAI's published OpenAPI spec (ChatCompletionNamedToolChoice),
read 2026-11-10. The regression test covers web.run in all four shapes and
checks the built request body against the declared tool name.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-11-12 01:55:14 -08:00
CodeWhale BotandClaude Sonnet 5.5 a7d9986382 fix(app-server): match a gone store spelled through a symlink or another letter case when retiring a dead owner receipt (CW-8054)
The dead-owner discard compared the lexically normalised selected store with the
receipt's data_dir, so a home spelled through a symlinked ancestor (for example
/tmp on macOS) or in another letter case never matched and startup stayed
refused. Both sides now resolve the deepest existing ancestor and re-append the
missing tail, and on macOS and Windows the final comparison folds letter case.
Adds a symlinked-home test (unix) and a case-spelling test (macOS).

The one new canonicalize is admitted to the blocking-calls budget; it runs
inside owner_work, off the async runtime.

Checks: nextest -p codewhale-app-server --test daemon_socket 18/18 passed;
clippy -p codewhale-app-server --all-targets --all-features 0 warnings;
blocking-calls budget within budget; fmt clean.

Squashed from bf3c88bde3 and 6cdf29d72c.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-12 01:55:13 -08:00
CodeWhale BotandClaude Sonnet 5.5 665fb506ff test(engine): settle the queue fixture and bound idle reads by measured time (CW-8070)
The settled-nap test waited a fixed 2.3s for a fresh queue file to age and
bounded fingerprint reads by the nominal 3s window. On a slow runner the queue
could still be unsettled when counting began, or the sleep could overshoot and
fit more 1s naps than the bound allowed. Settle the stamp through the existing
fixture, wait for every worker to report a settled schedule, and derive the
bound from measured elapsed time. Product code is unchanged.

nextest stress 20/20 passed (farm run 20261112T061942Z-nap-stress-90898e);
clippy -p codewhale-engine --all-targets --all-features clean.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-12 01:55:11 -08:00
CodeWhale BotandClaude Opus 5.5 5e2366a1c4 fix(subagent): remove the workspace-wide delegated coordination lock (CW-7966)
A stale or suspended Codewhale session in a folder held
`.codewhale/state/subagents.v1.lock`, and every later session there
failed the exclusive v1->v3 cutover acquire, fell back to private or
read-only sub-agent state for the life of the process, and repeated
"Runtime coordination was opened without its writer lock" on each
sidebar refresh.

The v3 store already owns its concurrency control: the bounded
per-operation `subagents.v3.transaction.lock` and per-writer boot
leases, with per-writer snapshot merge, record ownership, terminal
receipts and stale/foreign record rejection in `merge_publish`. This
removes `CoordinationProcessLock` and everything that only it used: the
process-lock slot and its required/exclusive/read-only flags, the
session-private fallback, the cutover-blocked marker, the per-admitted-
workspace writer locks, and the lock-only constructor flag. The v1->v3
cutover now runs under the transaction lock only, as the architecture
doc already allows. Store admission that fails is retried on the next
delegated write (`admitted_store_authority`) instead of staying refused
for the life of the process; a corrupt store still fails every write
because each transaction validates the image it reads.

A Runtime that admits a selected workspace other than its own state root
used to take an exclusive flock there; it now refuses shared-workspace
writers while any session holds a live boot lease in that workspace's own
store, treating only NotFound as "no session there". The state
directory's `.gitignore` is written by store admission after the cutover
and the image read, so a refused admission through a linked `.codewhale`
writes no new file there. `cleanup_due` is false while a state path
exists and the store is not admitted, so a refused store shows its
refusal once rather than on every sidebar refresh.
`new_exclusive_runtime_subagent_manager` stays as an alias of the shared
Runtime constructor for its app-server caller.

Also carries the out-of-ownership edits the change requires: the
coordination-lock wording in engine.rs, coord.rs and coord/ledger.rs
comments, the runtime_threads test comment, the six TUI conformance
goldens (the engine no longer creates subagents.v1.lock), the two docs,
and the blocking-calls budget for subagent/mod.rs (std_fs 3 to 2).

Regression: `a_stale_session_never_makes_a_new_session_read_only`, a
two-process check; it fails on main.
`a_linked_codewhale_admission_writes_no_gitignore_through_it` replaces the deleted linked-lock test.

Squashed from a5a2dade1c and c1e7863392 plus the review's requested
patch; see the review packet for the evidence.

Checks: subagent filter: 824 run, 824 passed (nextest, farm
20261112T062100Z-ref04land-nextest-81c0b0); TUI conformance::events 4/4
(20261112T062102Z-ref04land-golden-bb9851); fmt clean; blocking-calls
budget within budget.

CHANGELOG: the three 0.10.3 coordination-lock entries are replaced by one
entry that says what the code now does.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-12 01:55:09 -08:00
72 changed files with 905 additions and 2830 deletions

No files matched your search

+8 -51
View File
@@ -4,12 +4,11 @@
# alias = Display Name <[email protected]>
#
# The right-hand side must use GitHub's numeric noreply address so a harvested
# commit authored with it, or a Co-authored-by trailer in the commit's final
# trailer block, links to the contributor's account. GitHub resolves the
# address by its numeric id, so the id must be the contributor's own
# (`gh api users/<login> --jq .id`). The left-hand side may be a GitHub login,
# old-style noreply address, raw email from a contributor commit, or local
# machine email seen in older harvested history.
# commit authored with it links to the contributor's account. Only commit
# authorship puts someone in the repository's contributor list; a co-author
# trailer shows on their profile but not there. The left-hand side may be a
# GitHub login, old-style noreply address, raw email from a contributor commit,
# or local machine email seen in older harvested history.
hmbown = Hmbown <[email protected]>
[email protected] = Hmbown <[email protected]>
@@ -51,7 +50,6 @@ Oliver-ZPLiu = Oliver-ZPLiu <[email protected]>
idling11 = idling11 <[email protected]>
Hanmiao Li = idling11 <[email protected]>
[email protected] = idling11 <[email protected]>
894876246 = idling11 <[email protected]>
angziii = angziii <[email protected]>
aboimpinto = aboimpinto <[email protected]>
Paulo Aboim Pinto = aboimpinto <[email protected]>
@@ -81,24 +79,19 @@ Vince = Liu-Vince <[email protected]>
[email protected] = Liu-Vince <[email protected]>
LI-Jialu = LI-Jialu <[email protected]>
JiarenWang = JiarenWang <[email protected]>
wdw8276 = kitty <3972439+mars-base@users.noreply.github.com>
mars-base = kitty <[email protected]>
[email protected] = kitty <[email protected]>
wdw8276 = wdw8276 <3972439+wdw8276@users.noreply.github.com>
pengyou200902 = pengyou200902 <[email protected]>
linzhiqin2003 = linzhiqin2003 <[email protected]>
LING71671 = LING71671 <[email protected]>
[email protected] = LING71671 <[email protected]>
JasonOA888 = JasonOA888 <[email protected]>
Inference1 = Inference1 <[email protected]>
hongqitai = hongqitai <[email protected]>
gordonlu = gordonlu <[email protected]>
angus-guo = gus <217034332+im[email protected]>
imgusguo = gus <217034332+im[email protected]>
[email protected] = gus <[email protected]>
angus-guo = gus <217034332+angus-[email protected]>
gus.guo@tec-do.com = gus <217034332+angus-[email protected]>
gaord = gaord <[email protected]>
Ben Gao = gaord <[email protected]>
[email protected] = gaord <[email protected]>
bengao168 = gaord <[email protected]>
MXAntian = MXAntian <[email protected]>
[email protected] = MXAntian <[email protected]>
DarrellThomas = Darrell Thomas <[email protected]>
@@ -254,7 +247,6 @@ [email protected] = zhuowp <[email protected]>
[email protected] = 秋月凉梦 <[email protected]>
asto18089 = asto18089 <[email protected]>
[email protected] = asto18089 <[email protected]>
xiechimon = Xmon Dai <[email protected]>
yrk111222 = Rongkun Yan <[email protected]>
VincentCorleone = Vincent Corleone <[email protected]>
@@ -286,38 +278,3 @@ Felixkw12 = Felix Wunderlich <[email protected]>
[email protected] = Felix Wunderlich <[email protected]>
Felix Wunderlich = Felix Wunderlich <[email protected]>
[email protected] = luochen211 <[email protected]>
AiurArtanis = AiurArtanis <[email protected]>
[email protected] = AiurArtanis <[email protected]>
Apeiron0w0 = Apeiron <[email protected]>
[email protected] = Apeiron <[email protected]>
[email protected] = ChaceLyee2101 <[email protected]>
AmosLeeeeeeee = ChaceLyee2101 <[email protected]>
ddlw = ddlw <[email protected]>
mo-vic = movic <[email protected]>
[email protected] = movic <[email protected]>
[email protected] = merchloubna70-dot <[email protected]>
[email protected] = merchloubna70-dot <[email protected]>
xulongzhe = luola <[email protected]>
[email protected] = luola <[email protected]>
ZhulongNT = SONG ZHENRUI <[email protected]>
[email protected] = SONG ZHENRUI <[email protected]>
xiluoduyu = xiluoduyu <[email protected]>
[email protected] = xiluoduyu <[email protected]>
wlon = Stephen Xu <[email protected]>
[email protected] = Stephen Xu <[email protected]>
krisclarkdev = Kristopher Clark <[email protected]>
THatch26 = THatch26 <[email protected]>
jeoor = Kayro <[email protected]>
harvey2011888 = harvey2011888 <[email protected]>
duanchao-lab = DC <[email protected]>
dumbjack = dumbjack <[email protected]>
[email protected] = dumbjack <[email protected]>
AquaZhang = IceTear <[email protected]>
disappear00 = idontknow <[email protected]>
namezzb = sailorZ <[email protected]>
alphapku-ux = Alpha Wu <[email protected]>
BX166 = BX166 <[email protected]>
c020627 = c020627 <[email protected]>
[email protected] = Parman Mohammadalizadeh <[email protected]>
Parman Mohammadalizadeh = Parman Mohammadalizadeh <[email protected]>
-14
View File
@@ -14,17 +14,3 @@ Hunter Bown <[email protected]> Cursor Agent <[email protected]>
Hunter Bown <[email protected]> cursoragent <[email protected]>
Hunter Bown <[email protected]> gemini-code-assist[bot] <gemini-code-assist[bot]@users.noreply.github.com>
Hunter Bown <[email protected]> gemini-code-assist[bot] <956858+gemini-code-assist[bot]@users.noreply.github.com>
ddlw <[email protected]> <[email protected]>
dmitri-0 <[email protected]> <[email protected]>
Paulo Aboim Pinto <[email protected]> <[email protected]>
Paulo Aboim Pinto <[email protected]> <[email protected]>
Paulo Aboim Pinto <[email protected]> <[email protected]>
AiurArtanis <[email protected]> <[email protected]>
Apeiron <[email protected]> <[email protected]>
ChaceLyee2101 <[email protected]> <[email protected]>
movic <[email protected]> <[email protected]>
merchloubna70-dot <[email protected]> <[email protected]>
merchloubna70-dot <[email protected]> <[email protected]>
luola <[email protected]> <[email protected]>
SONG ZHENRUI <[email protected]> <[email protected]>
xiluoduyu <[email protected]> <[email protected]>
+3 -10
View File
@@ -259,16 +259,9 @@ prefer merging the contributor's PR itself). When a contributor's work
lands as our commit, the contributor is that commit's **author** (`git commit
--author` with their GitHub-linked address), it carries `Harvested from PR #N by
@handle`, and maintainers or agents go in `Co-authored-by`. GitHub's contributor
count takes commit authors and the `Co-authored-by` trailers in a commit's final
trailer block, and only addresses linked to an account: a trailer cut off by a
blank line, an unlinked email, or a noreply address carrying someone else's
numeric id credits nobody or the wrong person. Harvested commits authored by
maintainer identities without such a trailer left their contributors off it,
which is why `docs/CONTRIBUTORS.md` carries the full wall
(`scripts/contributor-wall.py`). Credit lost in history that cannot change is
restored by a later commit with one trailer per person, naming each pull
request; `scripts/check-contributor-credit.py` fails the release window when a
harvest, a trailer or a noreply id would leave someone uncounted.
list counts commit authors only: about 200 harvested commits authored by
maintainer identities left their contributors off it, which is why
`docs/CONTRIBUTORS.md` carries the full wall (`scripts/contributor-wall.py`).
A maintainer closes their PR by hand with a thank-you naming the commit;
nothing closes a PR automatically. Canonical human identities come from
`.github/AUTHOR_MAP`.
+1 -20
View File
@@ -7,15 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
- **Renaming sessions with Chinese titles:** the session picker no longer rejects a Chinese, Japanese, Korean or emoji title of more than about 33 characters as "Title must be 1–100 characters". It now counts characters, as the message says and as `/rename` and the Runtime API already did, so any title up to 100 characters is accepted. Thanks [@luochen211](https://github.com/luochen211) (PR 7322).
- **Non-ASCII file names:** `/diff`, the `@git` and `@diff` mentions and the Runtime API workspace diff now show paths with Chinese, emoji and other non-ASCII characters as written instead of as quoted octal escapes, and the turn context no longer lists the workspace root as an empty "Active paths" entry. Thanks [@luochen211](https://github.com/luochen211) (PR 7317).
- **Session names after a crash, and keep-forever snapshot retention:** a session name saved right after a crash that cut off the previous session-index line is no longer lost, because the new entry now starts on its own line; and a very large `[snapshots] max_age_days` (such as `9223372036854775807` for "keep forever") no longer wraps around and deletes every undo snapshot at session start. Thanks [@luochen211](https://github.com/luochen211) (PR 7316).
- **Blank environment variables:** exporting a variable empty (for example `CODEWHALE_MODEL=` from a compose `${VAR:-}` template) no longer hides its legacy alias. `DEEPSEEK_MODEL`, `DEEPSEEK_BASE_URL`, `KIMI_BASE_URL`, `DEEPSEEK_FORCE_HTTP1`, `DEEPSEEK_STREAM_OPEN_TIMEOUT_SECS` and the other provider alias variables now apply as they do when the primary is unset. Thanks [@luochen211](https://github.com/luochen211) (PR 7315).
- **MCP and language servers that send requests mid-call:** a tool call no longer fails with "neither a result nor an error" when an MCP server sends a `ping` or other request that reuses the call's id while the call is running, and a language server's `workspace/configuration` or similar request no longer ends a pending LSP request. Codewhale now answers `ping`, answers other server requests with "Method not found" (it never advertised them), keeps waiting for the real reply, and a refused answer over Streamable HTTP no longer drops the connection to legacy SSE. Thanks [@luochen211](https://github.com/luochen211) (PR 7319).
- **Qwen Code as an external agent:** Codewhale now finds an installed Qwen Code CLI (`qwen`, 0.6.1 or newer) and can run it as a session over ACP with `qwen --acp`. `GET /v1/agents/external` lists it, an older install is reported as needing `npm i -g @qwen-code/qwen-code@latest`, and sign-in stays inside Qwen Code (`/auth`). Thanks [@MannXo](https://github.com/MannXo) (#7293).
- **README and contributor credit:** the repository front page now opens the way the website does: the line "The model can be anyone's. The agent should be yours.", one sentence on what Codewhale is, the install command and one picture, with reference detail moved into the linked guides; the 18 translated READMEs follow the same structure and say they are machine-drafted. The contributor wall and the website now count the same 433 people (407 GitHub accounts and the commit authors whose email is not linked to an account), where the website had counted accounts only. `DEPENDENCY_SECURITY.md` moved into `docs/dependency-maintenance.md`, and `.github/SUPPORT.md` says where to ask for help.
- **Website Plugins page:** The website's Plugins page now describes what the released version does, and a labelled section says what 0.10.3 adds: one Plugins place, Standard and Creator mode, and the Permissions control.
- **First-time guide on the website:** a new docs page, "Use Codewhale for the first time" (`/docs/first-time`, English and Simplified Chinese), walks someone who has never used a terminal through opening one, installing, choosing the folder Codewhale works in and when that choice matters, opening a terminal in a folder from Finder or File Explorer, connecting a model, a first task, and stopping, undoing and quitting. The install page gains a short "New to the terminal?" block for macOS and Windows that links to it, and `llms.txt` carries the same steps.
- **Plugin docs and plugin-creator:** Docs now say everything you add is a plugin. The bundled plugin-creator skill no longer tells an agent never to trust or enable a plugin (in Full Access it may, for one it wrote and installed from its project), points code and screen plugins at cordis-plugin-development, and an untouched copy from 0.10.2 now upgrades. The Chinese extension and Runtime API docs now say editor ACP sessions get plugin tools and configured MCP servers.
- **Model thinking:** New installs show model thinking by default: it streams live in the transcript and collapses under a "Thought for …" header when the answer starts. Existing installs keep their saved setting; turn it on under Model reasoning in chat in /config.
- **Footer clock:** The footer clock and phase now say "thinking", matching the transcript.
@@ -676,7 +669,7 @@ work is declared complete here. Items moved to 0.10.4 by name are marked below.
- **Windows uninstall removes the receipt and update leftovers.** The installer's uninstaller also deletes `.codewhale-install.sha256` and `codewhale.old-*`/`codew.old-*` left by a self-update, so `bin` can be removed. Refs #7091.
- **Windows `/undo` matches recorded file paths regardless of case.** A write recorded under a different drive-letter or directory case, or a `\\?\` prefix, is no longer dropped from the restore plan. Refs #6970.
- **Windows: Fleet and metrics read live files again.** Replaying a Fleet ledger and reading audit or receipt records no longer fail with a sharing violation while their writer is still open; the same regular-file, link and identity checks apply. Plugin enablement now says which snapshot check refused (stage root, content identity or final check) instead of one generic message. Generated protocol and SDK exports keep LF line endings in Windows checkouts.
- **A held coordination lock no longer makes a new session read-only.** When another session owns the delegated-agent store, the new session starts on private state and says so, instead of refusing every sub-agent with “Failed to clean up sub-agents”.
- **Sessions in one folder no longer leave each other's sub-agents read-only or private:** a second session works beside another one, stale or suspended included, the older sub-agent store upgrades right away, and a store that cannot be opened is retried on the next sub-agent change instead of needing a restart. While another Codewhale session, a suspended one included, is open in a folder the desktop app or `serve` was asked to work in (other than the folder it started in), that Runtime still refuses sub-agents that would edit the folder directly; isolated worktrees are unaffected. A sub-agent store that cannot be opened shows its refusal once instead of repeating cleanup warnings on every sidebar refresh. Restart any session started with Codewhale 0.10.2 or older: once the folder upgrades it can no longer save sub-agent state (Refs #7232).
- **A dev server can outlive the turn in an interactive session.** `persist:true` now works in the TUI and desktop when the thread has Full Access; restricted postures and Fleet/sub-agents still refuse it.
- **`codewhale exec --add-dir <path>`** (repeatable) and `addDirs` on `POST /v1/threads` let the file tools read and write a sibling directory, a second checkout or a shared scratch root without turning on Full Access.
- **Unsettled usage is persisted in production, not only in tests.** The interactive sink now records a legacy usage receipt when a provider response has no settlement, so children and turns stop reporting settlement failure.
@@ -713,12 +706,6 @@ work is declared complete here. Items moved to 0.10.4 by name are marked below.
composer. Real Pinyin input on macOS and Windows is not yet confirmed.
Thanks [@cmdcorp6534](https://github.com/cmdcorp6534) for the report
(Refs #2323).
- **Sub-agents beside an older session:** while a Codewhale 0.10.2 session
still uses a folder's older delegated-agent store, a 0.10.3 session in the
same folder keeps working sub-agents with private state that is not saved,
instead of refusing every sub-agent action. The older session's files are
left untouched, and the store upgrades when a session starts after the older
one exits (Refs #7232).
- **Fleet members:** a replayed member start now matches only within its own
session, so two sessions that reuse an operation key no longer receive each
other's agent.
@@ -747,12 +734,6 @@ work is declared complete here. Items moved to 0.10.4 by name are marked below.
bare self-package exports resolve inside the selected package directory.
Invalid or incompatible ranges and paths outside the reviewed package still
refuse. Real published-plugin activation remains unqualified.
- **Coordination lock diagnostics (#7232):** stale lock-file contents no longer
identify an owner PID. A refused legacy admission keeps its reason in Work
and shows one notice per cause instead of repeating cleanup warnings on
every sidebar refresh. Legacy migration still requires the actual holder to
exit and the refused owning Runtime to restart; locks and retained state
remain protected.
- **A local proxy that stops mid-session no longer strands the session (#7231):** the model, catalog, search, vision, finance, MCP-registry, profile and remote-control clients watch a loopback proxy port and go direct while it refuses connections, then resume proxying when it returns. Only a definite refusal from every resolved address changes the route; remote proxies, `NO_PROXY` matches, credentials and the existing precedence are unchanged. Set `CODEWHALE_LOCAL_PROXY_FALLBACK=0` to keep the old build-time snapshot.
- **Proxy diagnostic privacy ([#7233](https://github.com/codewhale-hq/codewhale/issues/7233)):** malformed MCP proxy warnings omit configured values; local-proxy log labels mask userinfo and omit URL paths, queries and fragments. Routing is unchanged. All 23 existing proxy tests passed on macOS; runtime log capture and other platforms remain unqualified. Thanks [@SparkofSpike](https://github.com/SparkofSpike) for the report.
- **Fork, undo and retry work after a workspace drift notice (#7234):** the history-boundary walks now treat a whole `<context_update>` envelope as engine-owned, so a session that received one can be cut at every saved turn. A person quoting a notice or a truncated envelope still counts as their own prompt.
+325 -87
View File
@@ -140,17 +140,48 @@ impl EnvRuntimeOverrides {
Self {
provider,
provider_source,
model: env_non_blank(&["CODEWHALE_MODEL", "DEEPSEEK_MODEL", "DEEPSEEK_DEFAULT_TEXT_MODEL"]),
volcengine_model: env_non_blank(&["VOLCENGINE_MODEL", "VOLCENGINE_ARK_MODEL"]),
wanjie_ark_model: env_non_blank(&["WANJIE_ARK_MODEL", "WANJIE_MODEL", "WANJIE_MAAS_MODEL"]),
openrouter_model: env_non_blank(&["OPENROUTER_MODEL"]),
orcarouter_model: env_non_blank(&["ORCAROUTER_MODEL"]),
moonshot_model: env_non_blank(&["MOONSHOT_MODEL", "KIMI_MODEL_NAME", "KIMI_MODEL"]),
xiaomi_mimo_model: env_non_blank(&["XIAOMI_MIMO_MODEL", "MIMO_MODEL"]),
xiaomi_mimo_mode: env_non_blank(&["XIAOMI_MIMO_MODE", "MIMO_MODE"]),
novita_model: env_non_blank(&["NOVITA_MODEL"]),
fireworks_model: env_non_blank(&["FIREWORKS_MODEL"]),
arcee_model: env_non_blank(&["ARCEE_MODEL"]),
model: std::env::var("CODEWHALE_MODEL")
.or_else(|_| std::env::var("DEEPSEEK_MODEL"))
.or_else(|_| std::env::var("DEEPSEEK_DEFAULT_TEXT_MODEL"))
.ok()
.filter(|v| !v.trim().is_empty()),
volcengine_model: std::env::var("VOLCENGINE_MODEL")
.or_else(|_| std::env::var("VOLCENGINE_ARK_MODEL"))
.ok()
.filter(|v| !v.trim().is_empty()),
wanjie_ark_model: std::env::var("WANJIE_ARK_MODEL")
.or_else(|_| std::env::var("WANJIE_MODEL"))
.or_else(|_| std::env::var("WANJIE_MAAS_MODEL"))
.ok()
.filter(|v| !v.trim().is_empty()),
openrouter_model: std::env::var("OPENROUTER_MODEL")
.ok()
.filter(|v| !v.trim().is_empty()),
orcarouter_model: std::env::var("ORCAROUTER_MODEL")
.ok()
.filter(|v| !v.trim().is_empty()),
moonshot_model: std::env::var("MOONSHOT_MODEL")
.or_else(|_| std::env::var("KIMI_MODEL_NAME"))
.or_else(|_| std::env::var("KIMI_MODEL"))
.ok()
.filter(|v| !v.trim().is_empty()),
xiaomi_mimo_model: std::env::var("XIAOMI_MIMO_MODEL")
.or_else(|_| std::env::var("MIMO_MODEL"))
.ok()
.filter(|v| !v.trim().is_empty()),
xiaomi_mimo_mode: std::env::var("XIAOMI_MIMO_MODE")
.or_else(|_| std::env::var("MIMO_MODE"))
.ok()
.filter(|v| !v.trim().is_empty()),
novita_model: std::env::var("NOVITA_MODEL")
.ok()
.filter(|v| !v.trim().is_empty()),
fireworks_model: std::env::var("FIREWORKS_MODEL")
.ok()
.filter(|v| !v.trim().is_empty()),
arcee_model: std::env::var("ARCEE_MODEL")
.ok()
.filter(|v| !v.trim().is_empty()),
verbosity: env_non_blank(&["CODEWHALE_VERBOSITY", "DEEPSEEK_VERBOSITY"]),
auth_mode: env_non_blank(&["CODEWHALE_AUTH_MODE", "DEEPSEEK_AUTH_MODE"]),
log_level: env_non_blank(&["CODEWHALE_LOG_LEVEL", "DEEPSEEK_LOG_LEVEL"]),
@@ -185,7 +216,9 @@ impl EnvRuntimeOverrides {
None
}
}),
http_headers: env_non_blank(&["CODEWHALE_HTTP_HEADERS", "DEEPSEEK_HTTP_HEADERS"])
http_headers: std::env::var("CODEWHALE_HTTP_HEADERS")
.or_else(|_| std::env::var("DEEPSEEK_HTTP_HEADERS"))
.ok()
.and_then(|value| match parse_http_headers(&value) {
Ok(h) => Some(h),
Err(_) => {
@@ -194,85 +227,290 @@ impl EnvRuntimeOverrides {
}
})
.filter(|headers| !headers.is_empty()),
active_route_base_url: env_non_blank(&["CODEWHALE_BASE_URL", "DEEPSEEK_BASE_URL"]),
deepseek_anthropic_base_url: env_non_blank(&["DEEPSEEK_ANTHROPIC_BASE_URL", "DEEPSEEK_CLAUDE_BASE_URL"]),
nvidia_base_url: env_non_blank(&["NVIDIA_NIM_BASE_URL", "NIM_BASE_URL", "NVIDIA_BASE_URL"]),
openai_base_url: env_non_blank(&["OPENAI_BASE_URL"]),
atlascloud_base_url: env_non_blank(&["ATLASCLOUD_BASE_URL"]),
volcengine_base_url: env_non_blank(&["VOLCENGINE_BASE_URL", "VOLCENGINE_ARK_BASE_URL", "ARK_BASE_URL"]),
wanjie_ark_base_url: env_non_blank(&["WANJIE_ARK_BASE_URL", "WANJIE_BASE_URL", "WANJIE_MAAS_BASE_URL"]),
openrouter_base_url: env_non_blank(&["OPENROUTER_BASE_URL"]),
orcarouter_base_url: env_non_blank(&["ORCAROUTER_BASE_URL"]),
xiaomi_mimo_base_url: env_non_blank(&["XIAOMI_MIMO_BASE_URL", "MIMO_BASE_URL"]),
novita_base_url: env_non_blank(&["NOVITA_BASE_URL"]),
fireworks_base_url: env_non_blank(&["FIREWORKS_BASE_URL"]),
siliconflow_base_url: env_non_blank(&["SILICONFLOW_BASE_URL"]),
siliconflow_model: env_non_blank(&["SILICONFLOW_MODEL"]),
arcee_base_url: env_non_blank(&["ARCEE_BASE_URL"]),
moonshot_base_url: env_non_blank(&["MOONSHOT_BASE_URL", "KIMI_BASE_URL"]),
sglang_base_url: env_non_blank(&["SGLANG_BASE_URL"]),
vllm_base_url: env_non_blank(&["VLLM_BASE_URL"]),
ollama_base_url: env_non_blank(&["OLLAMA_BASE_URL"]),
ollama_cloud_base_url: env_non_blank(&["OLLAMA_CLOUD_BASE_URL"]),
ollama_cloud_model: env_non_blank(&["OLLAMA_CLOUD_MODEL"]),
huggingface_base_url: env_non_blank(&["HUGGINGFACE_BASE_URL", "HF_BASE_URL"]),
huggingface_model: env_non_blank(&["HUGGINGFACE_MODEL", "HF_MODEL"]),
modelscope_base_url: env_non_blank(&["MODELSCOPE_BASE_URL"]),
modelscope_model: env_non_blank(&["MODELSCOPE_MODEL"]),
together_base_url: env_non_blank(&["TOGETHER_BASE_URL"]),
together_model: env_non_blank(&["TOGETHER_MODEL"]),
qianfan_base_url: env_non_blank(&["QIANFAN_BASE_URL", "BAIDU_QIANFAN_BASE_URL"]),
qianfan_model: env_non_blank(&["QIANFAN_MODEL", "BAIDU_QIANFAN_MODEL"]),
openai_codex_base_url: env_non_blank(&["OPENAI_CODEX_BASE_URL", "CODEX_BASE_URL"]),
openai_codex_model: env_non_blank(&["OPENAI_CODEX_MODEL", "CODEX_MODEL"]),
anthropic_base_url: env_non_blank(&["ANTHROPIC_BASE_URL"]),
anthropic_model: env_non_blank(&["ANTHROPIC_MODEL"]),
openmodel_base_url: env_non_blank(&["OPENMODEL_BASE_URL"]),
openmodel_model: env_non_blank(&["OPENMODEL_MODEL"]),
zai_base_url: env_non_blank(&["ZAI_BASE_URL", "Z_AI_BASE_URL", "ZHIPU_BASE_URL", "ZHIPUAI_BASE_URL", "BIGMODEL_BASE_URL"]),
zai_model: env_non_blank(&["ZAI_MODEL", "Z_AI_MODEL", "ZHIPU_MODEL", "ZHIPUAI_MODEL", "BIGMODEL_MODEL", "GLM_MODEL"]),
stepfun_base_url: env_non_blank(&["STEPFUN_BASE_URL", "STEP_BASE_URL"]),
stepfun_model: env_non_blank(&["STEPFUN_MODEL", "STEP_MODEL"]),
minimax_base_url: env_non_blank(&["MINIMAX_BASE_URL"]),
minimax_anthropic_base_url: env_non_blank(&["MINIMAX_ANTHROPIC_BASE_URL"]),
minimax_model: env_non_blank(&["MINIMAX_MODEL"]),
deepinfra_base_url: env_non_blank(&["DEEPINFRA_BASE_URL"]),
deepinfra_model: env_non_blank(&["DEEPINFRA_MODEL"]),
sakana_base_url: env_non_blank(&["SAKANA_BASE_URL"]),
sakana_model: env_non_blank(&["SAKANA_MODEL"]),
longcat_base_url: env_non_blank(&["LONGCAT_BASE_URL"]),
longcat_model: env_non_blank(&["LONGCAT_MODEL"]),
opencode_go_base_url: env_non_blank(&["OPENCODE_GO_BASE_URL"]),
opencode_go_model: env_non_blank(&["OPENCODE_GO_MODEL"]),
opencode_zen_base_url: env_non_blank(&["OPENCODE_ZEN_BASE_URL"]),
opencode_zen_model: env_non_blank(&["OPENCODE_ZEN_MODEL"]),
meta_base_url: env_non_blank(&["META_MODEL_API_BASE_URL", "MODEL_API_BASE_URL"]),
meta_model: env_non_blank(&["META_MODEL_API_MODEL", "MODEL_API_MODEL"]),
xai_base_url: env_non_blank(&["XAI_BASE_URL"]),
xai_model: env_non_blank(&["XAI_MODEL"]),
google_base_url: env_non_blank(&["GOOGLE_BASE_URL", "GEMINI_BASE_URL"]),
google_model: env_non_blank(&["GOOGLE_MODEL", "GEMINI_MODEL"]),
mistral_base_url: env_non_blank(&["MISTRAL_BASE_URL"]),
mistral_model: env_non_blank(&["MISTRAL_MODEL"]),
telecomjs_base_url: env_non_blank(&["TELECOMJS_BASE_URL"]),
telecomjs_model: env_non_blank(&["TELECOMJS_MODEL"]),
edenai_base_url: env_non_blank(&["EDENAI_BASE_URL"]),
edenai_model: env_non_blank(&["EDENAI_MODEL"]),
zenmux_base_url: env_non_blank(&["ZENMUX_BASE_URL"]),
zenmux_model: env_non_blank(&["ZENMUX_MODEL"]),
csdn_base_url: env_non_blank(&["CSDN_BASE_URL"]),
csdn_model: env_non_blank(&["CSDN_MODEL"]),
concentrate_base_url: env_non_blank(&["CONCENTRATE_BASE_URL"]),
concentrate_model: env_non_blank(&["CONCENTRATE_MODEL"]),
active_route_base_url: std::env::var("CODEWHALE_BASE_URL")
.or_else(|_| std::env::var("DEEPSEEK_BASE_URL"))
.ok()
.filter(|v| !v.trim().is_empty()),
deepseek_anthropic_base_url: std::env::var("DEEPSEEK_ANTHROPIC_BASE_URL")
.or_else(|_| std::env::var("DEEPSEEK_CLAUDE_BASE_URL"))
.ok()
.filter(|v| !v.trim().is_empty()),
nvidia_base_url: std::env::var("NVIDIA_NIM_BASE_URL")
.or_else(|_| std::env::var("NIM_BASE_URL"))
.or_else(|_| std::env::var("NVIDIA_BASE_URL"))
.ok()
.filter(|v| !v.trim().is_empty()),
openai_base_url: std::env::var("OPENAI_BASE_URL")
.ok()
.filter(|v| !v.trim().is_empty()),
atlascloud_base_url: std::env::var("ATLASCLOUD_BASE_URL")
.ok()
.filter(|v| !v.trim().is_empty()),
volcengine_base_url: std::env::var("VOLCENGINE_BASE_URL")
.or_else(|_| std::env::var("VOLCENGINE_ARK_BASE_URL"))
.or_else(|_| std::env::var("ARK_BASE_URL"))
.ok()
.filter(|v| !v.trim().is_empty()),
wanjie_ark_base_url: std::env::var("WANJIE_ARK_BASE_URL")
.or_else(|_| std::env::var("WANJIE_BASE_URL"))
.or_else(|_| std::env::var("WANJIE_MAAS_BASE_URL"))
.ok()
.filter(|v| !v.trim().is_empty()),
openrouter_base_url: std::env::var("OPENROUTER_BASE_URL")
.ok()
.filter(|v| !v.trim().is_empty()),
orcarouter_base_url: std::env::var("ORCAROUTER_BASE_URL")
.ok()
.filter(|v| !v.trim().is_empty()),
xiaomi_mimo_base_url: std::env::var("XIAOMI_MIMO_BASE_URL")
.or_else(|_| std::env::var("MIMO_BASE_URL"))
.ok()
.filter(|v| !v.trim().is_empty()),
novita_base_url: std::env::var("NOVITA_BASE_URL")
.ok()
.filter(|v| !v.trim().is_empty()),
fireworks_base_url: std::env::var("FIREWORKS_BASE_URL")
.ok()
.filter(|v| !v.trim().is_empty()),
siliconflow_base_url: std::env::var("SILICONFLOW_BASE_URL")
.ok()
.filter(|v| !v.trim().is_empty()),
siliconflow_model: std::env::var("SILICONFLOW_MODEL")
.ok()
.filter(|v| !v.trim().is_empty()),
arcee_base_url: std::env::var("ARCEE_BASE_URL")
.ok()
.filter(|v| !v.trim().is_empty()),
moonshot_base_url: std::env::var("MOONSHOT_BASE_URL")
.or_else(|_| std::env::var("KIMI_BASE_URL"))
.ok()
.filter(|v| !v.trim().is_empty()),
sglang_base_url: std::env::var("SGLANG_BASE_URL")
.ok()
.filter(|v| !v.trim().is_empty()),
vllm_base_url: std::env::var("VLLM_BASE_URL")
.ok()
.filter(|v| !v.trim().is_empty()),
ollama_base_url: std::env::var("OLLAMA_BASE_URL")
.ok()
.filter(|v| !v.trim().is_empty()),
ollama_cloud_base_url: std::env::var("OLLAMA_CLOUD_BASE_URL")
.ok()
.filter(|v| !v.trim().is_empty()),
ollama_cloud_model: std::env::var("OLLAMA_CLOUD_MODEL")
.ok()
.filter(|v| !v.trim().is_empty()),
huggingface_base_url: std::env::var("HUGGINGFACE_BASE_URL")
.or_else(|_| std::env::var("HF_BASE_URL"))
.ok()
.filter(|v| !v.trim().is_empty()),
huggingface_model: std::env::var("HUGGINGFACE_MODEL")
.or_else(|_| std::env::var("HF_MODEL"))
.ok()
.filter(|v| !v.trim().is_empty()),
modelscope_base_url: std::env::var("MODELSCOPE_BASE_URL")
.ok()
.filter(|v| !v.trim().is_empty()),
modelscope_model: std::env::var("MODELSCOPE_MODEL")
.ok()
.filter(|v| !v.trim().is_empty()),
together_base_url: std::env::var("TOGETHER_BASE_URL")
.ok()
.filter(|v| !v.trim().is_empty()),
together_model: std::env::var("TOGETHER_MODEL")
.ok()
.filter(|v| !v.trim().is_empty()),
qianfan_base_url: std::env::var("QIANFAN_BASE_URL")
.ok()
.filter(|v| !v.trim().is_empty())
.or_else(|| {
std::env::var("BAIDU_QIANFAN_BASE_URL")
.ok()
.filter(|v| !v.trim().is_empty())
}),
qianfan_model: std::env::var("QIANFAN_MODEL")
.ok()
.filter(|v| !v.trim().is_empty())
.or_else(|| {
std::env::var("BAIDU_QIANFAN_MODEL")
.ok()
.filter(|v| !v.trim().is_empty())
}),
openai_codex_base_url: std::env::var("OPENAI_CODEX_BASE_URL")
.or_else(|_| std::env::var("CODEX_BASE_URL"))
.ok()
.filter(|v| !v.trim().is_empty()),
openai_codex_model: std::env::var("OPENAI_CODEX_MODEL")
.or_else(|_| std::env::var("CODEX_MODEL"))
.ok()
.filter(|v| !v.trim().is_empty()),
anthropic_base_url: std::env::var("ANTHROPIC_BASE_URL")
.ok()
.filter(|v| !v.trim().is_empty()),
anthropic_model: std::env::var("ANTHROPIC_MODEL")
.ok()
.filter(|v| !v.trim().is_empty()),
openmodel_base_url: std::env::var("OPENMODEL_BASE_URL")
.ok()
.filter(|v| !v.trim().is_empty()),
openmodel_model: std::env::var("OPENMODEL_MODEL")
.ok()
.filter(|v| !v.trim().is_empty()),
zai_base_url: std::env::var("ZAI_BASE_URL")
.or_else(|_| std::env::var("Z_AI_BASE_URL"))
.or_else(|_| std::env::var("ZHIPU_BASE_URL"))
.or_else(|_| std::env::var("ZHIPUAI_BASE_URL"))
.or_else(|_| std::env::var("BIGMODEL_BASE_URL"))
.ok()
.filter(|v| !v.trim().is_empty()),
zai_model: std::env::var("ZAI_MODEL")
.or_else(|_| std::env::var("Z_AI_MODEL"))
.or_else(|_| std::env::var("ZHIPU_MODEL"))
.or_else(|_| std::env::var("ZHIPUAI_MODEL"))
.or_else(|_| std::env::var("BIGMODEL_MODEL"))
.or_else(|_| std::env::var("GLM_MODEL"))
.ok()
.filter(|v| !v.trim().is_empty()),
stepfun_base_url: std::env::var("STEPFUN_BASE_URL")
.or_else(|_| std::env::var("STEP_BASE_URL"))
.ok()
.filter(|v| !v.trim().is_empty()),
stepfun_model: std::env::var("STEPFUN_MODEL")
.or_else(|_| std::env::var("STEP_MODEL"))
.ok()
.filter(|v| !v.trim().is_empty()),
minimax_base_url: std::env::var("MINIMAX_BASE_URL")
.ok()
.filter(|v| !v.trim().is_empty()),
minimax_anthropic_base_url: std::env::var("MINIMAX_ANTHROPIC_BASE_URL")
.ok()
.filter(|v| !v.trim().is_empty()),
minimax_model: std::env::var("MINIMAX_MODEL")
.ok()
.filter(|v| !v.trim().is_empty()),
deepinfra_base_url: std::env::var("DEEPINFRA_BASE_URL")
.ok()
.filter(|v| !v.trim().is_empty()),
deepinfra_model: std::env::var("DEEPINFRA_MODEL")
.ok()
.filter(|v| !v.trim().is_empty()),
sakana_base_url: std::env::var("SAKANA_BASE_URL")
.ok()
.filter(|v| !v.trim().is_empty()),
sakana_model: std::env::var("SAKANA_MODEL")
.ok()
.filter(|v| !v.trim().is_empty()),
longcat_base_url: std::env::var("LONGCAT_BASE_URL")
.ok()
.filter(|v| !v.trim().is_empty()),
longcat_model: std::env::var("LONGCAT_MODEL")
.ok()
.filter(|v| !v.trim().is_empty()),
opencode_go_base_url: std::env::var("OPENCODE_GO_BASE_URL")
.ok()
.filter(|v| !v.trim().is_empty()),
opencode_go_model: std::env::var("OPENCODE_GO_MODEL")
.ok()
.filter(|v| !v.trim().is_empty()),
opencode_zen_base_url: std::env::var("OPENCODE_ZEN_BASE_URL")
.ok()
.filter(|v| !v.trim().is_empty()),
opencode_zen_model: std::env::var("OPENCODE_ZEN_MODEL")
.ok()
.filter(|v| !v.trim().is_empty()),
meta_base_url: std::env::var("META_MODEL_API_BASE_URL")
.ok()
.filter(|v| !v.trim().is_empty())
.or_else(|| {
std::env::var("MODEL_API_BASE_URL")
.ok()
.filter(|v| !v.trim().is_empty())
}),
meta_model: std::env::var("META_MODEL_API_MODEL")
.ok()
.filter(|v| !v.trim().is_empty())
.or_else(|| {
std::env::var("MODEL_API_MODEL")
.ok()
.filter(|v| !v.trim().is_empty())
}),
xai_base_url: std::env::var("XAI_BASE_URL")
.ok()
.filter(|v| !v.trim().is_empty()),
xai_model: std::env::var("XAI_MODEL")
.ok()
.filter(|v| !v.trim().is_empty()),
google_base_url: std::env::var("GOOGLE_BASE_URL")
.ok()
.filter(|v| !v.trim().is_empty())
.or_else(|| {
std::env::var("GEMINI_BASE_URL")
.ok()
.filter(|v| !v.trim().is_empty())
}),
google_model: std::env::var("GOOGLE_MODEL")
.ok()
.filter(|v| !v.trim().is_empty())
.or_else(|| {
std::env::var("GEMINI_MODEL")
.ok()
.filter(|v| !v.trim().is_empty())
}),
mistral_base_url: std::env::var("MISTRAL_BASE_URL")
.ok()
.filter(|v| !v.trim().is_empty()),
mistral_model: std::env::var("MISTRAL_MODEL")
.ok()
.filter(|v| !v.trim().is_empty()),
telecomjs_base_url: std::env::var("TELECOMJS_BASE_URL")
.ok()
.filter(|v| !v.trim().is_empty()),
telecomjs_model: std::env::var("TELECOMJS_MODEL")
.ok()
.filter(|v| !v.trim().is_empty()),
edenai_base_url: std::env::var("EDENAI_BASE_URL")
.ok()
.filter(|v| !v.trim().is_empty()),
edenai_model: std::env::var("EDENAI_MODEL")
.ok()
.filter(|v| !v.trim().is_empty()),
zenmux_base_url: std::env::var("ZENMUX_BASE_URL")
.ok()
.filter(|v| !v.trim().is_empty()),
zenmux_model: std::env::var("ZENMUX_MODEL")
.ok()
.filter(|v| !v.trim().is_empty()),
csdn_base_url: std::env::var("CSDN_BASE_URL")
.ok()
.filter(|v| !v.trim().is_empty()),
csdn_model: std::env::var("CSDN_MODEL")
.ok()
.filter(|v| !v.trim().is_empty()),
concentrate_base_url: std::env::var("CONCENTRATE_BASE_URL")
.ok()
.filter(|v| !v.trim().is_empty()),
concentrate_model: std::env::var("CONCENTRATE_MODEL")
.ok()
.filter(|v| !v.trim().is_empty()),
// `CODEWHALE_API_BASE` is a trust boundary, not a plain string:
// an origin this route would refuse to send a bearer to is
// dropped here rather than resolved into a route.
codewhale_base_url: provider::codewhale_api_base_from_env(),
codewhale_model: env_non_blank(&["CODEWHALE_MODEL"]),
modelstudio_token_plan_base_url: env_non_blank(&["MODELSTUDIO_TOKEN_PLAN_BASE_URL"]),
modelstudio_token_plan_model: env_non_blank(&["MODELSTUDIO_TOKEN_PLAN_MODEL"]),
modelstudio_coding_plan_base_url: env_non_blank(&["MODELSTUDIO_CODING_PLAN_BASE_URL"]),
modelstudio_coding_plan_model: env_non_blank(&["MODELSTUDIO_CODING_PLAN_MODEL"]),
codewhale_model: std::env::var("CODEWHALE_MODEL")
.ok()
.filter(|v| !v.trim().is_empty()),
modelstudio_token_plan_base_url: std::env::var("MODELSTUDIO_TOKEN_PLAN_BASE_URL")
.ok()
.filter(|v| !v.trim().is_empty()),
modelstudio_token_plan_model: std::env::var("MODELSTUDIO_TOKEN_PLAN_MODEL")
.ok()
.filter(|v| !v.trim().is_empty()),
modelstudio_coding_plan_base_url: std::env::var("MODELSTUDIO_CODING_PLAN_BASE_URL")
.ok()
.filter(|v| !v.trim().is_empty()),
modelstudio_coding_plan_model: std::env::var("MODELSTUDIO_CODING_PLAN_MODEL")
.ok()
.filter(|v| !v.trim().is_empty()),
}
}
+7 -6
View File
@@ -27,11 +27,10 @@ pub fn resolve_stream_open_timeout(configured_secs: Option<u64>) -> std::time::D
secs.clamp(MIN_STREAM_OPEN_TIMEOUT_SECS, MAX_STREAM_OPEN_TIMEOUT_SECS),
),
_ => stream_open_timeout_from_env(
crate::env_overrides::env_non_blank(&[
"CODEWHALE_STREAM_OPEN_TIMEOUT_SECS",
"DEEPSEEK_STREAM_OPEN_TIMEOUT_SECS",
])
.as_deref(),
std::env::var("CODEWHALE_STREAM_OPEN_TIMEOUT_SECS")
.or_else(|_| std::env::var("DEEPSEEK_STREAM_OPEN_TIMEOUT_SECS"))
.ok()
.as_deref(),
),
}
}
@@ -51,7 +50,9 @@ pub fn stream_open_timeout_from_env(value: Option<&str>) -> std::time::Duration
/// `false`, ...) leaves HTTP/2 on.
#[must_use]
pub fn force_http1_from_env() -> bool {
crate::env_overrides::env_non_blank(&["CODEWHALE_FORCE_HTTP1", "DEEPSEEK_FORCE_HTTP1"])
std::env::var("CODEWHALE_FORCE_HTTP1")
.or_else(|_| std::env::var("DEEPSEEK_FORCE_HTTP1"))
.ok()
.map(|v| v.trim().to_ascii_lowercase())
.is_some_and(|v| matches!(v.as_str(), "1" | "true" | "yes" | "on"))
}
-54
View File
@@ -10767,60 +10767,6 @@ fn blank_or_unrecognized_env_overrides_do_not_shadow_config_or_legacy_vars() {
));
}
#[test]
fn blank_primary_env_vars_fall_through_to_their_legacy_aliases() {
let _lock = env_lock();
let _env = EnvGuard::without_deepseek_runtime_overrides();
let names = [
"CODEWHALE_FORCE_HTTP1",
"DEEPSEEK_FORCE_HTTP1",
"CODEWHALE_STREAM_OPEN_TIMEOUT_SECS",
"DEEPSEEK_STREAM_OPEN_TIMEOUT_SECS",
];
let saved: Vec<(&str, Option<OsString>)> = names
.iter()
.map(|name| (*name, env::var_os(name)))
.collect();
// SAFETY: env mutation is serialized by `env_lock` and restored below.
unsafe {
env::set_var("CODEWHALE_MODEL", "");
env::set_var("DEEPSEEK_MODEL", "legacy-model");
env::set_var("CODEWHALE_BASE_URL", " ");
env::set_var("DEEPSEEK_BASE_URL", "https://legacy.example/v1");
env::set_var("MOONSHOT_BASE_URL", "");
env::set_var("KIMI_BASE_URL", "https://kimi.example/v1");
env::set_var("CODEWHALE_FORCE_HTTP1", "");
env::set_var("DEEPSEEK_FORCE_HTTP1", "1");
env::set_var("CODEWHALE_STREAM_OPEN_TIMEOUT_SECS", "");
env::set_var("DEEPSEEK_STREAM_OPEN_TIMEOUT_SECS", "120");
}
let env_overrides = EnvRuntimeOverrides::load();
let force_http1 = crate::stream_timeouts::force_http1_from_env();
let open_timeout = crate::stream_timeouts::resolve_stream_open_timeout(None);
unsafe {
for (name, value) in saved {
match value {
Some(value) => env::set_var(name, value),
None => env::remove_var(name),
}
}
}
assert_eq!(env_overrides.model.as_deref(), Some("legacy-model"));
assert_eq!(
env_overrides.active_route_base_url.as_deref(),
Some("https://legacy.example/v1")
);
assert_eq!(
env_overrides.moonshot_base_url.as_deref(),
Some("https://kimi.example/v1")
);
assert!(force_http1);
assert_eq!(open_timeout, std::time::Duration::from_secs(120));
}
#[cfg(unix)]
#[test]
fn failed_legacy_state_copy_leaves_no_partial_primary() {
+3 -5
View File
@@ -1183,7 +1183,7 @@ impl Git {
overrides: Vec<(String, &'static str)>,
) -> anyhow::Result<Command> {
let mut command = Self::review_base(workspace)?;
let mut count = 3;
let mut count = 2;
for (key, value) in overrides {
// A subsection may contain '='; `-c key=value` would then
// override a different key. Separate env fields preserve it.
@@ -1195,7 +1195,7 @@ impl Git {
Ok(command)
}
/// Git with fsmonitor, hooks, lazy fetch, replace objects and path quoting disabled,
/// Git with fsmonitor, hooks, lazy fetch and replace objects disabled,
/// running in `workspace`. [`Self::review_command`] adds filter overrides.
pub fn review_base(workspace: &Path) -> anyhow::Result<Command> {
use anyhow::Context;
@@ -1257,7 +1257,7 @@ impl Git {
// Both phases must observe the same effective repository config.
.env_remove("GIT_CONFIG")
.env_remove("GIT_CONFIG_PARAMETERS")
.env("GIT_CONFIG_COUNT", "3")
.env("GIT_CONFIG_COUNT", "2")
.env("GIT_CONFIG_KEY_0", "core.fsmonitor")
.env("GIT_CONFIG_VALUE_0", "false")
.env("GIT_CONFIG_KEY_1", "core.hooksPath")
@@ -1265,8 +1265,6 @@ impl Git {
"GIT_CONFIG_VALUE_1",
if cfg!(windows) { "NUL" } else { "/dev/null" },
)
.env("GIT_CONFIG_KEY_2", "core.quotePath")
.env("GIT_CONFIG_VALUE_2", "false")
.env("GIT_NO_LAZY_FETCH", "1")
.env("GIT_NO_REPLACE_OBJECTS", "1");
Ok(command)
@@ -2152,7 +2152,10 @@ fn git_push_refspec_is_protected(refspec: &str) -> bool {
// Tag-like names (`v1`, `v0.9.1`): git resolves branch-vs-tag on the
// server, so treat them as publishes.
let mut chars = lower.chars();
chars.next() == Some('v') && chars.next().is_some_and(|ch| ch.is_ascii_digit())
if chars.next() == Some('v') && chars.next().is_some_and(|ch| ch.is_ascii_digit()) {
return true;
}
false
}
fn git_tag_tokens_are_publish_like(tokens: &[&str]) -> bool {
+2 -2
View File
@@ -314,8 +314,8 @@ pub struct EngineConfig {
pub session_id: Option<String>,
/// Optional host-owned root for delegated-agent runtime state.
///
/// When unset, the worker ledger, complete transcript artifacts and
/// coordination lock retain their historical location under
/// When unset, the worker ledger and complete transcript artifacts
/// retain their historical location under
/// `workspace/.codewhale/state`. Embedders may set a session-scoped root
/// to separate that control-plane state from the execution workspace.
/// Child cwd and file authority still derive from `workspace`; hosts using
@@ -203,13 +203,10 @@ impl Drop for TestOverlay {
fn overlay_granted() -> bool {
#[cfg(test)]
{
TEST_OVERLAY.with(std::cell::Cell::get)
}
#[cfg(not(test))]
{
false
if TEST_OVERLAY.with(std::cell::Cell::get) {
return true;
}
false
}
fn caller(call: &BrokerCall, tool_name: &'static str) -> Result<ToolContext, BrokerRefusal> {
-13
View File
@@ -187,19 +187,6 @@ pub const AGENTS: &[ExternalAgent] = &[
adapter_binary: None,
adapter_cli_env: None,
},
ExternalAgent {
id: "qwen-code",
name: "Qwen Code",
binary: "qwen",
acp_args: Some(&["--acp"]),
website: "https://github.com/QwenLM/qwen-code",
min_version: Some("0.6.1"),
upgrade: Some("npm i -g @qwen-code/qwen-code@latest"),
login: None,
adapter: None,
adapter_binary: None,
adapter_cli_env: None,
},
];
const FIXTURE_AGENT: ExternalAgent = ExternalAgent {
+5 -1
View File
@@ -1748,7 +1748,11 @@ impl FleetManager {
return Err(start_error);
}
};
Ok(started)
if !started {
return Ok(false);
}
Ok(true)
}
fn start_leased_workers(
-44
View File
@@ -1669,10 +1669,6 @@ pub trait McpTransport: Send + Sync {
async fn recv(&mut self) -> Result<Vec<u8>>;
async fn send_reply(&mut self, msg: Vec<u8>) -> Result<()> {
self.send(msg).await
}
/// Record the protocol revision negotiated at `initialize`. Only the
/// Streamable HTTP transport uses it (the `MCP-Protocol-Version` header on
/// subsequent requests); stdio and legacy SSE have no header channel, so
@@ -3129,30 +3125,6 @@ impl McpConnection {
result
}
async fn answer_server_request(&mut self, reply: &serde_json::Value) {
let result = match serde_json::to_vec(reply) {
Ok(bytes) => {
let cancel_token = self.cancel_token.clone();
tokio::select! {
biased;
_ = cancel_token.cancelled() => {
Err(anyhow::anyhow!("MCP connection was cancelled"))
}
result = self.transport.send_reply(bytes) => result,
}
}
Err(err) => Err(err.into()),
};
if let Err(err) = result {
tracing::debug!(
target: "mcp",
server = %self.name,
error = %err,
"could not answer an MCP server request"
);
}
}
/// Handshake and discovery receive: each frame wait is bounded by the read
/// knob, and a server that stays silent past it is treated as dead.
async fn recv(&mut self, expected_id: String) -> Result<serde_json::Value> {
@@ -3230,22 +3202,6 @@ impl McpConnection {
{
self.catalog_stale = true;
}
if let Some(method) = value.get("method") {
if let Some(id) = value.get("id") {
let method = method.as_str().unwrap_or_default();
let reply = if method == "ping" {
serde_json::json!({"jsonrpc": "2.0", "id": id, "result": {}})
} else {
serde_json::json!({
"jsonrpc": "2.0",
"id": id,
"error": {"code": -32601, "message": format!("Method not found: {method}")}
})
};
self.answer_server_request(&reply).await;
}
continue;
}
if response_id_matches(value.get("id"), &expected_id) {
// Marks the connection stale so it is rebuilt, but this is a
// reply to the request, so it never qualifies for a replay.
-15
View File
@@ -177,21 +177,6 @@ impl McpTransport for HttpTransport {
}
}
async fn send_reply(&mut self, msg: Vec<u8>) -> Result<()> {
match &mut self.mode {
HttpTransportMode::Streamable(transport) => {
transport.send(msg).await.map_err(|err| match err {
StreamableSendError::Incompatible(detail)
| StreamableSendError::StaleSession(detail) => anyhow::anyhow!(
"MCP Streamable HTTP refused a reply to a server request: {detail}"
),
StreamableSendError::Other(err) => err,
})
}
HttpTransportMode::Sse(transport) => transport.send(msg).await,
}
}
async fn recv(&mut self) -> Result<Vec<u8>> {
match &mut self.mode {
HttpTransportMode::Streamable(transport) => transport.recv().await,
-128
View File
@@ -3314,80 +3314,6 @@ async fn call_method_skips_notifications_and_unmatched_responses() {
assert_eq!(sent[0]["method"], "tools/call");
}
#[tokio::test]
async fn call_method_answers_server_requests_instead_of_taking_them_as_the_reply() {
let sent = Arc::new(Mutex::new(Vec::new()));
let transport = ScriptedValueTransport {
sent: Arc::clone(&sent),
responses: VecDeque::from([
json_frame(serde_json::json!({"jsonrpc": "2.0", "id": 1, "method": "ping"})),
json_frame(serde_json::json!({"jsonrpc": "2.0", "id": "1", "method": "roots/list"})),
json_frame(serde_json::json!({"jsonrpc": "2.0", "id": 1, "result": {"ok": true}})),
]),
};
let mut conn = test_connection(Box::new(transport));
let result = conn
.call_method("tools/call", serde_json::json!({"name": "echo"}), 1)
.await
.unwrap();
assert_eq!(result, serde_json::json!({"ok": true}));
let sent = sent.lock().unwrap();
assert_eq!(sent.len(), 3);
assert_eq!(
sent[1],
serde_json::json!({"jsonrpc": "2.0", "id": 1, "result": {}})
);
assert_eq!(sent[2]["id"], "1");
assert_eq!(sent[2]["error"]["code"], -32601);
}
struct ReplyRefusingTransport {
sent: Arc<Mutex<Vec<serde_json::Value>>>,
responses: VecDeque<Vec<u8>>,
}
#[async_trait::async_trait]
impl McpTransport for ReplyRefusingTransport {
async fn send(&mut self, msg: Vec<u8>) -> Result<()> {
let msg: serde_json::Value = serde_json::from_slice(&msg)?;
anyhow::ensure!(msg.get("method").is_some(), "status=405 Method Not Allowed");
self.sent.lock().unwrap().push(msg);
Ok(())
}
async fn recv(&mut self) -> Result<Vec<u8>> {
self.responses
.pop_front()
.context("scripted transport exhausted")
}
}
#[tokio::test]
async fn call_method_keeps_its_reply_when_answering_a_server_request_fails() {
let sent = Arc::new(Mutex::new(Vec::new()));
let transport = ReplyRefusingTransport {
sent: Arc::clone(&sent),
responses: VecDeque::from([
json_frame(serde_json::json!({"jsonrpc": "2.0", "id": 1, "method": "ping"})),
json_frame(serde_json::json!({"jsonrpc": "2.0", "id": 1, "result": {"ok": true}})),
]),
};
let mut conn = test_connection(Box::new(transport));
let result = conn
.call_method("tools/call", serde_json::json!({"name": "echo"}), 1)
.await
.unwrap();
assert_eq!(result, serde_json::json!({"ok": true}));
assert_eq!(conn.state, ConnectionState::Ready);
let sent = sent.lock().unwrap();
assert_eq!(sent.len(), 1);
assert_eq!(sent[0]["method"], "tools/call");
}
#[tokio::test]
async fn call_method_invalid_json_includes_server_output_preview() {
let sent = Arc::new(Mutex::new(Vec::new()));
@@ -6844,60 +6770,6 @@ async fn custom_headers_applied_to_get_preflight() {
);
}
#[tokio::test]
async fn refused_reply_to_a_server_request_does_not_fall_back_to_sse() {
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpListener;
let _env = crate::test_support::lock_test_env();
let _lock = lock_mcp_loopback_tests().await;
let _no_proxy = crate::test_support::EnvVarGuard::set("NO_PROXY", "*,127.0.0.1");
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
let requests = Arc::new(Mutex::new(Vec::new()));
let requests_srv = Arc::clone(&requests);
let server = tokio::spawn(async move {
loop {
let (mut socket, _) = listener.accept().await.unwrap();
let mut buf = [0u8; 4096];
let n = socket.read(&mut buf).await.unwrap();
let request_line = String::from_utf8_lossy(&buf[..n])
.lines()
.next()
.unwrap_or_default()
.to_string();
requests_srv.lock().unwrap().push(request_line);
socket
.write_all(
b"HTTP/1.1 405 Method Not Allowed\r\nConnection: close\r\nContent-Length: 0\r\n\r\n",
)
.await
.unwrap();
}
});
let url = format!("http://{addr}/mcp");
let mut transport = HttpTransport::new(
test_mcp_http_client(&url),
url,
tokio_util::sync::CancellationToken::new(),
Duration::from_secs(2),
);
let result = transport
.send_reply(json_frame(
serde_json::json!({"jsonrpc": "2.0", "id": 1, "result": {}}),
))
.await;
server.abort();
assert!(result.is_err(), "a refused reply reports the refusal");
let requests = requests.lock().unwrap();
assert_eq!(requests.len(), 1, "requests seen: {requests:?}");
assert!(requests[0].starts_with("POST "), "{requests:?}");
}
// === add_runtime_server_config conflict tests ===
#[test]
+10 -26
View File
@@ -244,38 +244,22 @@ pub async fn capture(
if let Some(snapshot) = supplied {
return snapshot.render().map(Some);
}
#[cfg(any(test, feature = "test-support"))]
{
if let Some(message) = TEST_ACCOUNT_LOAD_FAILURE.get() {
anyhow::bail!(message);
}
if !account_api_base_is_loopback() {
return Ok(crate::prompts::load_user_constitution_block());
}
}
// Unit tests never read an operator's account credentials. Transport tests
// exercise load_from with an explicitly isolated store instead.
#[cfg(not(any(test, feature = "test-support")))]
if let Some(snapshot) = load(profile).await? {
return snapshot.render().map(Some);
}
#[cfg(any(test, feature = "test-support"))]
{
let _ = profile;
if let Some(message) = TEST_ACCOUNT_LOAD_FAILURE.get() {
anyhow::bail!(message);
}
}
Ok(crate::prompts::load_user_constitution_block())
}
#[cfg(any(test, feature = "test-support"))]
fn account_api_base_is_loopback() -> bool {
std::env::var(codewhale_secrets::account::ACCOUNT_API_BASE_ENV)
.ok()
.and_then(|value| normalize_runtime_account_api_base(&value))
.and_then(|base| reqwest::Url::parse(&base).ok())
.and_then(|url| url.host_str().map(str::to_owned))
.is_some_and(|host| {
host.eq_ignore_ascii_case("localhost")
|| host
.trim_start_matches('[')
.trim_end_matches(']')
.parse::<std::net::IpAddr>()
.is_ok_and(|address| address.is_loopback())
})
}
#[cfg(any(test, feature = "test-support"))]
thread_local! {
static TEST_ACCOUNT_LOAD_FAILURE: std::cell::Cell<Option<&'static str>> =
+1 -2
View File
@@ -11615,10 +11615,9 @@ async fn compact_thread_receipt_keeps_exact_named_custom_identity() -> Result<()
#[tokio::test]
async fn compact_thread_with_real_engine_reaches_terminal_status() -> Result<()> {
// A real engine takes the workspace coordination lock exclusively, and
// nextest runs tests in parallel processes with this crate as their working
// directory. A `"."` workspace would therefore be the crate directory
// itself, and two exclusive holders of it cannot both start.
// itself, shared by every test process.
let workspace = tempfile::tempdir()?;
let manager = RuntimeThreadManager::open(
{
+1 -1
View File
@@ -1721,7 +1721,7 @@ mod tests {
.expect_err("mutation must fail when its receipt cannot persist")
.to_string();
assert!(
error.contains("Runtime coordination was opened without its writer lock"),
error.contains("delegated durable store admission failed"),
"{error}"
);
assert!(
@@ -133,9 +133,14 @@ impl WriteScopeClaim {
return true;
}
}
self.contracts
if self
.contracts
.iter()
.any(|contract| other.contracts.iter().any(|other| other == contract))
{
return true;
}
false
}
#[must_use]
@@ -321,9 +326,10 @@ pub struct CoordinationDetailProjection {
pub metrics: CoordinationDetailMetrics,
pub bounded: bool,
pub limit: usize,
/// Whether this process currently holds the workspace coordination flock.
/// When false, durable ledger writes are skipped and the UI must say so —
/// a counter must never tick on a turn the engine has already settled.
/// Whether this manager's durable store is admitted. When false, durable
/// ledger writes are refused until admission is retried and the UI must
/// say so — a counter must never tick on a turn the engine has already
/// settled.
#[serde(default = "default_process_lock_held")]
pub process_lock_held: bool,
/// Human-readable reason when [`Self::process_lock_held`] is false.
+134 -435
View File
@@ -499,7 +499,6 @@ const SUBAGENT_TRANSCRIPT_ARTIFACT_DIR: &str = "subagent-transcripts";
const SUBAGENT_STATE_SCHEMA_VERSION: u32 = 1;
const SUBAGENT_STATE_FILE: &str = "subagents.v3.json";
const LEGACY_SUBAGENT_STATE_FILE: &str = "subagents.v1.json";
const SUBAGENT_STATE_LOCK_FILE: &str = "subagents.v1.lock";
const SUBAGENT_RESTART_REASON: &str = "Interrupted by process restart";
const SUBAGENT_SESSION_CLOSED_REASON: &str = "Interrupted: parent session closed";
#[cfg(test)]
@@ -3933,12 +3932,6 @@ impl SubAgent {
}
}
/// Manager for active sub-agents.
struct CoordinationProcessLock {
release: Option<std::sync::mpsc::SyncSender<()>>,
thread: Option<std::thread::JoinHandle<()>>,
}
/// Marker prefix retained for presentation compatibility with same-process
/// handover notes from earlier hosts. It does not control lock acquisition
/// or retry.
@@ -3954,123 +3947,6 @@ pub const COORDINATION_LOCK_TIMEOUT_MARKER: &str =
pub const SESSION_PRIVATE_COORDINATION: &str = "another Codewhale session still uses this folder's older delegated-agent store, so its upgrade waits; sub-agents in this session keep private state that other sessions do not see and that is not saved, and the store upgrades when a session starts after the other one exits";
#[derive(Debug)]
struct SubAgentCutoverBlocked(anyhow::Error);
impl std::fmt::Display for SubAgentCutoverBlocked {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
impl std::error::Error for SubAgentCutoverBlocked {}
impl CoordinationProcessLock {
fn acquire(state_root: &Path, exclusive: bool) -> Result<Self> {
let requested_root = normalize_subagent_workspace(state_root);
// Root and path for the lock. A non-home workspace keeps itself as the
// root, so a repository-supplied `.codewhale` link is walked and
// refused before `create_dir_all` could follow it into the target. A
// home workspace resolves its own junction once and still walks
// everything below it.
let (walk_root, lock_path) = codewhale_util::files::workspace_state_path(
&requested_root,
&Path::new("state").join(SUBAGENT_STATE_LOCK_FILE),
);
let lock_dir = lock_path
.parent()
.map_or_else(|| walk_root.clone(), Path::to_path_buf);
// Refuse a linked `.codewhale` or `state` before creating anything:
// `create_dir_all` follows links and would populate the target.
reject_root_relative_symlinks(&walk_root, &lock_path)?;
fs::create_dir_all(&lock_dir)?;
let ignore = lock_dir.join(".gitignore");
let ignored = reject_root_relative_symlinks(&walk_root, &ignore).and_then(|()| {
match fs::OpenOptions::new()
.write(true)
.create_new(true)
.open(&ignore)
{
Ok(mut file) => file.write_all(b"*\n").map_err(Into::into),
Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => Ok(()),
Err(error) => Err(error.into()),
}
});
if let Err(error) = ignored {
tracing::warn!(target: "subagent", %error, "could not write {}", ignore.display());
}
// Creating a missing root can change its canonical spelling on
// Windows (for example by adding a `\\?\` prefix). Re-resolve both
// sides before checking containment.
let state_root = normalize_subagent_workspace(state_root);
let (walk_root, lock_path) = codewhale_util::files::workspace_state_path(
&state_root,
&Path::new("state").join(SUBAGENT_STATE_LOCK_FILE),
);
reject_root_relative_symlinks(&walk_root, &lock_path)?;
let lock_file = std::fs::OpenOptions::new()
.create(true)
.truncate(false)
.read(true)
.write(true)
.open(&lock_path)?;
let (ready_tx, ready_rx) = std::sync::mpsc::sync_channel(1);
let (release_tx, release_rx) = std::sync::mpsc::sync_channel(0);
let thread = std::thread::spawn(move || {
let mut lock = fd_lock::RwLock::new(lock_file);
let acquired = if exclusive {
lock.try_write().map(|guard| {
let _ = ready_tx.send(Ok::<(), String>(()));
let _guard = guard;
let _ = release_rx.recv();
})
} else {
lock.try_read().map(|guard| {
let _ = ready_tx.send(Ok::<(), String>(()));
let _guard = guard;
let _ = release_rx.recv();
})
};
match acquired {
Ok(()) => {}
Err(error) => {
let _ = ready_tx.send(Err(error.to_string()));
}
}
});
match ready_rx.recv_timeout(Duration::from_secs(5)) {
Ok(Ok(())) => Ok(Self {
release: Some(release_tx),
thread: Some(thread),
}),
Ok(Err(error)) => {
let _ = thread.join();
Err(anyhow!(
"delegated coordination lock is unavailable for {}: {error}; close any other running or suspended Codewhale session using this workspace, then restart this Runtime if admission was refused",
state_root.display()
))
}
Err(error) => {
drop(release_tx);
let _ = thread.join();
Err(anyhow!(
"{COORDINATION_LOCK_TIMEOUT_MARKER} for {}: {error}",
state_root.display()
))
}
}
}
}
impl Drop for CoordinationProcessLock {
fn drop(&mut self) {
self.release.take();
if let Some(thread) = self.thread.take() {
let _ = thread.join();
}
}
}
/// Only the captured Runtime owner admits roots. Held handles are not serialized;
/// a restored attributed claim must be re-admitted before it can authorize work.
struct AdmittedCoordinationRoot {
@@ -4106,19 +3982,12 @@ pub struct SubAgentManager {
worker_origins: HashMap<String, WorkerOrigin>,
worker_origin_schema: u32,
workspace: PathBuf,
/// Root that owns the delegated-agent ledger, complete transcript
/// artifacts and coordination lock. Defaults to `workspace`; embedders may
/// Root that owns the delegated-agent ledger and complete transcript
/// artifacts. Defaults to `workspace`; embedders may
/// isolate it without changing child execution cwd or file authority.
state_root: PathBuf,
state_path: Option<PathBuf>,
state_unreachable_warned: bool,
coordination_process_lock: std::sync::Mutex<Option<Arc<CoordinationProcessLock>>>,
coordination_process_lock_required: bool,
coordination_exclusive: bool,
coordination_snapshot_read_only: bool,
store_cutover_blocked: bool,
session_private: bool,
workspace_writer_locks: HashMap<PathBuf, Arc<CoordinationProcessLock>>,
/// Configured default per-child model-turn budget (`[subagents]
/// default_max_steps`, #5324). `None` keeps the unbounded Fleet default;
/// an explicit spawn `max_steps` still wins. Zero means unbounded.
@@ -4759,13 +4628,6 @@ impl SubAgentManager {
state_root,
state_path: None,
state_unreachable_warned: false,
coordination_process_lock: std::sync::Mutex::new(None),
coordination_process_lock_required: false,
coordination_exclusive: false,
coordination_snapshot_read_only: false,
store_cutover_blocked: false,
session_private: false,
workspace_writer_locks: HashMap::new(),
max_steps: None,
wall_time: None,
max_agents,
@@ -4831,22 +4693,6 @@ impl SubAgentManager {
return Err("coordination workspace identity is unsupported on this platform".into());
#[cfg(any(unix, windows))]
{
if self.coordination_exclusive
&& !self.coordination_snapshot_read_only
&& normalize_subagent_workspace(&lexical)
!= normalize_subagent_workspace(&self.state_root)
&& !self.workspace_writer_locks.contains_key(&canonical_root)
{
match CoordinationProcessLock::acquire(&lexical, true) {
Ok(lock) => {
self.workspace_writer_locks
.insert(canonical_root.clone(), Arc::new(lock));
}
Err(error) => {
tracing::warn!(target: "subagent", %error, "admitted coordination workspace without its writer lock; shared-workspace writers there stay refused until it is free");
}
}
}
let receipt = CoordinationClaimScope {
canonical_root,
platform: platform.into(),
@@ -4866,44 +4712,61 @@ impl SubAgentManager {
}
}
fn ensure_origin_writer_lock(&mut self, origin: &WorkerOrigin) -> Result<(), String> {
let (Some(selected), Some(scope)) = (&origin.selected_workspace, &origin.scope) else {
fn refuse_live_selected_store_writers(&self, origin: &WorkerOrigin) -> Result<(), String> {
let (Some(selected), Some(_)) = (&origin.selected_workspace, &origin.scope) else {
return Ok(());
};
if !self.coordination_exclusive
|| normalize_subagent_workspace(selected)
== normalize_subagent_workspace(&self.state_root)
|| self
.workspace_writer_locks
.contains_key(&scope.canonical_root)
if normalize_subagent_workspace(selected) == normalize_subagent_workspace(&self.state_root)
{
return Ok(());
}
let lock =
CoordinationProcessLock::acquire(selected, true).map_err(|error| error.to_string())?;
self.workspace_writer_locks
.insert(scope.canonical_root.clone(), Arc::new(lock));
let path = default_state_path(selected).map_err(|error| error.to_string())?;
let unreadable = |error: std::io::Error| {
format!(
"cannot check {} for another open Codewhale session ({error}); shared-workspace writers there stay refused, or launch the builder in an isolated worktree",
selected.display()
)
};
let directory = path
.parent()
.ok_or_else(|| "delegated store has no parent".to_string())?;
let entries = match fs::read_dir(directory) {
Ok(entries) => entries,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(()),
Err(error) => return Err(unreadable(error)),
};
for entry in entries {
let name = entry.map_err(unreadable)?.file_name();
let Some(writer) = name
.to_str()
.and_then(|name| name.strip_prefix("subagents.v3.writer."))
.and_then(|name| name.strip_suffix(".lock"))
else {
continue;
};
if valid_store_writer(writer) && store_writer_is_live(&path, writer) {
return Err(format!(
"another Codewhale session ({writer}) has {} open; shared-workspace writers there stay refused until it exits, or launch the builder in an isolated worktree",
selected.display()
));
}
}
Ok(())
}
fn ensure_worker_coordination_locks(
&mut self,
fn admit_worker_shared_writes(
&self,
spec: &AgentWorkerSpec,
origin: &WorkerOrigin,
) -> Result<(), String> {
let claim = worker_coordination_claim(spec)?;
if claim.as_ref().is_some_and(|(_, isolated)| *isolated) {
return Ok(());
match worker_coordination_claim(spec)? {
Some((_, false)) => self.refuse_live_selected_store_writers(origin),
_ => Ok(()),
}
self.ensure_coordination_process_lock()?;
if claim.is_some() {
self.ensure_origin_writer_lock(origin)?;
}
Ok(())
}
pub fn ensure_registered_worker_coordination_locks(
&mut self,
&self,
worker_id: &str,
) -> Result<(), String> {
let spec = self
@@ -4918,7 +4781,7 @@ impl SubAgentManager {
.cloned()
.ok_or_else(|| "worker origin is missing from the owner journal".to_string())?;
self.validate_worker_origin(&origin)?;
self.ensure_worker_coordination_locks(&spec, &origin)
self.admit_worker_shared_writes(&spec, &origin)
}
fn admitted_coordination_root(&self, workspace: &Path) -> Option<&AdmittedCoordinationRoot> {
@@ -5316,7 +5179,6 @@ impl SubAgentManager {
.map_err(|error| error.to_string());
}
self.ensure_coordination_process_lock()?;
if let Some(root) = self.admitted_coordination_root(workspace) {
root.validate()?;
}
@@ -5395,7 +5257,6 @@ impl SubAgentManager {
.map_err(|error| error.to_string());
}
self.ensure_coordination_process_lock()?;
self.coordination
.update_decision_status(decision_id, status, owner, expected_version)
}
@@ -5442,7 +5303,6 @@ impl SubAgentManager {
.map_err(|error| error.to_string());
}
self.ensure_coordination_process_lock()?;
let expected_owner = self.nearest_common_fan_in_owner(&input_decisions)?;
if owner != expected_owner {
return Err(format!(
@@ -5714,19 +5574,13 @@ impl SubAgentManager {
},
bounded: true,
limit,
process_lock_held: !self.session_private && {
// Retry acquire on each projection so a session that lost the
// flock at construction recovers once the previous owner exits.
let _ = self.coordination_process_lock_status();
self.holds_coordination_process_lock()
},
process_lock_note: if self.session_private {
Some(SESSION_PRIVATE_COORDINATION.to_string())
} else if self.holds_coordination_process_lock() {
None
} else {
self.coordination_process_lock_status().err()
},
process_lock_held: self.state_path.is_none() || self.store_authority.is_some(),
process_lock_note: (self.state_path.is_some() && self.store_authority.is_none()).then(
|| {
"delegated store admission failed; it is retried on the next delegated write"
.to_string()
},
),
}
}
@@ -5861,7 +5715,6 @@ impl SubAgentManager {
.and_then(|outcome| outcome);
}
self.ensure_coordination_process_lock()?;
let Some(existing) = self
.coordination
.write_claims
@@ -5948,7 +5801,6 @@ impl SubAgentManager {
.map_err(|error| error.to_string());
}
self.ensure_coordination_process_lock()?;
let active = self.active_coordination_owners();
let released = self
.coordination
@@ -6073,109 +5925,12 @@ impl SubAgentManager {
match SubAgentStoreAuthority::open(&self.state_root, &path, &self.current_session_boot_id) {
Ok(authority) => self.store_authority = Some(authority),
Err(error) => {
self.coordination_snapshot_read_only = true;
self.store_cutover_blocked = error
.chain()
.any(|cause| cause.is::<SubAgentCutoverBlocked>());
tracing::warn!(target: "subagent", %error, "delegated store admission failed; writable state remains refused");
tracing::warn!(target: "subagent", %error, "delegated store admission failed; it is retried on the next delegated write");
}
}
self
}
fn into_session_private(mut self) -> Self {
self.state_path = None;
self.store_authority = None;
self.coordination_snapshot_read_only = false;
self.coordination_process_lock_required = false;
self.session_private = true;
tracing::warn!(target: "subagent", "{SESSION_PRIVATE_COORDINATION}");
self
}
fn require_coordination_process_lock(mut self) -> Self {
self.coordination_process_lock_required = true;
if self.store_authority.is_none()
&& let Some(path) = self.state_path.as_ref()
{
match SubAgentStoreAuthority::open(
&self.state_root,
path,
&self.current_session_boot_id,
) {
Ok(authority) => self.store_authority = Some(authority),
Err(error) => {
self.coordination_snapshot_read_only = true;
tracing::warn!(target: "subagent", %error, "delegated store migration or admission failed; no durable writes are admitted");
return self;
}
}
}
let acquired =
CoordinationProcessLock::acquire(&self.state_root, self.coordination_exclusive)
.ok()
.map(Arc::new);
if acquired.is_none() {
return self.into_session_private();
}
*self
.coordination_process_lock
.get_mut()
.expect("coordination lock slot poisoned") = acquired;
self
}
/// Terminal sessions retry failed acquisition when the previous owner
/// exits. A Runtime loaded without its writer lock must restart rather
/// than publish its stale snapshot. The flock is held on an open fd —
/// deleting the lock file does not release it (#5036).
fn ensure_coordination_process_lock(&self) -> Result<(), String> {
if self.coordination_snapshot_read_only {
return Err(
"Runtime coordination was opened without its writer lock; stop and restart this Runtime process after the other session exits"
.into(),
);
}
if !self.coordination_process_lock_required {
return Ok(());
}
let mut slot = self
.coordination_process_lock
.lock()
.expect("coordination lock slot poisoned");
if slot.is_some() {
return Ok(());
}
match CoordinationProcessLock::acquire(&self.state_root, self.coordination_exclusive) {
Ok(lock) => {
*slot = Some(Arc::new(lock));
Ok(())
}
Err(error) => Err(error.to_string()),
}
}
/// Whether this process currently owns the workspace coordination flock.
/// Read-only inspection of the in-process slot — does not attempt acquire.
#[must_use]
pub fn holds_coordination_process_lock(&self) -> bool {
if !self.coordination_process_lock_required {
// Tests and non-durable managers do not require the flock.
return true;
}
self.coordination_process_lock
.lock()
.expect("coordination lock slot poisoned")
.is_some()
}
/// Probe lock ownership for UI/inspect surfaces. Retries acquire when the
/// slot is empty so a terminal session can recover after the previous
/// owner exits (#2.6 / #5036); read-only Runtime snapshots require restart.
pub fn coordination_process_lock_status(&self) -> Result<(), String> {
self.ensure_coordination_process_lock()
}
#[must_use]
pub fn with_running_heartbeat_timeout(mut self, timeout: Duration) -> Self {
self.running_heartbeat_timeout = if timeout.is_zero() {
@@ -6223,18 +5978,31 @@ impl SubAgentManager {
.is_some_and(|authority| authority.owned.lock().contains(id))
}
fn admitted_store_authority(&mut self) -> Result<Option<Arc<SubAgentStoreAuthority>>> {
let Some(path) = self.state_path.as_ref() else {
return Ok(None);
};
if let Some(authority) = self.store_authority.as_ref() {
return Ok(Some(authority.clone()));
}
let authority =
SubAgentStoreAuthority::open(&self.state_root, path, &self.current_session_boot_id)
.map_err(|error| {
anyhow!(
"delegated durable store admission failed ({error:#}); it is retried on the next delegated write"
)
})?;
self.store_authority = Some(authority.clone());
Ok(Some(authority))
}
pub(crate) fn with_store_transaction<T>(
&mut self,
operation: impl FnOnce(&mut Self) -> Result<T>,
) -> Result<T> {
if self.state_path.is_none() {
let Some(authority) = self.admitted_store_authority()? else {
return operation(self);
}
self.ensure_coordination_process_lock()
.map_err(anyhow::Error::msg)?;
let authority = self.store_authority.clone().ok_or_else(|| {
anyhow!("delegated durable store admission failed; inspect before retrying")
})?;
};
if authority.is_current_transaction() {
return operation(self);
}
@@ -6435,8 +6203,6 @@ impl SubAgentManager {
worker_id: &str,
payload: PersistedSubAgentState,
) -> Result<Option<AgentWorkerRecord>> {
self.ensure_coordination_process_lock()
.map_err(anyhow::Error::msg)?;
if self.state_path.is_none() {
return Ok(None);
}
@@ -6534,8 +6300,6 @@ impl SubAgentManager {
/// completes. Callers may `.join()` for synchronous semantics or drop it
/// for fire-and-forget.
fn persist_state(&self) -> Result<std::thread::JoinHandle<()>> {
self.ensure_coordination_process_lock()
.map_err(anyhow::Error::msg)?;
let Some((_, payload)) = self.build_persist_payload()? else {
return Ok(std::thread::spawn(|| {}));
};
@@ -6543,16 +6307,7 @@ impl SubAgentManager {
.store_authority
.clone()
.ok_or_else(|| anyhow!("delegated store authority is unavailable"))?;
let writer_locks = self
.coordination_process_lock
.lock()
.expect("coordination lock slot poisoned")
.iter()
.cloned()
.chain(self.workspace_writer_locks.values().cloned())
.collect::<Vec<_>>();
Ok(std::thread::spawn(move || {
let _writer_locks = writer_locks;
if let Err(error) = authority.publish(&payload, None, None) {
tracing::warn!(target: "subagent", %error, "failed to persist sub-agent checkpoint");
}
@@ -6560,8 +6315,6 @@ impl SubAgentManager {
}
fn persist_state_synchronously(&self) -> Result<()> {
self.ensure_coordination_process_lock()
.map_err(anyhow::Error::msg)?;
let Some((_, payload)) = self.build_persist_payload()? else {
return Ok(());
};
@@ -6624,8 +6377,6 @@ impl SubAgentManager {
/// Unlike `persist_state`, this performs disk I/O **synchronously** to
/// guarantee data is flushed before the process exits.
pub fn flush_pending_persist(&mut self) -> Result<()> {
self.ensure_coordination_process_lock()
.map_err(anyhow::Error::msg)?;
if self.persist_pending {
self.persist_state_synchronously()?;
self.last_persist_at = Some(Instant::now());
@@ -6848,8 +6599,7 @@ impl SubAgentManager {
}
pub fn finalize_session_close_for_session(&mut self, active_session_id: &str) -> Result<usize> {
if !self.coordination_snapshot_read_only
&& (self.state_path.is_none() || self.store_authority.is_some())
if (self.state_path.is_none() || self.store_authority.is_some())
&& !self.agents.values().any(|agent| {
self.owns_store_record(&agent.id)
&& self.agent_is_owned_by_session(agent, active_session_id)
@@ -7108,10 +6858,9 @@ impl SubAgentManager {
let Some((claim, isolated_worktree)) = worker_coordination_claim(spec)? else {
return Ok(());
};
// Isolated-worktree builders mutate their own checkout, not the
// shared workspace, so they must not contend for the per-workspace
// coordination process lock (#5036).
self.ensure_worker_coordination_locks(spec, origin)?;
if !isolated_worktree {
self.refuse_live_selected_store_writers(origin)?;
}
let claim = self.namespace_write_claim(&origin_workspace, isolated_worktree, claim)?;
let scope = if isolated_worktree {
None
@@ -7191,11 +6940,7 @@ impl SubAgentManager {
let previous_origins = self.worker_origins.clone();
let previous_schema = self.worker_origin_schema;
let claim = worker_coordination_claim(&spec)?;
// Same isolation rule as `preflight_worker_coordination` (#5036).
let isolated_worktree = claim
.as_ref()
.is_some_and(|(_, isolated_worktree)| *isolated_worktree);
self.ensure_worker_coordination_locks(&spec, &origin)?;
self.admit_worker_shared_writes(&spec, &origin)?;
let previous_worker_records = self.worker_records.clone();
let previous_coordination = self.coordination.clone();
let persisted_claim = claim
@@ -7247,18 +6992,6 @@ impl SubAgentManager {
let worker_id_for_log = spec.worker_id.clone();
self.register_worker_for_session(spec, &origin.owner_session_id, None);
self.install_worker_origin(&worker_id_for_log, origin);
if isolated_worktree && self.ensure_coordination_process_lock().is_err() {
// This process does not own the durable coordination state and an
// isolated-worktree builder must not be blocked on it: register
// in memory only and leave the durable ledger to the lock owner.
// The persist-layer lock check stays intact (#5036).
tracing::warn!(
target: "subagent",
worker = %worker_id_for_log,
"registered isolated-worktree worker in memory only; delegated coordination lock is owned elsewhere"
);
return Ok(());
}
if let Err(error) = self.persist_state_synchronously() {
self.worker_records = previous_worker_records;
self.coordination = previous_coordination;
@@ -7684,7 +7417,6 @@ impl SubAgentManager {
.map_err(|error| error.to_string());
}
self.ensure_coordination_process_lock()?;
let worker_id = spec.worker_id.clone();
let previous = self
.worker_records
@@ -10327,20 +10059,9 @@ impl SubAgentManager {
));
}
}
if write_capable {
// Isolated-worktree children mutate their own checkout, so they
// do not contend for the shared-workspace process lock (#5036).
if !options.isolated_worktree {
self.ensure_coordination_process_lock()
.map_err(anyhow::Error::msg)?;
self.ensure_origin_writer_lock(&origin)
.map_err(anyhow::Error::msg)?;
}
if self.coordination_process_lock_required && self.state_path.is_none() {
return Err(anyhow!(
"write-capable sub-agent launch requires a durable coordination state path"
));
}
if write_capable && !options.isolated_worktree {
self.refuse_live_selected_store_writers(&origin)
.map_err(anyhow::Error::msg)?;
}
let runtime_origin_turn_id = if runtime.context.runtime.active_thread_id.as_deref()
== Some(runtime.context.state_namespace.as_str())
@@ -10574,7 +10295,9 @@ impl SubAgentManager {
self.resume_targets.insert(source.clone(), agent_id.clone());
}
self.agents.insert(agent_id.clone(), agent);
let persist_result = self.persist_state_synchronously();
let persist_result = self
.admitted_store_authority()
.and_then(|_| self.persist_state_synchronously());
agent = self
.agents
.remove(&agent_id)
@@ -10887,8 +10610,6 @@ impl SubAgentManager {
identity: &ChildActivationIdentity,
) -> Result<impl FnOnce() -> Result<()> + Send + 'static + use<>> {
self.exact_activation(identity)?;
self.ensure_coordination_process_lock()
.map_err(anyhow::Error::msg)?;
let (path, payload) = self
.build_persist_payload()?
.ok_or_else(|| anyhow!("child activation has no durable worker store"))?;
@@ -10913,16 +10634,7 @@ impl SubAgentManager {
.clone()
.ok_or_else(|| anyhow!("child activation has no store authority"))?;
let identity = identity.clone();
let writer_locks = self
.coordination_process_lock
.lock()
.expect("coordination lock slot poisoned")
.iter()
.cloned()
.chain(self.workspace_writer_locks.values().cloned())
.collect::<Vec<_>>();
Ok(move || {
let _writer_locks = writer_locks;
store_authority.publish(&payload, None, None)?;
let persisted = read_durable_subagent_store(&state_root, &path)?.state;
let worker = persisted
@@ -11509,13 +11221,10 @@ impl SubAgentManager {
}
}
// Deliberately NOT here: a pass that terminalized every Running agent
// without a live task handle whenever this process lacked the
// coordination flock (#2.6). Two Codewhale sessions in one workspace is
// ordinary usage, and failure to append to a shared bookkeeping ledger
// is not evidence that live work has stopped. Liveness is decided by
// the heartbeat above — which is actual evidence — never by who owns
// the flock (owner report, 2026-08-04). See `docs/architecture/
// delegated-coordination.md` for what lock loss legitimately costs.
// without a live task handle because another session shares the
// workspace (#2.6). Two Codewhale sessions in one workspace is ordinary
// usage, and liveness is decided by the heartbeat above — which is
// actual evidence (owner report, 2026-08-04).
// The retained lineage owns continuation lookup and descendant
// control. Keep the agents projection while live work references it.
@@ -11645,11 +11354,12 @@ impl SubAgentManager {
}
/// #3803: whether the next sidebar refresh should attempt cleanup.
/// Read-only managers without a process lock only render their refusal.
/// Other managers use the last attempt's interval to bound cleanup locking.
/// A manager whose durable store is not admitted only renders its refusal;
/// admission is retried on the next delegated write. Other managers use the
/// last attempt's interval to bound cleanup locking.
#[must_use]
pub fn cleanup_due(&self, min_interval: Duration) -> bool {
(!self.coordination_snapshot_read_only || self.holds_coordination_process_lock())
(self.state_path.is_none() || self.store_authority.is_some())
&& self
.last_cleanup_at
.is_none_or(|last| last.elapsed() >= min_interval)
@@ -13790,22 +13500,6 @@ fn ensure_subagent_store_cutover(root: &Path, path: &Path) -> Result<()> {
let _ = codewhale_config::private_directory::PrivateDirectory::inspect(&legacy)?;
return Ok(());
}
let _migration = if legacy.is_dir() {
None
} else {
Some(
CoordinationProcessLock::acquire(root, true)
.map_err(|error| anyhow::Error::new(SubAgentCutoverBlocked(error)))?,
)
};
if path.exists() {
read_durable_subagent_store(root, path)?;
anyhow::ensure!(
legacy.is_dir(),
"delegated migration did not retain its legacy-write tombstone"
);
return Ok(());
}
let staging = parent.join("subagents.v1.cutover");
let backup_name = "legacy.json";
let original_name = "original.json";
@@ -13882,6 +13576,20 @@ fn ensure_subagent_store_cutover(root: &Path, path: &Path) -> Result<()> {
write_json_atomic(root, path, &DurableSubAgentStore::from_legacy(state))
}
fn write_state_gitignore(root: &Path, path: &Path) -> Result<()> {
let ignore = checked_subagent_state_path(root, &path.with_file_name(".gitignore"))?;
match fs::OpenOptions::new()
.write(true)
.create_new(true)
.open(&ignore)
{
Ok(mut file) => file.write_all(b"*\n")?,
Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => {}
Err(error) => return Err(error.into()),
}
Ok(())
}
fn store_writer_is_live(path: &Path, writer: &str) -> bool {
if !valid_store_writer(writer) {
return true;
@@ -13954,6 +13662,9 @@ impl SubAgentStoreAuthority {
let _opening = acquire_store_transaction(root, path)?;
ensure_subagent_store_cutover(root, path)?;
let image = read_durable_subagent_store(root, path)?;
if let Err(error) = write_state_gitignore(root, path) {
tracing::warn!(target: "subagent", %error, "could not ignore the delegated state directory");
}
sync_subagent_store_directory(
path.parent()
.ok_or_else(|| anyhow!("delegated store has no parent"))?,
@@ -14494,24 +14205,16 @@ pub fn new_shared_subagent_manager_with_timeout(
)
}
pub fn new_shared_runtime_subagent_manager(
workspace: PathBuf,
workers: usize,
) -> Result<SharedSubAgentManager> {
new_runtime_subagent_manager(workspace, workers, false)
}
pub fn new_exclusive_runtime_subagent_manager(
workspace: PathBuf,
workers: usize,
) -> Result<SharedSubAgentManager> {
new_runtime_subagent_manager(workspace, workers, true)
new_shared_runtime_subagent_manager(workspace, workers)
}
fn new_runtime_subagent_manager(
pub fn new_shared_runtime_subagent_manager(
workspace: PathBuf,
workers: usize,
exclusive: bool,
) -> Result<SharedSubAgentManager> {
type RuntimeManagerRegistry =
parking_lot::Mutex<HashMap<PathBuf, (usize, std::sync::Weak<RwLock<SubAgentManager>>)>>;
@@ -14536,12 +14239,6 @@ fn new_runtime_subagent_manager(
.with_admission_limit(workers)
.with_launch_concurrency(workers)
.with_state_path(state_path);
manager.coordination_exclusive = exclusive;
manager = manager.require_coordination_process_lock();
if let Err(error) = manager.ensure_coordination_process_lock() {
manager.coordination_snapshot_read_only = true;
tracing::warn!(target: "subagent", %error, "Runtime coordination is read-only; stop and restart this Runtime process after the other session exits to load current state and regain delegated writes");
}
manager.load_state()?;
let manager = Arc::new(RwLock::new(manager));
managers.insert(key, (workers, Arc::downgrade(&manager)));
@@ -14551,7 +14248,7 @@ fn new_runtime_subagent_manager(
/// Create a shared sub-agent manager with an explicit control-plane state root.
///
/// `workspace` remains the child execution and file-authority root. The worker
/// ledger, complete transcript artifacts and coordination lock live under
/// ledger and complete transcript artifacts live under
/// `state_root/.codewhale/state`. Distinct state roots intentionally do not
/// share write claims; an embedding host that runs them against the same
/// workspace must provide any required cross-session write coordination.
@@ -14594,25 +14291,8 @@ pub fn new_shared_subagent_manager_with_state_root_and_timeout(
if let Some(state_path) = state_path {
manager = manager.with_state_path(state_path);
}
manager = if manager.store_cutover_blocked {
manager.into_session_private()
} else {
manager.require_coordination_process_lock()
};
if let Err(error) = manager.ensure_coordination_process_lock() {
tracing::warn!(target: "subagent", %error, "this session cannot append to the shared coordination ledger");
}
// Loading is a read. It was previously gated behind the *write* flock,
// which left a second session in the same workspace blind to every
// decision and write claim already recorded — and holding a default,
// empty ledger that would be written straight over the real one the
// moment the first session exited and the flock became acquirable
// (owner report, 2026-08-04). Read unconditionally; the write gate on
// `persist_state`/`persist_state_synchronously` still checks this
// manager's coordinator admission before publishing.
if let Err(err) = manager.load_state() {
manager.coordination_snapshot_read_only = true;
tracing::warn!(target: "subagent", ?err, "failed to load sub-agent state; durable writes remain refused");
tracing::warn!(target: "subagent", ?err, "failed to load sub-agent state");
}
Arc::new(RwLock::new(manager))
}
@@ -23153,10 +22833,29 @@ mod owner_scoped_coordination_tests {
let mut manager = authority.try_write().unwrap();
admit(&mut manager, &original);
admit(&mut manager, &selected);
let independent =
SubAgentManager::new(selected.clone(), 2).require_coordination_process_lock();
assert!(independent.session_private);
assert!(independent.ensure_coordination_process_lock().is_ok());
let independent = SubAgentManager::new(selected.clone(), 2)
.with_state_path(default_state_path(&selected).unwrap());
assert!(independent.store_authority.is_some());
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let state_directory = default_state_path(&selected)
.unwrap()
.parent()
.unwrap()
.to_path_buf();
let mode = fs::metadata(&state_directory).unwrap().permissions().mode();
fs::set_permissions(&state_directory, fs::Permissions::from_mode(0o000)).unwrap();
let unreadable =
manager.preflight_worker_coordination(&writer("selected", &selected, "src"));
fs::set_permissions(&state_directory, fs::Permissions::from_mode(mode)).unwrap();
assert!(unreadable.is_err(), "an unlistable store must fail closed");
}
let refused = manager
.preflight_worker_coordination(&writer("selected", &selected, "src"))
.unwrap_err();
assert!(refused.contains("another Codewhale session"), "{refused}");
drop(independent);
manager
.preflight_worker_coordination(&writer("original", &original, "src"))
.unwrap();
+236 -272
View File
@@ -1223,21 +1223,18 @@ fn coordination_hot_paths_count_only_active_authoritative_owners() {
}
#[test]
fn runtime_mount_coordination_lock_is_shared_with_parallel_sessions() {
fn runtime_mount_and_parallel_sessions_both_append_durably() {
let tmp = tempdir().expect("tempdir");
let runtime_authority = new_shared_runtime_subagent_manager(tmp.path().to_path_buf(), 8)
.expect("admit TUI runtime writer");
let session =
SubAgentManager::new(tmp.path().to_path_buf(), 8).require_coordination_process_lock();
session
.ensure_coordination_process_lock()
.expect("a second session appends beside the runtime");
assert!(!session.coordination_snapshot_read_only);
let session = SubAgentManager::new(tmp.path().to_path_buf(), 8)
.with_state_path(default_state_path(tmp.path()).expect("state path"));
assert!(session.store_authority.is_some());
session
.persist_state_synchronously()
.expect("session append");
let runtime = runtime_authority.try_write().expect("runtime lock");
assert!(!runtime.coordination_snapshot_read_only);
assert!(runtime.store_authority.is_some());
runtime
.persist_state_synchronously()
.expect("runtime append");
@@ -1363,10 +1360,9 @@ fn headless_worker_registration_enforces_live_claims_and_projects_context() {
.iter()
.any(|record| record.owner_session_id == "owner-worker-c")
);
let independent =
SubAgentManager::new(tmp.path().to_path_buf(), 8).require_coordination_process_lock();
assert!(independent.session_private);
assert!(independent.ensure_coordination_process_lock().is_ok());
let independent = SubAgentManager::new(tmp.path().to_path_buf(), 8)
.with_state_path(default_state_path(tmp.path()).expect("state path"));
assert!(independent.store_authority.is_some());
drop(independent);
drop(authority_a);
drop(authority_b);
@@ -1575,25 +1571,21 @@ fn prior_session_owners_never_count_as_active_coordination_owners() {
}
#[test]
fn isolated_worktree_workers_skip_the_coordination_process_lock() {
fn parallel_sessions_admit_shared_and_isolated_writers_durably() {
let tmp = tempdir().expect("tempdir");
let workspace = tmp.path().canonicalize().expect("canonical workspace");
let state_path = default_state_path(&workspace).expect("state path");
let holder = SubAgentManager::new(workspace.clone(), 4).require_coordination_process_lock();
holder
.ensure_coordination_process_lock()
.expect("holder owns the lock");
let holder = SubAgentManager::new(workspace.clone(), 4).with_state_path(state_path.clone());
assert!(holder.store_authority.is_some());
let mut contender =
SubAgentManager::new(workspace.clone(), 4).require_coordination_process_lock();
contender
.ensure_coordination_process_lock()
.expect("second manager now also owns shared lock (coexistence)");
let mut contender = SubAgentManager::new(workspace.clone(), 4).with_state_path(state_path);
assert!(contender.store_authority.is_some());
let shared = make_write_worker_spec("shared-writer", workspace.clone(), "src/shared");
contender
.preflight_worker_coordination(&shared)
.expect("shared-workspace writer proceeds with shared lock");
.expect("shared-workspace writer proceeds beside another session");
let mut isolated = make_write_worker_spec("isolated-writer", workspace.clone(), "src/iso");
isolated
@@ -1603,69 +1595,64 @@ fn isolated_worktree_workers_skip_the_coordination_process_lock() {
.worktree = true;
contender
.preflight_worker_coordination(&isolated)
.expect("isolated-worktree writer preflights without the lock");
.expect("isolated-worktree writer preflights beside another session");
contender
.register_worker_with_coordination(isolated)
.expect("isolated-worktree writer registers without the lock");
// Once the previous owner exits, a manager that failed earlier must
// acquire the lock on retry instead of replaying a memoized failure.
.expect("isolated-worktree writer registers durably");
drop(holder);
contender
.ensure_coordination_process_lock()
.expect("lock acquisition retries after the holder exits");
contender
.preflight_worker_coordination(&shared)
.expect("shared-workspace writer proceeds once the lock is held");
.expect("shared-workspace writer proceeds after the other session exits");
}
#[test]
fn coordination_detail_projection_reports_process_lock_ownership() {
fn coordination_detail_projection_reports_store_admission_and_its_retry() {
let tmp = tempdir().expect("tempdir");
let workspace = tmp.path().canonicalize().expect("canonical workspace");
let state_path = default_state_path(&workspace).expect("state path");
let holder = SubAgentManager::new(workspace.clone(), 4).require_coordination_process_lock();
holder
.ensure_coordination_process_lock()
.expect("holder owns the lock");
let holder = SubAgentManager::new(workspace.clone(), 4).with_state_path(state_path.clone());
let held = holder.coordination_detail_projection(None, 8);
assert!(
held.process_lock_held,
"holder projection must report lock held"
);
assert!(held.process_lock_held);
assert!(held.process_lock_note.is_none());
let contender = SubAgentManager::new(workspace, 4).require_coordination_process_lock();
let contender = SubAgentManager::new(workspace.clone(), 4).with_state_path(state_path.clone());
let shared = contender.coordination_detail_projection(None, 8);
// With shared locks both holders coexist — contender reports held and no note.
assert!(shared.process_lock_held);
assert!(shared.process_lock_note.is_none());
let committed = std::fs::read(&state_path).expect("committed store");
std::fs::write(&state_path, b"{").expect("corrupt store");
let mut refused = SubAgentManager::new(workspace, 4).with_state_path(state_path.clone());
let projection = refused.coordination_detail_projection(None, 8);
assert!(!projection.process_lock_held);
assert!(projection.process_lock_note.is_some());
assert!(!refused.cleanup_due(Duration::ZERO));
assert!(refused.release_stale_write_claims(None).is_err());
assert!(!refused.cleanup_due(Duration::ZERO));
std::fs::write(&state_path, committed).expect("restore store");
refused
.release_stale_write_claims(None)
.expect("admission is retried once the store is readable");
assert!(
shared.process_lock_held,
"contender projection must report lock held with shared locks"
);
assert!(
shared.process_lock_note.is_none(),
"shared projection carries no note"
refused
.coordination_detail_projection(None, 8)
.process_lock_held
);
assert!(refused.cleanup_due(Duration::ZERO));
}
/// A second Codewhale session in the same workspace is ordinary usage. Losing
/// the coordination flock means "I cannot append to the shared ledger" — it
/// must never mean "my agents are dead". Bookkeeping failure and liveness are
/// separate concerns (owner report, 2026-08-04).
/// A second Codewhale session in the same workspace is ordinary usage and
/// must never mean "my agents are dead". Bookkeeping and liveness are separate
/// concerns (owner report, 2026-08-04).
#[test]
fn a_second_session_without_the_lock_keeps_its_running_agents_live() {
fn a_second_session_keeps_its_running_agents_live() {
let tmp = tempdir().expect("tempdir");
let workspace = tmp.path().canonicalize().expect("canonical workspace");
let holder = SubAgentManager::new(workspace.clone(), 4).require_coordination_process_lock();
holder
.ensure_coordination_process_lock()
.expect("holder owns the lock");
let mut contender = SubAgentManager::new(workspace, 4).require_coordination_process_lock();
contender
.ensure_coordination_process_lock()
.expect("contender now also owns shared lock");
let _first = SubAgentManager::new(workspace.clone(), 4);
let mut contender = SubAgentManager::new(workspace, 4);
let (input_tx, _input_rx) = mpsc::unbounded_channel();
let mut agent = SubAgent::new(
@@ -1680,9 +1667,6 @@ fn a_second_session_without_the_lock_keeps_its_running_agents_live() {
PathBuf::from("."),
contender.session_boot_id().to_string(),
);
// Fresh, well inside the heartbeat window: nothing about this agent is
// stale. The only thing "wrong" with it is that this process cannot write
// the shared ledger.
agent.last_activity_at = Instant::now();
let agent_id = agent.id.clone();
contender.agents.insert(agent_id.clone(), agent);
@@ -1692,7 +1676,7 @@ fn a_second_session_without_the_lock_keeps_its_running_agents_live() {
.expect("durable worker operation");
assert_eq!(
cancelled, 0,
"lock loss alone must not terminalize anything"
"another session alone must not terminalize anything"
);
let snap = contender
.get_result(&agent_id)
@@ -1700,26 +1684,21 @@ fn a_second_session_without_the_lock_keeps_its_running_agents_live() {
assert_eq!(
snap.status,
SubAgentStatus::Running,
"a live agent stays Running when this session cannot write the ledger: {:?}",
"a live agent stays Running beside another session: {:?}",
snap.status
);
}
/// A second session shares the workspace coordination read-lock and must load
/// the existing ledger before it persists any of its own state. That preserves
/// prior decisions instead of replacing them with an empty ledger.
/// A second session must load the existing ledger before it persists any of
/// its own state. That preserves prior decisions instead of replacing them
/// with an empty ledger.
#[test]
fn a_second_session_with_the_shared_lock_loads_the_workspace_ledger() {
fn a_second_session_loads_the_workspace_ledger() {
let tmp = tempdir().expect("tempdir");
let workspace = tmp.path().canonicalize().expect("canonical workspace");
let state_path = default_state_path(&workspace).expect("state path");
let mut first = SubAgentManager::new(workspace.clone(), 4)
.with_state_path(state_path.clone())
.require_coordination_process_lock();
first
.ensure_coordination_process_lock()
.expect("first session owns the lock");
let mut first = SubAgentManager::new(workspace.clone(), 4).with_state_path(state_path.clone());
first
.record_coordination_decision(DecisionRecord {
decision_id: "shared-decision".to_string(),
@@ -1737,20 +1716,15 @@ fn a_second_session_with_the_shared_lock_loads_the_workspace_ledger() {
.persist_state_synchronously()
.expect("first session persists the ledger");
// Second session in the same workspace while the first still holds the
// shared flock. It coexists and must see the workspace ledger.
let second = new_shared_subagent_manager(workspace.clone(), 4);
{
let guard = second.blocking_read();
assert!(
guard.holds_coordination_process_lock(),
"test premise: the second session joins the shared flock"
);
assert!(guard.store_authority.is_some());
let ledger = guard.coordination_snapshot();
assert_eq!(
ledger.decisions.len(),
1,
"second session must load the workspace ledger it cannot write"
"second session must load the workspace ledger"
);
assert_eq!(ledger.decisions[0].decision_id, "shared-decision");
}
@@ -1759,12 +1733,9 @@ fn a_second_session_with_the_shared_lock_loads_the_workspace_ledger() {
drop(first);
{
let guard = second.blocking_write();
guard
.ensure_coordination_process_lock()
.expect("shared lock remains available after the first session exits");
guard
.persist_state_synchronously()
.expect("second session persists once it owns the lock");
.expect("second session persists after the first exits");
}
let mut replayed = SubAgentManager::new(workspace, 4).with_state_path(state_path);
replayed.load_state().expect("reload the workspace ledger");
@@ -1776,23 +1747,15 @@ fn a_second_session_with_the_shared_lock_loads_the_workspace_ledger() {
}
/// The complement of the test above: an orphan still terminalizes, but only on
/// heartbeat evidence. Lock ownership is not part of the decision either way.
/// heartbeat evidence. Another session is not part of the decision either way.
#[test]
fn cleanup_terminalizes_orphans_on_heartbeat_evidence_not_on_lock_ownership() {
fn cleanup_terminalizes_orphans_on_heartbeat_evidence_not_on_other_sessions() {
let tmp = tempdir().expect("tempdir");
let workspace = tmp.path().canonicalize().expect("canonical workspace");
let holder = SubAgentManager::new(workspace.clone(), 4).require_coordination_process_lock();
holder
.ensure_coordination_process_lock()
.expect("holder owns the lock");
let mut contender = SubAgentManager::new(workspace, 4)
.with_running_heartbeat_timeout(Duration::from_secs(30))
.require_coordination_process_lock();
contender
.ensure_coordination_process_lock()
.expect("contender now also owns shared lock");
let _first = SubAgentManager::new(workspace.clone(), 4);
let mut contender =
SubAgentManager::new(workspace, 4).with_running_heartbeat_timeout(Duration::from_secs(30));
let insert_orphan = |manager: &mut SubAgentManager, name: &str, idle: Duration| {
let (input_tx, _input_rx) = mpsc::unbounded_channel();
@@ -1832,7 +1795,7 @@ fn cleanup_terminalizes_orphans_on_heartbeat_evidence_not_on_lock_ownership() {
.expect("fresh orphan still listed")
.status,
SubAgentStatus::Running,
"a fresh orphan is not terminalized just because the flock is elsewhere"
"a fresh orphan is not terminalized just because another session is open"
);
assert!(
matches!(
@@ -11351,7 +11314,7 @@ async fn write_capable_agent_does_not_launch_when_durable_registration_fails() {
.expect_err("writer must fail before spawn when its durable claim cannot commit")
.to_string();
assert!(
error.contains("durably register") || error.contains("writer lock"),
error.contains("durably register") || error.contains("store admission failed"),
"{error}"
);
@@ -12230,7 +12193,10 @@ fn persist_state_rejects_state_path_outside_state_root() {
.persist_state()
.expect_err("outside state path should fail");
assert!(format!("{err:#}").contains("without its writer lock"));
assert!(
format!("{err:#}").contains("must stay within state root"),
"{err:#}"
);
}
/// A user who keeps application state on another volume makes the workspace's
@@ -12405,9 +12371,7 @@ fn explicit_state_roots_isolate_managers_for_the_same_execution_workspace() {
let mut manager = manager.try_write().expect("manager write lock");
assert_eq!(manager.workspace.as_path(), workspace.as_path());
assert_eq!(manager.state_root.as_path(), expected_root.as_path());
manager
.ensure_coordination_process_lock()
.expect("independent state-root coordination lock");
assert!(manager.store_authority.is_some());
manager.register_worker(make_worker_spec(worker_id, workspace.clone()));
manager
.persist_state_synchronously()
@@ -12462,31 +12426,21 @@ fn persist_state_rejects_symlinked_state_directory() {
);
}
/// A linked `.codewhale` must be refused before anything is created: the
/// coordination lock used to `create_dir_all` through the link first.
#[cfg(unix)]
#[test]
fn coordination_lock_refuses_linked_codewhale_without_writing_through_it() {
fn a_linked_codewhale_admission_writes_no_gitignore_through_it() {
let tmp = tempdir().expect("tempdir");
let workspace = tmp.path().join("workspace");
let outside = tmp.path().join("outside");
let root = tmp.path().canonicalize().expect("canonical root");
let workspace = root.join("workspace");
let outside = root.join("outside");
std::fs::create_dir_all(&workspace).expect("mkdir workspace");
std::fs::create_dir_all(&outside).expect("mkdir outside");
std::os::unix::fs::symlink(&outside, workspace.join(".codewhale")).expect("link .codewhale");
let Err(err) = CoordinationProcessLock::acquire(&workspace, false) else {
panic!("a linked .codewhale must be refused");
};
let message = format!("{err:#}");
let manager = new_shared_subagent_manager(workspace, 4);
assert!(manager.blocking_read().store_authority.is_none());
assert!(
message.contains("must not traverse symlinks")
|| message.contains("must stay within state root"),
"{message}"
);
assert_eq!(
std::fs::read_dir(&outside).expect("read outside").count(),
0,
"nothing may be created through the link"
!outside.join("state").join(".gitignore").exists(),
"a refused admission writes no .gitignore through the link"
);
}
@@ -20874,24 +20828,66 @@ fn write_json_atomic_survives_concurrent_writers() {
assert!(leftover.is_empty(), "temp files leaked: {leftover:?}");
}
fn two_process_decision(id: &str) -> DecisionRecord {
DecisionRecord {
decision_id: id.to_string(),
subject: id.to_string(),
status: DecisionStatus::Accepted,
owner: "root".to_string(),
scope: vec!["src".to_string()],
constraints: Vec::new(),
evidence_handles: Vec::new(),
version: 1,
sequence: 0,
}
}
fn wait_for_two_process_file(path: &Path, child: &mut std::process::Child, what: &str) {
let deadline = Instant::now() + Duration::from_secs(30);
while !path.exists() && Instant::now() < deadline {
std::thread::sleep(Duration::from_millis(10));
}
if !path.exists() {
let _ = child.kill();
panic!("{what} never became ready");
}
}
#[test]
fn coordination_process_lock_rejects_second_process() {
const ROLE_ENV: &str = "CODEWHALE_TEST_COORDINATION_LOCK_ROLE";
const WORKSPACE_ENV: &str = "CODEWHALE_TEST_COORDINATION_LOCK_WORKSPACE";
fn a_stale_session_never_makes_a_new_session_read_only() {
const ROLE_ENV: &str = "CODEWHALE_TEST_COORDINATION_ROLE";
const WORKSPACE_ENV: &str = "CODEWHALE_TEST_COORDINATION_WORKSPACE";
const TEST_NAME: &str =
"tools::subagent::tests::coordination_process_lock_rejects_second_process";
"tools::subagent::tests::a_stale_session_never_makes_a_new_session_read_only";
if let Some(role) = std::env::var_os(ROLE_ENV) {
let workspace = PathBuf::from(std::env::var_os(WORKSPACE_ENV).expect("workspace env"));
if role == "legacy-holder" {
let _legacy_session = CoordinationProcessLock::acquire(&workspace, false)
.expect("an older session holds the shared lock");
std::fs::write(workspace.join("holder.ready"), b"ready").unwrap();
let deadline = Instant::now() + Duration::from_secs(10);
let wait_for_release = || {
let deadline = Instant::now() + Duration::from_secs(60);
while !workspace.join("holder.release").exists() && Instant::now() < deadline {
std::thread::sleep(Duration::from_millis(10));
}
assert!(workspace.join("holder.release").exists(), "release timeout");
};
if role == "legacy-holder" {
let lock_file = std::fs::OpenOptions::new()
.create(true)
.truncate(false)
.read(true)
.write(true)
.open(
workspace
.join(".codewhale")
.join("state")
.join("subagents.v1.lock"),
)
.expect("open legacy lock");
let legacy = fd_lock::RwLock::new(lock_file);
let _guard = legacy
.try_read()
.expect("an older session holds the legacy lock");
std::fs::write(workspace.join("holder.ready"), b"ready").unwrap();
wait_for_release();
return;
}
let manager = new_shared_subagent_manager_with_timeout(
@@ -20901,78 +20897,82 @@ fn coordination_process_lock_rejects_second_process() {
Duration::from_secs(30),
4,
);
let mut guard = manager.try_write().unwrap();
assert!(guard.store_authority.is_some(), "{role:?} store admitted");
assert!(
guard
.coordination_detail_projection(None, 8)
.process_lock_held
);
let name = role.to_string_lossy().into_owned();
guard
.record_coordination_decision(two_process_decision(&format!("{name}-decision")))
.expect("record a decision beside the other session");
guard.register_worker(make_worker_spec(
&format!("{name}-worker"),
workspace.clone(),
));
guard
.persist_state_synchronously()
.expect("publish beside the other session");
if role == "holder" {
manager
.try_read()
.unwrap()
.ensure_coordination_process_lock()
.expect("holder owns lock");
std::fs::write(workspace.join("holder.ready"), b"ready").unwrap();
let deadline = Instant::now() + Duration::from_secs(10);
while !workspace.join("holder.release").exists() && Instant::now() < deadline {
std::thread::sleep(Duration::from_millis(10));
}
assert!(workspace.join("holder.release").exists(), "release timeout");
wait_for_release();
} else {
manager
.try_read()
.unwrap()
.ensure_coordination_process_lock()
.expect("second process now also succeeds with shared lock");
assert!(
guard
.coordination_snapshot()
.decisions
.iter()
.any(|decision| decision.decision_id == "holder-decision"),
"the new session merges the open session's ledger"
);
}
return;
}
let dir = tempdir().expect("tempdir");
let workspace = dir.path().canonicalize().expect("canonical workspace");
let test_binary = std::env::current_exe().expect("test binary");
let mut holder = std::process::Command::new(&test_binary)
.arg("--exact")
.arg(TEST_NAME)
.arg("--nocapture")
.env(ROLE_ENV, "holder")
.env(WORKSPACE_ENV, &workspace)
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.spawn()
.expect("spawn lock holder");
let deadline = Instant::now() + Duration::from_secs(5);
while !workspace.join("holder.ready").exists() && Instant::now() < deadline {
std::thread::sleep(Duration::from_millis(10));
}
if !workspace.join("holder.ready").exists() {
let _ = holder.kill();
let output = holder.wait_with_output().expect("holder output");
panic!(
"holder never acquired lock:\nstdout:\n{}\nstderr:\n{}",
let spawn = |role: &str, workspace: &Path| {
std::process::Command::new(&test_binary)
.arg("--exact")
.arg(TEST_NAME)
.arg("--nocapture")
.env(ROLE_ENV, role)
.env(WORKSPACE_ENV, workspace)
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.spawn()
.expect("spawn session process")
};
let finish = |child: std::process::Child, what: &str| {
let output = child.wait_with_output().expect("session output");
assert!(
output.status.success(),
"{what} failed:\nstdout:\n{}\nstderr:\n{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
}
let contender = std::process::Command::new(&test_binary)
.arg("--exact")
.arg(TEST_NAME)
.arg("--nocapture")
.env(ROLE_ENV, "contender")
.env(WORKSPACE_ENV, &workspace)
.output()
.expect("spawn lock contender");
assert!(
contender.status.success(),
"contender failed:\nstdout:\n{}\nstderr:\n{}",
String::from_utf8_lossy(&contender.stdout),
String::from_utf8_lossy(&contender.stderr)
);
};
let dir = tempdir().expect("tempdir");
let workspace = dir.path().canonicalize().expect("canonical workspace");
let mut holder = spawn("holder", &workspace);
wait_for_two_process_file(&workspace.join("holder.ready"), &mut holder, "open session");
finish(spawn("contender", &workspace), "new session");
std::fs::write(workspace.join("holder.release"), b"release").unwrap();
let output = holder.wait_with_output().expect("holder output");
assert!(
output.status.success(),
"holder failed:\nstdout:\n{}\nstderr:\n{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
finish(holder, "open session");
let workers = load_persisted_agent_worker_records_with_state_root(&workspace, &workspace)
.expect("read merged store");
for id in ["holder-worker", "contender-worker"] {
assert!(
workers.iter().any(|record| record.spec.worker_id == id),
"{id} survives the other session's publication"
);
}
let mut replayed = SubAgentManager::new(workspace.clone(), 4)
.with_state_path(default_state_path(&workspace).unwrap());
replayed.load_state().expect("reload merged ledger");
assert_eq!(replayed.coordination.decisions.len(), 2);
let legacy_dir = tempdir().expect("tempdir");
let legacy_workspace = legacy_dir
@@ -20988,75 +20988,42 @@ fn coordination_process_lock_rejects_second_process() {
)
.unwrap();
let legacy_bytes = std::fs::read(&legacy_file).unwrap();
let mut legacy_holder = std::process::Command::new(&test_binary)
.arg("--exact")
.arg(TEST_NAME)
.arg("--nocapture")
.env(ROLE_ENV, "legacy-holder")
.env(WORKSPACE_ENV, &legacy_workspace)
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.spawn()
.expect("spawn legacy lock holder");
let deadline = Instant::now() + Duration::from_secs(5);
while !legacy_workspace.join("holder.ready").exists() && Instant::now() < deadline {
std::thread::sleep(Duration::from_millis(10));
}
if !legacy_workspace.join("holder.ready").exists() {
let _ = legacy_holder.kill();
let output = legacy_holder
.wait_with_output()
.expect("legacy holder output");
panic!(
"legacy holder never acquired lock:\nstdout:\n{}\nstderr:\n{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
}
let private = new_shared_subagent_manager(legacy_workspace.clone(), 4);
let mut legacy_holder = spawn("legacy-holder", &legacy_workspace);
wait_for_two_process_file(
&legacy_workspace.join("holder.ready"),
&mut legacy_holder,
"older session",
);
let upgraded = new_shared_subagent_manager(legacy_workspace.clone(), 4);
{
let guard = private.blocking_read();
let mut guard = upgraded.blocking_write();
assert!(
guard.ensure_coordination_process_lock().is_ok(),
"a session beside an older one keeps working sub-agents"
guard.store_authority.is_some(),
"a stale older session must not leave the new one read-only"
);
let projection = guard.coordination_detail_projection(None, 8);
assert!(!projection.process_lock_held);
assert!(
projection
.process_lock_note
.as_deref()
.is_some_and(|note| note.starts_with(SESSION_PRIVATE_COORDINATION)),
projection.process_lock_held,
"{:?}",
projection.process_lock_note
);
assert!(projection.process_lock_note.is_none());
guard
.record_coordination_decision(two_process_decision("upgraded-decision"))
.expect("the new session writes while the older one is still open");
assert!(guard.cleanup_due(Duration::from_secs(3600)));
}
assert_eq!(
std::fs::read(&legacy_file).unwrap(),
legacy_bytes,
"the older session's store stays untouched"
);
assert!(
!state_dir.join(SUBAGENT_STATE_FILE).exists(),
"no forced cutover while the older session runs"
);
std::fs::write(legacy_workspace.join("holder.release"), b"release").unwrap();
let output = legacy_holder
.wait_with_output()
.expect("legacy holder output");
assert!(
output.status.success(),
"legacy holder failed:\nstdout:\n{}\nstderr:\n{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
let upgraded = new_shared_subagent_manager(legacy_workspace, 4);
assert!(
state_dir.join(SUBAGENT_STATE_FILE).exists() && legacy_file.is_dir(),
"the next session upgrades the store once the older one exits"
"the store upgrades without waiting for the older session to exit"
);
assert!(upgraded.blocking_read().holds_coordination_process_lock());
drop(private);
assert_eq!(
std::fs::read(legacy_file.join("legacy.json")).unwrap(),
legacy_bytes,
"the older store is retained byte for byte"
);
std::fs::write(legacy_workspace.join("holder.release"), b"release").unwrap();
finish(legacy_holder, "older session");
}
// === agent(action="wait") + peek throttling (#4097) ===
@@ -24171,35 +24138,32 @@ fn validate_resume_from_source(
}
/// Owner report 2026-08-04: a model/provider switch spawns the new engine's
/// manager while the old engine still holds the workspace coordination flock
/// in this same process. That losing acquisition must classify itself as a
/// same-process handover (not "another Codewhale process"), and must
/// self-heal via the projection retry once the previous owner drops.
/// manager while the old engine's manager is still open in this same process.
/// Both stay admitted, and the new one keeps writing after the old one drops.
#[test]
fn coordination_lock_loss_to_own_process_reads_as_handover_and_self_heals() {
fn a_same_process_engine_handover_keeps_both_managers_admitted() {
let tmp = tempdir().expect("tempdir");
let state_path = default_state_path(tmp.path()).expect("state path");
let first =
SubAgentManager::new(tmp.path().to_path_buf(), 1).require_coordination_process_lock();
SubAgentManager::new(tmp.path().to_path_buf(), 1).with_state_path(state_path.clone());
let second = SubAgentManager::new(tmp.path().to_path_buf(), 1).with_state_path(state_path);
assert!(
first.holds_coordination_process_lock(),
"first manager must own the flock"
first
.coordination_detail_projection(None, 8)
.process_lock_held
);
let second =
SubAgentManager::new(tmp.path().to_path_buf(), 1).require_coordination_process_lock();
assert!(
second.holds_coordination_process_lock(),
"second manager now also holds shared flock (coexistence)"
second
.coordination_detail_projection(None, 8)
.process_lock_held
);
let note = second.coordination_process_lock_status();
assert!(note.is_ok(), "shared lock should not error: {note:?}");
first
.persist_state_synchronously()
.expect("old engine publishes");
drop(first);
assert!(
second.coordination_process_lock_status().is_ok(),
"retry must acquire once the previous engine dropped the flock"
);
assert!(second.holds_coordination_process_lock());
second
.persist_state_synchronously()
.expect("new engine publishes after the old one drops");
}
// -- resume-from-checkpoint tests (checkpoint-based continuation) --
+6 -51
View File
@@ -255,12 +255,11 @@ impl StdioLspTransport {
let child = Arc::new(AsyncMutex::new(Some(child)));
let dispatcher_child = child.clone();
let dispatcher_pending = pending.clone();
let dispatcher_outbound = tx_outbound.clone();
let dispatcher_task = spawn_supervised(
"lsp-dispatcher",
std::panic::Location::caller(),
async move {
dispatcher_task(rx_inbound, tx_diag, dispatcher_outbound, dispatcher_pending).await;
dispatcher_task(rx_inbound, tx_diag, dispatcher_pending).await;
// EOF, malformed frames, or an overflowing diagnostics queue
// terminate the producer too, so its pipes cannot stay stuck.
if let Some(mut child) = dispatcher_child.lock().await.take() {
@@ -537,19 +536,15 @@ impl LspTransport for StdioLspTransport {
/// Send a JSON value as one Content-Length-framed JSON-RPC message.
async fn send_message(tx: &mpsc::Sender<Vec<u8>>, value: &Value) -> Result<()> {
tx.send(frame_message(value)?)
.await
.map_err(|_| anyhow!("LSP outbound channel closed"))?;
Ok(())
}
fn frame_message(value: &Value) -> Result<Vec<u8>> {
let body = serde_json::to_vec(value).context("serialize LSP message")?;
let header = format!("Content-Length: {}\r\n\r\n", body.len());
let mut frame = Vec::with_capacity(header.len() + body.len());
frame.extend_from_slice(header.as_bytes());
frame.extend_from_slice(&body);
Ok(frame)
tx.send(frame)
.await
.map_err(|_| anyhow!("LSP outbound channel closed"))?;
Ok(())
}
/// Background task that drains the outbound queue and writes each frame to
@@ -643,7 +638,6 @@ fn parse_header(buf: &[u8]) -> Result<Option<(usize, usize)>> {
async fn dispatcher_task(
mut rx: mpsc::Receiver<Value>,
tx_diag: mpsc::Sender<DiagnosticMessage>,
tx_outbound: mpsc::Sender<Vec<u8>>,
pending: Arc<AsyncMutex<HashMap<i64, oneshot::Sender<Value>>>>,
) {
while let Some(value) = rx.recv().await {
@@ -660,19 +654,6 @@ async fn dispatcher_task(
}
continue;
}
if let Some(method) = method {
if let Some(id) = value.get("id") {
let reply = json!({
"jsonrpc": "2.0",
"id": id,
"error": {"code": -32601, "message": format!("unsupported method {method}")},
});
if let Ok(frame) = frame_message(&reply) {
let _ = tx_outbound.try_send(frame);
}
}
continue;
}
// Replies have an `id` and a `result` or `error`.
if let Some(id) = value.get("id").and_then(|v| v.as_i64()) {
let mut map = pending.lock().await;
@@ -799,31 +780,6 @@ pub(super) mod tests {
assert_eq!(diags[0].message, "missing semicolon");
}
#[tokio::test]
async fn server_request_sharing_a_pending_id_is_answered_not_taken_as_the_reply() {
let (tx_in, rx_in) = mpsc::channel(4);
let (tx_out, mut rx_out) = mpsc::channel::<Vec<u8>>(4);
let (tx_diag, _rx_diag) = mpsc::channel(4);
let pending = Arc::new(AsyncMutex::new(HashMap::new()));
let (reply_tx, reply_rx) = oneshot::channel();
pending.lock().await.insert(2, reply_tx);
let dispatcher = tokio::spawn(dispatcher_task(rx_in, tx_diag, tx_out, pending));
tx_in
.send(json!({"jsonrpc":"2.0","id":2,"method":"workspace/configuration","params":{"items":[]}}))
.await
.unwrap();
tx_in
.send(json!({"jsonrpc":"2.0","id":2,"result":"definition"}))
.await
.unwrap();
assert_eq!(reply_rx.await.unwrap()["result"], "definition");
let answer = next_document(&mut rx_out).await;
assert_eq!(answer["id"], 2);
assert_eq!(answer["error"]["code"], -32601);
drop(tx_in);
dispatcher.await.unwrap();
}
#[test]
fn round_trips_uri_path() {
let path = fixture_path("example/foo.rs");
@@ -1353,12 +1309,11 @@ while True:
let (mut producer, reader) = tokio::io::duplex(MAX_LSP_HEADER_BYTES * 2);
let (tx, rx) = mpsc::channel(8);
let (diag, _diagnostics) = mpsc::channel(8);
let (outbound, _outbound_rx) = mpsc::channel(8);
let pending = Arc::new(AsyncMutex::new(HashMap::new()));
let (reply, receiver) = oneshot::channel();
pending.lock().await.insert(1, reply);
let read_task = tokio::spawn(reader_task(reader, tx));
let dispatch = tokio::spawn(dispatcher_task(rx, diag, outbound, pending.clone()));
let dispatch = tokio::spawn(dispatcher_task(rx, diag, pending.clone()));
producer.write_all(&frame).await.unwrap();
assert!(
timeout(Duration::from_secs(1), receiver)
-16
View File
@@ -1371,9 +1371,6 @@ fn relativize_candidate(
.map(fs::Metadata::is_dir)
.unwrap_or_else(|| candidate.ends_with('/'));
if rel_path.as_os_str().is_empty() {
return None;
}
let rel_string = path_to_string(&rel_path)?;
Some((rel_string, exists, is_dir))
}
@@ -1658,19 +1655,6 @@ mod tests {
assert!(!entry.is_dir);
}
#[test]
fn workspace_root_is_not_an_active_path() {
let tmp = TempDir::new().expect("tempdir");
let root = tmp.path().canonicalize().expect("canonical root");
fs::write(root.join("笔记 📝.md"), "旧").expect("write");
let mut ws = WorkingSet::default();
let mention = format!("<file path=\"{}/笔记 📝.md\">", root.display());
ws.observe_user_message(&mention, &root);
assert!(!ws.entries.contains_key(""), "{:?}", ws.entries.keys());
}
#[test]
fn observe_tool_call_extracts_paths_from_input() {
let tmp = TempDir::new().expect("tempdir");
+1 -36
View File
@@ -1457,7 +1457,6 @@ impl SnapshotRepo {
.arg(&self.git_dir)
.arg("--work-tree")
.arg(&self.work_tree)
.args(["-c", "core.quotePath=false"])
.args(args);
let output = run_bounded_git(&mut cmd, subcommand)?;
if !output.status.success() {
@@ -1851,7 +1850,7 @@ impl SnapshotRepo {
.duration_since(UNIX_EPOCH)
.map_err(|e| io_other(format!("clock error: {e}")))?
.as_secs() as i64;
let cutoff = now.saturating_sub(i64::try_from(max_age.as_secs()).unwrap_or(i64::MAX));
let cutoff = now - max_age.as_secs() as i64;
let snapshots = self.list(usize::MAX)?;
if snapshots.is_empty() {
@@ -3949,22 +3948,6 @@ mod tests {
assert_eq!(list[0].label, "turn:1");
}
#[test]
fn prune_with_an_age_beyond_i64_keeps_every_snapshot() {
let tmp = tempdir().unwrap();
let (repo, _home) = make_repo(tmp.path());
std::fs::write(repo.work_tree().join("f.txt"), "v0").unwrap();
repo.snapshot("turn:0").unwrap();
std::thread::sleep(Duration::from_millis(1100));
let removed = repo
.prune_older_than(Duration::from_secs(u64::MAX))
.unwrap();
assert_eq!(removed, 0);
assert_eq!(repo.list(10).unwrap().len(), 1);
}
/// The 2026-08-04 regression: with a cut in the MIDDLE of history,
/// `prune_older_than` used to `update-ref HEAD <oldest survivor>`, which
/// orphaned (and gc destroyed) the NEWEST snapshots while keeping the
@@ -4909,24 +4892,6 @@ mod tests {
assert_eq!(none.patch, "");
}
#[test]
fn work_tree_changes_since_names_non_ascii_paths_as_written() {
let tmp = tempdir().unwrap();
let (repo, _home) = make_repo(tmp.path());
let ws = repo.work_tree().to_path_buf();
std::fs::write(ws.join("笔记 📝.md"), b"old\n").unwrap();
let before = repo.snapshot("pre-turn:1").expect("snapshot");
git_output(&repo, &["config", "core.quotePath", "true"]);
std::fs::write(ws.join("笔记 📝.md"), b"new\n").unwrap();
let changes = repo
.work_tree_changes_since(&before, 1 << 20)
.expect("changes");
assert_eq!(changes.names, "笔记 📝.md\n");
assert!(changes.stat.contains("笔记 📝.md"), "{}", changes.stat);
assert!(changes.patch.contains("b/笔记 📝.md"), "{}", changes.patch);
}
/// A tree the repo no longer holds — the receipt outlived the object —
/// is reported as absent rather than as a diff failure, so a caller can
/// tell pruning from a broken repo without reading git's stderr.
+2 -37
View File
@@ -11,7 +11,7 @@ mod runtime_aliases;
use std::collections::HashMap;
use std::fs::{self, OpenOptions};
use std::io::{BufRead, BufReader, Read, Seek, SeekFrom, Write};
use std::io::{BufRead, BufReader, Write};
use std::path::{Path, PathBuf};
use std::sync::{Arc, LazyLock, Mutex, MutexGuard};
@@ -1453,7 +1453,6 @@ impl StateStore {
self.with_session_index_lock(|| {
let mut file = OpenOptions::new()
.create(true)
.read(true)
.append(true)
.open(&self.session_index_path)
.with_context(|| {
@@ -1462,14 +1461,7 @@ impl StateStore {
self.session_index_path.display()
)
})?;
let mut last = *b"\n";
if file.metadata()?.len() > 0 {
file.seek(SeekFrom::End(-1))?;
file.read_exact(&mut last)?;
}
let separator = if last[0] == b'\n' { "" } else { "\n" };
writeln!(file, "{separator}{encoded}")
.context("failed to append session index entry")?;
writeln!(file, "{encoded}").context("failed to append session index entry")?;
// Durability: without this a crash mid-write can leave a torn
// final line. Reads tolerate one (see `session_index_map`), but
// not losing the entry beats recovering from having lost it.
@@ -2930,33 +2922,6 @@ mod tests {
);
}
#[test]
fn session_index_append_after_an_unterminated_torn_line_is_kept() {
let store = temp_state_store("session-index-unterminated");
store
.append_thread_name("thread-1", Some("first".to_string()), 1, None)
.expect("append first entry");
{
let mut file = OpenOptions::new()
.append(true)
.open(&store.session_index_path)
.expect("open session index");
write!(file, "{{\"thread_id\":\"thread-2\",\"thread_na").expect("write torn line");
}
store
.append_thread_name("thread-3", Some("third".to_string()), 3, None)
.expect("append third entry");
assert_eq!(
store
.find_thread_name_by_id("thread-3")
.expect("lookup thread-3")
.as_deref(),
Some("third"),
);
}
/// Hook fired by compaction between writing the snapshot and renaming it
/// over the live index — the window a concurrent append can be lost in.
/// Only the store whose path a test registered is affected, so tests
+1 -20
View File
@@ -7,15 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
- **Renaming sessions with Chinese titles:** the session picker no longer rejects a Chinese, Japanese, Korean or emoji title of more than about 33 characters as "Title must be 1–100 characters". It now counts characters, as the message says and as `/rename` and the Runtime API already did, so any title up to 100 characters is accepted. Thanks [@luochen211](https://github.com/luochen211) (PR 7322).
- **Non-ASCII file names:** `/diff`, the `@git` and `@diff` mentions and the Runtime API workspace diff now show paths with Chinese, emoji and other non-ASCII characters as written instead of as quoted octal escapes, and the turn context no longer lists the workspace root as an empty "Active paths" entry. Thanks [@luochen211](https://github.com/luochen211) (PR 7317).
- **Session names after a crash, and keep-forever snapshot retention:** a session name saved right after a crash that cut off the previous session-index line is no longer lost, because the new entry now starts on its own line; and a very large `[snapshots] max_age_days` (such as `9223372036854775807` for "keep forever") no longer wraps around and deletes every undo snapshot at session start. Thanks [@luochen211](https://github.com/luochen211) (PR 7316).
- **Blank environment variables:** exporting a variable empty (for example `CODEWHALE_MODEL=` from a compose `${VAR:-}` template) no longer hides its legacy alias. `DEEPSEEK_MODEL`, `DEEPSEEK_BASE_URL`, `KIMI_BASE_URL`, `DEEPSEEK_FORCE_HTTP1`, `DEEPSEEK_STREAM_OPEN_TIMEOUT_SECS` and the other provider alias variables now apply as they do when the primary is unset. Thanks [@luochen211](https://github.com/luochen211) (PR 7315).
- **MCP and language servers that send requests mid-call:** a tool call no longer fails with "neither a result nor an error" when an MCP server sends a `ping` or other request that reuses the call's id while the call is running, and a language server's `workspace/configuration` or similar request no longer ends a pending LSP request. Codewhale now answers `ping`, answers other server requests with "Method not found" (it never advertised them), keeps waiting for the real reply, and a refused answer over Streamable HTTP no longer drops the connection to legacy SSE. Thanks [@luochen211](https://github.com/luochen211) (PR 7319).
- **Qwen Code as an external agent:** Codewhale now finds an installed Qwen Code CLI (`qwen`, 0.6.1 or newer) and can run it as a session over ACP with `qwen --acp`. `GET /v1/agents/external` lists it, an older install is reported as needing `npm i -g @qwen-code/qwen-code@latest`, and sign-in stays inside Qwen Code (`/auth`). Thanks [@MannXo](https://github.com/MannXo) (#7293).
- **README and contributor credit:** the repository front page now opens the way the website does: the line "The model can be anyone's. The agent should be yours.", one sentence on what Codewhale is, the install command and one picture, with reference detail moved into the linked guides; the 18 translated READMEs follow the same structure and say they are machine-drafted. The contributor wall and the website now count the same 433 people (407 GitHub accounts and the commit authors whose email is not linked to an account), where the website had counted accounts only. `DEPENDENCY_SECURITY.md` moved into `docs/dependency-maintenance.md`, and `.github/SUPPORT.md` says where to ask for help.
- **Website Plugins page:** The website's Plugins page now describes what the released version does, and a labelled section says what 0.10.3 adds: one Plugins place, Standard and Creator mode, and the Permissions control.
- **First-time guide on the website:** a new docs page, "Use Codewhale for the first time" (`/docs/first-time`, English and Simplified Chinese), walks someone who has never used a terminal through opening one, installing, choosing the folder Codewhale works in and when that choice matters, opening a terminal in a folder from Finder or File Explorer, connecting a model, a first task, and stopping, undoing and quitting. The install page gains a short "New to the terminal?" block for macOS and Windows that links to it, and `llms.txt` carries the same steps.
- **Plugin docs and plugin-creator:** Docs now say everything you add is a plugin. The bundled plugin-creator skill no longer tells an agent never to trust or enable a plugin (in Full Access it may, for one it wrote and installed from its project), points code and screen plugins at cordis-plugin-development, and an untouched copy from 0.10.2 now upgrades. The Chinese extension and Runtime API docs now say editor ACP sessions get plugin tools and configured MCP servers.
- **Model thinking:** New installs show model thinking by default: it streams live in the transcript and collapses under a "Thought for …" header when the answer starts. Existing installs keep their saved setting; turn it on under Model reasoning in chat in /config.
- **Footer clock:** The footer clock and phase now say "thinking", matching the transcript.
@@ -676,7 +669,7 @@ work is declared complete here. Items moved to 0.10.4 by name are marked below.
- **Windows uninstall removes the receipt and update leftovers.** The installer's uninstaller also deletes `.codewhale-install.sha256` and `codewhale.old-*`/`codew.old-*` left by a self-update, so `bin` can be removed. Refs #7091.
- **Windows `/undo` matches recorded file paths regardless of case.** A write recorded under a different drive-letter or directory case, or a `\\?\` prefix, is no longer dropped from the restore plan. Refs #6970.
- **Windows: Fleet and metrics read live files again.** Replaying a Fleet ledger and reading audit or receipt records no longer fail with a sharing violation while their writer is still open; the same regular-file, link and identity checks apply. Plugin enablement now says which snapshot check refused (stage root, content identity or final check) instead of one generic message. Generated protocol and SDK exports keep LF line endings in Windows checkouts.
- **A held coordination lock no longer makes a new session read-only.** When another session owns the delegated-agent store, the new session starts on private state and says so, instead of refusing every sub-agent with “Failed to clean up sub-agents”.
- **Sessions in one folder no longer leave each other's sub-agents read-only or private:** a second session works beside another one, stale or suspended included, the older sub-agent store upgrades right away, and a store that cannot be opened is retried on the next sub-agent change instead of needing a restart. While another Codewhale session, a suspended one included, is open in a folder the desktop app or `serve` was asked to work in (other than the folder it started in), that Runtime still refuses sub-agents that would edit the folder directly; isolated worktrees are unaffected. A sub-agent store that cannot be opened shows its refusal once instead of repeating cleanup warnings on every sidebar refresh. Restart any session started with Codewhale 0.10.2 or older: once the folder upgrades it can no longer save sub-agent state (Refs #7232).
- **A dev server can outlive the turn in an interactive session.** `persist:true` now works in the TUI and desktop when the thread has Full Access; restricted postures and Fleet/sub-agents still refuse it.
- **`codewhale exec --add-dir <path>`** (repeatable) and `addDirs` on `POST /v1/threads` let the file tools read and write a sibling directory, a second checkout or a shared scratch root without turning on Full Access.
- **Unsettled usage is persisted in production, not only in tests.** The interactive sink now records a legacy usage receipt when a provider response has no settlement, so children and turns stop reporting settlement failure.
@@ -713,12 +706,6 @@ work is declared complete here. Items moved to 0.10.4 by name are marked below.
composer. Real Pinyin input on macOS and Windows is not yet confirmed.
Thanks [@cmdcorp6534](https://github.com/cmdcorp6534) for the report
(Refs #2323).
- **Sub-agents beside an older session:** while a Codewhale 0.10.2 session
still uses a folder's older delegated-agent store, a 0.10.3 session in the
same folder keeps working sub-agents with private state that is not saved,
instead of refusing every sub-agent action. The older session's files are
left untouched, and the store upgrades when a session starts after the older
one exits (Refs #7232).
- **Fleet members:** a replayed member start now matches only within its own
session, so two sessions that reuse an operation key no longer receive each
other's agent.
@@ -747,12 +734,6 @@ work is declared complete here. Items moved to 0.10.4 by name are marked below.
bare self-package exports resolve inside the selected package directory.
Invalid or incompatible ranges and paths outside the reviewed package still
refuse. Real published-plugin activation remains unqualified.
- **Coordination lock diagnostics (#7232):** stale lock-file contents no longer
identify an owner PID. A refused legacy admission keeps its reason in Work
and shows one notice per cause instead of repeating cleanup warnings on
every sidebar refresh. Legacy migration still requires the actual holder to
exit and the refused owning Runtime to restart; locks and retained state
remain protected.
- **A local proxy that stops mid-session no longer strands the session (#7231):** the model, catalog, search, vision, finance, MCP-registry, profile and remote-control clients watch a loopback proxy port and go direct while it refuses connections, then resume proxying when it returns. Only a definite refusal from every resolved address changes the route; remote proxies, `NO_PROXY` matches, credentials and the existing precedence are unchanged. Set `CODEWHALE_LOCAL_PROXY_FALLBACK=0` to keep the old build-time snapshot.
- **Proxy diagnostic privacy ([#7233](https://github.com/codewhale-hq/codewhale/issues/7233)):** malformed MCP proxy warnings omit configured values; local-proxy log labels mask userinfo and omit URL paths, queries and fragments. Routing is unchanged. All 23 existing proxy tests passed on macOS; runtime log capture and other platforms remain unqualified. Thanks [@SparkofSpike](https://github.com/SparkofSpike) for the report.
- **Fork, undo and retry work after a workspace drift notice (#7234):** the history-boundary walks now treat a whole `<context_update>` envelope as engine-owned, so a session that received one can be cut at every saved turn. A person quoting a notice or a truncated envelope still counts as their own prompt.
@@ -157,8 +157,7 @@ fn session_diff(app: &App) -> std::io::Result<Option<DebugDiffObservation>> {
/// The workspace repository's own uncommitted changes: what `/diff` shows
/// before the session has a restore point to compare against.
fn workspace_git_diff(workspace: &std::path::Path) -> DebugDiffObservation {
let git =
|args: &[&str]| Git::output(&[&["-c", "core.quotePath=false"], args].concat(), workspace);
let git = |args: &[&str]| Git::output(args, workspace);
// Outside a repository `git diff` prints nothing on stdout, which would
// read as "no changes".
match git(&["rev-parse", "--is-inside-work-tree"]) {
+2 -2
View File
@@ -284,8 +284,8 @@ impl ContextMenuView {
if start > 0 {
rows.push(MenuRow::MoreAbove(start));
}
for (idx, entry) in self.entries.iter().enumerate().take(end).skip(start) {
if idx > start && entry.section_start {
for idx in start..end {
if idx > start && self.entries[idx].section_start {
rows.push(MenuRow::Divider);
}
rows.push(MenuRow::Entry(idx));
-19
View File
@@ -403,25 +403,6 @@ mod tests {
assert!(!payload.truncated);
}
#[test]
fn mentions_name_non_ascii_paths_as_written() {
let dir = tempfile::tempdir().unwrap();
init_repo(dir.path());
Command::new("git")
.args(["config", "core.quotePath", "true"])
.current_dir(dir.path())
.output()
.unwrap();
std::fs::write(dir.path().join("笔记 📝.md"), "旧\n").unwrap();
commit_all(dir.path(), "initial");
std::fs::write(dir.path().join("笔记 📝.md"), "新\n").unwrap();
let status = resolve_git_mention(GitMentionKind::Status, dir.path());
let diff = resolve_git_mention(GitMentionKind::Diff, dir.path());
assert!(status.block.contains("笔记 📝.md"), "{}", status.block);
assert!(diff.block.contains("b/笔记 📝.md"), "{}", diff.block);
}
#[test]
fn diff_covers_staged_and_unstaged_changes() {
let dir = tempfile::tempdir().unwrap();
+3 -25
View File
@@ -22,8 +22,8 @@ use crate::tui::views::{
};
use crate::tui::views::{ModalKind, ModalView, ViewAction, ViewEvent};
use codewhale_engine::session_manager::{
MAX_SESSION_TITLE_CHARS, SavedSession, SessionListFilter, SessionManager, SessionMetadata,
extract_title, extract_user_prompt, strip_thinking_tags,
SavedSession, SessionListFilter, SessionManager, SessionMetadata, extract_title,
extract_user_prompt, strip_thinking_tags,
};
use codewhale_engine::session_projection::{MAX_PROJECTED_SESSIONS, SessionQuery, SessionSortMode};
use codewhale_localization::{Locale, MessageId, tr};
@@ -787,7 +787,7 @@ impl SessionPickerView {
self.status = Some(tr(self.locale, MessageId::SessionsNoSelection).into_owned());
return ViewAction::None;
};
if new_title.is_empty() || new_title.chars().count() > MAX_SESSION_TITLE_CHARS {
if new_title.is_empty() || new_title.len() > 100 {
self.status = Some(tr(self.locale, MessageId::SessionsTitleLength).into_owned());
return ViewAction::None;
}
@@ -2045,28 +2045,6 @@ mod tests {
);
}
#[test]
fn rename_selected_limits_title_by_characters() {
let _lock = crate::test_support::lock_test_env();
let tmp = tempfile::tempdir().expect("tempdir");
let _home = crate::test_support::EnvVarGuard::set("CODEWHALE_HOME", tmp.path());
let manager = SessionManager::default_location().expect("session manager");
let mut saved = saved_session_with_messages(vec![text_message("user", "hello")]);
saved.metadata.id = "session-cjk".to_string();
manager.save_session(&saved).expect("save session");
let mut view = picker_with(vec![saved.metadata.clone()], None);
let title = "中文会话标题".repeat(6);
assert!(matches!(
view.rename_selected(&title),
ViewAction::Emit(ViewEvent::SessionRenamed { .. })
));
assert_eq!(
manager.load_session("session-cjk").unwrap().metadata.title,
title
);
}
#[test]
fn archive_toggle_persists_hides_the_row_and_emits_an_archive_receipt() {
let _lock = crate::test_support::lock_test_env();
+4 -4
View File
@@ -1441,8 +1441,8 @@ fn prose_cells_fill_full_width_on_ultrawide_by_default() {
let mut cache = TranscriptViewCache::new();
cache.ensure_filtered(&refs, &revisions, 220, options, &HashMap::new(), None, None);
for (idx, cell) in cache.per_cell.iter().enumerate().take(3) {
let width = max_line_width(&cell.lines);
for idx in 0..3 {
let width = max_line_width(&cache.per_cell[idx].lines);
assert!(
width > RETIRED_RAIL_MEASURE,
"prose cell {idx} wrapped to {width} columns — still on the retired \
@@ -1495,8 +1495,8 @@ fn transcript_prose_measure_caps_prose_but_not_tools() {
let mut cache = TranscriptViewCache::new();
cache.ensure_filtered(&refs, &revisions, 220, options, &HashMap::new(), None, None);
for (idx, cell) in cache.per_cell.iter().enumerate().take(3) {
let width = max_line_width(&cell.lines);
for idx in 0..3 {
let width = max_line_width(&cache.per_cell[idx].lines);
assert!(
width > 105,
"prose cell {idx} wrapped to {width} columns — still on the retired \
+1 -1
View File
@@ -1110,7 +1110,7 @@ mod screen_mode_tests {
viewport: ratatui::Viewport::Inline(3),
},
)
.map_err(|err| match err {})
.map_err(|err| io::Error::other(err.to_string()))
},
)
.expect("a successful probe must switch");
@@ -24,4 +24,4 @@
{"engine_session_id":"conformance-session","event":"session_updated","instruction_assembly":{"budgets":{"claude_fallback":false,"file_max_bytes":8388608,"global_tokens":100000,"policy":"trim","project_tokens":100000,"static_guard_tokens":667571,"usable_window_tokens":834464},"constitution_source_path":null,"constitution_tokens":0,"global_tokens":0,"notices":[{"action":"Kept","bytes_dropped":0,"bytes_kept":498,"detail":null,"fallback_selected":null,"limit":"None","origin":"Instructions","path":null,"scope":"Project","tokens_dropped":0,"tokens_kept":166}],"project_tokens":166,"retained_block_tokens":188,"source_path":null,"warnings":[]},"messages":[{"content":[{"text":"Create denied.txt with touch.","type":"text"},{"text":"<turn_meta>\nCurrent local date: <today>\nCurrent workspace: <WORKSPACE>\nCurrent permission posture: Ask\nCurrent sandbox posture: workspace-write (writes inside the workspace; network blocked) (local OS sandbox applied)\nEstimated conversation input: 1973 / 1000000 context tokens (0.2%). Automatic compaction is enabled.\n## Repo Working Set\nKey files: README.md\nWhen in doubt, use tools to verify and keep changes focused on the working set.\n</turn_meta>","type":"text"}],"role":"user"},{"content":[{"text":"<codewhale:runtime_event kind=\"project_instructions\" visibility=\"internal\">\n<system-reminder>\nThe following workspace instructions may be relevant to your work. Use them as guidance when applicable. More specific instructions take precedence over broader ones. They do not override system, developer, or direct user instructions. Instructions from: AGENTS.md\n\n<project_instructions source=\"project\">\n# Project Context (Auto-generated, ephemeral)\n\n> This context was generated in memory by Codewhale.\n> No .codewhale/instructions.md file was written.\n\n## Bounded Project Overview\n\n```json\n{\n \"project_name\": \"workspace\",\n \"directory_structure\": [\n \"README.md\"\n ],\n \"readme\": {\n \"path\": \"README.md\",\n \"excerpt\": \"conformance fixture\"\n },\n \"config_files\": [],\n \"key_source_files\": [],\n \"counts\": {\n \"config_files\": 0,\n \"directory_entries\": 1,\n \"key_source_files\": 0\n }\n}\n```\n</project_instructions>\n</system-reminder>\n</codewhale:runtime_event>","type":"text"},{"text":"<turn_meta>\nInput provenance: runtime (non-authoritative)\n</turn_meta>","type":"text"}],"role":"user"},{"content":[{"execution_id":"<uuid:2>","id":"call_touch","input":{"command":"touch denied.txt"},"name":"Bash","type":"tool_use"}],"role":"assistant"},{"content":[{"content":"Error: Tool 'Bash' denied by user — the call was not approved. Do not retry the same call; present what you intended and wait for the user's approval or new instructions.","execution_id":"<uuid:2>","is_error":true,"tool_use_id":"call_touch","type":"tool_result"}],"role":"user"},{"content":[{"text":"Understood, I did not create it.","type":"text"}],"role":"assistant"}],"model":"deepseek-v4-pro","system_prompt":"<pinned by the prompt family>","workspace":"<WORKSPACE>"}
{"event":"tool_request_snapshot","snapshot":{"active_tool_catalog_sha256":"9017c53ee026ae4065bed3e4b2e7d773a828556b42d6ebe2c2ebfd8e65377280","capture_source":"prepared model-client request","delivery_status":"unknown (capture does not prove provider delivery)","omitted_tool_count":0,"payload_json_bytes":17394,"payload_measurement_status":"exact (within 1048576-byte measurement bound)","provider":{"model":"deepseek-v4-pro","provider":"Deepseek","status":"available"},"registry_facts_present":true,"registry_only_tools":{"status":"known","value":{"count":40,"omitted":8,"rendered":[{"truncated":false,"value":"Git"},{"truncated":false,"value":"Run"},{"truncated":false,"value":"Web"},{"truncated":false,"value":"apply_patch"},{"truncated":false,"value":"automation"},{"truncated":false,"value":"context_archive_get"},{"truncated":false,"value":"context_archive_search"},{"truncated":false,"value":"diagnostics"},{"truncated":false,"value":"finance"},{"truncated":false,"value":"github"},{"truncated":false,"value":"handle_read"},{"truncated":false,"value":"harness"},{"truncated":false,"value":"image_ocr"},{"truncated":false,"value":"list_dir"},{"truncated":false,"value":"lsp"},{"truncated":false,"value":"note"},{"truncated":false,"value":"notify"},{"truncated":false,"value":"pandoc_convert"},{"truncated":false,"value":"project_map"},{"truncated":false,"value":"registry_sync"},{"truncated":false,"value":"request_plugin_install"},{"truncated":false,"value":"request_user_input"},{"truncated":false,"value":"retrieve_tool_result"},{"truncated":false,"value":"revert_turn"},{"truncated":false,"value":"review"},{"truncated":false,"value":"send_later"},{"truncated":false,"value":"session_get"},{"truncated":false,"value":"session_search"},{"truncated":false,"value":"start_mcp_server"},{"truncated":false,"value":"start_registry_mcp_server"},{"truncated":false,"value":"tasks"},{"truncated":false,"value":"terminal/cancel"}]}},"registry_tool_count":{"status":"known","value":65},"rendered_tool_count":15,"schema_version":1,"step":1,"terminal":{"automatic_compaction_attempts":0,"effective_max_steps":null,"emergency_compaction_attempts":0,"empty_stop_retries":0,"final_report_requested":false,"last_prepared_output_limit_tokens":65536,"last_provider_finish_reason":{"truncated":false,"value":"end_turn"},"last_reported_input_tokens":200,"last_response_tool_calls":0,"last_response_tool_calls_suppressed":0,"model_requests_started":2,"model_step_index":1,"permission_denial_rounds_without_progress":0,"permission_strategy_switches":0,"reason":"provider_no_tool_call","reasoning_only_reprompts":0,"route_context_window_tokens":1000000,"status":"completed","step_budget_source":"max_steps","stream_resumes":0,"transparent_stream_retries":0,"transport_retries":0},"tool_count":15,"tools":[{"allowed_callers":{"status":"known","value":{"count":1,"omitted":0,"rendered":[{"truncated":false,"value":"direct"}]}},"approval":{"status":"known","value":{"truncated":false,"value":"Required"}},"cache_control_type":{"reason":"request field absent","status":"unknown"},"capabilities":{"status":"known","value":{"count":3,"omitted":0,"rendered":[{"truncated":false,"value":"ExecutesCode"},{"truncated":false,"value":"Sandboxable"},{"truncated":false,"value":"RequiresApproval"}]}},"defer_loading":{"status":"known","value":false},"description":{"truncated":true,"value":"Execute a shell command in the workspace and return stdout and stderr (last 2000 lines or 50KB). Optional timeout is foreground seconds (default 30 seconds); a command still running then moves to the background by itself — never killed for time — returning output so far, last-output age, build phase, and a task_id. Call task_shell_wait with that task_id to read more or wait; do not sleep-poll with bash. Before a final answer, collect every still-relevant background task (wait only when genuinely blocked) an"},"input_examples":{"reason":"request field absent","status":"unknown"},"input_schema_json":{"truncated":false,"value":"{\"additionalProperties\":false,\"properties\":{\"command\":{\"description\":\"The command to execute. Actual execution shell: `/bin/bash`. Use Bash syntax: pipelines, redirections, $(command), and && / || are supported. Quote paths and variable expansions, such as \\\"$path\\\"; use single quotes for literal text. For literal multiline input, use a quoted heredoc delimiter (<<'EOF') with its closing delimiter on a separate line. Use only installed programs; do not assume GNU-specific flags on macOS/BSD. Example: printf '%s\\\\n' 'sample'.\",\"type\":\"string\"},\"description\":{\"description\":\"Optional few-word summary shown to the user beside the command.\",\"type\":\"string\"},\"justification\":{\"description\":\"Required with sandbox_permissions: one sentence explaining why this exact command needs wider access.\",\"type\":\"string\"},\"read_only\":{\"description\":\"Set true to run analysis code (including Python/SQLite) or a network fetch (curl) with mandatory native filesysteLine truncated
{"event":"turn_complete","parent_route_usage":{"input_tokens":350,"output_tokens":21},"routed_usage_dropped_records":0,"status":"completed","tool_catalog":"<pinned by the prompt family>","turn_id":"<uuid:1>","usage":{"input_tokens":350,"output_tokens":21}}
{"harness_summary":{"model_requests":2,"non_streaming_requests":0,"workspace_after":{".codewhale/state/.gitignore":"cdbcae15105d6b78",".codewhale/state/subagents.v1.json/legacy.json":"9064f16acf90984c",".codewhale/state/subagents.v1.lock":"e3b0c44298fc1c14",".codewhale/state/subagents.v3.json":"e874da2951e53d2b",".codewhale/state/subagents.v3.transaction.lock":"e3b0c44298fc1c14","README.md":"41f49bffb38de5ba"}}}
{"harness_summary":{"model_requests":2,"non_streaming_requests":0,"workspace_after":{".codewhale/state/.gitignore":"cdbcae15105d6b78",".codewhale/state/subagents.v1.json/legacy.json":"9064f16acf90984c",".codewhale/state/subagents.v3.json":"e874da2951e53d2b",".codewhale/state/subagents.v3.transaction.lock":"e3b0c44298fc1c14","README.md":"41f49bffb38de5ba"}}}
@@ -15,4 +15,4 @@
{"event":"tool_request_snapshot","snapshot":{"active_tool_catalog_sha256":"9017c53ee026ae4065bed3e4b2e7d773a828556b42d6ebe2c2ebfd8e65377280","capture_source":"prepared model-client request","delivery_status":"unknown (capture does not prove provider delivery)","omitted_tool_count":0,"payload_json_bytes":17394,"payload_measurement_status":"exact (within 1048576-byte measurement bound)","provider":{"model":"deepseek-v4-pro","provider":"Deepseek","status":"available"},"registry_facts_present":true,"registry_only_tools":{"status":"known","value":{"count":40,"omitted":8,"rendered":[{"truncated":false,"value":"Git"},{"truncated":false,"value":"Run"},{"truncated":false,"value":"Web"},{"truncated":false,"value":"apply_patch"},{"truncated":false,"value":"automation"},{"truncated":false,"value":"context_archive_get"},{"truncated":false,"value":"context_archive_search"},{"truncated":false,"value":"diagnostics"},{"truncated":false,"value":"finance"},{"truncated":false,"value":"github"},{"truncated":false,"value":"handle_read"},{"truncated":false,"value":"harness"},{"truncated":false,"value":"image_ocr"},{"truncated":false,"value":"list_dir"},{"truncated":false,"value":"lsp"},{"truncated":false,"value":"note"},{"truncated":false,"value":"notify"},{"truncated":false,"value":"pandoc_convert"},{"truncated":false,"value":"project_map"},{"truncated":false,"value":"registry_sync"},{"truncated":false,"value":"request_plugin_install"},{"truncated":false,"value":"request_user_input"},{"truncated":false,"value":"retrieve_tool_result"},{"truncated":false,"value":"revert_turn"},{"truncated":false,"value":"review"},{"truncated":false,"value":"send_later"},{"truncated":false,"value":"session_get"},{"truncated":false,"value":"session_search"},{"truncated":false,"value":"start_mcp_server"},{"truncated":false,"value":"start_registry_mcp_server"},{"truncated":false,"value":"tasks"},{"truncated":false,"value":"terminal/cancel"}]}},"registry_tool_count":{"status":"known","value":65},"rendered_tool_count":15,"schema_version":1,"step":0,"terminal":{"automatic_compaction_attempts":0,"effective_max_steps":null,"emergency_compaction_attempts":0,"empty_stop_retries":0,"final_report_requested":false,"last_prepared_output_limit_tokens":65536,"last_provider_finish_reason":null,"last_reported_input_tokens":120,"last_response_tool_calls":0,"last_response_tool_calls_suppressed":null,"model_requests_started":1,"model_step_index":0,"permission_denial_rounds_without_progress":0,"permission_strategy_switches":0,"reason":"interrupted","reasoning_only_reprompts":0,"route_context_window_tokens":1000000,"status":"interrupted","step_budget_source":"max_steps","stream_resumes":0,"transparent_stream_retries":0,"transport_retries":0},"tool_count":15,"tools":[{"allowed_callers":{"status":"known","value":{"count":1,"omitted":0,"rendered":[{"truncated":false,"value":"direct"}]}},"approval":{"status":"known","value":{"truncated":false,"value":"Required"}},"cache_control_type":{"reason":"request field absent","status":"unknown"},"capabilities":{"status":"known","value":{"count":3,"omitted":0,"rendered":[{"truncated":false,"value":"ExecutesCode"},{"truncated":false,"value":"Sandboxable"},{"truncated":false,"value":"RequiresApproval"}]}},"defer_loading":{"status":"known","value":false},"description":{"truncated":true,"value":"Execute a shell command in the workspace and return stdout and stderr (last 2000 lines or 50KB). Optional timeout is foreground seconds (default 30 seconds); a command still running then moves to the background by itself — never killed for time — returning output so far, last-output age, build phase, and a task_id. Call task_shell_wait with that task_id to read more or wait; do not sleep-poll with bash. Before a final answer, collect every still-relevant background task (wait only when genuinely blocked) an"},"input_examples":{"reason":"request field absent","status":"unknown"},"input_schema_json":{"truncated":false,"value":"{\"additionalProperties\":false,\"properties\":{\"command\":{\"description\":\"The command to execute. Actual execution shell: `/bin/bash`. Use Bash syntax: pipelines, redirections, $(command), and && / || are supported. Quote paths and variable expansions, such as \\\"$path\\\"; use single quotes for literal text. For literal multiline input, use a quoted heredoc delimiter (<<'EOF') with its closing delimiter on a separate line. Use only installed programs; do not assume GNU-specific flags on macOS/BSD. Example: printf '%s\\\\n' 'sample'.\",\"type\":\"string\"},\"description\":{\"description\":\"Optional few-word summary shown to the user beside the command.\",\"type\":\"string\"},\"justification\":{\"description\":\"Required with sandbox_permissions: one sentence explaining why this exact command needs wider access.\",\"type\":\"string\"},\"read_only\":{\"description\":\"Set true to run analysis code (including Python/SQLite) or a network fetch (curl) with mandatory native filesystem read-only isolation. Outbound networkLine truncated
{"event":"turn_complete","parent_route_usage":{"input_tokens":120,"output_tokens":0},"routed_usage_dropped_records":0,"status":"interrupted","tool_catalog":"<pinned by the prompt family>","turn_id":"<uuid:1>","usage":{"input_tokens":120,"output_tokens":0}}
{"event":"status","message":"Turn interrupted."}
{"harness_summary":{"model_requests":1,"non_streaming_requests":0,"workspace_after":{".codewhale/state/.gitignore":"cdbcae15105d6b78",".codewhale/state/subagents.v1.json/legacy.json":"9064f16acf90984c",".codewhale/state/subagents.v1.lock":"e3b0c44298fc1c14",".codewhale/state/subagents.v3.json":"e874da2951e53d2b",".codewhale/state/subagents.v3.transaction.lock":"e3b0c44298fc1c14","NOTES.md":"f957b19529906961","README.md":"41f49bffb38de5ba"}}}
{"harness_summary":{"model_requests":1,"non_streaming_requests":0,"workspace_after":{".codewhale/state/.gitignore":"cdbcae15105d6b78",".codewhale/state/subagents.v1.json/legacy.json":"9064f16acf90984c",".codewhale/state/subagents.v3.json":"e874da2951e53d2b",".codewhale/state/subagents.v3.transaction.lock":"e3b0c44298fc1c14","NOTES.md":"f957b19529906961","README.md":"41f49bffb38de5ba"}}}
@@ -25,4 +25,4 @@
{"engine_session_id":"conformance-session","event":"session_updated","instruction_assembly":{"budgets":{"claude_fallback":false,"file_max_bytes":8388608,"global_tokens":100000,"policy":"trim","project_tokens":100000,"static_guard_tokens":667571,"usable_window_tokens":834464},"constitution_source_path":null,"constitution_tokens":0,"global_tokens":0,"notices":[{"action":"Kept","bytes_dropped":0,"bytes_kept":514,"detail":null,"fallback_selected":null,"limit":"None","origin":"Instructions","path":null,"scope":"Project","tokens_dropped":0,"tokens_kept":172}],"project_tokens":172,"retained_block_tokens":193,"source_path":null,"warnings":[]},"messages":[{"content":[{"text":"What does README.md say?","type":"text"},{"text":"<turn_meta>\nCurrent local date: <today>\nCurrent workspace: <WORKSPACE>\nCurrent permission posture: Ask\nCurrent sandbox posture: workspace-write (writes inside the workspace; network blocked) (local OS sandbox applied)\nEstimated conversation input: 1972 / 1000000 context tokens (0.2%). Automatic compaction is enabled.\n## Repo Working Set\nKey files: README.md\nActive paths (prioritize these):\n- README.md (file)\nWhen in doubt, use tools to verify and keep changes focused on the working set.\n</turn_meta>","type":"text"}],"role":"user"},{"content":[{"text":"<codewhale:runtime_event kind=\"project_instructions\" visibility=\"internal\">\n<system-reminder>\nThe following workspace instructions may be relevant to your work. Use them as guidance when applicable. More specific instructions take precedence over broader ones. They do not override system, developer, or direct user instructions. Instructions from: AGENTS.md\n\n<project_instructions source=\"project\">\n# Project Context (Auto-generated, ephemeral)\n\n> This context was generated in memory by Codewhale.\n> No .codewhale/instructions.md file was written.\n\n## Bounded Project Overview\n\n```json\n{\n \"project_name\": \"workspace\",\n \"directory_structure\": [\n \"README.md\",\n \"NOTES.md\"\n ],\n \"readme\": {\n \"path\": \"README.md\",\n \"excerpt\": \"conformance fixture\"\n },\n \"config_files\": [],\n \"key_source_files\": [],\n \"counts\": {\n \"config_files\": 0,\n \"directory_entries\": 2,\n \"key_source_files\": 0\n }\n}\n```\n</project_instructions>\n</system-reminder>\n</codewhale:runtime_event>","type":"text"},{"text":"<turn_meta>\nInput provenance: runtime (non-authoritative)\n</turn_meta>","type":"text"}],"role":"user"},{"content":[{"execution_id":"<uuid:2>","id":"call_read_readme","input":{"action":"read","path":"README.md"},"name":"File","type":"tool_use"}],"role":"assistant"},{"content":[{"content":"content_hash=\"sha256:41f49bffb38de5ba88945f09121ffc590f9d7f6ff80fdb9ea8ad10224fecea61\"\nconformance fixture","execution_id":"<uuid:2>","tool_use_id":"call_read_readme","type":"tool_result"}],"role":"user"},{"content":[{"text":"It says: conformance fixture.","type":"text"}],"role":"assistant"}],"model":"deepseek-v4-pro","system_prompt":"<pinned by the prompt family>","workspace":"<WORKSPACE>"}
{"event":"tool_request_snapshot","snapshot":{"active_tool_catalog_sha256":"9017c53ee026ae4065bed3e4b2e7d773a828556b42d6ebe2c2ebfd8e65377280","capture_source":"prepared model-client request","delivery_status":"unknown (capture does not prove provider delivery)","omitted_tool_count":0,"payload_json_bytes":17394,"payload_measurement_status":"exact (within 1048576-byte measurement bound)","provider":{"model":"deepseek-v4-pro","provider":"Deepseek","status":"available"},"registry_facts_present":true,"registry_only_tools":{"status":"known","value":{"count":40,"omitted":8,"rendered":[{"truncated":false,"value":"Git"},{"truncated":false,"value":"Run"},{"truncated":false,"value":"Web"},{"truncated":false,"value":"apply_patch"},{"truncated":false,"value":"automation"},{"truncated":false,"value":"context_archive_get"},{"truncated":false,"value":"context_archive_search"},{"truncated":false,"value":"diagnostics"},{"truncated":false,"value":"finance"},{"truncated":false,"value":"github"},{"truncated":false,"value":"handle_read"},{"truncated":false,"value":"harness"},{"truncated":false,"value":"image_ocr"},{"truncated":false,"value":"list_dir"},{"truncated":false,"value":"lsp"},{"truncated":false,"value":"note"},{"truncated":false,"value":"notify"},{"truncated":false,"value":"pandoc_convert"},{"truncated":false,"value":"project_map"},{"truncated":false,"value":"registry_sync"},{"truncated":false,"value":"request_plugin_install"},{"truncated":false,"value":"request_user_input"},{"truncated":false,"value":"retrieve_tool_result"},{"truncated":false,"value":"revert_turn"},{"truncated":false,"value":"review"},{"truncated":false,"value":"send_later"},{"truncated":false,"value":"session_get"},{"truncated":false,"value":"session_search"},{"truncated":false,"value":"start_mcp_server"},{"truncated":false,"value":"start_registry_mcp_server"},{"truncated":false,"value":"tasks"},{"truncated":false,"value":"terminal/cancel"}]}},"registry_tool_count":{"status":"known","value":65},"rendered_tool_count":15,"schema_version":1,"step":1,"terminal":{"automatic_compaction_attempts":0,"effective_max_steps":null,"emergency_compaction_attempts":0,"empty_stop_retries":0,"final_report_requested":false,"last_prepared_output_limit_tokens":65536,"last_provider_finish_reason":{"truncated":false,"value":"end_turn"},"last_reported_input_tokens":200,"last_response_tool_calls":0,"last_response_tool_calls_suppressed":0,"model_requests_started":2,"model_step_index":1,"permission_denial_rounds_without_progress":0,"permission_strategy_switches":0,"reason":"provider_no_tool_call","reasoning_only_reprompts":0,"route_context_window_tokens":1000000,"status":"completed","step_budget_source":"max_steps","stream_resumes":0,"transparent_stream_retries":0,"transport_retries":0},"tool_count":15,"tools":[{"allowed_callers":{"status":"known","value":{"count":1,"omitted":0,"rendered":[{"truncated":false,"value":"direct"}]}},"approval":{"status":"known","value":{"truncated":false,"value":"Required"}},"cache_control_type":{"reason":"request field absent","status":"unknown"},"capabilities":{"status":"known","value":{"count":3,"omitted":0,"rendered":[{"truncated":false,"value":"ExecutesCode"},{"truncated":false,"value":"Sandboxable"},{"truncated":false,"value":"RequiresApproval"}]}},"defer_loading":{"status":"known","value":false},"description":{"truncated":true,"value":"Execute a shell command in the workspace and return stdout and stderr (last 2000 lines or 50KB). Optional timeout is foreground seconds (default 30 seconds); a command still running then moves to the background by itself — never killed for time — returning output so far, last-output age, build phase, and a task_id. Call task_shell_wait with that task_id to read more or wait; do not sleep-poll with bash. Before a final answer, collect every still-relevant background task (wait only when genuinely blocked) an"},"input_examples":{"reason":"request field absent","status":"unknown"},"input_schema_json":{"truncated":false,"value":"{\"additionalProperties\":false,\"properties\":{\"command\":{\"description\":\"The command to execute. Actual execution shell: `/bin/bash`. Use Bash syntax: pipelines, redirections, $(command), and && / || are supported. Quote paths and variable expansions, such as \\\"$path\\\"; use single quotes for literal text. For literal multiline input, use a quoted heredoc delimiter (<<'EOF') with its closing delimiter on a separate line. Use only installed programs; do not assume GNU-specific flags on macOS/BSD. Example: printf '%s\\\\n' 'sample'.\",\"type\":\"string\"},\"description\":{\"description\":\"Optional few-word summary shown to the user beside the command.\",\"type\":\"string\"},\"justification\":{\"description\":\"Required with sandbox_permissions: one sentence explaining why this exact command needs wider access.\",\"type\":\"string\"},\"read_only\":{\"description\":\"Set true to run analysis code (including Python/SQLite) or a network fetch (curl) with mandatory native filesysteLine truncated
{"event":"turn_complete","parent_route_usage":{"input_tokens":350,"output_tokens":26},"routed_usage_dropped_records":0,"status":"completed","tool_catalog":"<pinned by the prompt family>","turn_id":"<uuid:1>","usage":{"input_tokens":350,"output_tokens":26}}
{"harness_summary":{"model_requests":2,"non_streaming_requests":0,"workspace_after":{".codewhale/state/.gitignore":"cdbcae15105d6b78",".codewhale/state/subagents.v1.json/legacy.json":"9064f16acf90984c",".codewhale/state/subagents.v1.lock":"e3b0c44298fc1c14",".codewhale/state/subagents.v3.json":"e874da2951e53d2b",".codewhale/state/subagents.v3.transaction.lock":"e3b0c44298fc1c14","NOTES.md":"f957b19529906961","README.md":"41f49bffb38de5ba"}}}
{"harness_summary":{"model_requests":2,"non_streaming_requests":0,"workspace_after":{".codewhale/state/.gitignore":"cdbcae15105d6b78",".codewhale/state/subagents.v1.json/legacy.json":"9064f16acf90984c",".codewhale/state/subagents.v3.json":"e874da2951e53d2b",".codewhale/state/subagents.v3.transaction.lock":"e3b0c44298fc1c14","NOTES.md":"f957b19529906961","README.md":"41f49bffb38de5ba"}}}
@@ -31,4 +31,4 @@
{"engine_session_id":"conformance-session","event":"session_updated","instruction_assembly":{"budgets":{"claude_fallback":false,"file_max_bytes":8388608,"global_tokens":100000,"policy":"trim","project_tokens":100000,"static_guard_tokens":667571,"usable_window_tokens":834464},"constitution_source_path":null,"constitution_tokens":0,"global_tokens":0,"notices":[{"action":"Kept","bytes_dropped":0,"bytes_kept":514,"detail":null,"fallback_selected":null,"limit":"None","origin":"Instructions","path":null,"scope":"Project","tokens_dropped":0,"tokens_kept":172}],"project_tokens":172,"retained_block_tokens":193,"source_path":null,"warnings":[]},"messages":[{"content":[{"text":"Read README.md and NOTES.md.","type":"text"},{"text":"<turn_meta>\nCurrent local date: <today>\nCurrent workspace: <WORKSPACE>\nCurrent permission posture: Ask\nCurrent sandbox posture: workspace-write (writes inside the workspace; network blocked) (local OS sandbox applied)\nEstimated conversation input: 1973 / 1000000 context tokens (0.2%). Automatic compaction is enabled.\n## Repo Working Set\nKey files: README.md\nActive paths (prioritize these):\n- NOTES.md (file)\n- README.md (file)\nWhen in doubt, use tools to verify and keep changes focused on the working set.\n</turn_meta>","type":"text"}],"role":"user"},{"content":[{"text":"<codewhale:runtime_event kind=\"project_instructions\" visibility=\"internal\">\n<system-reminder>\nThe following workspace instructions may be relevant to your work. Use them as guidance when applicable. More specific instructions take precedence over broader ones. They do not override system, developer, or direct user instructions. Instructions from: AGENTS.md\n\n<project_instructions source=\"project\">\n# Project Context (Auto-generated, ephemeral)\n\n> This context was generated in memory by Codewhale.\n> No .codewhale/instructions.md file was written.\n\n## Bounded Project Overview\n\n```json\n{\n \"project_name\": \"workspace\",\n \"directory_structure\": [\n \"README.md\",\n \"NOTES.md\"\n ],\n \"readme\": {\n \"path\": \"README.md\",\n \"excerpt\": \"conformance fixture\"\n },\n \"config_files\": [],\n \"key_source_files\": [],\n \"counts\": {\n \"config_files\": 0,\n \"directory_entries\": 2,\n \"key_source_files\": 0\n }\n}\n```\n</project_instructions>\n</system-reminder>\n</codewhale:runtime_event>","type":"text"},{"text":"<turn_meta>\nInput provenance: runtime (non-authoritative)\n</turn_meta>","type":"text"}],"role":"user"},{"content":[{"execution_id":"<uuid:2>","id":"call_read_a","input":{"action":"read","path":"README.md"},"name":"File","type":"tool_use"},{"execution_id":"<uuid:3>","id":"call_read_b","input":{"action":"read","path":"NOTES.md"},"name":"File","type":"tool_use"}],"role":"assistant"},{"content":[{"content":"content_hash=\"sha256:41f49bffb38de5ba88945f09121ffc590f9d7f6ff80fdb9ea8ad10224fecea61\"\nconformance fixture","execution_id":"<uuid:2>","tool_use_id":"call_read_a","type":"tool_result"}],"role":"user"},{"content":[{"content":"content_hash=\"sha256:f957b19529906961933c5c30f8713c500a9bb5d9d0695c40d48c97a26a3594ec\"\nsecond file","execution_id":"<uuid:3>","tool_use_id":"call_read_b","type":"tool_result"}],"role":"user"},{"content":[{"text":"Both files read.","type":"text"}],"role":"assistant"}],"model":"deepseek-v4-pro","system_prompt":"<pinned by the prompt family>","workspace":"<WORKSPACE>"}
{"event":"tool_request_snapshot","snapshot":{"active_tool_catalog_sha256":"9017c53ee026ae4065bed3e4b2e7d773a828556b42d6ebe2c2ebfd8e65377280","capture_source":"prepared model-client request","delivery_status":"unknown (capture does not prove provider delivery)","omitted_tool_count":0,"payload_json_bytes":17394,"payload_measurement_status":"exact (within 1048576-byte measurement bound)","provider":{"model":"deepseek-v4-pro","provider":"Deepseek","status":"available"},"registry_facts_present":true,"registry_only_tools":{"status":"known","value":{"count":40,"omitted":8,"rendered":[{"truncated":false,"value":"Git"},{"truncated":false,"value":"Run"},{"truncated":false,"value":"Web"},{"truncated":false,"value":"apply_patch"},{"truncated":false,"value":"automation"},{"truncated":false,"value":"context_archive_get"},{"truncated":false,"value":"context_archive_search"},{"truncated":false,"value":"diagnostics"},{"truncated":false,"value":"finance"},{"truncated":false,"value":"github"},{"truncated":false,"value":"handle_read"},{"truncated":false,"value":"harness"},{"truncated":false,"value":"image_ocr"},{"truncated":false,"value":"list_dir"},{"truncated":false,"value":"lsp"},{"truncated":false,"value":"note"},{"truncated":false,"value":"notify"},{"truncated":false,"value":"pandoc_convert"},{"truncated":false,"value":"project_map"},{"truncated":false,"value":"registry_sync"},{"truncated":false,"value":"request_plugin_install"},{"truncated":false,"value":"request_user_input"},{"truncated":false,"value":"retrieve_tool_result"},{"truncated":false,"value":"revert_turn"},{"truncated":false,"value":"review"},{"truncated":false,"value":"send_later"},{"truncated":false,"value":"session_get"},{"truncated":false,"value":"session_search"},{"truncated":false,"value":"start_mcp_server"},{"truncated":false,"value":"start_registry_mcp_server"},{"truncated":false,"value":"tasks"},{"truncated":false,"value":"terminal/cancel"}]}},"registry_tool_count":{"status":"known","value":65},"rendered_tool_count":15,"schema_version":1,"step":1,"terminal":{"automatic_compaction_attempts":0,"effective_max_steps":null,"emergency_compaction_attempts":0,"empty_stop_retries":0,"final_report_requested":false,"last_prepared_output_limit_tokens":65536,"last_provider_finish_reason":{"truncated":false,"value":"end_turn"},"last_reported_input_tokens":200,"last_response_tool_calls":0,"last_response_tool_calls_suppressed":0,"model_requests_started":2,"model_step_index":1,"permission_denial_rounds_without_progress":0,"permission_strategy_switches":0,"reason":"provider_no_tool_call","reasoning_only_reprompts":0,"route_context_window_tokens":1000000,"status":"completed","step_budget_source":"max_steps","stream_resumes":0,"transparent_stream_retries":0,"transport_retries":0},"tool_count":15,"tools":[{"allowed_callers":{"status":"known","value":{"count":1,"omitted":0,"rendered":[{"truncated":false,"value":"direct"}]}},"approval":{"status":"known","value":{"truncated":false,"value":"Required"}},"cache_control_type":{"reason":"request field absent","status":"unknown"},"capabilities":{"status":"known","value":{"count":3,"omitted":0,"rendered":[{"truncated":false,"value":"ExecutesCode"},{"truncated":false,"value":"Sandboxable"},{"truncated":false,"value":"RequiresApproval"}]}},"defer_loading":{"status":"known","value":false},"description":{"truncated":true,"value":"Execute a shell command in the workspace and return stdout and stderr (last 2000 lines or 50KB). Optional timeout is foreground seconds (default 30 seconds); a command still running then moves to the background by itself — never killed for time — returning output so far, last-output age, build phase, and a task_id. Call task_shell_wait with that task_id to read more or wait; do not sleep-poll with bash. Before a final answer, collect every still-relevant background task (wait only when genuinely blocked) an"},"input_examples":{"reason":"request field absent","status":"unknown"},"input_schema_json":{"truncated":false,"value":"{\"additionalProperties\":false,\"properties\":{\"command\":{\"description\":\"The command to execute. Actual execution shell: `/bin/bash`. Use Bash syntax: pipelines, redirections, $(command), and && / || are supported. Quote paths and variable expansions, such as \\\"$path\\\"; use single quotes for literal text. For literal multiline input, use a quoted heredoc delimiter (<<'EOF') with its closing delimiter on a separate line. Use only installed programs; do not assume GNU-specific flags on macOS/BSD. Example: printf '%s\\\\n' 'sample'.\",\"type\":\"string\"},\"description\":{\"description\":\"Optional few-word summary shown to the user beside the command.\",\"type\":\"string\"},\"justification\":{\"description\":\"Required with sandbox_permissions: one sentence explaining why this exact command needs wider access.\",\"type\":\"string\"},\"read_only\":{\"description\":\"Set true to run analysis code (including Python/SQLite) or a network fetch (curl) with mandatory native filesysteLine truncated
{"event":"turn_complete","parent_route_usage":{"input_tokens":360,"output_tokens":46},"routed_usage_dropped_records":0,"status":"completed","tool_catalog":"<pinned by the prompt family>","turn_id":"<uuid:1>","usage":{"input_tokens":360,"output_tokens":46}}
{"harness_summary":{"model_requests":2,"non_streaming_requests":0,"workspace_after":{".codewhale/state/.gitignore":"cdbcae15105d6b78",".codewhale/state/subagents.v1.json/legacy.json":"9064f16acf90984c",".codewhale/state/subagents.v1.lock":"e3b0c44298fc1c14",".codewhale/state/subagents.v3.json":"e874da2951e53d2b",".codewhale/state/subagents.v3.transaction.lock":"e3b0c44298fc1c14","NOTES.md":"f957b19529906961","README.md":"41f49bffb38de5ba"}}}
{"harness_summary":{"model_requests":2,"non_streaming_requests":0,"workspace_after":{".codewhale/state/.gitignore":"cdbcae15105d6b78",".codewhale/state/subagents.v1.json/legacy.json":"9064f16acf90984c",".codewhale/state/subagents.v3.json":"e874da2951e53d2b",".codewhale/state/subagents.v3.transaction.lock":"e3b0c44298fc1c14","NOTES.md":"f957b19529906961","README.md":"41f49bffb38de5ba"}}}
@@ -15,4 +15,4 @@
{"engine_session_id":"conformance-session","event":"session_updated","instruction_assembly":{"budgets":{"claude_fallback":false,"file_max_bytes":8388608,"global_tokens":100000,"policy":"trim","project_tokens":100000,"static_guard_tokens":667571,"usable_window_tokens":834464},"constitution_source_path":null,"constitution_tokens":0,"global_tokens":0,"notices":[{"action":"Kept","bytes_dropped":0,"bytes_kept":514,"detail":null,"fallback_selected":null,"limit":"None","origin":"Instructions","path":null,"scope":"Project","tokens_dropped":0,"tokens_kept":172}],"project_tokens":172,"retained_block_tokens":193,"source_path":null,"warnings":[]},"messages":[{"content":[{"text":"Say hello.","type":"text"},{"text":"<turn_meta>\nCurrent local date: <today>\nCurrent workspace: <WORKSPACE>\nCurrent permission posture: Ask\nCurrent sandbox posture: workspace-write (writes inside the workspace; network blocked) (local OS sandbox applied)\nEstimated conversation input: 1968 / 1000000 context tokens (0.2%). Automatic compaction is enabled.\n## Repo Working Set\nKey files: README.md\nWhen in doubt, use tools to verify and keep changes focused on the working set.\n</turn_meta>","type":"text"}],"role":"user"},{"content":[{"text":"<codewhale:runtime_event kind=\"project_instructions\" visibility=\"internal\">\n<system-reminder>\nThe following workspace instructions may be relevant to your work. Use them as guidance when applicable. More specific instructions take precedence over broader ones. They do not override system, developer, or direct user instructions. Instructions from: AGENTS.md\n\n<project_instructions source=\"project\">\n# Project Context (Auto-generated, ephemeral)\n\n> This context was generated in memory by Codewhale.\n> No .codewhale/instructions.md file was written.\n\n## Bounded Project Overview\n\n```json\n{\n \"project_name\": \"workspace\",\n \"directory_structure\": [\n \"README.md\",\n \"NOTES.md\"\n ],\n \"readme\": {\n \"path\": \"README.md\",\n \"excerpt\": \"conformance fixture\"\n },\n \"config_files\": [],\n \"key_source_files\": [],\n \"counts\": {\n \"config_files\": 0,\n \"directory_entries\": 2,\n \"key_source_files\": 0\n }\n}\n```\n</project_instructions>\n</system-reminder>\n</codewhale:runtime_event>","type":"text"},{"text":"<turn_meta>\nInput provenance: runtime (non-authoritative)\n</turn_meta>","type":"text"}],"role":"user"},{"content":[{"text":"Hello there.","type":"text"}],"role":"assistant"}],"model":"deepseek-v4-pro","system_prompt":"<pinned by the prompt family>","workspace":"<WORKSPACE>"}
{"event":"tool_request_snapshot","snapshot":{"active_tool_catalog_sha256":"9017c53ee026ae4065bed3e4b2e7d773a828556b42d6ebe2c2ebfd8e65377280","capture_source":"prepared model-client request","delivery_status":"unknown (capture does not prove provider delivery)","omitted_tool_count":0,"payload_json_bytes":17394,"payload_measurement_status":"exact (within 1048576-byte measurement bound)","provider":{"model":"deepseek-v4-pro","provider":"Deepseek","status":"available"},"registry_facts_present":true,"registry_only_tools":{"status":"known","value":{"count":40,"omitted":8,"rendered":[{"truncated":false,"value":"Git"},{"truncated":false,"value":"Run"},{"truncated":false,"value":"Web"},{"truncated":false,"value":"apply_patch"},{"truncated":false,"value":"automation"},{"truncated":false,"value":"context_archive_get"},{"truncated":false,"value":"context_archive_search"},{"truncated":false,"value":"diagnostics"},{"truncated":false,"value":"finance"},{"truncated":false,"value":"github"},{"truncated":false,"value":"handle_read"},{"truncated":false,"value":"harness"},{"truncated":false,"value":"image_ocr"},{"truncated":false,"value":"list_dir"},{"truncated":false,"value":"lsp"},{"truncated":false,"value":"note"},{"truncated":false,"value":"notify"},{"truncated":false,"value":"pandoc_convert"},{"truncated":false,"value":"project_map"},{"truncated":false,"value":"registry_sync"},{"truncated":false,"value":"request_plugin_install"},{"truncated":false,"value":"request_user_input"},{"truncated":false,"value":"retrieve_tool_result"},{"truncated":false,"value":"revert_turn"},{"truncated":false,"value":"review"},{"truncated":false,"value":"send_later"},{"truncated":false,"value":"session_get"},{"truncated":false,"value":"session_search"},{"truncated":false,"value":"start_mcp_server"},{"truncated":false,"value":"start_registry_mcp_server"},{"truncated":false,"value":"tasks"},{"truncated":false,"value":"terminal/cancel"}]}},"registry_tool_count":{"status":"known","value":65},"rendered_tool_count":15,"schema_version":1,"step":0,"terminal":{"automatic_compaction_attempts":0,"effective_max_steps":null,"emergency_compaction_attempts":0,"empty_stop_retries":0,"final_report_requested":false,"last_prepared_output_limit_tokens":65536,"last_provider_finish_reason":{"truncated":false,"value":"end_turn"},"last_reported_input_tokens":120,"last_response_tool_calls":0,"last_response_tool_calls_suppressed":0,"model_requests_started":1,"model_step_index":0,"permission_denial_rounds_without_progress":0,"permission_strategy_switches":0,"reason":"provider_no_tool_call","reasoning_only_reprompts":0,"route_context_window_tokens":1000000,"status":"completed","step_budget_source":"max_steps","stream_resumes":0,"transparent_stream_retries":0,"transport_retries":0},"tool_count":15,"tools":[{"allowed_callers":{"status":"known","value":{"count":1,"omitted":0,"rendered":[{"truncated":false,"value":"direct"}]}},"approval":{"status":"known","value":{"truncated":false,"value":"Required"}},"cache_control_type":{"reason":"request field absent","status":"unknown"},"capabilities":{"status":"known","value":{"count":3,"omitted":0,"rendered":[{"truncated":false,"value":"ExecutesCode"},{"truncated":false,"value":"Sandboxable"},{"truncated":false,"value":"RequiresApproval"}]}},"defer_loading":{"status":"known","value":false},"description":{"truncated":true,"value":"Execute a shell command in the workspace and return stdout and stderr (last 2000 lines or 50KB). Optional timeout is foreground seconds (default 30 seconds); a command still running then moves to the background by itself — never killed for time — returning output so far, last-output age, build phase, and a task_id. Call task_shell_wait with that task_id to read more or wait; do not sleep-poll with bash. Before a final answer, collect every still-relevant background task (wait only when genuinely blocked) an"},"input_examples":{"reason":"request field absent","status":"unknown"},"input_schema_json":{"truncated":false,"value":"{\"additionalProperties\":false,\"properties\":{\"command\":{\"description\":\"The command to execute. Actual execution shell: `/bin/bash`. Use Bash syntax: pipelines, redirections, $(command), and && / || are supported. Quote paths and variable expansions, such as \\\"$path\\\"; use single quotes for literal text. For literal multiline input, use a quoted heredoc delimiter (<<'EOF') with its closing delimiter on a separate line. Use only installed programs; do not assume GNU-specific flags on macOS/BSD. Example: printf '%s\\\\n' 'sample'.\",\"type\":\"string\"},\"description\":{\"description\":\"Optional few-word summary shown to the user beside the command.\",\"type\":\"string\"},\"justification\":{\"description\":\"Required with sandbox_permissions: one sentence explaining why this exact command needs wider access.\",\"type\":\"string\"},\"read_only\":{\"description\":\"Set true to run analysis code (including Python/SQLite) or a network fetch (curl) with mandatory native filesysteLine truncated
{"event":"turn_complete","parent_route_usage":{"input_tokens":120,"output_tokens":4},"routed_usage_dropped_records":0,"status":"completed","tool_catalog":"<pinned by the prompt family>","turn_id":"<uuid:1>","usage":{"input_tokens":120,"output_tokens":4}}
{"harness_summary":{"model_requests":1,"non_streaming_requests":0,"workspace_after":{".codewhale/state/.gitignore":"cdbcae15105d6b78",".codewhale/state/subagents.v1.json/legacy.json":"9064f16acf90984c",".codewhale/state/subagents.v1.lock":"e3b0c44298fc1c14",".codewhale/state/subagents.v3.json":"e874da2951e53d2b",".codewhale/state/subagents.v3.transaction.lock":"e3b0c44298fc1c14","NOTES.md":"f957b19529906961","README.md":"41f49bffb38de5ba"}}}
{"harness_summary":{"model_requests":1,"non_streaming_requests":0,"workspace_after":{".codewhale/state/.gitignore":"cdbcae15105d6b78",".codewhale/state/subagents.v1.json/legacy.json":"9064f16acf90984c",".codewhale/state/subagents.v3.json":"e874da2951e53d2b",".codewhale/state/subagents.v3.transaction.lock":"e3b0c44298fc1c14","NOTES.md":"f957b19529906961","README.md":"41f49bffb38de5ba"}}}
@@ -13,4 +13,4 @@
{"event":"tool_call_complete","result":{"content":"Not executed: the provider stream failed before the model response completed (Model not exist.).","metadata":{"error_category":"model_stream_failed","model_output_incomplete":true,"side_effect_status":"not_started"},"outcome":"ok","success":false},"tool_call_id":"<uuid:2>","tool_name":"File"}
{"event":"tool_request_snapshot","snapshot":{"active_tool_catalog_sha256":"9017c53ee026ae4065bed3e4b2e7d773a828556b42d6ebe2c2ebfd8e65377280","capture_source":"prepared model-client request","delivery_status":"unknown (capture does not prove provider delivery)","omitted_tool_count":0,"payload_json_bytes":17394,"payload_measurement_status":"exact (within 1048576-byte measurement bound)","provider":{"model":"deepseek-v4-pro","provider":"Deepseek","status":"available"},"registry_facts_present":true,"registry_only_tools":{"status":"known","value":{"count":40,"omitted":8,"rendered":[{"truncated":false,"value":"Git"},{"truncated":false,"value":"Run"},{"truncated":false,"value":"Web"},{"truncated":false,"value":"apply_patch"},{"truncated":false,"value":"automation"},{"truncated":false,"value":"context_archive_get"},{"truncated":false,"value":"context_archive_search"},{"truncated":false,"value":"diagnostics"},{"truncated":false,"value":"finance"},{"truncated":false,"value":"github"},{"truncated":false,"value":"handle_read"},{"truncated":false,"value":"harness"},{"truncated":false,"value":"image_ocr"},{"truncated":false,"value":"list_dir"},{"truncated":false,"value":"lsp"},{"truncated":false,"value":"note"},{"truncated":false,"value":"notify"},{"truncated":false,"value":"pandoc_convert"},{"truncated":false,"value":"project_map"},{"truncated":false,"value":"registry_sync"},{"truncated":false,"value":"request_plugin_install"},{"truncated":false,"value":"request_user_input"},{"truncated":false,"value":"retrieve_tool_result"},{"truncated":false,"value":"revert_turn"},{"truncated":false,"value":"review"},{"truncated":false,"value":"send_later"},{"truncated":false,"value":"session_get"},{"truncated":false,"value":"session_search"},{"truncated":false,"value":"start_mcp_server"},{"truncated":false,"value":"start_registry_mcp_server"},{"truncated":false,"value":"tasks"},{"truncated":false,"value":"terminal/cancel"}]}},"registry_tool_count":{"status":"known","value":65},"rendered_tool_count":15,"schema_version":1,"step":0,"terminal":{"automatic_compaction_attempts":0,"effective_max_steps":null,"emergency_compaction_attempts":0,"empty_stop_retries":0,"final_report_requested":false,"last_prepared_output_limit_tokens":65536,"last_provider_finish_reason":null,"last_reported_input_tokens":120,"last_response_tool_calls":1,"last_response_tool_calls_suppressed":1,"model_requests_started":1,"model_step_index":0,"permission_denial_rounds_without_progress":0,"permission_strategy_switches":0,"reason":"failed","reasoning_only_reprompts":0,"route_context_window_tokens":1000000,"status":"failed","step_budget_source":"max_steps","stream_resumes":0,"transparent_stream_retries":0,"transport_retries":0},"tool_count":15,"tools":[{"allowed_callers":{"status":"known","value":{"count":1,"omitted":0,"rendered":[{"truncated":false,"value":"direct"}]}},"approval":{"status":"known","value":{"truncated":false,"value":"Required"}},"cache_control_type":{"reason":"request field absent","status":"unknown"},"capabilities":{"status":"known","value":{"count":3,"omitted":0,"rendered":[{"truncated":false,"value":"ExecutesCode"},{"truncated":false,"value":"Sandboxable"},{"truncated":false,"value":"RequiresApproval"}]}},"defer_loading":{"status":"known","value":false},"description":{"truncated":true,"value":"Execute a shell command in the workspace and return stdout and stderr (last 2000 lines or 50KB). Optional timeout is foreground seconds (default 30 seconds); a command still running then moves to the background by itself — never killed for time — returning output so far, last-output age, build phase, and a task_id. Call task_shell_wait with that task_id to read more or wait; do not sleep-poll with bash. Before a final answer, collect every still-relevant background task (wait only when genuinely blocked) an"},"input_examples":{"reason":"request field absent","status":"unknown"},"input_schema_json":{"truncated":false,"value":"{\"additionalProperties\":false,\"properties\":{\"command\":{\"description\":\"The command to execute. Actual execution shell: `/bin/bash`. Use Bash syntax: pipelines, redirections, $(command), and && / || are supported. Quote paths and variable expansions, such as \\\"$path\\\"; use single quotes for literal text. For literal multiline input, use a quoted heredoc delimiter (<<'EOF') with its closing delimiter on a separate line. Use only installed programs; do not assume GNU-specific flags on macOS/BSD. Example: printf '%s\\\\n' 'sample'.\",\"type\":\"string\"},\"description\":{\"description\":\"Optional few-word summary shown to the user beside the command.\",\"type\":\"string\"},\"justification\":{\"description\":\"Required with sandbox_permissions: one sentence explaining why this exact command needs wider access.\",\"type\":\"string\"},\"read_only\":{\"description\":\"Set true to run analysis code (including Python/SQLite) or a network fetch (curl) with mandatory native filesystem read-only isolation. Outbound network is on only iLine truncated
{"error":"Model not exist.","event":"turn_complete","parent_route_usage":{"input_tokens":120,"output_tokens":0},"routed_usage_dropped_records":0,"status":"failed","tool_catalog":"<pinned by the prompt family>","turn_id":"<uuid:1>","usage":{"input_tokens":120,"output_tokens":0}}
{"harness_summary":{"model_requests":1,"non_streaming_requests":0,"workspace_after":{".codewhale/state/.gitignore":"cdbcae15105d6b78",".codewhale/state/subagents.v1.json/legacy.json":"9064f16acf90984c",".codewhale/state/subagents.v1.lock":"e3b0c44298fc1c14",".codewhale/state/subagents.v3.json":"e874da2951e53d2b",".codewhale/state/subagents.v3.transaction.lock":"e3b0c44298fc1c14","NOTES.md":"f957b19529906961","README.md":"41f49bffb38de5ba"}}}
{"harness_summary":{"model_requests":1,"non_streaming_requests":0,"workspace_after":{".codewhale/state/.gitignore":"cdbcae15105d6b78",".codewhale/state/subagents.v1.json/legacy.json":"9064f16acf90984c",".codewhale/state/subagents.v3.json":"e874da2951e53d2b",".codewhale/state/subagents.v3.transaction.lock":"e3b0c44298fc1c14","NOTES.md":"f957b19529906961","README.md":"41f49bffb38de5ba"}}}
-1
View File
@@ -44,7 +44,6 @@ changelog or release notes, and this contributor record.
**Contributions**
- **[MannXo](https://github.com/MannXo)** — added Qwen Code to the external agent registry so an installed Qwen Code CLI runs as a session over ACP ([#7293](https://github.com/codewhale-hq/Codewhale/pull/7293)).
- **[SamhandsomeLee](https://github.com/SamhandsomeLee)** — drove Responses request, URL and stream behavior from one checked dialect profile table ([#7261](https://github.com/codewhale-hq/Codewhale/pull/7261)).
- **[gaord](https://github.com/gaord)** — ran the read-only `/system`, `/tokens` and `/cost` builtins over the Runtime API through one gated terminal-host catalog ([#7277](https://github.com/codewhale-hq/Codewhale/pull/7277)).
@@ -68,8 +68,8 @@ ids to customers.
## Subagent state
Subagent state lives in the workspace, under `.codewhale/state/`, in
`subagents.v1.json` (the manager's `SUBAGENT_STATE_FILE`; the lock is
`subagents.v1.lock`), loaded by `load_state` and written by
`subagents.v3.json` (the manager's `SUBAGENT_STATE_FILE`), loaded by
`load_state` and written by
`persist_state*` in `crates/engine/src/tools/subagent/mod.rs`. A legacy
`.deepseek/state/` copy is still read for migration.
-11
View File
@@ -161,17 +161,6 @@ separately from the #6953 reporter-observed exports; the x64 profile now allows
468 pairs. The candidate still has to pass the import check against its final
executable bytes, and neither addition is a 14393 or installer run.
Release candidate run 43186389246 on source `f5a3dda975` then stopped the same
check on three more pairs, `GetSidIdentifierAuthority`, `GetSidSubAuthority` and
`GetSidSubAuthorityCount` from advapi32.dll, on both 64-bit targets. Probe run
43197611105 (Windows 11 ARM64 build 26200.9725) resolved all 447 imports of
that candidate's ARM64 executables, and the 3 pairs were added. For x64 the 3
pairs are documented Microsoft API contracts (Windows XP minimum on each
function's reference page), labelled apart from the #6953 reporter-observed
exports; the x64 profile now allows 471 pairs and the ARM64 profile 473. The
same limit applies: the candidate still has to pass the import check against
its final executable bytes, and neither addition is a 14393 or installer run.
At this snapshot, startup/installation of the final release bytes on an actual
1607 machine and the candidate's generated uninstaller were still unqualified.
The final frozen source, release import dumps, archive/native/npm installation,
+1 -1
View File
@@ -3270,7 +3270,7 @@ refuses OpenAI Codex. An Engine without this route answers `404`.
Reports which external agent CLIs this computer already has, without starting a
session. It covers Claude, Codex, DeepSeek Harness, Antigravity (`agy acp`),
OpenCode, Kimi Code, Mistral Vibe, Qoder, CodeBuddy, Grok and Qwen Code. `state` is `installed` or `not_installed`; a binary is
OpenCode, Kimi Code, Mistral Vibe, Qoder, CodeBuddy and Grok. `state` is `installed` or `not_installed`; a binary is
looked up on `PATH` (absolute entries only; on Windows the spawnable PATHEXT suffixes `.com/.exe/.bat/.cmd` and `%APPDATA%\npm`) and in the usual per-user bin directories. For an installed
agent the route runs `--version` and, where the CLI speaks Agent Client Protocol
itself, one `initialize` request on stdin, concurrently. `acp` is `native` when
+30 -12
View File
@@ -29,7 +29,7 @@ resolved values are persisted in their launch manifest.
By default, delegated control-plane state remains workspace-scoped at
`<workspace>/.codewhale/state`: the worker ledger, complete transcript
artifacts, and coordination lock share that root. An embedding host may set
artifacts, and store locks share that root. An embedding host may set
`EngineConfig::subagent_state_root` to keep those files under a session-owned
root without changing child cwd, tool path authority, or the execution
workspace recorded in receipts.
@@ -124,19 +124,32 @@ cannot replace a newer one, and a terminal receipt cannot be resurrected as
Running by a checkpoint. An explicit terminal retry accepts only the same
winning outcome; authoritative newer accounting metadata is retained.
Normal interactive/exec/Fleet/background sessions continue to share the
existing admission flock. The explicit exclusive Runtime constructor retains
its host admission policy; it is not the mechanism that conserves shared
snapshots. No manager holds the transaction lock while a child executes or
while it waits for a provider or channel capacity. Conflicting lock contention
returns a bounded retry error. Unsupported locking or failed store admission
refuses durable writes rather than silently creating an empty writable ledger.
There is no workspace-wide session lock. Interactive, exec, Fleet, background
and Runtime managers in one folder are ordinary neighbours: each holds only its
own boot lease and takes the transaction lock per operation. The Runtime
constructor keeps one manager per state path inside its process. No manager
holds the transaction lock while a child executes or while it waits for a
provider or channel capacity. Conflicting lock contention returns a bounded
retry error. Unsupported locking or failed store admission refuses durable
writes rather than silently creating an empty writable ledger, and admission is
retried on the next delegated write instead of staying refused for the life of
the process.
A Runtime that admits a selected workspace other than its own state root keeps
that workspace's claims in its own store. Before it admits a shared-workspace
writer there, and each time it leases such a writer, it refuses while any
session holds a live boot lease in the selected workspace's own store, and it
refuses when that store's directory cannot be listed. Only sessions that hold
a boot lease are seen: a Codewhale 0.10.2 or older session holds none. A
session that opens in that workspace later does not see the Runtime's claims,
and its lease refuses the Runtime's next shared-workspace writer there; give
writers isolated worktrees when both are expected.
Each writer holds its own boot lease. Other managers preserve its live records
and use its active claims during admission. An existing unlocked lease can
establish that its writer ended; a missing, invalid or unreadable lease is
uncertain and fails closed. Local task handles and a failed admission flock do
not establish that another process's work stopped. Parked and headless
uncertain and fails closed. Local task handles do not establish that another
process's work stopped. Parked and headless
WaitingForUser claim rules retain their existing lifecycle meaning; they do not
transfer a still-live writer's record ownership to a different boot.
@@ -216,10 +229,15 @@ terminal receipt or claim that a remote effect was undone.
### Legacy cutover and recovery
The first current-format admission takes the legacy admission lock exclusively
only for migration, after previous holders have left. It retains the original
The first current-format admission migrates under the short transaction lock.
It does not wait for older sessions that still hold the retired
`subagents.v1.lock`. It retains the original
legacy bytes and a validated recovery copy, then leaves
`subagents.v1.json` as a private directory containing that recovery material.
An older session still open in that folder can no longer save its delegated
state once the directory replaces its file; restart it. If it publishes its
file between the two cutover renames, the cutover stops at a retained-file
refusal until a person inspects both files; nothing is deleted.
The current format3 file is published under the same short transaction lock.
Interrupted cutover states are validated and resumed without replacing a later
current-format revision with the old backup. Future or malformed envelopes
+2 -7
View File
@@ -97,13 +97,8 @@ Stage 5 of the loop: [cw-orient](../cw-orient/SKILL.md) →
Co-authored-by: <maintainer or agent identity>
```
GitHub's contributor count takes authors and `Co-authored-by` trailers in
the final trailer block (no blank line between trailers) whose address is
linked to the account, and `check-contributor-credit.py` flags a harvested
handle who is not credited, a trailer GitHub cannot parse, and a noreply id
that belongs to someone else. It accepts credit only from a noreply address
whose id matches `.github/AUTHOR_MAP`, or from a raw email listed there, so
add the contributor's line to `AUTHOR_MAP` when it is missing.
GitHub's contributor list counts authors only, and
`check-contributor-credit.py` flags a harvested handle who is not credited.
Resolve the address from `.github/AUTHOR_MAP`, else
`gh api users/<handle> --jq '"\(.id)+\(.login)@users.noreply.github.com"'`.
A clean cherry-pick keeps the author by itself. This step is the single
-5
View File
@@ -127,11 +127,6 @@ DLL 的身份一致,[该 API 的文档前提是 Vista](https://learn.microsoft
x64 没有导出探测,因此这 12 对作为低于 14393 下限的微软文档 API 契约单独记录,与 #6953 报告者实测导出分开标注;x64 允许列表现为 468 对。
候选仍须以最终可执行文件通过导入检查,两项新增都不是 14393 或安装器运行。
源 `f5a3dda975` 的发布候选运行 43186389246 随后在两个 64 位目标上又停止于三对导入:advapi32.dll 的 `GetSidIdentifierAuthority`、`GetSidSubAuthority` 和 `GetSidSubAuthorityCount`。
探测运行 43197611105(Windows 11 ARM64 内部版本 26200.9725)解析了该候选 ARM64 程序的全部 447 个导入,新增的 3 对已加入基线。
x64 的这 3 对作为微软文档 API 契约记录(各函数参考页的最低客户端为 Windows XP),与 #6953 报告者实测导出分开标注;x64 允许列表现为 471 对,ARM64 为 473 对。
限制不变:候选仍须以最终可执行文件通过导入检查,两项新增都不是 14393 或安装器运行。
此快照中,最终发布文件在真实 1607 上启动/安装及候选程序生成的卸载器仍未验收。
最终冻结源码、发布导入记录、各安装途径、打包/原生应用崩溃恢复和最低 OS 回执应
进入发布验收记录,见[英文发布检查单](../RELEASE_CHECKLIST.md)。
+1 -1
View File
@@ -257,7 +257,7 @@
},
"crates/engine/src/tools/subagent/mod.rs": {
"path_canonicalize": 4,
"std_fs": 3,
"std_fs": 2,
"thread_sleep": 1
},
"crates/engine/src/tools/subagent/worktree.rs": {
+20 -91
View File
@@ -17,7 +17,7 @@ live in the commit bodies, so no network call is needed.
Usage: scripts/check-contributor-credit.py [<since-rev>]
"""
from __future__ import annotations
import functools, re, subprocess, sys
import re, subprocess, sys
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
@@ -35,7 +35,6 @@ def sh(*args: str) -> str:
return subprocess.run(args, cwd=ROOT, capture_output=True, text=True, check=True).stdout
@functools.cache
def author_map() -> dict[str, str]:
"""alias -> github handle, from .github/AUTHOR_MAP."""
out: dict[str, str] = {}
@@ -53,43 +52,19 @@ def author_map() -> dict[str, str]:
return out
NOREPLY = re.compile(r"\d+\+([^@<>\s]+)@users\.noreply\.github\.com")
def not_a_person(name: str, email: str) -> bool:
low, addr = name.strip().lower(), email.lower()
return any(low.startswith(skip) for skip in SKIP) or "[bot]" in addr \
or addr.endswith(("[email protected]", "[email protected]"))
def maintainer(name: str, email: str) -> bool:
handle = author_map().get(email.lower()) or author_map().get(name.strip().lower()) or name.strip()
return any(h.lower().startswith(skip) for skip in SKIP for h in (name.strip(), handle))
def restoring(subject: str, name: str, email: str) -> bool:
return subject.startswith("chore(credit):") and maintainer(name, email)
def noreply_ids() -> dict[str, str]:
out: dict[str, str] = {}
path = ROOT / ".github" / "AUTHOR_MAP"
if path.exists():
for login_id, login in re.findall(r"(\d+)\+([^@<>\s]+)@users\.noreply\.github\.com", path.read_text()):
out[login.lower()] = login_id
return out
def expected(since: str) -> dict[str, str]:
"""handle -> why we think they are owed credit."""
amap = author_map()
found: dict[str, str] = {}
def add(name: str, email: str, why: str) -> None:
low = name.strip().lower()
# Model co-authorship ("Claude Opus 5 (1M context)") and bot addresses
# are not people owed credit; match on prefix since the model name and
# its context window change release to release.
if not_a_person(name, email):
if any(low.startswith(skip) for skip in SKIP):
return
if "[bot]" in email.lower() or email.lower().endswith(("[email protected]", "[email protected]")):
return
handle = amap.get(email.lower()) or amap.get(name.strip().lower())
if not handle:
@@ -99,17 +74,16 @@ def expected(since: str) -> dict[str, str]:
return
found.setdefault(handle, why)
log = sh("git", "log", f"{since}..HEAD", "--format=%an%x1f%ae%x1f%s%x1f%b%x00")
work = [parts for parts in (entry.lstrip("\n").split("\x1f", 3) for entry in log.split("\0"))
if len(parts) == 4 and not restoring(parts[2], parts[0], parts[1])]
for author, email, _, _ in work:
add(author, email, "commit author")
for _, _, _, body in work:
for name, addr in re.findall(r"(?im)^co-authored-by:\s*(.+?)\s*<([^>]+)>", body):
add(name, addr, "co-author trailer")
for pr, handle in re.findall(r"(?i)harvested from PR #(\d+) by @([A-Za-z0-9-]+)", body):
if handle.lower() not in SKIP:
found.setdefault(handle, f"harvested PR #{pr}")
for line in sh("git", "log", f"{since}..HEAD", "--format=%an\t%ae").splitlines():
name, _, email = line.partition("\t")
add(name, email, "commit author")
body = sh("git", "log", f"{since}..HEAD", "--format=%b")
for name, email in re.findall(r"(?im)^co-authored-by:\s*(.+?)\s*<([^>]+)>", body):
add(name, email, "co-author trailer")
for pr, handle in re.findall(r"(?i)harvested from PR #(\d+) by @([A-Za-z0-9-]+)", body):
if handle.lower() not in SKIP:
found.setdefault(handle, f"harvested PR #{pr}")
return found
@@ -134,50 +108,17 @@ def main() -> int:
print(f"Contributor credit since {since}: {len(owed)} contributor(s) found.")
amap = author_map()
ids = noreply_ids()
lost = []
broken: list[tuple[str, str]] = []
credited: set[str] = set()
def login(name: str, addr: str) -> str:
nm = NOREPLY.search(addr)
return (amap.get(addr.lower()) or amap.get(name.strip().lower()) or (nm.group(1) if nm else name.strip())).lower()
def who(name: str, addr: str) -> str:
nm = re.search(r"(\d+)\+([^@<>\s]+)@users\.noreply\.github\.com", addr)
if not nm:
return amap.get(addr.lower(), "").lower() if "@" in addr else ""
known = ids.get(nm.group(2).lower())
if known is not None and known != nm.group(1):
broken.append((nm.group(2).lower(), f"uses {addr}, but @{nm.group(2)} is id {known} "
f"in AUTHOR_MAP; GitHub credits whoever owns id {nm.group(1)}"))
return nm.group(2).lower() if known == nm.group(1) else ""
log = sh("git", "log", f"{since}..HEAD",
"--format=%h%x1f%an%x1f%ae%x1f%(trailers:key=Co-authored-by,unfold)%x1f%B%x00")
for entry in log.split("\0"):
parts = entry.strip().split("\x1f", 4)
if len(parts) < 5:
for entry in sh("git", "log", f"{since}..HEAD", "--format=%h\t%an\t%ae\t%B%x00").split("\0"):
parts = entry.strip().split("\t", 3)
if len(parts) < 4:
continue
sha, name, email, parsed, body = parts
sha, name, email, body = parts
m = re.search(r"\d+\+([^@<>\s]+)@users\.noreply\.github\.com", email)
author = (amap.get(email.lower()) or amap.get(name.lower()) or (m.group(1) if m else name)).lower()
trailers = re.findall(r"(?im)^co-authored-by:\s*(.+?)\s*<([^>]+)>", parsed)
mark = len(broken)
credited.add(who(name, email))
credited.update(who(n, e) for n, e in trailers)
broken[mark:] = [(login, f"{sha} {why}") for login, why in broken[mark:]]
for co_name, co_email in re.findall(r"(?im)^\s*co-authored-by:\s*(.+?)\s*<([^>]+)>", body):
if (co_name, co_email) not in trailers and not not_a_person(co_name, co_email):
broken.append((login(co_name, co_email),
f"{sha} has 'Co-authored-by: {co_name}' outside the final trailer block; GitHub ignores it"))
if restoring(body.split("\n", 1)[0], name, email):
continue
for handle in re.findall(r"(?i)harvested from PR #\d+ by @([A-Za-z0-9-]+)", body):
if handle.lower() != author:
lost.append(f" {sha} harvested @{handle} but is authored by {name}")
broken.append((amap.get(handle.lower(), handle).lower(),
f"{sha} harvested @{handle} with neither authorship nor a Co-authored-by trailer for them"))
if any(author.startswith(skip) for skip in SKIP):
for co_name, co_email in re.findall(r"(?im)^co-authored-by:\s*(.+?)\s*<([^>]+)>", body):
cm = re.search(r"\d+\+([^@<>\s]+)@users\.noreply\.github\.com", co_email)
@@ -187,18 +128,8 @@ def main() -> int:
and "[bot]" not in co_email.lower() and not co_email.lower().endswith(("[email protected]", "[email protected]")):
lost.append(f" {sha} credits @{human} only as a co-author; authored by {name}")
if lost:
print("\nCommits whose human contributor is not the commit author (GitHub counts them only through a\n"
"well-formed Co-authored-by trailer whose address is linked to their account):\n")
print("\nCommits whose human contributor is not the commit author (GitHub's contributor list will miss them):\n")
print("\n".join(lost))
broken = [(login, why) for login, why in broken if login.lower() not in credited]
if broken:
print("\nCredit GitHub cannot see:\n")
print("\n".join(f" {why}" for _, why in broken))
print("\nPut each contributor in the final trailer block as\n"
"'Co-authored-by: Name <[email protected]>', with no blank line between trailers,\n"
"using the id from `gh api users/<login>`. A later commit can carry the trailer when history cannot change.\n"
"Credit is taken only from such an address, or from a raw email listed in .github/AUTHOR_MAP; add the\n"
"email a contributor commits with there once it is linked to their account.")
if missing:
print("\nUncredited work landed in this release:\n")
print("\n".join(missing))
@@ -208,8 +139,6 @@ def main() -> int:
"is still their work -- see AGENTS.md 'Landing other people's work'."
)
return 1
if broken:
return 1
print("Every contributor in the window is credited on all three surfaces.")
return 0
-3
View File
@@ -538,9 +538,6 @@ def report_huggingface_coverage(
def report_env_lookup_order(
label: str, source: str, expected_order: list[str], context: str
) -> list[str]:
non_blank_chain = "env_non_blank(&[" + ", ".join(f'"{name}"' for name in expected_order) + "])"
if non_blank_chain in source:
return []
lookup_needles = [f'std::env::var("{name}")' for name in expected_order]
return report_string_order(label, source, lookup_needles, context)
+6 -6
View File
@@ -1,10 +1,10 @@
#!/usr/bin/env python3
"""Everyone whose work landed in or is credited by Codewhale, on one wall.
GitHub's contributor count takes only commit authors and co-author trailers
whose email is linked to an account. Harvested pull requests without a trailer,
unlinked commit emails and credited bug reports never reach it, so it shows
about half of the people who built Codewhale.
GitHub's contributor list counts only commit authors whose email is linked to
an account. Harvested pull requests, co-author trailers, unlinked commit
emails and credited bug reports never reach it, so it shows about half of the
people who built Codewhale.
refresh rebuild docs/contributors.json from git history, the CHANGELOG,
docs/CONTRIBUTORS.md and GitHub (needs `gh`)
@@ -202,8 +202,8 @@ def render() -> None:
"<!-- contributor-wall:start -->\n## Everyone\n\n"
f"[![{total} people built Codewhale](assets/contributors.png)](#everyone)\n\n"
f"{total} people have code, co-authored commits, carried-forward pull requests or credited reports in Codewhale. "
"GitHub's contributor count includes only commit authors and co-authors whose email is linked to their "
"account, so it misses many of them. If your commits are missing there, add your commit email to your "
"GitHub's contributor list shows only commit authors whose email is linked to their account, so it counts "
f"{data['github_contributor_list']} of them. If your commits are missing there, add your commit email to your "
"GitHub account and they will link.\n\n"
f"{links}\n\nAlso with us, from commits whose email is not linked to an account: {' · '.join(extra)}.\n"
"<!-- contributor-wall:end -->\n"
+53 -144
View File
@@ -6,7 +6,7 @@
"osArchitecture": "x64",
"qualification": {
"result": "PASS",
"kind": "reporter exact-OS export resolution plus 15 documented Microsoft named API minima",
"kind": "reporter exact-OS export resolution plus 12 documented Microsoft named API minima",
"evidence": {
"issue": "https://github.com/codewhale-hq/Codewhale/issues/6953",
"binary": "https://github.com/codewhale-hq/Codewhale/releases/download/v0.10.1/codewhale-windows-x64.exe",
@@ -16,7 +16,7 @@
"previous_sha256": "f7b35a1e928f67884637f36b01fc72854f50690130f191bac3bcf62ece3c1d84"
},
"runtimeAcceptance": "Final 0.10.2 release bytes on 14393 not run",
"qualifiedPairs": 471,
"qualifiedPairs": 468,
"namedApiContracts": [
{
"dll": "advapi32.dll",
@@ -89,24 +89,6 @@
"symbol": "expf",
"minimumClient": "Exact API-set pair introduced in 10.0.10240",
"source": "https://learn.microsoft.com/en-us/uwp/win32-and-com/win32-extension-apis"
},
{
"dll": "advapi32.dll",
"symbol": "GetSidIdentifierAuthority",
"minimumClient": "Windows XP",
"source": "https://learn.microsoft.com/en-us/windows/win32/api/securitybaseapi/nf-securitybaseapi-getsididentifierauthority"
},
{
"dll": "advapi32.dll",
"symbol": "GetSidSubAuthority",
"minimumClient": "Windows XP",
"source": "https://learn.microsoft.com/en-us/windows/win32/api/securitybaseapi/nf-securitybaseapi-getsidsubauthority"
},
{
"dll": "advapi32.dll",
"symbol": "GetSidSubAuthorityCount",
"minimumClient": "Windows XP",
"source": "https://learn.microsoft.com/en-us/windows/win32/api/securitybaseapi/nf-securitybaseapi-getsidsubauthoritycount"
}
],
"namedApiContractEvidence": {
@@ -115,21 +97,7 @@
"binarySha256": "cd5a1f9d4b53eb0f2ac1e29fbbedc71f8845d63ab85115192f60b78cf197e2fa",
"importReceiptSha256": "a872374cb767467732c5f36e4b315d58fd424107a9a725800673b9181a714f02",
"ucrtPlatformSource": "https://learn.microsoft.com/en-us/cpp/porting/upgrade-your-code-to-the-universal-crt?view=msvc-170",
"scope": "Documentation-backed static API contracts below the unchanged 14393 floor; not reporter-observed exports, a native x64 export probe, executable startup, installer or ordinary-user acceptance",
"additionalCandidates": [
{
"candidateRun": "https://github.com/codewhale-hq/Codewhale/actions/runs/43186389246",
"source": "f5a3dda9755170aeafecd0841347a5578a540290",
"binarySha256": "6fa43dc8c8680cf3e30a38ea950154b28e30aa97fe373fe82fae6086aa58e1c1",
"importReceiptSha256": "9cc807d09bee35101a926773934937427a53214e6e1a54fb85e622b3f18a6ae3",
"pairs": [
"advapi32.dll!GetSidIdentifierAuthority",
"advapi32.dll!GetSidSubAuthority",
"advapi32.dll!GetSidSubAuthorityCount"
],
"scope": "Three pairs from crates/config private_directory.rs; documentation-backed static contracts, same limits as above"
}
]
"scope": "Documentation-backed static API contracts below the unchanged 14393 floor; not reporter-observed exports, a native x64 export probe, executable startup, installer or ordinary-user acceptance"
}
},
"imports": {
@@ -153,9 +121,6 @@
"GetSecurityDescriptorControl",
"GetSecurityDescriptorDacl",
"GetSecurityInfo",
"GetSidIdentifierAuthority",
"GetSidSubAuthority",
"GetSidSubAuthorityCount",
"GetTokenInformation",
"ImpersonateAnonymousToken",
"InitializeAcl",
@@ -666,25 +631,25 @@
"qualification": {
"result": "PASS",
"kind": "exact native system export resolution",
"time": "2026-11-12T10:08:11.6545236Z",
"time": "2026-11-07T16:01:13.7658879Z",
"osVersion": "10.0.26200.0",
"osBuild": "26200.9725",
"processArchitecture": "ARM64",
"runner": "GitHub-hosted windows-11-arm; its registry ProductName reads \"Windows 10 Enterprise\", as Windows 11 reports it",
"source": "f5a3dda9755170aeafecd0841347a5578a540290",
"sourceUrl": "https://github.com/codewhale-hq/Codewhale/commit/f5a3dda9755170aeafecd0841347a5578a540290",
"binarySha256": "c8c6f97d0ccf20e392b2457cb970187e79a29caa28d091dbfefe3a1393b99040",
"source": "549ff01ebb78856408164058fcb52db1e639ee0d",
"sourceUrl": "https://github.com/codewhale-hq/Codewhale/commit/549ff01ebb78856408164058fcb52db1e639ee0d",
"binarySha256": "38558ab2b4dc54590503543a0a584c5a82da6f078dd616b5b68e301bf09621b0",
"binaries": [
"codewhale-windows-arm64.exe",
"codew-windows-arm64.exe",
"codewhale-tui-windows-arm64.exe"
],
"resolved": 447,
"resolved": 444,
"unresolved": 0,
"buildRun": "https://github.com/codewhale-hq/Codewhale/actions/runs/43186389246",
"probeRun": "https://github.com/codewhale-hq/Codewhale/actions/runs/43197611105",
"buildRun": "https://github.com/codewhale-hq/Codewhale/actions/runs/42381818665",
"probeRun": "https://github.com/codewhale-hq/Codewhale/actions/runs/42389141087",
"probeWorkflow": ".github/workflows/windows-arm64-export-probe.yml",
"profile": "dist, built by release-artifacts.yml (windows-arm64 job) in release candidate run 43186389246",
"profile": "dist, built by release-artifacts.yml (windows-arm64 job) in release candidate run 42381818665",
"runtimeAcceptance": "Static export resolution only; this probe did not launch the release executables. The only ARM64 runtime acceptance is the cf7199b52e debug application run of 2026-10-13; the packaged release and older OS builds are unproven",
"exportProbeMethod": "ARM64 process: LoadLibraryExW per imported DLL with LOAD_LIBRARY_SEARCH_SYSTEM32 (0x00000800), then GetProcAddress for every imported symbol; module path and SHA256 recorded",
"scope": "System-context export resolution only; no Authenticode check, oldest supported OS, or ordinary-user install inference",
@@ -718,119 +683,66 @@
"supersedes": {
"result": "PASS",
"kind": "exact native system export resolution",
"time": "2026-11-07T16:01:13.7658879Z",
"time": "2026-10-16T10:57:53.0754367Z",
"osVersion": "10.0.26200.0",
"osBuild": "26200.9725",
"osBuild": "26200.9457",
"processArchitecture": "ARM64",
"runner": "GitHub-hosted windows-11-arm; its registry ProductName reads \"Windows 10 Enterprise\", as Windows 11 reports it",
"source": "549ff01ebb78856408164058fcb52db1e639ee0d",
"sourceUrl": "https://github.com/codewhale-hq/Codewhale/commit/549ff01ebb78856408164058fcb52db1e639ee0d",
"binarySha256": "38558ab2b4dc54590503543a0a584c5a82da6f078dd616b5b68e301bf09621b0",
"source": "db34bc8949a5b5e7e6fd5a88cb67e998011ad9df",
"sourceUrl": "https://github.com/codewhale-hq/Codewhale/commit/db34bc8949a5b5e7e6fd5a88cb67e998011ad9df",
"binarySha256": "a9a2a521d8d0c971b9d49c2c463cdb49d0c7a4eb700b73e6fed2c743e688eebb",
"binaries": [
"codewhale-windows-arm64.exe",
"codew-windows-arm64.exe",
"codewhale-tui-windows-arm64.exe"
],
"resolved": 444,
"resolved": 445,
"unresolved": 0,
"buildRun": "https://github.com/codewhale-hq/Codewhale/actions/runs/42381818665",
"probeRun": "https://github.com/codewhale-hq/Codewhale/actions/runs/42389141087",
"buildRun": "https://github.com/codewhale-hq/Codewhale/actions/runs/38867608774",
"probeRun": "https://github.com/codewhale-hq/Codewhale/actions/runs/38890634267",
"probeWorkflow": ".github/workflows/windows-arm64-export-probe.yml",
"profile": "dist, built by release-artifacts.yml (windows-arm64 job) in release candidate run 42381818665",
"profile": "release, built by release-artifacts.yml (windows-arm64 job) in release candidate run 38867608774",
"runtimeAcceptance": "Static export resolution only; this probe did not launch the release executables. The only ARM64 runtime acceptance is the cf7199b52e debug application run of 2026-10-13; the packaged release and older OS builds are unproven",
"exportProbeMethod": "ARM64 process: LoadLibraryExW per imported DLL with LOAD_LIBRARY_SEARCH_SYSTEM32 (0x00000800), then GetProcAddress for every imported symbol; module path and SHA256 recorded",
"scope": "System-context export resolution only; no Authenticode check, oldest supported OS, or ordinary-user install inference",
"systemModuleHashes": {
"advapi32.dll": "31a87f532c0e99521de26fb25d1ec33cfbb92359952580c88350891602b6cf35",
"api-ms-win-core-synch-l1-2-0.dll": "06bf19bcd0b4af9e78c46fc81fe8fb1bf950fb5f295d44de889c5028129b5b56",
"api-ms-win-crt-convert-l1-1-0.dll": "e59858bf4245cb0889d8da773612b75d58ac716be5d6bb801e1b39594dccfc6d",
"api-ms-win-crt-heap-l1-1-0.dll": "e59858bf4245cb0889d8da773612b75d58ac716be5d6bb801e1b39594dccfc6d",
"api-ms-win-crt-locale-l1-1-0.dll": "e59858bf4245cb0889d8da773612b75d58ac716be5d6bb801e1b39594dccfc6d",
"api-ms-win-crt-math-l1-1-0.dll": "e59858bf4245cb0889d8da773612b75d58ac716be5d6bb801e1b39594dccfc6d",
"api-ms-win-crt-runtime-l1-1-0.dll": "e59858bf4245cb0889d8da773612b75d58ac716be5d6bb801e1b39594dccfc6d",
"api-ms-win-crt-stdio-l1-1-0.dll": "e59858bf4245cb0889d8da773612b75d58ac716be5d6bb801e1b39594dccfc6d",
"api-ms-win-crt-string-l1-1-0.dll": "e59858bf4245cb0889d8da773612b75d58ac716be5d6bb801e1b39594dccfc6d",
"api-ms-win-crt-time-l1-1-0.dll": "e59858bf4245cb0889d8da773612b75d58ac716be5d6bb801e1b39594dccfc6d",
"api-ms-win-crt-utility-l1-1-0.dll": "e59858bf4245cb0889d8da773612b75d58ac716be5d6bb801e1b39594dccfc6d",
"bcrypt.dll": "2a016e6f29683a7e6be564424d286d0885ff4846fff87cefab4d4e5df47a8d4a",
"bcryptprimitives.dll": "6626bf08ddb38e3c37804d7a000c9684977fb0f5f7ab8eb1c0a91a654ec975fc",
"combase.dll": "6082ef654c1a0978e127a1e045cb3078b1e80f8f7108b5c6cb4bc601c69b7f51",
"crypt32.dll": "3f53b3351e75cb8847b87ce40349d48279cdddab4b19c4c6c9ad38eef6aa6baf",
"kernel32.dll": "905c5e00f64ddf9e3439a617b25d4dd1bd0fe54143ade689422b89ccb324ba3a",
"ntdll.dll": "0a7201420df35a67e76aa2f2f4adb793b0c0b4fbc927d7da112268a7beb74930",
"oleaut32.dll": "61cd70f21624866fb46102644d5af24563a30c85921ed8deed89139cab340f0b",
"shell32.dll": "b9a118ff822ab1e950c7a1009de223ecfea5d5f0a23b8a969d58ff19ce28d9cf",
"shlwapi.dll": "b4df8c76ea7999912ddf12ba026c5f4a55b683fe9234878f506080575481779a",
"user32.dll": "b95f8cce02f51a7cbf77f17582ad3eee8abc577c324963d0026e8dbb977fa097",
"userenv.dll": "e9b94fa6123048d535265214322a6a09e450dd85941292bf5258bf39d5b86f9e",
"advapi32.dll": "021452c20f89c50d64c304391ec18169f95a9fa14762f76cfd2dded627dbf0bb",
"api-ms-win-core-synch-l1-2-0.dll": "26be64c26bac33368a650372cb5fb1e3235f1bd7883471bd3291dbf6372ae274",
"api-ms-win-crt-convert-l1-1-0.dll": "529e795875178b906ea8758abc8de5c28339f336b3e7a121d7778ae040908de9",
"api-ms-win-crt-heap-l1-1-0.dll": "529e795875178b906ea8758abc8de5c28339f336b3e7a121d7778ae040908de9",
"api-ms-win-crt-locale-l1-1-0.dll": "529e795875178b906ea8758abc8de5c28339f336b3e7a121d7778ae040908de9",
"api-ms-win-crt-math-l1-1-0.dll": "529e795875178b906ea8758abc8de5c28339f336b3e7a121d7778ae040908de9",
"api-ms-win-crt-runtime-l1-1-0.dll": "529e795875178b906ea8758abc8de5c28339f336b3e7a121d7778ae040908de9",
"api-ms-win-crt-stdio-l1-1-0.dll": "529e795875178b906ea8758abc8de5c28339f336b3e7a121d7778ae040908de9",
"api-ms-win-crt-string-l1-1-0.dll": "529e795875178b906ea8758abc8de5c28339f336b3e7a121d7778ae040908de9",
"api-ms-win-crt-time-l1-1-0.dll": "529e795875178b906ea8758abc8de5c28339f336b3e7a121d7778ae040908de9",
"api-ms-win-crt-utility-l1-1-0.dll": "529e795875178b906ea8758abc8de5c28339f336b3e7a121d7778ae040908de9",
"bcrypt.dll": "12e3562488e5ca3a26884408d99d81898237c8ca3c5c3c9e40ca6b8f5ed361cd",
"bcryptprimitives.dll": "d3da1097e9e1fc16c910350e40c4405ef52f36f134f9bf165fbe02194c1eaa82",
"combase.dll": "850b43094ae61a5d3b8f40f0cddf89b4a90190bf5fc57c5242e0bf5b06644b2f",
"crypt32.dll": "e3e800f1905658674ce4af1d3efb31d722b9255aaae4fa46c54302d329ba4ab6",
"kernel32.dll": "6a7bd4daff9c0c4cc8b9478ecd9828c3f19be257a4a18db9559cae929197fffa",
"ntdll.dll": "60ef561645b50dce71ad0919dbbef3ad334a0489432cde718e75b73b558c7997",
"oleaut32.dll": "5ebb761b3f96b227006e79d6a92cf5d98ac5a5fd3c136d35dd05391bed867753",
"shell32.dll": "ef0fbd71a46fe4b35bff893ea65241cd08ea5231de704c106e39920c03698fe9",
"shlwapi.dll": "67f713491bc18614d6ea29b920e3c609cabf9eecf054990e030476a2e9f76a2c",
"user32.dll": "f2fc06e7e5016a15ad7f7f4ee250154b7d41d0a0706bf14927a3bbd4c42727dd",
"userenv.dll": "18bd1660ab7cacd90789f1bebe35bc6a5730b1e75e5582f53575473fae3db530",
"vcruntime140.dll": "3c56f4167e2b3d8e6338497731e6aae8cd7ec46bd6789f9423a8d9cf9a630310",
"winmm.dll": "4fd628e0a495b1fa7e7653b562a4f5484592c9cfe733184f9808dfce18e26680",
"ws2_32.dll": "d10c37755ef07b55829406cfaa22c623ae6f86cce5a5fed42d77d75c29ae824a"
"winmm.dll": "2ebb728e5c510492df8fde4ec81b8a8554f2f33d1686b837d22c962b7d2fa9b7",
"ws2_32.dll": "e1266872f796f68b4be9c20640ca940a7a96e1ba98c741d472ae305fccf87ca9"
},
"supersedes": {
"result": "PASS",
"kind": "exact native system export resolution",
"time": "2026-10-16T10:57:53.0754367Z",
"osVersion": "10.0.26200.0",
"osBuild": "26200.9457",
"processArchitecture": "ARM64",
"runner": "GitHub-hosted windows-11-arm; its registry ProductName reads \"Windows 10 Enterprise\", as Windows 11 reports it",
"source": "db34bc8949a5b5e7e6fd5a88cb67e998011ad9df",
"sourceUrl": "https://github.com/codewhale-hq/Codewhale/commit/db34bc8949a5b5e7e6fd5a88cb67e998011ad9df",
"binarySha256": "a9a2a521d8d0c971b9d49c2c463cdb49d0c7a4eb700b73e6fed2c743e688eebb",
"binaries": [
"codewhale-windows-arm64.exe",
"codew-windows-arm64.exe",
"codewhale-tui-windows-arm64.exe"
],
"resolved": 445,
"time": "2026-10-13T10:43:07.5742747Z",
"osVersion": "10.0.26300",
"osBuild": "26300",
"source": "cf7199b52ecb9147e4670ae7874924ffc30a6a67",
"binarySha256": "ca51f91562a0569ee43855715856e1cfee74e8a834bd6206183a3175373fdb2d",
"resolved": 453,
"unresolved": 0,
"buildRun": "https://github.com/codewhale-hq/Codewhale/actions/runs/38867608774",
"probeRun": "https://github.com/codewhale-hq/Codewhale/actions/runs/38890634267",
"probeWorkflow": ".github/workflows/windows-arm64-export-probe.yml",
"profile": "release, built by release-artifacts.yml (windows-arm64 job) in release candidate run 38867608774",
"runtimeAcceptance": "Static export resolution only; this probe did not launch the release executables. The only ARM64 runtime acceptance is the cf7199b52e debug application run of 2026-10-13; the packaged release and older OS builds are unproven",
"exportProbeMethod": "ARM64 process: LoadLibraryExW per imported DLL with LOAD_LIBRARY_SEARCH_SYSTEM32 (0x00000800), then GetProcAddress for every imported symbol; module path and SHA256 recorded",
"scope": "System-context export resolution only; no Authenticode check, oldest supported OS, or ordinary-user install inference",
"systemModuleHashes": {
"advapi32.dll": "021452c20f89c50d64c304391ec18169f95a9fa14762f76cfd2dded627dbf0bb",
"api-ms-win-core-synch-l1-2-0.dll": "26be64c26bac33368a650372cb5fb1e3235f1bd7883471bd3291dbf6372ae274",
"api-ms-win-crt-convert-l1-1-0.dll": "529e795875178b906ea8758abc8de5c28339f336b3e7a121d7778ae040908de9",
"api-ms-win-crt-heap-l1-1-0.dll": "529e795875178b906ea8758abc8de5c28339f336b3e7a121d7778ae040908de9",
"api-ms-win-crt-locale-l1-1-0.dll": "529e795875178b906ea8758abc8de5c28339f336b3e7a121d7778ae040908de9",
"api-ms-win-crt-math-l1-1-0.dll": "529e795875178b906ea8758abc8de5c28339f336b3e7a121d7778ae040908de9",
"api-ms-win-crt-runtime-l1-1-0.dll": "529e795875178b906ea8758abc8de5c28339f336b3e7a121d7778ae040908de9",
"api-ms-win-crt-stdio-l1-1-0.dll": "529e795875178b906ea8758abc8de5c28339f336b3e7a121d7778ae040908de9",
"api-ms-win-crt-string-l1-1-0.dll": "529e795875178b906ea8758abc8de5c28339f336b3e7a121d7778ae040908de9",
"api-ms-win-crt-time-l1-1-0.dll": "529e795875178b906ea8758abc8de5c28339f336b3e7a121d7778ae040908de9",
"api-ms-win-crt-utility-l1-1-0.dll": "529e795875178b906ea8758abc8de5c28339f336b3e7a121d7778ae040908de9",
"bcrypt.dll": "12e3562488e5ca3a26884408d99d81898237c8ca3c5c3c9e40ca6b8f5ed361cd",
"bcryptprimitives.dll": "d3da1097e9e1fc16c910350e40c4405ef52f36f134f9bf165fbe02194c1eaa82",
"combase.dll": "850b43094ae61a5d3b8f40f0cddf89b4a90190bf5fc57c5242e0bf5b06644b2f",
"crypt32.dll": "e3e800f1905658674ce4af1d3efb31d722b9255aaae4fa46c54302d329ba4ab6",
"kernel32.dll": "6a7bd4daff9c0c4cc8b9478ecd9828c3f19be257a4a18db9559cae929197fffa",
"ntdll.dll": "60ef561645b50dce71ad0919dbbef3ad334a0489432cde718e75b73b558c7997",
"oleaut32.dll": "5ebb761b3f96b227006e79d6a92cf5d98ac5a5fd3c136d35dd05391bed867753",
"shell32.dll": "ef0fbd71a46fe4b35bff893ea65241cd08ea5231de704c106e39920c03698fe9",
"shlwapi.dll": "67f713491bc18614d6ea29b920e3c609cabf9eecf054990e030476a2e9f76a2c",
"user32.dll": "f2fc06e7e5016a15ad7f7f4ee250154b7d41d0a0706bf14927a3bbd4c42727dd",
"userenv.dll": "18bd1660ab7cacd90789f1bebe35bc6a5730b1e75e5582f53575473fae3db530",
"vcruntime140.dll": "3c56f4167e2b3d8e6338497731e6aae8cd7ec46bd6789f9423a8d9cf9a630310",
"winmm.dll": "2ebb728e5c510492df8fde4ec81b8a8554f2f33d1686b837d22c962b7d2fa9b7",
"ws2_32.dll": "e1266872f796f68b4be9c20640ca940a7a96e1ba98c741d472ae305fccf87ca9"
},
"supersedes": {
"time": "2026-10-13T10:43:07.5742747Z",
"osVersion": "10.0.26300",
"osBuild": "26300",
"source": "cf7199b52ecb9147e4670ae7874924ffc30a6a67",
"binarySha256": "ca51f91562a0569ee43855715856e1cfee74e8a834bd6206183a3175373fdb2d",
"resolved": 453,
"unresolved": 0,
"buildCommand": "cargo build --locked --target aarch64-pc-windows-msvc -p codewhale-cli --bin codewhale",
"profile": "debug, default features",
"runtimeAcceptance": "Only cf7199b52e ARM64 debug application acceptance; final packaged release and older OS unproven"
}
"buildCommand": "cargo build --locked --target aarch64-pc-windows-msvc -p codewhale-cli --bin codewhale",
"profile": "debug, default features",
"runtimeAcceptance": "Only cf7199b52e ARM64 debug application acceptance; final packaged release and older OS unproven"
}
}
},
@@ -855,9 +767,6 @@
"GetSecurityDescriptorControl",
"GetSecurityDescriptorDacl",
"GetSecurityInfo",
"GetSidIdentifierAuthority",
"GetSidSubAuthority",
"GetSidSubAuthorityCount",
"GetTokenInformation",
"ImpersonateAnonymousToken",
"InitializeAcl",
-19
View File
@@ -1,19 +0,0 @@
import { DocArticle } from "../_components/doc-article";
import { getDocsFirstTime } from "@/lib/i18n/dictionaries";
import { buildPageMetadata } from "@/lib/page-meta";
export async function generateMetadata({ params }: { params: Promise<{ locale: string }> }) {
const { locale } = await params;
const t = getDocsFirstTime(locale);
return buildPageMetadata({
path: "/docs/first-time",
locale,
title: t.metaTitle,
description: t.metaDescription,
});
}
export default async function FirstTimePage({ params }: { params: Promise<{ locale: string }> }) {
const { locale } = await params;
return <DocArticle t={getDocsFirstTime(locale)} locale={locale} />;
}
-24
View File
@@ -97,30 +97,6 @@ export default async function InstallPage({ params }: { params: Promise<{ locale
<div lang={copyLocale}>
{locale === "zh" ? mirrors : null}
<Block id="install-new-to-terminal" title={t(INSTALL_COPY.newTitle)}>
{p(INSTALL_COPY.newLead)}
<Channel id="install-new-macos" title={t(INSTALL_COPY.newMacTitle)}>
<ol>
<li><Rich text={t(INSTALL_COPY.newMac1)} /></li>
<li><Rich text={t(INSTALL_COPY.newMac2)} />{code(INSTALL_COMMANDS.shell)}</li>
<li><Rich text={t(INSTALL_COPY.newMac3)} /></li>
</ol>
</Channel>
<Channel id="install-new-windows" title={t(INSTALL_COPY.newWinTitle)}>
<ol>
<li><Rich text={t(INSTALL_COPY.newWin1)} /></li>
<li><Rich text={t(INSTALL_COPY.newWin2)} />{code(INSTALL_COMMANDS.windows)}</li>
<li><Rich text={t(INSTALL_COPY.newWin3)} /></li>
</ol>
</Channel>
<p>
<Link href={`/${locale}/docs/first-time`} className="section-link">
{t(INSTALL_COPY.newNext)}
<Icon name="arrow-right" className="icon icon-flip" />
</Link>
</p>
</Block>
<Block id="install-path" title={t(INSTALL_COPY.pathTitle)}>
{p(INSTALL_COPY.pathPlaces)}
<ul>
-1
View File
@@ -23,7 +23,6 @@
.install-guide ul { list-style: disc; }
.install-guide ol { list-style: decimal; }
.install-guide li { margin-block: var(--space-1); }
.install-guide li > .code { margin-block: var(--space-3); }
.install-guide ::marker { color: var(--muted); }
.install-guide a:not(.btn) { color: var(--accent); text-decoration: underline; text-decoration-color: color-mix(in srgb, var(--accent) 40%, transparent); text-underline-offset: 0.2em; }
.install-guide a:not(.btn):hover { text-decoration-color: var(--accent); }
+6 -37
View File
@@ -1,35 +1,4 @@
[
{
"id": "first-time",
"slug": "first-time",
"sources": [
{
"path": "docs/GUIDE.md",
"sha256": "2fd312552cabc519c990071a16111fe251f902104ebd65f22513039d94e3412d",
"content": "# Codewhale User Guide\n\nThis guide is for your first hour with Codewhale. It explains the main\nworkflow, the important safety controls, and where to go next when you need a\ncomplete reference.\n\nCodewhale has deeper reference documents for installation, configuration,\nproviders, modes, keybindings, tools, and operations. Use this page as a guided\nwalkthrough, then follow the \"Next\" links when you need every option.\n\n## 1. Welcome to Codewhale\n\nCodewhale is a terminal coding agent. You run it from a workspace, give it a\ntask, and it can use structured tools to inspect files, run commands, edit\ncode, and report back with evidence.\n\nThe important difference from a normal chat model is that Codewhale is built\naround a harness:\n\n- It keeps the active workspace and session visible.\n- It routes each turn through explicit modes and approval rules.\n- It shows tool calls in the transcript instead of hiding the work.\n- It can preserve sessions, fork conversations, and continue later.\n- It can run sub-agents for focused background work.\n\nYou can use Codewhale for small questions:\n\n```text\nExplain the authentication flow in this repository.\n```\n\nYou can also use it for multi-step work:\n\n```text\nFind the failing validation path, propose a fix, and wait for my approval\nbefore editing files.\n```\n\nFor a new repository, start conservatively. Ask Codewhale to explore and plan\nbefore asking it to change files. That gives you a reviewable path and makes it\neasier to catch wrong assumptions early.\n\nNext: [ARCHITECTURE.md](ARCHITECTURE.md) explains the internal harness and\nruntime model.\n\n## 2. First Launch\n\nFor a new macOS or Linux installation, use the official GitHub release.\nThe installer verifies the release checksums and provides the same runtime\nunder the `codewhale` and `codew` command names:\n\n```bash\ncurl -fsSL https://codewhale.net/install.sh | sh\n```\n\nWindows users should choose the matching installer or archive from\n[GitHub Releases](https://github.com/codewhale-hq/CodeWhale/releases/latest).\nFor an existing direct install, use `codewhale update --check`, then\n`codewhale update`. npm and Cargo remain secondary packaging routes; Cargo\nalso supports source builds where a compatible prebuilt is unavailable.\nFor occupied directories, package-managed installs, and PATH setup, follow\n[the installation and migration guide](INSTALL.md#recommended-official-github-releases).\nAndroid/Termux uses its own [preview archive or source-build path](INSTALL.md#android--termux-arm64).\n\nDocker is also available when you want an isolated runtime. The published\nimage is currently 0.10.0; to test current `main`, [build it from source](DOCKER.md#building-locally).\n\n```bash\ndocker volume create codewhale-home\ndocker run --rm -it \\\n -e DEEPSEEK_API_KEY=\"$DEEPSEEK_API_KEY\" \\\n -v codewhale-home:/home/codewhale/.codewhale \\\n -v \"$PWD:/workspace\" \\\n -w /workspace \\\n ghcr.io/hmbown/codewhale:latest\n```\n\nOnce the install directory is on PATH, launch Codewhale from the repository or\ndirectory you want it to work in:\n\n```bash\ncodewhale\n```\n\nFor the default GitHub installer destination, you can use\n`\"$HOME/.local/bin/codewhale\"` until that directory is on PATH.\n\nOn first launch, Codewhale opens the composer, even with no key or route.\nWith no key or route, workspace trust is asked once a provider is connected;\notherwise only when the folder requires a decision. A provider is asked for\nwhen you send your first message: the message stays in the box while you sign\nin or use your own key, and a local model is offered only when a local Ollama\nserver answers. The provider step also has an explicit offline route.\n\nEverything optional stays available after that. Use `/setup` for the\nprogressive setup and repair guide, `/settings` for the full typed editor, and\n`/constitution` when you want your own constitution on top of the bundled one.\nThe localized telemetry choice appears only after the workspace is ready and\ndoes not block the composer.\n\nDeepSeek is the default provider. If you want to configure its key before or\nafter the first launch, the most direct setup path is:\n\n```bash\ncodewhale auth set --provider deepseek\n```\n\nYou can also provide a key through the environment:\n\n```bash\nexport DEEPSEEK_API_KEY=\"your-key\"\ncodewhale\n```\n\nNew Codewhale config is stored under `~/.codewhale/config.toml`. Legacy\n`~/.deepseek/config.toml` files are still supported for users migrating from\nthe old name.\n\nUse `/constitution` to review or change standing guidance. After setup, run a\ndoctor check:\n\n```bash\ncodewhale doctor\n```\n\nUse the JSON form when you need a machine-readable report for an issue:\n\n```bash\ncodewhale doctor --json\n```\n\nBoth forms are offline by default. They report structural configuration and\nliteral unknown/not-probed credential states without loading workspace `.env`\ncredentials, openiLine truncated
},
{
"path": "docs/INSTALL.md",
"sha256": "35bc2795284622177f1be7f85544174a4cbd9d2881e52fbdedb6f1262b636815",
"content": "# Installing Codewhale\n\n> 阅读简体中文版:[zh_hans/INSTALL.md](zh_hans/INSTALL.md)\n\nCodewhale is an open-source coding agent that runs in your terminal. You give\nit a task (\"fix the failing test\", \"add a CLI flag\"). It reads your repository,\nedits files and runs commands. In the default **Accept Edits** posture it\napplies file edits inside the repository immediately and shows you the diff,\nand asks before each shell command, so commit or stash anything you care about\nfirst. **Ask** (`Shift+Tab` round the dial, or `/config`) asks before every\nedit as well. It works with many model\nproviders. **DeepSeek** is the default.\n\nThe command is `codewhale`. `codew` is a shorter alias for the same program.\n\nThe installation paths were first checked with **v0.10.0** (released 2026-09-22) on a\nfresh **Ubuntu 24.04 x86_64** machine. The dated\n[install receipts](https://github.com/codewhale-hq/Codewhale/blob/37ecdfcc49bc68a9b0d058b97c3946e62c34bd31/docs/install-report/v0.10.0-2026-09-23/RECEIPTS.md) record the commands and output checked at that time. Steps that\ncould not be run on that machine are marked **(untested on this VM: reason)**.\nmacOS, Windows and Android are out of scope, apart from a few notes. A second\npass re-ran the installer, manual-download, archive and npm paths, the no-key\nchecks and zsh completion on **macOS 26.1 (Apple silicon)**; see\n[macOS notes](#macos-notes). Steps that need a model call were not re-run\nthere.\n\nInstall commands that use `latest` resolve to the latest **published** GitHub\nRelease or package. Between releases, `main` may already describe the next\nversion (for example a v0.10.3 source candidate before its tag). A\nprebuilt candidate is available through the official installer only after its\ntag, checksums and release assets exist. Contributors can build `main` now.\n\n## No account needed\n\nCodewhale needs no account. Keys you save with `/provider` or\n`codewhale auth set` stay on your machine; see\n[API keys and providers](#8-api-keys-and-providers). New Codewhale accounts\nare not open, so this guide does not cover account key storage.\n\n## Test and contribute to current source\n\n`latest` selects the latest published release. To test unreleased repairs,\nbuild canonical GitHub `main`, or CNB `main` after verifying its commit matches.\nThe source version number alone does not establish a published package.\n\n```bash\ngit clone --branch main https://github.com/codewhale-hq/CodeWhale.git\ncd CodeWhale\ncargo build --release --locked -p codewhale-cli\ntarget/release/codewhale --version\n```\n\nSee the Cargo section for build prerequisites; use the repository's stable Rust\nselection (declared minimum 1.89). Windows uses `target/release/codewhale.exe`\nand needs MSVC. A development build is separate from a signed release package.\nFor mainland-China downloads and source validation, see [CNB mirror](CNB_MIRROR.md).\nThe [Tencent Lighthouse bootstrap](../scripts/tencent-lighthouse/README.md) is\nbilingual. An npm registry mirror serves the wrapper; native binary downloads\nstill need an available release asset source.\n\n---\n\n## 60-second quickstart (Linux or macOS)\n\n```bash\n# 1. Install. Downloads two checksum-verified binaries into ~/.local/bin (no sudo).\ncurl -fsSL https://codewhale.net/install.sh | sh\n\n# 2. The installer adds ~/.local/bin to your shell profile (it prints what it added and\n# how to undo it; --no-modify-path skips that). Shells that are already open do not\n# reread the profile, so use it in this one now, then check it.\nexport PATH=\"$HOME/.local/bin:$PATH\"; hash -r\ncodewhale --version # -> codewhale 0.10.0 (1be1a703b975)\n\n# 3. Give it a DeepSeek API key (from https://platform.deepseek.com/api_keys).\ncodewhale auth set --provider deepseek # prompts for the key; nothing is echoed\ncodewhale auth status --provider deepseek # \"active source: secret store\"\n\n# 4. Run your first task inside a git repository.\ncd ~/your-project\ncodewhale\n```\n\nIn the TUI, type something concrete:\n\n```text\ncreate a Python file primes.py that prints the first 10 primes, run it, and show me the output\n```\n\nCodewhale writes the file, shows you a diff, then asks **APPROVAL: bash\npython3 primes.py – Do you want to proceed?** Press `y` to allow it once. It\nruns the command and reports the output. Press `Ctrl-D` (with an empty input\nbox) to quit. It prints the command to resume the session later.\n\n> **If nothing happens after you send your first message,** you have not\n> connected a model. v0.10.2 shows `no model connected · run /provider` on its\n> first screen and returns your first message to the composer. Type `/provider`,\n> choose a provider or paste a Custom endpoint, then send it again. If messages say\n> they were saved for later after you connect, quit with Ctrl+D and run `codewhale` again.\n\n---\n\n## Contents\n\n1. [Before you start](#1-before-you-start)\n2. [Install: rLine truncated
},
{
"path": "docs/MODES.md",
"sha256": "bfd3ed8cae274f9b72d1805232f41d4342cc29b45b53617a26f06c3776941202",
"content": "# Modes and Permission Postures\n\n> 阅读简体中文版:[zh_hans/MODES.md](zh_hans/MODES.md)\n\nCodewhale has three related concepts:\n\n- **TUI mode**: what kind of visible interaction you're in (Plan/Work).\n- **Permission posture**: how aggressively the UI asks before executing tools.\n- **Workflow**: named steps that coordinate sub-agents, with dependencies and\n results, available in any TUI mode.\n\nModel selection is separate. `--model auto` and `/model auto` route each turn to\na concrete model and thinking level; they are not TUI modes and are not on the\npermission dial.\n\nWorkflow runs named steps through the same sub-agent runtime; Fleet manages\ntheir saved roles and model assignments. A workflow adds ordering, result\nhandoffs, gates and a progress view. The active mode and permission posture\nstill control what each step may execute.\n\nBefore assigning steps, query `agent(action=\"roster\")` for the saved Fleet\nmodels and roles. A plan child can select a listed model with `model`, or use\nthe saved `role`/`profile` assignment. An Exact Fleet fixes each member's route.\n\n## TUI Modes\n\nOne dial says how much Codewhale may do without asking. Press `Shift+Tab` to\nstep it: **Ask → Accept Edits → Plan → Auto-Review → Full Access → Ask**. Plan\nis the Plan mode; every other stop is Work with that permission posture. The\nfooter shows the current stop and `Shift+Tab to change`. New installs start on\nAccept Edits when no permission posture is saved. An explicit saved choice,\nincluding Ask from an earlier version, stays in control.\n`Tab` only completes composer menus, commands, file mentions and suggestions;\nit never changes the mode, and never sends or queues composer text (use\n`Enter`).\nEvery change of effective permission writes a `Permissions changed to …` line\nin the transcript.\nPress `Ctrl+T` to cycle reasoning effort.\nRun `/mode` to open the mode picker, or switch directly with `/mode work` or\n`/mode plan`. `operate` and its spellings still parse, as Work, and say that\nOperate was retired, so an older setting or script never errors.\n\n- **Plan**: design-first prompting. The stable primitive names remain familiar, but the runtime centrally refuses file mutation and shell execution. Read-only inspection and policy-allowed research, including deferred Web search/fetch, remain available. When a Plan turn successfully finishes a nonempty response, including a prose-only plan, the TUI asks how to continue: Work (Ask), Work (Accept Edits), Work (Auto-Review), or keep planning. Work sends the exact approved response as a normal follow-up and preserves that turn's existing To-dos, or adds one pending plan item without replacing unrelated work.\n- **Work** (internally `agent`): ordinary multi-step execution. The first-turn toolbox includes `read`, `write`, `edit`, `file_search`, `grep_files`, `bash`, `agent`, `todo_write`, `present`, and `load_skill`, plus `create_goal`, `get_goal`, `update_goal`, and `task_shell_wait` so goal and background-shell controls are available without discovery. The agent calls `create_goal` when a request is a durable objective; the host never infers a goal from wording, and the contract must not tell the model otherwise. Approval, sandbox, repository law, and managed policy decide what may execute.\n- **Operate is retired.** It was Work with an orchestration contract, and that contract now lives in every session's constitution: make a goal of a durable request, keep the plan visible, fan independent work out to parallel sub-agents, and have an independent reviewer try to refute a non-trivial change. A saved `default_mode = \"operate\"` loads as Work and says so once; a Runtime API request with `mode: \"operate\"` runs as `agent` with the default posture (Accept Edits) and logs one notice per process, never an error. Agents forked from an older Operate conversation do not inherit its contract. Multi-agent work stays on `agent`, `workflow` and `/fleet` in every mode.\n\n`Act` and `/mode act` remain compatibility aliases for Work. Saved settings\nstill normalize to the internal value `agent`.\n\n### Tool availability by mode\n\n| Tool family | Plan | Work |\n|:---|:---:|:---:|\n| `read` and policy-allowed deferred research tools | yes | yes |\n| `write` and `edit` | visible names; execution denied | approval- and policy-gated |\n| `bash` | visible name; execution denied | approval- and policy-gated; delegation is preferred when parallelism or isolation helps |\n| `agent` | yes, subject to child-depth authority | yes, subject to child-depth authority |\n| Deferred native, MCP, and plugin tools | discoverable through `tool_search` when policy permits | same |\n| Paid or external-service tools | follows permission posture | follows permission posture |\n| Access outside the workspace root | explicit trusted paths only | only through trusted paths or trust mode; fleet profiles never widen it |\n\nMode changes scheduling emphasLine truncated
},
{
"path": "docs/SANDBOX.md",
"sha256": "1d765dc9b89ff76aedd512a49983058fae999e25fc5dd84868f38f268d00b0b1",
"content": "# Sandbox threat model\n\n> 阅读简体中文版:[zh_hans/SANDBOX.md](zh_hans/SANDBOX.md)。\n\nCodewhale can launch shell commands proposed by a model. Approval policy,\nworkspace-aware tools, and an operating-system command wrapper are separate\ncontrols: an approval is not a sandbox, and selecting `workspace-write` does\nnot prove that the current platform has an OS wrapper available.\n\nThis document describes only behavior wired into the command execution path.\nSee [Authorization order](AUTHORIZATION_ORDER.md) for the policy layers that\nrun before execution reaches this boundary.\n\n## Platform overview\n\n| Mechanism | Platform | Selection | What Codewhale reports |\n|---|---|---|---|\n| Seatbelt (`sandbox-exec`) | macOS | Automatic when the runtime probe succeeds | `macos-seatbelt` |\n| Bubblewrap (`/usr/bin/bwrap`) | Linux | Default when installed and a wrapped probe run succeeds; `prefer_bwrap = false` opts out | `linux-bwrap` |\n| No OS wrapper | Linux without usable bwrap, or opted out | `prefer_bwrap = false`, or bwrap absent/unusable | `none` |\n| No OS wrapper | Windows | Current implementation | `none` |\n| OpenSandbox-compatible service | Any supported host | `sandbox_backend = \"opensandbox\"` | External execution path |\n\nThe repository also contains a future Windows helper contract. It is not\nwired into child-command launch, so Codewhale does not advertise it as an\nactive sandbox. Source-only sandbox code is not evidence that a command was\nrestricted.\n\n## macOS: Seatbelt\n\nCodewhale probes `/usr/bin/sandbox-exec` by running a minimal profile. When the\nprobe succeeds and the selected `SandboxPolicy` requests a sandbox, the child\ncommand is wrapped with a generated Seatbelt profile.\n\nThe profile can provide:\n\n- broad filesystem reads;\n- writes limited by the selected policy, including the workspace and specific\n runtime/cache paths needed by supported tools;\n- network access only when the policy enables it. A `read-only` policy with\n network (the agent `read_only: true` shell mode) gets outbound IPv4 and the\n system DNS resolver only. IPv6 and the host's own addresses, loopback\n included, are denied, except the port of a local proxy named in\n `HTTP_PROXY`, `HTTPS_PROXY` or `ALL_PROXY`, and other local sockets stay\n denied.\n\nIf the probe fails or `sandbox-exec` is unavailable, Codewhale reports no OS\nsandbox and launches the command without a Seatbelt wrapper. It does not set a\nSeatbelt marker on that fallback.\n\n## Linux: default bubblewrap\n\nLinux command sandboxing is on by default whenever bubblewrap works. Set the\ntop-level configuration key to opt out:\n\n```toml\nprefer_bwrap = false\n```\n\nCodewhale selects bubblewrap only when `/usr/bin/bwrap` is a regular\nexecutable file AND a wrapped probe run proves it can create its namespaces\non this host — the exec bit alone lies where user namespaces are restricted\n(e.g. Ubuntu 24.04 with `kernel.apparmor_restrict_unprivileged_userns`),\nwhere every wrapped command would fail rather than run unsandboxed. The\nwrapper derives its mounts and network namespace from the resolved\n`SandboxPolicy`:\n\n```text\n/usr/bin/bwrap \\\n --unshare-all \\\n [--share-net] \\\n --ro-bind / / \\\n --dev /dev \\\n --proc /proc \\\n --tmpfs /tmp \\\n [--dev-bind <device-root> <device-root> ...] \\\n --bind <writable-root> <writable-root> ... \\\n --ro-bind <protected-descendant> <protected-descendant> ... \\\n [--ro-bind <extra-ro-root> <extra-ro-root> ...] \\\n --chdir <cwd> \\\n -- <program> <args>\n```\n\nThe sandbox always gets a private `/dev` (fresh device nodes, so `>/dev/null`\nworks), a private `/proc`, and a tmpfs `/tmp` (#5410). A `workspace-write`\npolicy then binds the host `/tmp` (and `TMPDIR`) over that tmpfs unless it\nexcludes them, so by default a sandboxed command shares the host's `/tmp`,\nwritable, with the usual sticky-bit rules. Two optional top-level\nconfig keys extend the mounts: `bwrap_ro_roots` (extra host paths bind-mounted\nread-only, applied last so they can narrow a policy-writable path) and\n`bwrap_dev_roots` (host character/block device nodes bind-mounted read-write;\ndirectories are never honored). Missing paths are skipped silently.\n\nThat gives the child a read-only root view. For `workspace-write`, every safe,\nexisting policy root is mounted read-write: the working directory, configured\nadditional roots, `/tmp` and `TMPDIR` unless excluded, and verified Git\nworktree metadata roots. Existing `.codewhale` and `.deepseek` descendants are\nremounted read-only after their writable parent. Missing paths, non-directory\npaths, and `/` are not promoted to writable mounts.\n\nFor `read-only`, there are no writable binds, so the working directory remains\ninside the read-only root view. `--unshare-all` isolates the network namespace\nby default. Codewhale adds `--share-net` only when the policy's\n`network_access` is true. The agent `read_only: true` sheLine truncated
},
{
"path": "docs/KEYBINDINGS.md",
"sha256": "5b3a71595db86ed30eb79675c2b934d60dce897f0202142794be849537a36da9",
"content": "# Keybindings\n\n> 阅读简体中文版:[zh_hans/KEYBINDINGS.md](zh_hans/KEYBINDINGS.md)\n\nThis is the source-of-truth catalog of every keyboard shortcut the TUI recognizes. Bindings are grouped by **context** — the focus or modal state they fire in. A binding listed under \"Composer\" only takes effect when the composer is focused; one under \"Transcript\" only when the transcript has focus; and so on.\n\nGlobal key chords are not yet user-configurable — tracked for a future release (#436, #437). Hotbar slot actions are configurable with `[[hotbar]]` and `/hotbar`; the Hotbar activation chord remains `Alt-1` through `Alt-8`.\n\n## Global (any context)\n\n| Chord | Action |\n|----------------------|---------------------------------------------------------------|\n| `/help`, `F1` or `Ctrl-/` | Toggle the help overlay. `/help` is what the chrome advertises: it is the one route every terminal delivers, where `F1` is eaten by tmux and some emulators and `Ctrl-/` encoding varies |\n| `F2` | Toggle the typed Settings editor |\n| `F3` | Open the provider/model picker (same as `/provider`) |\n| `Ctrl-K` | Open the command palette (slash-command finder) |\n| `Ctrl-C` | Cancel current turn / dismiss modal / clear a non-empty draft (recoverable — see `Ctrl-Z`) / arm-then-confirm quit |\n| `Ctrl-B` | Release the current shell wait so the turn can continue; foreground commands move into `/jobs`, and background tasks keep running. Use `/jobs` or `Bash` with `action: \"wait\"` to inspect output |\n| `Ctrl-D` | Quit (only when the composer is empty) |\n| `Tab` | Complete a slash command, `@` file mention or prompt suggestion. Tab never changes the mode |\n| `Shift+Tab` | Step the permission dial: Ask → Accept Edits → Plan → Auto-Review → Full Access → Ask. Plan is the Plan mode; every other stop is Work with that posture. Live regardless of composer contents or whether a turn is running (suppressed only while a modal other than Config is open). During a turn the change, including into or out of Plan, applies at the agent's next step; a call you already approved runs only if the new posture is equal or broader. Model, thinking level and provider stay locked until the turn ends |\n| `Ctrl-T` | Cycle reasoning effort for the active model. Walks the same ladder as `/model` and `/effort` (catalog or documented route dialect). Always-thinking models omit `off`; Grok 4.6 includes `xhigh`. |\n| `Ctrl-Shift-T` | Toggle live transcript overlay (sticky-tail auto-scroll) |\n| `Ctrl-R` | Open the resume-session picker |\n| `Ctrl-L` | Compact the conversation context (status line shows progress; no-op while a compaction is already running) |\n| `Ctrl-O` | Open the reasoning detail for the selected or current turn, regardless of composer contents |\n| `Ctrl-Alt-O` | Open the whole-turn Turn Inspector, regardless of composer contents |\n| `Alt-V` / `Option-V` (macOS) | Open the details pager for the selected tool/sub-agent card; with nothing selected, the newest visible error, else the first visible card with details, else the most recent one. Click a card first to keep Alt-V on it while newer cards arrive. Terminals that emit the legacy Option-V glyph are also handled |\n| `Ctrl-Shift-E` / `Cmd-Shift-E` | Toggle the file tree in the workbar |\n| `Ctrl-]` or `Ctrl-Tab` | Cycle the pet view while the pet main view is showing; otherwise cycle the work dock: TODO → Agents → Jobs → Background. `Ctrl-]` is portable; `Ctrl-Tab` requires the kitty keyboard protocol. Terminal.app uses it to switch terminal tabs |\n| `Ctrl-Shift-Tab` | Cycle the pet view or work dock backwards where the terminal sends the kitty keyboard chord. Terminal.app uses it to switch terminal tabs |\n| `Alt-G` / `Alt-Shift-G` | Scroll transcript to top / bottom when the composer is empty |\n| `Alt-1`-`Alt-8` | Dispatch Hotbar slots 1-8 when no modal or inline picker is open |\n| `Alt-!` / `Alt-@` / `Alt-#` / `Alt-$` | Select the workbar panel: Tasks / Agents / Context / Files |\n| `Ctrl-Alt-0` | Toggle the workbar off / back to the bottom placement |\n| `Alt-L` | Open the pager for the last message (composer empty) |\n| `Alt-P` / `Alt-A` / `Alt-Y` | Jump to Plan / Work, or request Full Access (`Alt-Y` is the legacy permission channel — Work + Full Access — not a separate mode; it honors a locked approval policy) |\n| `Ctrl-X` (Activity workbar) | Cancel all running background shell jobs |\n| `Esc` | Close topmost modal · cancel slash menu · dismiss toast. During Line truncated
}
]
},
{
"id": "install",
"slug": "install",
@@ -365,8 +334,8 @@
"sources": [
{
"path": "docs/RUNTIME_API.md",
"sha256": "6f3b8885978ef2e1b33d0fdf66ad6ccf5c25464c1951b59658aec00745390db2",
"content": "# Codewhale Engine Runtime API & Integration Contract\n\n> 阅读简体中文版:[zh_hans/RUNTIME_API.md](zh_hans/RUNTIME_API.md)。\n\n`codewhale app-server --http` exposes the [Codewhale Engine](ARCHITECTURE.md) through\nits canonical local Runtime API and control plane.\nLocal SDKs, mobile/remote-control clients, and editor integrations talk to it\ninstead of screen-scraping terminal output. It serves the full HTTP/SSE runtime\nAPI (`/v1/*`), a JSON-RPC control transport over stdio, and the phone-friendly\nmobile page. `codewhale doctor --json` provides machine-readable health, and\n`codewhale serve --acp` speaks the Agent Client Protocol over stdio for editors\nsuch as Zed.\n\n`codewhale serve --http` / `serve --mobile` remain as **compatibility aliases**\nfor `codewhale app-server --http` / `--mobile`; both launch the identical\nserver. Desktop, web, SDK and custom integrations should target\n`app-server --http`. `--stdio` and `--socket` expose compatibility controls.\nBare `app-server` selects initialized JSONL control over stdio. Local stdio,\nsocket and ACP owners do not bind a hidden TCP listener. Explicit `--http`\nretains HTTP/SSE and compatibility routes on the same Runtime owner.\n\n`codewhale exec` is the separate one-shot headless worker path (stream-json,\nfleet worker subprocess, CI primitive). It is not part of this API, but it\nshares the same runtime, provider/model resolution, permission profiles, and\nevent vocabulary.\n\nThis document is the stable integration contract for native workbench\napplications (and other local supervisors) that embed the Codewhale Engine.\n\n## Architecture\n\n```\nlocal supervisor / SDK / automation harness\n │\n ├─ codewhale app-server --http → HTTP/SSE runtime API (/v1/*) [canonical]\n ├─ codewhale app-server --mobile → runtime API + mobile control page\n ├─ codewhale app-server --stdio → JSON-RPC control transport over stdio\n ├─ codewhale app-server --socket → same JSON-RPC over a unix domain socket (desktop daemon)\n ├─ codewhale doctor --json → machine-readable health & capability\n ├─ codewhale serve --acp → ACP stdio agent for editors such as Zed\n ├─ codewhale serve --mcp → MCP stdio server\n ├─ codewhale serve --http/--mobile → legacy aliases for `app-server --http/--mobile`\n └─ codewhale exec [args] → one-shot headless worker (stream-json)\n```\n\nThe engine runs as a local-only process. All APIs bind to `localhost` by\ndefault. No hosted relay, no provider-token custody, no secret leakage.\n\nFor the read-only record of what a thread or turn did, see\n[`docs/RECEIPTS.md`](RECEIPTS.md): `codewhale receipts` on the CLI and the\n`/receipt` routes under **Threads** below.\n\n## Runtime API entrypoints\n\n| Entry | Transport | Use |\n|---|---|---|\n| `codewhale web [--port 7878]` | HTTP/SSE on `127.0.0.1:7878` + embedded client | First-class loopback-only browser client; opens the default browser |\n| `codewhale app-server --http` | HTTP/SSE on `127.0.0.1:7878` | Full `/v1/*` runtime API (canonical) |\n| `codewhale app-server --mobile` | HTTP/SSE on loopback + `/mobile` | Runtime API + local mobile control page |\n| `codewhale app-server --stdio` | Initialized JSONL over stdio | Compatibility controls and explicit tool-free turns through the held manager; no hidden TCP listener or full Codex interaction channel. |\n| `codewhale app-server --socket [--socket-path P]` | RPC text messages over WebSocket on a `0600` Unix domain socket (macOS/Linux) or an owner-only named pipe (Windows) | Desktop daemon: multi-client, kernel peer identity checked, `daemon/attach` claim handshake. Windows native-app qualification remains open. |\n| `codewhale app-server` | Initialized JSONL over stdio | Same selection as `--stdio`; HTTP requires explicit `--http`. |\n| `codewhale serve --http` / `--mobile` | same server as `app-server --http`/`--mobile` | Compatibility aliases |\n\n`app-server --http` and `--mobile` launch the same mature runtime API server\nhistorically reached through `serve --http` — no routes or behavior changed, so\nevery endpoint documented below is identical across both entrypoints. The\nruntime API token is read from `--auth-token`, then `CODEWHALE_RUNTIME_TOKEN`,\nthen `DEEPSEEK_RUNTIME_TOKEN`; use `--insecure-no-auth` only with a loopback\nbind. The `serve` compatibility aliases keep their `--insecure` flag.\nBare `app-server` does not open an HTTP port. Selected attached HTTP listeners\nretain their explicit bearer and non-loopback security checks.\n\nInitialized stdio/private-socket controls support `thread/loaded/list` with\n`{cursor?: string | null, limit?: number | null}`. The result is\n`{data: string[], nextCursor: string | null}` from the held manager's loaded\nengine snapshot, including idle and archived engines still in memory. It dLine truncated
"sha256": "c4f348c27735a7df47a79faf11a6d5e3284a1e67a76c9372e76a8809236af45a",
"content": "# Codewhale Engine Runtime API & Integration Contract\n\n> 阅读简体中文版:[zh_hans/RUNTIME_API.md](zh_hans/RUNTIME_API.md)。\n\n`codewhale app-server --http` exposes the [Codewhale Engine](ARCHITECTURE.md) through\nits canonical local Runtime API and control plane.\nLocal SDKs, mobile/remote-control clients, and editor integrations talk to it\ninstead of screen-scraping terminal output. It serves the full HTTP/SSE runtime\nAPI (`/v1/*`), a JSON-RPC control transport over stdio, and the phone-friendly\nmobile page. `codewhale doctor --json` provides machine-readable health, and\n`codewhale serve --acp` speaks the Agent Client Protocol over stdio for editors\nsuch as Zed.\n\n`codewhale serve --http` / `serve --mobile` remain as **compatibility aliases**\nfor `codewhale app-server --http` / `--mobile`; both launch the identical\nserver. Desktop, web, SDK and custom integrations should target\n`app-server --http`. `--stdio` and `--socket` expose compatibility controls.\nBare `app-server` selects initialized JSONL control over stdio. Local stdio,\nsocket and ACP owners do not bind a hidden TCP listener. Explicit `--http`\nretains HTTP/SSE and compatibility routes on the same Runtime owner.\n\n`codewhale exec` is the separate one-shot headless worker path (stream-json,\nfleet worker subprocess, CI primitive). It is not part of this API, but it\nshares the same runtime, provider/model resolution, permission profiles, and\nevent vocabulary.\n\nThis document is the stable integration contract for native workbench\napplications (and other local supervisors) that embed the Codewhale Engine.\n\n## Architecture\n\n```\nlocal supervisor / SDK / automation harness\n │\n ├─ codewhale app-server --http → HTTP/SSE runtime API (/v1/*) [canonical]\n ├─ codewhale app-server --mobile → runtime API + mobile control page\n ├─ codewhale app-server --stdio → JSON-RPC control transport over stdio\n ├─ codewhale app-server --socket → same JSON-RPC over a unix domain socket (desktop daemon)\n ├─ codewhale doctor --json → machine-readable health & capability\n ├─ codewhale serve --acp → ACP stdio agent for editors such as Zed\n ├─ codewhale serve --mcp → MCP stdio server\n ├─ codewhale serve --http/--mobile → legacy aliases for `app-server --http/--mobile`\n └─ codewhale exec [args] → one-shot headless worker (stream-json)\n```\n\nThe engine runs as a local-only process. All APIs bind to `localhost` by\ndefault. No hosted relay, no provider-token custody, no secret leakage.\n\nFor the read-only record of what a thread or turn did, see\n[`docs/RECEIPTS.md`](RECEIPTS.md): `codewhale receipts` on the CLI and the\n`/receipt` routes under **Threads** below.\n\n## Runtime API entrypoints\n\n| Entry | Transport | Use |\n|---|---|---|\n| `codewhale web [--port 7878]` | HTTP/SSE on `127.0.0.1:7878` + embedded client | First-class loopback-only browser client; opens the default browser |\n| `codewhale app-server --http` | HTTP/SSE on `127.0.0.1:7878` | Full `/v1/*` runtime API (canonical) |\n| `codewhale app-server --mobile` | HTTP/SSE on loopback + `/mobile` | Runtime API + local mobile control page |\n| `codewhale app-server --stdio` | Initialized JSONL over stdio | Compatibility controls and explicit tool-free turns through the held manager; no hidden TCP listener or full Codex interaction channel. |\n| `codewhale app-server --socket [--socket-path P]` | RPC text messages over WebSocket on a `0600` Unix domain socket (macOS/Linux) or an owner-only named pipe (Windows) | Desktop daemon: multi-client, kernel peer identity checked, `daemon/attach` claim handshake. Windows native-app qualification remains open. |\n| `codewhale app-server` | Initialized JSONL over stdio | Same selection as `--stdio`; HTTP requires explicit `--http`. |\n| `codewhale serve --http` / `--mobile` | same server as `app-server --http`/`--mobile` | Compatibility aliases |\n\n`app-server --http` and `--mobile` launch the same mature runtime API server\nhistorically reached through `serve --http` — no routes or behavior changed, so\nevery endpoint documented below is identical across both entrypoints. The\nruntime API token is read from `--auth-token`, then `CODEWHALE_RUNTIME_TOKEN`,\nthen `DEEPSEEK_RUNTIME_TOKEN`; use `--insecure-no-auth` only with a loopback\nbind. The `serve` compatibility aliases keep their `--insecure` flag.\nBare `app-server` does not open an HTTP port. Selected attached HTTP listeners\nretain their explicit bearer and non-loopback security checks.\n\nInitialized stdio/private-socket controls support `thread/loaded/list` with\n`{cursor?: string | null, limit?: number | null}`. The result is\n`{data: string[], nextCursor: string | null}` from the held manager's loaded\nengine snapshot, including idle and archived engines still in memory. It dLine truncated
}
]
},
@@ -418,8 +387,8 @@
},
{
"path": "docs/CONTRIBUTORS.md",
"sha256": "4368939ea860f6db9484fd86e613574b72eca71361f08a1130ec87c5f5c66c78",
"content": "# Contributors\n\nCodewhale is built in the open with a growing community of contributors. Every\nissue report and pull request is real project work — welcome at any experience\nlevel. This is the full per-PR contributor record, grouped into release/time\nbands with the newest band first so it stays scannable. Expand any band to see\neveryone.\n\nFor the live list, see the\n[GitHub contributors page](https://github.com/codewhale-hq/CodeWhale/graphs/contributors),\n[`AUTHOR_MAP`](https://github.com/codewhale-hq/CodeWhale/blob/main/.github/AUTHOR_MAP),\nand [CHANGELOG.md](../CHANGELOG.md).\n\n<!-- contributor-wall:start -->\n## Everyone\n\n[![433 people built Codewhale](assets/contributors.png)](#everyone)\n\n433 people have code, co-authored commits, carried-forward pull requests or credited reports in Codewhale. GitHub's contributor list shows only commit authors whose email is linked to their account, so it counts 212 of them. If your commits are missing there, add your commit email to your GitHub account and they will link.\n\n[0gl20shk0sbt36](https://github.com/0gl20shk0sbt36) · [1Git2Clone](https://github.com/1Git2Clone) · [20bytes](https://github.com/20bytes) · [47Cid](https://github.com/47Cid) · [7jrxt42BxFZo4iAnN4CX](https://github.com/7jrxt42BxFZo4iAnN4CX) · [8sero](https://github.com/8sero) · [A-Corner](https://github.com/A-Corner) · [aboimpinto](https://github.com/aboimpinto) · [Aboysky](https://github.com/Aboysky) · [adity982](https://github.com/adity982) · [AdityaVG13](https://github.com/AdityaVG13) · [AGSaturn](https://github.com/AGSaturn) · [ai-dengwsh](https://github.com/ai-dengwsh) · [aiapienthusiast](https://github.com/aiapienthusiast) · [Aitensa](https://github.com/Aitensa) · [AiurArtanis](https://github.com/AiurArtanis) · [alitvak69](https://github.com/alitvak69) · [all-lopezg](https://github.com/all-lopezg) · [alvin1](https://github.com/alvin1) · [amuthantamil](https://github.com/amuthantamil) · [AnaheimEX](https://github.com/AnaheimEX) · [Andrea-Bruno](https://github.com/Andrea-Bruno) · [Angel-Hair](https://github.com/Angel-Hair) · [angziii](https://github.com/angziii) · [AnonymousUser443](https://github.com/AnonymousUser443) · [antarikshraya](https://github.com/antarikshraya) · [Anyexyz](https://github.com/Anyexyz) · [Apeiron0w0](https://github.com/Apeiron0w0) · [aqilaziz](https://github.com/aqilaziz) · [AresNing](https://github.com/AresNing) · [ArronAI007](https://github.com/ArronAI007) · [Artenx](https://github.com/Artenx) · [asdfg314284230](https://github.com/asdfg314284230) · [Asteria1028](https://github.com/Asteria1028) · [asto18089](https://github.com/asto18089) · [atmosuwiryo](https://github.com/atmosuwiryo) · [axobase001](https://github.com/axobase001) · [aznikline](https://github.com/aznikline) · [baendlorel](https://github.com/baendlorel) · [banqii](https://github.com/banqii) · [Beltran12138](https://github.com/Beltran12138) · [bevis-wong](https://github.com/bevis-wong) · [BH8GCJ](https://github.com/BH8GCJ) · [BigBenLabs](https://github.com/BigBenLabs) · [Biilow](https://github.com/Biilow) · [bistack](https://github.com/bistack) · [BrathonBai](https://github.com/BrathonBai) · [bruce6135](https://github.com/bruce6135) · [BryonGo](https://github.com/BryonGo) · [buiducnhat](https://github.com/buiducnhat) · [buko](https://github.com/buko) · [BX166](https://github.com/BX166) · [c020627](https://github.com/c020627) · [cacdcaecawae](https://github.com/cacdcaecawae) · [caeserchen](https://github.com/caeserchen) · [caiyilian](https://github.com/caiyilian) · [CCChisato](https://github.com/CCChisato) · [ccomma](https://github.com/ccomma) · [cenab](https://github.com/cenab) · [ChaceLyee2101](https://github.com/ChaceLyee2101) · [chatman-media](https://github.com/chatman-media) · [chennest](https://github.com/chennest) · [chinaqy110](https://github.com/chinaqy110) · [chnjames](https://github.com/chnjames) · [chuntseevolving](https://github.com/chuntseevolving) · [ci4ic4](https://github.com/ci4ic4) · [cian-lien](https://github.com/cian-lien) · [cmdcorp6534](https://github.com/cmdcorp6534) · [cmyyy](https://github.com/cmyyy) · [codepgq](https://github.com/codepgq) · [Colorful-glassblock](https://github.com/Colorful-glassblock) · [ComeFromTheMars](https://github.com/ComeFromTheMars) · [CrepuscularIRIS](https://github.com/CrepuscularIRIS) · [ct-jaryn](https://github.com/ct-jaryn) · [ctxyao](https://github.com/ctxyao) · [cy2311](https://github.com/cy2311) · [cyq1017](https://github.com/cyq1017) · [czf0718](https://github.com/czf0718) · [dajiaohuang](https://github.com/dajiaohuang) · [DarrellThomas](https://github.com/DarrellThomas) · [ddlw](https://github.com/ddlw) · [demian-welt](https://github.com/demian-welt) · [Dev-next-gen](https://github.com/Dev-next-gen) · [dfwqdyl-ui](https://github.com/dfwqdyl-ui) · [DI-HUO-MING-YI](https://github.com/DI-HUO-MING-YI) · [DieMoe233](https://github.Line truncated
"sha256": "bedd1cc62969108856f75d823d13bde9f7217a936d4b0e7af3d2e6232cad5770",
"content": "# Contributors\n\nCodewhale is built in the open with a growing community of contributors. Every\nissue report and pull request is real project work — welcome at any experience\nlevel. This is the full per-PR contributor record, grouped into release/time\nbands with the newest band first so it stays scannable. Expand any band to see\neveryone.\n\nFor the live list, see the\n[GitHub contributors page](https://github.com/codewhale-hq/CodeWhale/graphs/contributors),\n[`AUTHOR_MAP`](https://github.com/codewhale-hq/CodeWhale/blob/main/.github/AUTHOR_MAP),\nand [CHANGELOG.md](../CHANGELOG.md).\n\n<!-- contributor-wall:start -->\n## Everyone\n\n[![433 people built Codewhale](assets/contributors.png)](#everyone)\n\n433 people have code, co-authored commits, carried-forward pull requests or credited reports in Codewhale. GitHub's contributor list shows only commit authors whose email is linked to their account, so it counts 212 of them. If your commits are missing there, add your commit email to your GitHub account and they will link.\n\n[0gl20shk0sbt36](https://github.com/0gl20shk0sbt36) · [1Git2Clone](https://github.com/1Git2Clone) · [20bytes](https://github.com/20bytes) · [47Cid](https://github.com/47Cid) · [7jrxt42BxFZo4iAnN4CX](https://github.com/7jrxt42BxFZo4iAnN4CX) · [8sero](https://github.com/8sero) · [A-Corner](https://github.com/A-Corner) · [aboimpinto](https://github.com/aboimpinto) · [Aboysky](https://github.com/Aboysky) · [adity982](https://github.com/adity982) · [AdityaVG13](https://github.com/AdityaVG13) · [AGSaturn](https://github.com/AGSaturn) · [ai-dengwsh](https://github.com/ai-dengwsh) · [aiapienthusiast](https://github.com/aiapienthusiast) · [Aitensa](https://github.com/Aitensa) · [AiurArtanis](https://github.com/AiurArtanis) · [alitvak69](https://github.com/alitvak69) · [all-lopezg](https://github.com/all-lopezg) · [alvin1](https://github.com/alvin1) · [amuthantamil](https://github.com/amuthantamil) · [AnaheimEX](https://github.com/AnaheimEX) · [Andrea-Bruno](https://github.com/Andrea-Bruno) · [Angel-Hair](https://github.com/Angel-Hair) · [angziii](https://github.com/angziii) · [AnonymousUser443](https://github.com/AnonymousUser443) · [antarikshraya](https://github.com/antarikshraya) · [Anyexyz](https://github.com/Anyexyz) · [Apeiron0w0](https://github.com/Apeiron0w0) · [aqilaziz](https://github.com/aqilaziz) · [AresNing](https://github.com/AresNing) · [ArronAI007](https://github.com/ArronAI007) · [Artenx](https://github.com/Artenx) · [asdfg314284230](https://github.com/asdfg314284230) · [Asteria1028](https://github.com/Asteria1028) · [asto18089](https://github.com/asto18089) · [atmosuwiryo](https://github.com/atmosuwiryo) · [axobase001](https://github.com/axobase001) · [aznikline](https://github.com/aznikline) · [baendlorel](https://github.com/baendlorel) · [banqii](https://github.com/banqii) · [Beltran12138](https://github.com/Beltran12138) · [bevis-wong](https://github.com/bevis-wong) · [BH8GCJ](https://github.com/BH8GCJ) · [BigBenLabs](https://github.com/BigBenLabs) · [Biilow](https://github.com/Biilow) · [bistack](https://github.com/bistack) · [BrathonBai](https://github.com/BrathonBai) · [bruce6135](https://github.com/bruce6135) · [BryonGo](https://github.com/BryonGo) · [buiducnhat](https://github.com/buiducnhat) · [buko](https://github.com/buko) · [BX166](https://github.com/BX166) · [c020627](https://github.com/c020627) · [cacdcaecawae](https://github.com/cacdcaecawae) · [caeserchen](https://github.com/caeserchen) · [caiyilian](https://github.com/caiyilian) · [CCChisato](https://github.com/CCChisato) · [ccomma](https://github.com/ccomma) · [cenab](https://github.com/cenab) · [ChaceLyee2101](https://github.com/ChaceLyee2101) · [chatman-media](https://github.com/chatman-media) · [chennest](https://github.com/chennest) · [chinaqy110](https://github.com/chinaqy110) · [chnjames](https://github.com/chnjames) · [chuntseevolving](https://github.com/chuntseevolving) · [ci4ic4](https://github.com/ci4ic4) · [cian-lien](https://github.com/cian-lien) · [cmdcorp6534](https://github.com/cmdcorp6534) · [cmyyy](https://github.com/cmyyy) · [codepgq](https://github.com/codepgq) · [Colorful-glassblock](https://github.com/Colorful-glassblock) · [ComeFromTheMars](https://github.com/ComeFromTheMars) · [CrepuscularIRIS](https://github.com/CrepuscularIRIS) · [ct-jaryn](https://github.com/ct-jaryn) · [ctxyao](https://github.com/ctxyao) · [cy2311](https://github.com/cy2311) · [cyq1017](https://github.com/cyq1017) · [czf0718](https://github.com/czf0718) · [dajiaohuang](https://github.com/dajiaohuang) · [DarrellThomas](https://github.com/DarrellThomas) · [ddlw](https://github.com/ddlw) · [demian-welt](https://github.com/demian-welt) · [Dev-next-gen](https://github.com/Dev-next-gen) · [dfwqdyl-ui](https://github.com/dfwqdyl-ui) · [DI-HUO-MING-YI](https://github.com/DI-HUO-MING-YI) · [DieMoe233](https://github.Line truncated
},
{
"path": "docs/RELEASE_CHECKLIST.md",
@@ -434,8 +403,8 @@
"sources": [
{
"path": "docs/RUNTIME_API.md",
"sha256": "6f3b8885978ef2e1b33d0fdf66ad6ccf5c25464c1951b59658aec00745390db2",
"content": "# Codewhale Engine Runtime API & Integration Contract\n\n> 阅读简体中文版:[zh_hans/RUNTIME_API.md](zh_hans/RUNTIME_API.md)。\n\n`codewhale app-server --http` exposes the [Codewhale Engine](ARCHITECTURE.md) through\nits canonical local Runtime API and control plane.\nLocal SDKs, mobile/remote-control clients, and editor integrations talk to it\ninstead of screen-scraping terminal output. It serves the full HTTP/SSE runtime\nAPI (`/v1/*`), a JSON-RPC control transport over stdio, and the phone-friendly\nmobile page. `codewhale doctor --json` provides machine-readable health, and\n`codewhale serve --acp` speaks the Agent Client Protocol over stdio for editors\nsuch as Zed.\n\n`codewhale serve --http` / `serve --mobile` remain as **compatibility aliases**\nfor `codewhale app-server --http` / `--mobile`; both launch the identical\nserver. Desktop, web, SDK and custom integrations should target\n`app-server --http`. `--stdio` and `--socket` expose compatibility controls.\nBare `app-server` selects initialized JSONL control over stdio. Local stdio,\nsocket and ACP owners do not bind a hidden TCP listener. Explicit `--http`\nretains HTTP/SSE and compatibility routes on the same Runtime owner.\n\n`codewhale exec` is the separate one-shot headless worker path (stream-json,\nfleet worker subprocess, CI primitive). It is not part of this API, but it\nshares the same runtime, provider/model resolution, permission profiles, and\nevent vocabulary.\n\nThis document is the stable integration contract for native workbench\napplications (and other local supervisors) that embed the Codewhale Engine.\n\n## Architecture\n\n```\nlocal supervisor / SDK / automation harness\n │\n ├─ codewhale app-server --http → HTTP/SSE runtime API (/v1/*) [canonical]\n ├─ codewhale app-server --mobile → runtime API + mobile control page\n ├─ codewhale app-server --stdio → JSON-RPC control transport over stdio\n ├─ codewhale app-server --socket → same JSON-RPC over a unix domain socket (desktop daemon)\n ├─ codewhale doctor --json → machine-readable health & capability\n ├─ codewhale serve --acp → ACP stdio agent for editors such as Zed\n ├─ codewhale serve --mcp → MCP stdio server\n ├─ codewhale serve --http/--mobile → legacy aliases for `app-server --http/--mobile`\n └─ codewhale exec [args] → one-shot headless worker (stream-json)\n```\n\nThe engine runs as a local-only process. All APIs bind to `localhost` by\ndefault. No hosted relay, no provider-token custody, no secret leakage.\n\nFor the read-only record of what a thread or turn did, see\n[`docs/RECEIPTS.md`](RECEIPTS.md): `codewhale receipts` on the CLI and the\n`/receipt` routes under **Threads** below.\n\n## Runtime API entrypoints\n\n| Entry | Transport | Use |\n|---|---|---|\n| `codewhale web [--port 7878]` | HTTP/SSE on `127.0.0.1:7878` + embedded client | First-class loopback-only browser client; opens the default browser |\n| `codewhale app-server --http` | HTTP/SSE on `127.0.0.1:7878` | Full `/v1/*` runtime API (canonical) |\n| `codewhale app-server --mobile` | HTTP/SSE on loopback + `/mobile` | Runtime API + local mobile control page |\n| `codewhale app-server --stdio` | Initialized JSONL over stdio | Compatibility controls and explicit tool-free turns through the held manager; no hidden TCP listener or full Codex interaction channel. |\n| `codewhale app-server --socket [--socket-path P]` | RPC text messages over WebSocket on a `0600` Unix domain socket (macOS/Linux) or an owner-only named pipe (Windows) | Desktop daemon: multi-client, kernel peer identity checked, `daemon/attach` claim handshake. Windows native-app qualification remains open. |\n| `codewhale app-server` | Initialized JSONL over stdio | Same selection as `--stdio`; HTTP requires explicit `--http`. |\n| `codewhale serve --http` / `--mobile` | same server as `app-server --http`/`--mobile` | Compatibility aliases |\n\n`app-server --http` and `--mobile` launch the same mature runtime API server\nhistorically reached through `serve --http` — no routes or behavior changed, so\nevery endpoint documented below is identical across both entrypoints. The\nruntime API token is read from `--auth-token`, then `CODEWHALE_RUNTIME_TOKEN`,\nthen `DEEPSEEK_RUNTIME_TOKEN`; use `--insecure-no-auth` only with a loopback\nbind. The `serve` compatibility aliases keep their `--insecure` flag.\nBare `app-server` does not open an HTTP port. Selected attached HTTP listeners\nretain their explicit bearer and non-loopback security checks.\n\nInitialized stdio/private-socket controls support `thread/loaded/list` with\n`{cursor?: string | null, limit?: number | null}`. The result is\n`{data: string[], nextCursor: string | null}` from the held manager's loaded\nengine snapshot, including idle and archived engines still in memory. It dLine truncated
"sha256": "c4f348c27735a7df47a79faf11a6d5e3284a1e67a76c9372e76a8809236af45a",
"content": "# Codewhale Engine Runtime API & Integration Contract\n\n> 阅读简体中文版:[zh_hans/RUNTIME_API.md](zh_hans/RUNTIME_API.md)。\n\n`codewhale app-server --http` exposes the [Codewhale Engine](ARCHITECTURE.md) through\nits canonical local Runtime API and control plane.\nLocal SDKs, mobile/remote-control clients, and editor integrations talk to it\ninstead of screen-scraping terminal output. It serves the full HTTP/SSE runtime\nAPI (`/v1/*`), a JSON-RPC control transport over stdio, and the phone-friendly\nmobile page. `codewhale doctor --json` provides machine-readable health, and\n`codewhale serve --acp` speaks the Agent Client Protocol over stdio for editors\nsuch as Zed.\n\n`codewhale serve --http` / `serve --mobile` remain as **compatibility aliases**\nfor `codewhale app-server --http` / `--mobile`; both launch the identical\nserver. Desktop, web, SDK and custom integrations should target\n`app-server --http`. `--stdio` and `--socket` expose compatibility controls.\nBare `app-server` selects initialized JSONL control over stdio. Local stdio,\nsocket and ACP owners do not bind a hidden TCP listener. Explicit `--http`\nretains HTTP/SSE and compatibility routes on the same Runtime owner.\n\n`codewhale exec` is the separate one-shot headless worker path (stream-json,\nfleet worker subprocess, CI primitive). It is not part of this API, but it\nshares the same runtime, provider/model resolution, permission profiles, and\nevent vocabulary.\n\nThis document is the stable integration contract for native workbench\napplications (and other local supervisors) that embed the Codewhale Engine.\n\n## Architecture\n\n```\nlocal supervisor / SDK / automation harness\n │\n ├─ codewhale app-server --http → HTTP/SSE runtime API (/v1/*) [canonical]\n ├─ codewhale app-server --mobile → runtime API + mobile control page\n ├─ codewhale app-server --stdio → JSON-RPC control transport over stdio\n ├─ codewhale app-server --socket → same JSON-RPC over a unix domain socket (desktop daemon)\n ├─ codewhale doctor --json → machine-readable health & capability\n ├─ codewhale serve --acp → ACP stdio agent for editors such as Zed\n ├─ codewhale serve --mcp → MCP stdio server\n ├─ codewhale serve --http/--mobile → legacy aliases for `app-server --http/--mobile`\n └─ codewhale exec [args] → one-shot headless worker (stream-json)\n```\n\nThe engine runs as a local-only process. All APIs bind to `localhost` by\ndefault. No hosted relay, no provider-token custody, no secret leakage.\n\nFor the read-only record of what a thread or turn did, see\n[`docs/RECEIPTS.md`](RECEIPTS.md): `codewhale receipts` on the CLI and the\n`/receipt` routes under **Threads** below.\n\n## Runtime API entrypoints\n\n| Entry | Transport | Use |\n|---|---|---|\n| `codewhale web [--port 7878]` | HTTP/SSE on `127.0.0.1:7878` + embedded client | First-class loopback-only browser client; opens the default browser |\n| `codewhale app-server --http` | HTTP/SSE on `127.0.0.1:7878` | Full `/v1/*` runtime API (canonical) |\n| `codewhale app-server --mobile` | HTTP/SSE on loopback + `/mobile` | Runtime API + local mobile control page |\n| `codewhale app-server --stdio` | Initialized JSONL over stdio | Compatibility controls and explicit tool-free turns through the held manager; no hidden TCP listener or full Codex interaction channel. |\n| `codewhale app-server --socket [--socket-path P]` | RPC text messages over WebSocket on a `0600` Unix domain socket (macOS/Linux) or an owner-only named pipe (Windows) | Desktop daemon: multi-client, kernel peer identity checked, `daemon/attach` claim handshake. Windows native-app qualification remains open. |\n| `codewhale app-server` | Initialized JSONL over stdio | Same selection as `--stdio`; HTTP requires explicit `--http`. |\n| `codewhale serve --http` / `--mobile` | same server as `app-server --http`/`--mobile` | Compatibility aliases |\n\n`app-server --http` and `--mobile` launch the same mature runtime API server\nhistorically reached through `serve --http` — no routes or behavior changed, so\nevery endpoint documented below is identical across both entrypoints. The\nruntime API token is read from `--auth-token`, then `CODEWHALE_RUNTIME_TOKEN`,\nthen `DEEPSEEK_RUNTIME_TOKEN`; use `--insecure-no-auth` only with a loopback\nbind. The `serve` compatibility aliases keep their `--insecure` flag.\nBare `app-server` does not open an HTTP port. Selected attached HTTP listeners\nretain their explicit bearer and non-loopback security checks.\n\nInitialized stdio/private-socket controls support `thread/loaded/list` with\n`{cursor?: string | null, limit?: number | null}`. The result is\n`{data: string[], nextCursor: string | null}` from the held manager's loaded\nengine snapshot, including idle and archived engines still in memory. It dLine truncated
},
{
"path": "docs/RUNTIME_ROUTES.md",
-1
View File
@@ -1,7 +1,6 @@
{
"tag": "v0.10.2",
"slugs": [
"first-time",
"install",
"guide",
"auth",
-32
View File
@@ -3,38 +3,6 @@
"tag": "v0.10.2",
"version": "0.10.2",
"topics": [
{
"id": "first-time",
"slug": "first-time",
"sources": [
{
"path": "docs/GUIDE.md",
"sha256": "43b307ea1fc95898f1ec2484f0b9073b7774d14504d4b12c41e4bf0d5af3b336",
"content": "# Codewhale User Guide\n\nThis guide is for your first hour with Codewhale. It explains the main\nworkflow, the important safety controls, and where to go next when you need a\ncomplete reference.\n\nCodewhale has deeper reference documents for installation, configuration,\nproviders, modes, keybindings, tools, and operations. Use this page as a guided\nwalkthrough, then follow the \"Next\" links when you need every option.\n\n## 1. Welcome to Codewhale\n\nCodewhale is a terminal coding agent. You run it from a workspace, give it a\ntask, and it can use structured tools to inspect files, run commands, edit\ncode, and report back with evidence.\n\nThe important difference from a normal chat model is that Codewhale is built\naround a harness:\n\n- It keeps the active workspace and session visible.\n- It routes each turn through explicit modes and approval rules.\n- It shows tool calls in the transcript instead of hiding the work.\n- It can preserve sessions, fork conversations, and continue later.\n- It can run sub-agents for focused background work.\n\nYou can use Codewhale for small questions:\n\n```text\nExplain the authentication flow in this repository.\n```\n\nYou can also use it for multi-step work:\n\n```text\nFind the failing validation path, propose a fix, and wait for my approval\nbefore editing files.\n```\n\nFor a new repository, start conservatively. Ask Codewhale to explore and plan\nbefore asking it to change files. That gives you a reviewable path and makes it\neasier to catch wrong assumptions early.\n\nNext: [ARCHITECTURE.md](ARCHITECTURE.md) explains the internal harness and\nruntime model.\n\n## 2. First Launch\n\nFor a new macOS or Linux installation, use the official GitHub release.\nThe installer verifies the release checksums and provides the same runtime\nunder the `codewhale` and `codew` command names:\n\n```bash\ncurl -fsSL https://codewhale.net/install.sh | sh\n```\n\nWindows users should choose the matching installer or archive from\n[GitHub Releases](https://github.com/codewhale-hq/CodeWhale/releases/latest).\nFor an existing direct install, use `codewhale update --check`, then\n`codewhale update`. npm and Cargo remain secondary packaging routes; Cargo\nalso supports source builds where a compatible prebuilt is unavailable.\nFor occupied directories, package-managed installs, and PATH setup, follow\n[the installation and migration guide](INSTALL.md#recommended-official-github-releases).\nAndroid/Termux uses its own [preview archive or source-build path](INSTALL.md#android--termux-arm64).\n\nDocker is also available when you want an isolated runtime. The published\nimage is currently 0.10.0; to test current `main`, [build it from source](DOCKER.md#building-locally).\n\n```bash\ndocker volume create codewhale-home\ndocker run --rm -it \\\n -e DEEPSEEK_API_KEY=\"$DEEPSEEK_API_KEY\" \\\n -v codewhale-home:/home/codewhale/.codewhale \\\n -v \"$PWD:/workspace\" \\\n -w /workspace \\\n ghcr.io/hmbown/codewhale:latest\n```\n\nOnce the install directory is on PATH, launch Codewhale from the repository or\ndirectory you want it to work in:\n\n```bash\ncodewhale\n```\n\nFor the default GitHub installer destination, you can use\n`\"$HOME/.local/bin/codewhale\"` until that directory is on PATH.\n\nOn first launch, Codewhale opens the composer, even with no key or route.\nWith no key or route, workspace trust is asked once a provider is connected;\notherwise only when the folder requires a decision. A provider is asked for\nwhen you send your first message: the message stays in the box while you sign\nin or use your own key, and a local model is offered only when a local Ollama\nserver answers. The provider step also has an explicit offline route.\n\nEverything optional stays available after that. Use `/setup` for the\nprogressive setup and repair guide, `/settings` for the full typed editor, and\n`/constitution` when you want your own constitution on top of the bundled one.\nThe localized telemetry choice appears only after the workspace is ready and\ndoes not block the composer.\n\nDeepSeek is the default provider. If you want to configure its key before or\nafter the first launch, the most direct setup path is:\n\n```bash\ncodewhale auth set --provider deepseek\n```\n\nYou can also provide a key through the environment:\n\n```bash\nexport DEEPSEEK_API_KEY=\"your-key\"\ncodewhale\n```\n\nNew Codewhale config is stored under `~/.codewhale/config.toml`. Legacy\n`~/.deepseek/config.toml` files are still supported for users migrating from\nthe old name.\n\nUse `/constitution` to review or change standing guidance. After setup, run a\ndoctor check:\n\n```bash\ncodewhale doctor\n```\n\nUse the JSON form when you need a machine-readable report for an issue:\n\n```bash\ncodewhale doctor --json\n```\n\nBoth forms are offline by default. They report structural configuration and\nliteral unknown/not-probed credential states without loading workspace `.env`\ncredentials, opeLine truncated
},
{
"path": "docs/INSTALL.md",
"sha256": "e8d15c92107272146ad38f66c487bda9c821a4fd85b46c0a1c884907acc6803a",
"content": "# Installing Codewhale\n\n> 阅读简体中文版:[zh_hans/INSTALL.md](zh_hans/INSTALL.md)\n\nCodewhale is an open-source coding agent that runs in your terminal. You give\nit a task (\"fix the failing test\", \"add a CLI flag\"). It reads your repository,\nedits files and runs commands. In the default **Ask** posture it applies file\nedits inside the workspace immediately (and shows you the diff), but asks before\nrunning shell commands, so commit or stash anything you care about first. It\nworks with many model providers. **DeepSeek** is the default.\n\nThe command is `codewhale`. `codew` is a shorter alias for the same program.\n\nThe installation paths were first checked with **v0.10.0** (released 2026-09-22) on a\nfresh **Ubuntu 24.04 x86_64** machine. The dated\n[install receipts](https://github.com/codewhale-hq/Codewhale/blob/37ecdfcc49bc68a9b0d058b97c3946e62c34bd31/docs/install-report/v0.10.0-2026-09-23/RECEIPTS.md) record the commands and output checked at that time. Steps that\ncould not be run on that machine are marked **(untested on this VM: reason)**.\nmacOS, Windows and Android are out of scope, apart from a few notes. A second\npass re-ran the installer, manual-download, archive and npm paths, the no-key\nchecks and zsh completion on **macOS 26.1 (Apple silicon)**; see\n[macOS notes](#macos-notes). Steps that need a model call were not re-run\nthere.\n\nInstall commands that use `latest` resolve to the latest **published** GitHub\nRelease or package. Between releases, `main` may already describe the next\nversion (for example a v0.10.2 source candidate before its tag). A\nprebuilt candidate is available through the official installer only after its\ntag, checksums and release assets exist. Contributors can build `main` now.\n\n## No account needed\n\nCodewhale needs no account. Keys you save with `/provider` or\n`codewhale auth set` stay on your machine; see\n[API keys and providers](#8-api-keys-and-providers). New Codewhale accounts\nare not open, so this guide does not cover account key storage.\n\n## Test and contribute to current source\n\n`latest` selects the latest published release. To test unreleased repairs,\nbuild canonical GitHub `main`, or CNB `main` after verifying its commit matches.\nThe source version number alone does not establish a published package.\n\n```bash\ngit clone --branch main https://github.com/codewhale-hq/CodeWhale.git\ncd CodeWhale\ncargo build --release --locked -p codewhale-cli\ntarget/release/codewhale --version\n```\n\nSee the Cargo section for build prerequisites; use the repository's stable Rust\nselection (declared minimum 1.89). Windows uses `target/release/codewhale.exe`\nand needs MSVC. A development build is separate from a signed release package.\nFor mainland-China downloads and source validation, see [CNB mirror](CNB_MIRROR.md).\nThe [Tencent Lighthouse bootstrap](../scripts/tencent-lighthouse/README.md) is\nbilingual. An npm registry mirror serves the wrapper; native binary downloads\nstill need an available release asset source.\n\n---\n\n## 60-second quickstart (Linux or macOS)\n\n```bash\n# 1. Install. Downloads two checksum-verified binaries into ~/.local/bin (no sudo).\ncurl -fsSL https://codewhale.net/install.sh | sh\n\n# 2. Make sure ~/.local/bin is on your PATH, now and in future terminals.\necho 'export PATH=\"$HOME/.local/bin:$PATH\"' >> ~/.bashrc # zsh: use ~/.zshrc\nexport PATH=\"$HOME/.local/bin:$PATH\"\ncodewhale --version # -> codewhale 0.10.0 (1be1a703b975)\n\n# 3. Give it a DeepSeek API key (from https://platform.deepseek.com/api_keys).\ncodewhale auth set --provider deepseek # prompts for the key; nothing is echoed\ncodewhale auth status --provider deepseek # \"active source: secret store\"\n\n# 4. Run your first task inside a git repository.\ncd ~/your-project\ncodewhale\n```\n\nIn the TUI, type something concrete:\n\n```text\ncreate a Python file primes.py that prints the first 10 primes, run it, and show me the output\n```\n\nCodewhale writes the file, shows you a diff, then asks **APPROVAL: bash\npython3 primes.py – Do you want to proceed?** Press `y` to allow it once. It\nruns the command and reports the output. Press `Ctrl-D` (with an empty input\nbox) to quit. It prints the command to resume the session later.\n\n> **If nothing happens after you send your first message,** you have no key\n> configured. v0.10.0 doesn't warn you in that case. Press **F3**. If DeepSeek\n> shows `missing key`, press Enter, paste the key, then pick a model and\n> confirm.\n\n---\n\n## Contents\n\n1. [Before you start](#1-before-you-start)\n2. [Install: recommended installer](#2-recommended-installer-curl--sh)\n3. [Install: manual download from GitHub Releases](#3-manual-download-from-github-releases)\n4. [Install: npm](#4-npm)\n5. [Install: Cargo / build from source](#5-cargo-and-building-from-source)\n6. [Install: Homebrew (Linux) and Nix](#6-homebrew-on-linux-and-nix)\n7. [Updating and rolling back]Line truncated
},
{
"path": "docs/MODES.md",
"sha256": "ea1e371421770a8582195f4c80c70fe9d067b3800ef143c7fea1fdcf444f0ecd",
"content": "# Modes and Permission Postures\n\n> 阅读简体中文版:[zh_hans/MODES.md](zh_hans/MODES.md)\n\nCodewhale has three related concepts:\n\n- **TUI mode**: what kind of visible interaction you're in (Plan/Work/Operate).\n- **Permission posture**: how aggressively the UI asks before executing tools.\n- **Workflow**: named steps that coordinate sub-agents, with dependencies and\n results, available in any TUI mode.\n\nModel selection is separate. `--model auto` and `/model auto` route each turn to\na concrete model and thinking level; they are not TUI modes and are not part of\nthe `Tab` cycle.\n\nWorkflow runs named steps through the same sub-agent runtime; Fleet manages\ntheir saved roles and model assignments. A workflow adds ordering, result\nhandoffs, gates and a progress view. The active mode and permission posture\nstill control what each step may execute.\n\nBefore assigning steps, query `agent(action=\"roster\")` for the saved Fleet\nmodels and roles. A plan child can select a listed model with `model`, or use\nthe saved `role`/`profile` assignment. An Exact Fleet fixes each member's route.\n\n## TUI Modes\n\nPress `Tab` to complete composer menus or cycle through the visible modes\nwhen the composer is empty: **Plan → Work → Operate → Plan**. `Tab` never sends\nor queues composer text; use `Enter` to send or queue it.\nPress `Shift+Tab` to cycle permission posture (Ask → Auto-Review → Full Access).\nEvery change of effective permission writes a `Permissions changed to …` line\nin the transcript.\nPress `Ctrl+T` to cycle reasoning effort.\nRun `/mode` to open the mode picker, or switch directly with `/mode work`,\n`/mode plan`, or `/mode operate`.\n\n- **Plan**: design-first prompting. The stable primitive names remain familiar, but the runtime centrally refuses file mutation and shell execution. Read-only inspection and policy-allowed research, including deferred Web search/fetch, remain available. When a Plan turn successfully finishes a nonempty response, including a prose-only plan, the TUI asks how to continue: Work (Ask), Work (Auto-Review), or keep planning. Work sends the exact approved response as a normal follow-up and preserves that turn's existing To-dos, or adds one pending plan item without replacing unrelated work.\n- **Work** (internally `agent`): ordinary multi-step execution. The first-turn toolbox includes `read`, `write`, `edit`, `file_search`, `bash`, `agent`, `workflow`, `todo_write`, and `load_skill`, plus `create_goal`, `get_goal`, `update_goal`, and `task_shell_wait` so goal and background-shell controls are available without discovery. The agent calls `create_goal` when a request is a durable objective; the host never infers a goal from wording, and the contract must not tell the model otherwise. Approval, sandbox, repository law, and managed policy decide what may execute.\n- **Operate**: Work at full strength, the \"ultra\" mode. Same tools and authority as Work; the difference is how they are used. Each substantive request becomes a goal (`create_goal`), the plan stays visible (`todo_write`), multi-part work runs through `workflow` and parallel `agent` workers, non-trivial changes get an independent reviewer before they are called done, long commands keep running in the background, and recurring or watch-type work is offered as an `automation` (created after approval). It keeps going until the goal is verified, blocked on you, or paused.\n\n`Act` and `/mode act` remain compatibility aliases for Work. Saved settings\nstill normalize to the internal value `agent`.\n\n### Tool availability by mode\n\n| Tool family | Plan | Work | Operate |\n|:---|:---:|:---:|:---:|\n| `read` and policy-allowed deferred research tools | yes | yes | yes |\n| `write` and `edit` | visible names; execution denied | approval- and policy-gated | same as Work |\n| `bash` | visible name; execution denied | approval- and policy-gated | same as Work; delegation is preferred when parallelism or isolation helps |\n| `agent` | yes, subject to child-depth authority | yes, subject to child-depth authority | yes, subject to child-depth authority |\n| Deferred native, MCP, and plugin tools | discoverable through `tool_search` when policy permits | same | same |\n| Paid or external-service tools | follows permission posture | follows permission posture | follows permission posture |\n| Access outside the workspace root | explicit trusted paths only | only through trusted paths or trust mode | same trusted-path/trust policy as Work; fleet profiles never widen it |\n\nOperate changes scheduling emphasis, not authority. It neither adds a\nmode-specific tool denial nor bypasses the active approval, sandbox, shell,\nask-rule, repository-law, or managed-policy boundary. Plan remains the\nmode-specific execution boundary for shell and write-capable tools; that\nauthority difference does not require a different primitive vocabulary.\n\n### Operate loop (one screen)\n\n```text\nUseLine truncated
},
{
"path": "docs/SANDBOX.md",
"sha256": "1626a34e894f491c15959ebe82b87d2f669c792f9c9457fb912023dcee1a5a33",
"content": "# Sandbox threat model\n\n> 阅读简体中文版:[zh_hans/SANDBOX.md](zh_hans/SANDBOX.md)。\n\nCodewhale can launch shell commands proposed by a model. Approval policy,\nworkspace-aware tools, and an operating-system command wrapper are separate\ncontrols: an approval is not a sandbox, and selecting `workspace-write` does\nnot prove that the current platform has an OS wrapper available.\n\nThis document describes only behavior wired into the command execution path.\nSee [Authorization order](AUTHORIZATION_ORDER.md) for the policy layers that\nrun before execution reaches this boundary.\n\n## Platform overview\n\n| Mechanism | Platform | Selection | What Codewhale reports |\n|---|---|---|---|\n| Seatbelt (`sandbox-exec`) | macOS | Automatic when the runtime probe succeeds | `macos-seatbelt` |\n| Bubblewrap (`/usr/bin/bwrap`) | Linux | Default when installed and a wrapped probe run succeeds; `prefer_bwrap = false` opts out | `linux-bwrap` |\n| No OS wrapper | Linux without usable bwrap, or opted out | `prefer_bwrap = false`, or bwrap absent/unusable | `none` |\n| No OS wrapper | Windows | Current implementation | `none` |\n| OpenSandbox-compatible service | Any supported host | `sandbox_backend = \"opensandbox\"` | External execution path |\n\nThe repository contains a seccomp implementation module plus a future Windows\nhelper contract. They are not wired into child-command launch, so Codewhale\ndoes not advertise them as active sandboxes. Source-only sandbox code is not\nevidence that a command was restricted.\n\n## macOS: Seatbelt\n\nCodewhale probes `/usr/bin/sandbox-exec` by running a minimal profile. When the\nprobe succeeds and the selected `SandboxPolicy` requests a sandbox, the child\ncommand is wrapped with a generated Seatbelt profile.\n\nThe profile can provide:\n\n- broad filesystem reads;\n- writes limited by the selected policy, including the workspace and specific\n runtime/cache paths needed by supported tools;\n- network access only when the policy enables it. A `read-only` policy with\n network (the agent `read_only: true` shell mode) gets outbound IPv4 and the\n system DNS resolver only. IPv6 and the host's own addresses, loopback\n included, are denied, except the port of a local proxy named in\n `HTTP_PROXY`, `HTTPS_PROXY` or `ALL_PROXY`, and other local sockets stay\n denied.\n\nIf the probe fails or `sandbox-exec` is unavailable, Codewhale reports no OS\nsandbox and launches the command without a Seatbelt wrapper. It does not set a\nSeatbelt marker on that fallback.\n\n## Linux: default bubblewrap\n\nLinux command sandboxing is on by default whenever bubblewrap works. Set the\ntop-level configuration key to opt out:\n\n```toml\nprefer_bwrap = false\n```\n\nCodewhale selects bubblewrap only when `/usr/bin/bwrap` is a regular\nexecutable file AND a wrapped probe run proves it can create its namespaces\non this host — the exec bit alone lies where user namespaces are restricted\n(e.g. Ubuntu 24.04 with `kernel.apparmor_restrict_unprivileged_userns`),\nwhere every wrapped command would fail rather than run unsandboxed. The\nwrapper derives its mounts and network namespace from the resolved\n`SandboxPolicy`:\n\n```text\n/usr/bin/bwrap \\\n --unshare-all \\\n [--share-net] \\\n --ro-bind / / \\\n --dev /dev \\\n --proc /proc \\\n --tmpfs /tmp \\\n [--dev-bind <device-root> <device-root> ...] \\\n --bind <writable-root> <writable-root> ... \\\n --ro-bind <protected-descendant> <protected-descendant> ... \\\n [--ro-bind <extra-ro-root> <extra-ro-root> ...] \\\n --chdir <cwd> \\\n -- <program> <args>\n```\n\nThe sandbox always gets a private `/dev` (fresh device nodes, so `>/dev/null`\nworks), a private `/proc`, and a tmpfs `/tmp` (#5410). Two optional top-level\nconfig keys extend the mounts: `bwrap_ro_roots` (extra host paths bind-mounted\nread-only, applied last so they can narrow a policy-writable path) and\n`bwrap_dev_roots` (host character/block device nodes bind-mounted read-write;\ndirectories are never honored). Missing paths are skipped silently.\n\nThat gives the child a read-only root view. For `workspace-write`, every safe,\nexisting policy root is mounted read-write: the working directory, configured\nadditional roots, `/tmp` and `TMPDIR` unless excluded, and verified Git\nworktree metadata roots. Existing `.codewhale` and `.deepseek` descendants are\nremounted read-only after their writable parent. Missing paths, non-directory\npaths, and `/` are not promoted to writable mounts.\n\nFor `read-only`, there are no writable binds, so the working directory remains\ninside the read-only root view. `--unshare-all` isolates the network namespace\nby default. Codewhale adds `--share-net` only when the policy's\n`network_access` is true. The agent `read_only: true` shell mode turns it on\nonly in Full Access (see [Subagents](SUBAGENTS.md)); it then shares the host\nnetwork namespace, including loopback and abstract unix sockets, like a\nnetworLine truncated
},
{
"path": "docs/KEYBINDINGS.md",
"sha256": "26d213c47e899e376f240d2f9f08efeac49911a7472ee7d7ed11fea249509ab3",
"content": "# Keybindings\n\n> 阅读简体中文版:[zh_hans/KEYBINDINGS.md](zh_hans/KEYBINDINGS.md)\n\nThis is the source-of-truth catalog of every keyboard shortcut the TUI recognizes. Bindings are grouped by **context** — the focus or modal state they fire in. A binding listed under \"Composer\" only takes effect when the composer is focused; one under \"Transcript\" only when the transcript has focus; and so on.\n\nGlobal key chords are not yet user-configurable — tracked for a future release (#436, #437). Hotbar slot actions are configurable with `[[hotbar]]` and `/hotbar`; the Hotbar activation chord remains `Alt-1` through `Alt-8`.\n\n## Global (any context)\n\n| Chord | Action |\n|----------------------|---------------------------------------------------------------|\n| `/help`, `F1` or `Ctrl-/` | Toggle the help overlay. `/help` is what the chrome advertises: it is the one route every terminal delivers, where `F1` is eaten by tmux and some emulators and `Ctrl-/` encoding varies |\n| `F2` | Toggle the typed Settings editor |\n| `F3` | Open the provider/model picker (same as `/provider`) |\n| `Ctrl-K` | Open the command palette (slash-command finder) |\n| `Ctrl-C` | Cancel current turn / dismiss modal / arm-then-confirm quit |\n| `Ctrl-B` | Release the current shell wait so the turn can continue; foreground commands move into `/jobs`, and background tasks keep running. Use `/jobs` or `Bash` with `action: \"wait\"` to inspect output |\n| `Ctrl-D` | Quit (only when the composer is empty) |\n| `Tab` | When the composer is empty, cycle TUI mode: Plan → Work → Operate → Plan |\n| `Shift+Tab` | Cycle permission posture: Ask → Auto-Review → Full Access. Live regardless of composer contents or whether a turn is running (suppressed only while a modal other than Config is open) |\n| `Ctrl-T` | Cycle reasoning effort for the active model. Walks the same ladder as `/model` and `/effort` (catalog or documented route dialect). Always-thinking models omit `off`; Grok 4.6 includes `xhigh`. |\n| `Ctrl-Shift-T` | Toggle live transcript overlay (sticky-tail auto-scroll) |\n| `Ctrl-R` | Open the resume-session picker |\n| `Ctrl-L` | Compact the conversation context (status line shows progress; no-op while a compaction is already running) |\n| `Ctrl-O` | Open the reasoning detail for the selected or current turn, regardless of composer contents |\n| `Ctrl-Alt-O` | Open the whole-turn Turn Inspector, regardless of composer contents |\n| `Alt-V` / `Option-V` (macOS) | Open the details pager for the selected tool/sub-agent card; with nothing selected, the newest visible error, else the first visible card with details, else the most recent one. Click a card first to keep Alt-V on it while newer cards arrive. Terminals that emit the legacy Option-V glyph are also handled |\n| `Ctrl-Shift-E` / `Cmd-Shift-E` | Toggle the file tree in the workbar |\n| `Ctrl-]` or `Ctrl-Tab` | Cycle the pet view while the pet main view is showing; otherwise cycle the work dock: TODO → Agents → Jobs → Background. `Ctrl-]` is portable; `Ctrl-Tab` requires the kitty keyboard protocol. Terminal.app uses it to switch terminal tabs |\n| `Ctrl-Shift-Tab` | Cycle the pet view or work dock backwards where the terminal sends the kitty keyboard chord. Terminal.app uses it to switch terminal tabs |\n| `Alt-G` / `Alt-Shift-G` | Scroll transcript to top / bottom when the composer is empty |\n| `Alt-1`-`Alt-8` | Dispatch Hotbar slots 1-8 when no modal or inline picker is open |\n| `Alt-!` / `Alt-@` / `Alt-#` / `Alt-$` | Select the workbar panel: Tasks / Agents / Context / Files |\n| `Ctrl-Alt-0` | Toggle the workbar off / back to the bottom placement |\n| `Alt-L` | Open the pager for the last message (composer empty) |\n| `Alt-P` / `Alt-A` / `Alt-Y` | Jump to Plan / Work, or request Full Access (`Alt-Y` is the legacy permission channel — Work + Full Access — not a separate mode; it honors a locked approval policy) |\n| `Ctrl-X` (Activity workbar) | Cancel all running background shell jobs |\n| `Esc` | Close topmost modal · cancel slash menu · dismiss toast. During a compact that is serving an in-flight turn, Esc stops the turn (the compact is collateral). |\n\n## Composer\n\nEditing the message you're about to send.\n\n| Chord | Action |\n|-----------------------------|---------------------------------------------------------|\n| `Enter` | Send when idle; queue while busLine truncated
}
],
"missing": []
},
{
"id": "install",
"slug": "install",
-273
View File
@@ -2299,279 +2299,6 @@
],
"sourceNote": "Source documents: docs/public-surface-facts.json (trust), docs/TELEMETRY.md, docs/SANDBOX.md · Update docs-map.ts when changing."
},
"docs-first-time": {
"metaTitle": "Use Codewhale for the first time · Codewhale Docs",
"metaDescription": "Open a terminal, install Codewhale, pick the folder it works in, connect a model and finish a first task, with no terminal experience needed.",
"bodyClassName": "text-ink-soft leading-relaxed",
"title": "Use Codewhale for the first time",
"lede": "This page takes you from opening a terminal for the first time to finishing a small task with Codewhale. It also explains which folder to start in, because that folder decides what Codewhale can change.",
"sections": [
{
"id": "what-you-need",
"title": "Check what you need",
"blocks": [
{
"p": "You need a Mac, a Windows x64 PC or a Linux computer, and an internet connection for the install. To get answers you also need a model: an API key from a hosted provider such as DeepSeek, or a model running on your own computer with Ollama."
},
{
"p": "Codewhale is free and needs no account. A hosted provider bills you directly for what you use, and a model you run yourself needs no key."
}
]
},
{
"id": "open-terminal",
"title": "Open a terminal",
"blocks": [
{
"p": "A terminal is a window where you type commands instead of clicking. A command runs when you press Return, which is Enter on Windows."
},
{
"rows": [
[
"macOS",
"Press Cmd+Space, type Terminal and press Return."
],
[
"Windows",
"Open the Start menu, type PowerShell and press Enter."
],
[
"Linux",
"Open your applications menu and search for Terminal. On Ubuntu, Ctrl+Alt+T opens one."
]
]
},
{
"note": "Paste with Cmd+V on a Mac and Ctrl+V on Windows. Most Linux terminals paste with Ctrl+Shift+V."
}
]
},
{
"id": "install",
"title": "Install Codewhale",
"blocks": [
{
"p": "On a Mac or Linux, paste this command and press Return. It downloads Codewhale, checks the download, and adds Codewhale to your PATH, the list of folders your terminal searches for commands."
},
{
"code": "curl -fsSL https://codewhale.net/install.sh | sh",
"lang": "macOS · Linux"
},
{
"p": "On Windows, paste this command and press Enter:"
},
{
"code": "winget install --id HunterBown.CodeWhale --exact --source winget",
"lang": "Windows"
},
{
"p": "When it finishes, open a new terminal window so it picks up the new PATH. Then check that it worked: a version number means Codewhale is installed."
},
{
"code": "codewhale --version",
"lang": "Terminal"
},
{
"p": "If the terminal says `command not found`, follow [the PATH steps on the install page](/install#install-path)."
}
]
},
{
"id": "choose-folder",
"title": "Choose the folder to work in",
"blocks": [
{
"p": "Codewhale works in the folder your terminal is in when you start it. That folder is its workspace: Codewhale reads and changes files there, and its file tools cannot reach other folders unless you allow it."
},
{
"p": "On a Mac, an operating-system sandbox also stops its commands from writing outside the workspace, apart from temporary files. Linux gets the same sandbox when bubblewrap is installed. Windows has no such sandbox yet, so Codewhale relies on asking you first."
},
{
"rows": [
[
"A folder for one project",
"The best choice for most work. Codewhale sees only that project, and `/undo` can take back what it changed."
],
[
"A new, empty folder",
"A good start for something new, such as a website or a script. The next section shows how to make one."
],
[
"A folder that holds several projects",
"Lets one task read and change files across all of them. Pick the smallest folder that holds everything the task needs; in a very large folder Codewhale turns snapshots off and tells you."
],
[
"Your home folder, Desktop, Documents or Downloads",
"Codewhale runs there, but `/undo` and snapshots are off in these folders themselves because they are too broad to snapshot safely. A folder inside one of them works normally."
]
]
},
{
"note": "Snapshots are Codewhale's own copies of the files it changes. It keeps them in its settings folder, not in your project, and they work without git."
},
{
"p": "To let Codewhale reach one more folder, type `/trust add` and that folder's path in Codewhale. Full Access mode reaches every folder and runs commands without the sandbox, so use it only when you want Codewhale to work across your whole computer."
},
{
"p": "By default, Codewhale makes file edits in a git repository without asking and shows you what changed. In a folder without git, it asks before each edit."
}
]
},
{
"id": "open-in-folder",
"title": "Open a terminal in that folder",
"blocks": [
{
"rows": [
[
"macOS Finder",
"Control-click the folder and choose Services > New Terminal at Folder. If it is missing, turn it on in System Settings > Keyboard > Keyboard Shortcuts > Services > Files and Folders."
],
[
"Windows File Explorer",
"Right-click the folder and choose Open in Terminal. On Windows 10, hold Shift while you right-click and choose Open PowerShell window here."
],
[
"Any terminal",
"Type `cd` and a space, drag the folder from Finder or File Explorer into the window, and press Return."
],
[
"Without moving",
"Start Codewhale with `-C` and the folder's path, for example `codewhale -C ~/Projects/my-site` on a Mac."
]
]
},
{
"p": "These commands find your way around from inside a terminal. They work on a Mac, on Linux and in PowerShell."
},
{
"codeTerms": true,
"rows": [
[
"pwd",
"Shows the folder you are in."
],
[
"ls",
"Lists what is in it."
],
[
"cd Documents",
"Moves into the Documents folder inside the current one."
],
[
"cd ..",
"Moves up one folder."
],
[
"mkdir my-site",
"Makes a new folder called my-site."
]
]
},
{
"note": "Type the first letters of a folder name and press Tab, and the terminal finishes the name for you."
}
]
},
{
"id": "connect",
"title": "Start Codewhale and connect a model",
"blocks": [
{
"code": "codewhale",
"lang": "Terminal"
},
{
"p": "Codewhale opens a message box right away. When you send your first message, it asks where your model runs: pick a hosted provider and paste its API key, or pick Ollama when it is running on your computer."
},
{
"p": "Keys you enter stay on your computer, in Codewhale's settings folder `~/.codewhale`. Change the provider later with `/provider`."
},
{
"p": "If Codewhale asks whether to work with the instructions and files in this folder, answer yes for a folder you made yourself."
}
]
},
{
"id": "first-task",
"title": "Give it a first task",
"blocks": [
{
"p": "Type what you want in plain words and press Return. A safe first task changes nothing, for example: `Explain what is in this folder and what each file is for. Do not change anything.`"
},
{
"p": "Then try one that makes a file, for example: `Make a file called packing-list.md with a packing list for a weekend trip.`"
},
{
"p": "By default, Codewhale asks before it runs a command that is not read-only. Read what the approval says will run before you allow it."
}
]
},
{
"id": "stop-undo-quit",
"title": "Stop, undo and quit",
"blocks": [
{
"codeTerms": true,
"rows": [
[
"Esc",
"Stops the task that is running. Ctrl+C does the same."
],
[
"/undo",
"Takes back your last request and the file changes it made."
],
[
"/diff",
"Shows what changed in this folder."
],
[
"/receipts",
"Lists what Codewhale did: files, commands and approvals."
],
[
"/exit",
"Quits Codewhale. Ctrl+D also quits when the message box is empty."
],
[
"codewhale --continue",
"Run it in the same folder to pick up your last conversation there."
]
]
}
]
},
{
"id": "help",
"title": "Get help",
"blocks": [
{
"p": "Type `/help` in Codewhale to list its commands. [Ask in the Codewhale Discord](https://discord.gg/37gfS3ksug), or read [what to do when something goes wrong](/docs/troubleshooting)."
}
]
}
],
"next": [
{
"href": "/docs/guide",
"label": "Start your first task",
"note": "More first prompts and how to check the answer."
},
{
"href": "/docs/auth",
"label": "Connect a provider",
"note": "Save a key, check which key is used, or run a local model."
},
{
"href": "/docs/modes",
"label": "Set modes and approvals",
"note": "Decide which commands and edits stop for your approval."
}
],
"sourceNote": "Source documents: docs/GUIDE.md, docs/INSTALL.md, docs/MODES.md, docs/SANDBOX.md, docs/KEYBINDINGS.md · Update docs-map.ts when changing."
},
"states": {
"loadingLabel": "Loading…",
"emptyTitle": "Nothing here yet",
-273
View File
@@ -2296,279 +2296,6 @@
],
"sourceNote": "来源文档:docs/public-surface-facts.json(trust)、docs/TELEMETRY.md、docs/SANDBOX.md · 修改时同步更新 docs-map.ts。"
},
"docs-first-time": {
"metaTitle": "第一次使用 Codewhale · Codewhale 文档",
"metaDescription": "打开终端、安装 Codewhale、选择它工作的文件夹、连接模型并完成第一个任务,不需要任何终端经验。",
"bodyClassName": "text-ink-soft leading-[1.9] tracking-wide",
"title": "第一次使用 Codewhale",
"lede": "这一页带你从第一次打开终端开始,一直到用 Codewhale 完成一个小任务。它还会说明应该从哪个文件夹启动,因为这个文件夹决定了 Codewhale 能修改什么。",
"sections": [
{
"id": "what-you-need",
"title": "确认你需要准备什么",
"blocks": [
{
"p": "你需要一台 Mac、一台 Windows x64 电脑或一台 Linux 电脑,安装时还需要联网。要得到回答,你还需要一个模型:来自 DeepSeek 等托管提供商的 API 密钥,或者用 Ollama 在你自己电脑上运行的模型。"
},
{
"p": "Codewhale 免费,也不需要账号。托管提供商会按你的用量直接向你收费;你自己运行的模型不需要密钥。"
}
]
},
{
"id": "open-terminal",
"title": "打开终端",
"blocks": [
{
"p": "终端是一个用输入命令代替点击的窗口。按下回车键后,命令就会运行。"
},
{
"rows": [
[
"macOS",
"按 Cmd+空格键,输入 Terminal,然后按回车键。"
],
[
"Windows",
"打开“开始”菜单,输入 PowerShell,然后按回车键。"
],
[
"Linux",
"打开应用程序菜单并搜索“终端”。在 Ubuntu 上,按 Ctrl+Alt+T 也能打开。"
]
]
},
{
"note": "在 Mac 上用 Cmd+V 粘贴,在 Windows 上用 Ctrl+V 粘贴。大多数 Linux 终端用 Ctrl+Shift+V 粘贴。"
}
]
},
{
"id": "install",
"title": "安装 Codewhale",
"blocks": [
{
"p": "在 Mac 或 Linux 上,粘贴下面这条命令并按回车键。它会下载 Codewhale、校验下载内容,并把 Codewhale 加入 PATH,也就是终端查找命令时会搜索的文件夹列表。"
},
{
"code": "curl -fsSL https://codewhale.net/install.sh | sh",
"lang": "macOS · Linux"
},
{
"p": "在 Windows 上,粘贴下面这条命令并按回车键:"
},
{
"code": "winget install --id HunterBown.CodeWhale --exact --source winget",
"lang": "Windows"
},
{
"p": "安装完成后,打开一个新的终端窗口,让它读取新的 PATH。然后确认安装成功:显示版本号就说明 Codewhale 已经安装好了。"
},
{
"code": "codewhale --version",
"lang": "终端"
},
{
"p": "如果终端提示 `command not found`,请按照[安装页面上的 PATH 步骤](/install#install-path)操作。"
}
]
},
{
"id": "choose-folder",
"title": "选择工作的文件夹",
"blocks": [
{
"p": "Codewhale 在你启动它时终端所在的文件夹里工作。这个文件夹就是它的工作区:Codewhale 在这里读取和修改文件,除非你允许,它的文件工具无法访问其他文件夹。"
},
{
"p": "在 Mac 上,操作系统沙箱还会阻止它的命令写入工作区以外的位置(临时文件除外)。在 Linux 上,安装 bubblewrap 后也有同样的沙箱。Windows 目前还没有这种沙箱,因此 Codewhale 依靠先征求你的同意。"
},
{
"rows": [
[
"只放一个项目的文件夹",
"大多数情况下的最佳选择。Codewhale 只看到这个项目,`/undo` 可以撤回它做的修改。"
],
[
"一个新的空文件夹",
"适合开始做新东西,比如一个网站或一个脚本。下一节会说明如何创建。"
],
[
"包含多个项目的文件夹",
"让一个任务可以读取和修改所有这些项目里的文件。请选择能装下任务所需全部内容的最小文件夹;文件夹非常大时,Codewhale 会关闭快照并告诉你。"
],
[
"你的主文件夹、桌面、文稿或下载",
"Codewhale 可以在这里运行,但在这些文件夹本身中 `/undo` 和快照是关闭的,因为它们范围太大,无法安全地做快照。它们里面的子文件夹可以正常使用。"
]
]
},
{
"note": "快照是 Codewhale 为它修改的文件保存的副本。它们保存在 Codewhale 的设置文件夹里,而不是你的项目里,并且不需要 git。"
},
{
"p": "要让 Codewhale 多访问一个文件夹,请在 Codewhale 中输入 `/trust add` 和该文件夹的路径。Full Access 模式可以访问所有文件夹,并且在没有沙箱的情况下运行命令,所以只有在你希望 Codewhale 在整台电脑范围内工作时才使用它。"
},
{
"p": "默认情况下,在 git 仓库中 Codewhale 会直接修改文件,并向你展示改了什么。在没有 git 的文件夹中,它会在每次修改前询问你。"
}
]
},
{
"id": "open-in-folder",
"title": "在那个文件夹中打开终端",
"blocks": [
{
"rows": [
[
"macOS 访达",
"按住 Control 点按该文件夹,选择“服务”>“新建位于文件夹位置的终端窗口”。如果没有这一项,请在“系统设置”>“键盘”>“键盘快捷键”>“服务”>“文件和文件夹”中打开它。"
],
[
"Windows 文件资源管理器",
"右键点击该文件夹,选择“在终端中打开”。在 Windows 10 上,按住 Shift 再右键点击,选择“在此处打开 PowerShell 窗口”。"
],
[
"任意终端",
"输入 `cd` 和一个空格,把文件夹从访达或文件资源管理器拖进窗口,然后按回车键。"
],
[
"不切换文件夹",
"启动 Codewhale 时加上 `-C` 和文件夹路径,例如在 Mac 上输入 `codewhale -C ~/Projects/my-site`。"
]
]
},
{
"p": "下面这些命令可以在终端里切换和查看文件夹。它们在 Mac、Linux 和 PowerShell 中都能用。"
},
{
"codeTerms": true,
"rows": [
[
"pwd",
"显示你当前所在的文件夹。"
],
[
"ls",
"列出其中的内容。"
],
[
"cd Documents",
"进入当前文件夹里的 Documents 文件夹。"
],
[
"cd ..",
"回到上一级文件夹。"
],
[
"mkdir my-site",
"新建一个名为 my-site 的文件夹。"
]
]
},
{
"note": "输入文件夹名的前几个字母后按 Tab 键,终端会帮你补全名字。"
}
]
},
{
"id": "connect",
"title": "启动 Codewhale 并连接模型",
"blocks": [
{
"code": "codewhale",
"lang": "终端"
},
{
"p": "Codewhale 会立即打开一个输入框。你发送第一条消息时,它会询问模型在哪里运行:选择一个托管提供商并粘贴它的 API 密钥,或者在你的电脑上运行着 Ollama 时选择 Ollama。"
},
{
"p": "你输入的密钥保存在你自己的电脑上,位于 Codewhale 的设置文件夹 `~/.codewhale` 中。之后可以用 `/provider` 更换提供商。"
},
{
"p": "如果 Codewhale 询问是否使用这个文件夹中的说明和文件,对于你自己创建的文件夹,回答“是”即可。"
}
]
},
{
"id": "first-task",
"title": "交给它第一个任务",
"blocks": [
{
"p": "用平常的话写下你想做的事,然后按回车键。安全的第一个任务不修改任何东西,例如:`说明这个文件夹里有什么,以及每个文件的用途。不要修改任何内容。`"
},
{
"p": "然后试一个会创建文件的任务,例如:`创建一个名为 packing-list.md 的文件,写一份周末出游的行李清单。`"
},
{
"p": "默认情况下,Codewhale 在运行非只读命令前会先询问你。在允许之前,请先看清审批里写明要运行什么。"
}
]
},
{
"id": "stop-undo-quit",
"title": "停止、撤回和退出",
"blocks": [
{
"codeTerms": true,
"rows": [
[
"Esc",
"停止正在运行的任务。Ctrl+C 的作用相同。"
],
[
"/undo",
"撤回你的上一个请求及其造成的文件修改。"
],
[
"/diff",
"显示这个文件夹中改了什么。"
],
[
"/receipts",
"列出 Codewhale 做过的事:文件、命令和审批。"
],
[
"/exit",
"退出 Codewhale。输入框为空时,按 Ctrl+D 也会退出。"
],
[
"codewhale --continue",
"在同一个文件夹中运行它,可以继续你在那里的上一次对话。"
]
]
}
]
},
{
"id": "help",
"title": "获取帮助",
"blocks": [
{
"p": "在 Codewhale 中输入 `/help` 可以列出它的命令。你也可以[在 Codewhale Discord 中提问](https://discord.gg/37gfS3ksug),或阅读[出现问题时怎么办](/docs/troubleshooting)。"
}
]
}
],
"next": [
{
"href": "/docs/guide",
"label": "开始第一个任务",
"note": "更多第一个提示词的示例,以及如何检查回答。"
},
{
"href": "/docs/auth",
"label": "连接模型提供商",
"note": "保存密钥、查看当前使用的密钥,或运行本地模型。"
},
{
"href": "/docs/modes",
"label": "设置模式与审批",
"note": "决定哪些命令和修改需要你批准。"
}
],
"sourceNote": "来源文档:docs/GUIDE.md、docs/INSTALL.md、docs/MODES.md、docs/SANDBOX.md、docs/KEYBINDINGS.md · 修改时请同步更新 docs-map.ts。"
},
"states": {
"loadingLabel": "加载中…",
"emptyTitle": "这里还没有内容",
-24
View File
@@ -48,30 +48,6 @@ export const INSTALL_COPY = {
scriptLink: { en: "View install script", zh: "查看安装脚本" },
releaseLine: { en: "This installs the latest published release, v{version}.", zh: "这会安装最新已发布版本 v{version}。" },
newTitle: { en: "New to the terminal?", zh: "第一次使用终端?" },
newLead: {
en: "You install Codewhale by pasting one command into a terminal. These steps show how to open one and how to check that it worked.",
zh: "你在终端中粘贴一条命令来安装 Codewhale。下面的步骤说明如何打开终端,以及如何确认安装成功。",
},
newMacTitle: { en: "macOS", zh: "macOS" },
newMac1: { en: "Press Cmd+Space, type Terminal and press Return.", zh: "按 Cmd+空格键,输入 Terminal,然后按回车键。" },
newMac2: { en: "Paste this command with Cmd+V and press Return:", zh: "用 Cmd+V 粘贴下面这条命令,然后按回车键:" },
newMac3: {
en: "When it finishes, open a new window with Cmd+N, type `codewhale --version` and press Return. A version number means it worked.",
zh: "安装完成后,按 Cmd+N 打开一个新窗口,输入 `codewhale --version` 并按回车键。显示版本号就说明安装成功了。",
},
newWinTitle: { en: "Windows", zh: "Windows" },
newWin1: { en: "Open the Start menu, type PowerShell and press Enter.", zh: "打开“开始”菜单,输入 PowerShell,然后按回车键。" },
newWin2: { en: "Paste this command with Ctrl+V and press Enter:", zh: "用 Ctrl+V 粘贴下面这条命令,然后按回车键:" },
newWin3: {
en: "When it finishes, close the window, open PowerShell again, type `codewhale --version` and press Enter. A version number means it worked.",
zh: "安装完成后,关闭窗口,重新打开 PowerShell,输入 `codewhale --version` 并按回车键。显示版本号就说明安装成功了。",
},
newNext: {
en: "Read the first-time guide: which folder to work in, your first task, and how to undo",
zh: "阅读首次使用指南:在哪个文件夹工作、第一个任务,以及如何撤回",
},
pathTitle: { en: "What the installer does to your PATH", zh: "安装程序对 PATH 做了什么" },
pathPlaces: {
en: "The installer puts `codewhale` and `codew` in `~/.local/bin`. When that folder is not on your PATH, it adds three marked lines to the startup file for your shell:",
+2 -2
View File
@@ -4,7 +4,7 @@ import {
getDocsAuth, getDocsClaudeCode, getDocsConfiguration, getDocsFleet,
getDocsHooks, getDocsMcp, getDocsModes, getDocsPet, getDocsReview,
getDocsRuntimeApi, getDocsSandbox, getDocsSkills, getDocsSubagents,
getDocsTroubleshooting, getDocsTrust, getDocsWeb, getDocsWork, getDocsFirstTime,
getDocsTroubleshooting, getDocsTrust, getDocsWeb, getDocsWork,
getDocsGuide, getDocsVocabulary, getConstitution, getChrome, getHome, fill, pickText,
} from "./i18n/dictionaries";
import type { DocsBlock, DocsPageDict } from "./i18n/dictionaries/types";
@@ -31,7 +31,7 @@ const ARTICLES: Record<string, (locale: string) => DocsPageDict> = {
pet: getDocsPet, review: getDocsReview, "runtime-api": getDocsRuntimeApi,
sandbox: getDocsSandbox, skills: getDocsSkills, subagents: getDocsSubagents,
troubleshooting: getDocsTroubleshooting, trust: getDocsTrust, web: getDocsWeb,
work: getDocsWork, "first-time": getDocsFirstTime,
work: getDocsWork,
};
function blockText(block: DocsBlock): string {
-12
View File
@@ -40,18 +40,6 @@ export const DOC_CATEGORY_LABELS: Record<DocTopic["category"], { en: string; zh:
};
export const DOC_TOPICS: DocTopic[] = [
{
id: "first-time",
slug: "first-time",
label: { en: "Use Codewhale for the first time", zh: "第一次使用 Codewhale" },
description: {
en: "Open a terminal, install, pick the folder Codewhale works in, connect a model and finish a first task.",
zh: "打开终端、安装、选择 Codewhale 工作的文件夹、连接模型并完成第一个任务。",
},
repoSource: ["docs/GUIDE.md", "docs/INSTALL.md", "docs/MODES.md", "docs/SANDBOX.md", "docs/KEYBINDINGS.md"],
hasPage: true,
category: "getting-started",
},
{
id: "install",
slug: "install",
+1 -2
View File
@@ -8,7 +8,7 @@ import { FEATURE_CATALOGUE, type Feature } from "./features";
import {
getDocsAuth, getDocsClaudeCode, getDocsConfiguration, getDocsFleet, getDocsHooks, getDocsMcp, getDocsModes, getDocsPet,
getDocsReview, getDocsRuntimeApi, getDocsSandbox, getDocsSkills, getDocsSubagents, getDocsTroubleshooting, getDocsTrust,
getDocsWeb, getDocsWork, getDocsFirstTime,
getDocsWeb, getDocsWork,
} from "./i18n/dictionaries";
import type { DocsBlock, DocsPageDict } from "./i18n/dictionaries/types";
import { DOC_TOPICS } from "./docs-map";
@@ -50,7 +50,6 @@ const PAGES: Record<string, (locale: string) => DocsPageDict> = {
fleet: getDocsFleet, hooks: getDocsHooks, mcp: getDocsMcp, modes: getDocsModes, pet: getDocsPet, review: getDocsReview,
"runtime-api": getDocsRuntimeApi, sandbox: getDocsSandbox, skills: getDocsSkills, subagents: getDocsSubagents,
troubleshooting: getDocsTroubleshooting, trust: getDocsTrust, web: getDocsWeb, work: getDocsWork,
"first-time": getDocsFirstTime,
};
describe("docs describe the published release first", () => {
-4
View File
@@ -14,7 +14,6 @@ import {
EN_DOCS_PET,
EN_DOCS_AUTH,
EN_DOCS_TRUST,
EN_DOCS_FIRST_TIME,
EN_DOCS_REVIEW,
EN_DOCS_VOCABULARY,
EN_COMPUTER_USE,
@@ -40,7 +39,6 @@ import {
getDocsPet,
getDocsAuth,
getDocsTrust,
getDocsFirstTime,
getComputerUse,
getChangelog,
getDocsShell,
@@ -287,7 +285,6 @@ describe("website dictionaries", () => {
["docs-pet", getDocsPet, EN_DOCS_PET],
["docs-auth", getDocsAuth, EN_DOCS_AUTH],
["docs-trust", getDocsTrust, EN_DOCS_TRUST],
["docs-first-time", getDocsFirstTime, EN_DOCS_FIRST_TIME],
["docs-review", getDocsReview, EN_DOCS_REVIEW],
["docs-vocabulary", getDocsVocabulary, EN_DOCS_VOCABULARY],
["changelog", getChangelog, EN_CHANGELOG],
@@ -350,7 +347,6 @@ describe("website dictionaries", () => {
["docs-subagents", getDocsSubagents],
["docs-troubleshooting", getDocsTroubleshooting],
["docs-trust", getDocsTrust],
["docs-first-time", getDocsFirstTime],
["docs-web", getDocsWeb],
["docs-work", getDocsWork],
["docs-pet", getDocsPet],
@@ -1,196 +0,0 @@
import type { DocsFirstTimeDict } from "../types";
export const docsFirstTime: DocsFirstTimeDict = {
metaTitle: "Use Codewhale for the first time · Codewhale Docs",
metaDescription:
"Open a terminal, install Codewhale, pick the folder it works in, connect a model and finish a first task, with no terminal experience needed.",
bodyClassName: "text-ink-soft leading-relaxed",
title: "Use Codewhale for the first time",
lede:
"This page takes you from opening a terminal for the first time to finishing a small task with Codewhale. It also explains which folder to start in, because that folder decides what Codewhale can change.",
sections: [
{
id: "what-you-need",
title: "Check what you need",
blocks: [
{
p: "You need a Mac, a Windows x64 PC or a Linux computer, and an internet connection for the install. To get answers you also need a model: an API key from a hosted provider such as DeepSeek, or a model running on your own computer with Ollama.",
},
{
p: "Codewhale is free and needs no account. A hosted provider bills you directly for what you use, and a model you run yourself needs no key.",
},
],
},
{
id: "open-terminal",
title: "Open a terminal",
blocks: [
{
p: "A terminal is a window where you type commands instead of clicking. A command runs when you press Return, which is Enter on Windows.",
},
{
rows: [
["macOS", "Press Cmd+Space, type Terminal and press Return."],
["Windows", "Open the Start menu, type PowerShell and press Enter."],
["Linux", "Open your applications menu and search for Terminal. On Ubuntu, Ctrl+Alt+T opens one."],
],
},
{
note: "Paste with Cmd+V on a Mac and Ctrl+V on Windows. Most Linux terminals paste with Ctrl+Shift+V.",
},
],
},
{
id: "install",
title: "Install Codewhale",
blocks: [
{
p: "On a Mac or Linux, paste this command and press Return. It downloads Codewhale, checks the download, and adds Codewhale to your PATH, the list of folders your terminal searches for commands.",
},
{ code: "curl -fsSL https://codewhale.net/install.sh | sh", lang: "macOS · Linux" },
{ p: "On Windows, paste this command and press Enter:" },
{ code: "winget install --id HunterBown.CodeWhale --exact --source winget", lang: "Windows" },
{
p: "When it finishes, open a new terminal window so it picks up the new PATH. Then check that it worked: a version number means Codewhale is installed.",
},
{ code: "codewhale --version", lang: "Terminal" },
{
p: "If the terminal says `command not found`, follow [the PATH steps on the install page](/install#install-path).",
},
],
},
{
id: "choose-folder",
title: "Choose the folder to work in",
blocks: [
{
p: "Codewhale works in the folder your terminal is in when you start it. That folder is its workspace: Codewhale reads and changes files there, and its file tools cannot reach other folders unless you allow it.",
},
{
p: "On a Mac, an operating-system sandbox also stops its commands from writing outside the workspace, apart from temporary files. Linux gets the same sandbox when bubblewrap is installed. Windows has no such sandbox yet, so Codewhale relies on asking you first.",
},
{
rows: [
["A folder for one project", "The best choice for most work. Codewhale sees only that project, and `/undo` can take back what it changed."],
["A new, empty folder", "A good start for something new, such as a website or a script. The next section shows how to make one."],
["A folder that holds several projects", "Lets one task read and change files across all of them. Pick the smallest folder that holds everything the task needs; in a very large folder Codewhale turns snapshots off and tells you."],
["Your home folder, Desktop, Documents or Downloads", "Codewhale runs there, but `/undo` and snapshots are off in these folders themselves because they are too broad to snapshot safely. A folder inside one of them works normally."],
],
},
{
note: "Snapshots are Codewhale's own copies of the files it changes. It keeps them in its settings folder, not in your project, and they work without git.",
},
{
p: "To let Codewhale reach one more folder, type `/trust add` and that folder's path in Codewhale. Full Access mode reaches every folder and runs commands without the sandbox, so use it only when you want Codewhale to work across your whole computer.",
},
{
p: "By default, Codewhale makes file edits in a git repository without asking and shows you what changed. In a folder without git, it asks before each edit.",
},
],
},
{
id: "open-in-folder",
title: "Open a terminal in that folder",
blocks: [
{
rows: [
["macOS Finder", "Control-click the folder and choose Services > New Terminal at Folder. If it is missing, turn it on in System Settings > Keyboard > Keyboard Shortcuts > Services > Files and Folders."],
["Windows File Explorer", "Right-click the folder and choose Open in Terminal. On Windows 10, hold Shift while you right-click and choose Open PowerShell window here."],
["Any terminal", "Type `cd` and a space, drag the folder from Finder or File Explorer into the window, and press Return."],
["Without moving", "Start Codewhale with `-C` and the folder's path, for example `codewhale -C ~/Projects/my-site` on a Mac."],
],
},
{ p: "These commands find your way around from inside a terminal. They work on a Mac, on Linux and in PowerShell." },
{
codeTerms: true,
rows: [
["pwd", "Shows the folder you are in."],
["ls", "Lists what is in it."],
["cd Documents", "Moves into the Documents folder inside the current one."],
["cd ..", "Moves up one folder."],
["mkdir my-site", "Makes a new folder called my-site."],
],
},
{
note: "Type the first letters of a folder name and press Tab, and the terminal finishes the name for you.",
},
],
},
{
id: "connect",
title: "Start Codewhale and connect a model",
blocks: [
{ code: "codewhale", lang: "Terminal" },
{
p: "Codewhale opens a message box right away. When you send your first message, it asks where your model runs: pick a hosted provider and paste its API key, or pick Ollama when it is running on your computer.",
},
{
p: "Keys you enter stay on your computer, in Codewhale's settings folder `~/.codewhale`. Change the provider later with `/provider`.",
},
{
p: "If Codewhale asks whether to work with the instructions and files in this folder, answer yes for a folder you made yourself.",
},
],
},
{
id: "first-task",
title: "Give it a first task",
blocks: [
{
p: "Type what you want in plain words and press Return. A safe first task changes nothing, for example: `Explain what is in this folder and what each file is for. Do not change anything.`",
},
{
p: "Then try one that makes a file, for example: `Make a file called packing-list.md with a packing list for a weekend trip.`",
},
{
p: "By default, Codewhale asks before it runs a command that is not read-only. Read what the approval says will run before you allow it.",
},
],
},
{
id: "stop-undo-quit",
title: "Stop, undo and quit",
blocks: [
{
codeTerms: true,
rows: [
["Esc", "Stops the task that is running. Ctrl+C does the same."],
["/undo", "Takes back your last request and the file changes it made."],
["/diff", "Shows what changed in this folder."],
["/receipts", "Lists what Codewhale did: files, commands and approvals."],
["/exit", "Quits Codewhale. Ctrl+D also quits when the message box is empty."],
["codewhale --continue", "Run it in the same folder to pick up your last conversation there."],
],
},
],
},
{
id: "help",
title: "Get help",
blocks: [
{
p: "Type `/help` in Codewhale to list its commands. [Ask in the Codewhale Discord](https://discord.gg/37gfS3ksug), or read [what to do when something goes wrong](/docs/troubleshooting).",
},
],
},
],
next: [
{
href: "/docs/guide",
label: "Start your first task",
note: "More first prompts and how to check the answer.",
},
{
href: "/docs/auth",
label: "Connect a provider",
note: "Save a key, check which key is used, or run a local model.",
},
{
href: "/docs/modes",
label: "Set modes and approvals",
note: "Decide which commands and edits stop for your approval.",
},
],
sourceNote:
"Source documents: docs/GUIDE.md, docs/INSTALL.md, docs/MODES.md, docs/SANDBOX.md, docs/KEYBINDINGS.md · Update docs-map.ts when changing.",
};
-12
View File
@@ -37,7 +37,6 @@ import type {
DocsSubagentsDict,
DocsTroubleshootingDict,
DocsTrustDict,
DocsFirstTimeDict,
DocsWebDict,
DocsWorkDict,
DocsPetDict,
@@ -101,8 +100,6 @@ import { docsAuth as enDocsAuth } from "./en/docs-auth";
import { docsAuth as zhDocsAuth } from "./zh/docs-auth";
import { docsTrust as enDocsTrust } from "./en/docs-trust";
import { docsTrust as zhDocsTrust } from "./zh/docs-trust";
import { docsFirstTime as enDocsFirstTime } from "./en/docs-first-time";
import { docsFirstTime as zhDocsFirstTime } from "./zh/docs-first-time";
import { docsReview as enDocsReview } from "./en/docs-review";
import { docsReview as zhDocsReview } from "./zh/docs-review";
import { docsVocabulary as enDocsVocabulary } from "./en/docs-vocabulary";
@@ -340,10 +337,6 @@ const DOCS_TRUST: Record<string, DocsTrustDict> = {
zh: zhDocsTrust,
};
const DOCS_FIRST_TIME: Record<string, DocsFirstTimeDict> = {
zh: zhDocsFirstTime,
};
const DOCS_REVIEW: Record<string, DocsReviewDict> = {
zh: zhDocsReview,
};
@@ -499,10 +492,6 @@ export function getDocsTrust(locale: string): DocsTrustDict {
return DOCS_TRUST[locale] ?? enDocsTrust;
}
export function getDocsFirstTime(locale: string): DocsFirstTimeDict {
return DOCS_FIRST_TIME[locale] ?? enDocsFirstTime;
}
export function getDocsReview(locale: string): DocsReviewDict {
return DOCS_REVIEW[locale] ?? enDocsReview;
}
@@ -585,7 +574,6 @@ export const EN_DOCS_WORK = enDocsWork;
export const EN_DOCS_PET = enDocsPet;
export const EN_DOCS_AUTH = enDocsAuth;
export const EN_DOCS_TRUST = enDocsTrust;
export const EN_DOCS_FIRST_TIME = enDocsFirstTime;
export const EN_DOCS_REVIEW = enDocsReview;
export const EN_DOCS_VOCABULARY = enDocsVocabulary;
export const EN_COMPUTER_USE = enComputerUse;
-2
View File
@@ -442,8 +442,6 @@ export type DocsAuthDict = DocsPageDict;
export type DocsTrustDict = DocsPageDict;
export type DocsFirstTimeDict = DocsPageDict;
/** `app/[locale]/changelog/page.tsx` — version-aware release record. */
export interface ChangelogDict {
metaTitle: string;
@@ -1,196 +0,0 @@
import type { DocsFirstTimeDict } from "../types";
export const docsFirstTime: DocsFirstTimeDict = {
metaTitle: "第一次使用 Codewhale · Codewhale 文档",
metaDescription:
"打开终端、安装 Codewhale、选择它工作的文件夹、连接模型并完成第一个任务,不需要任何终端经验。",
bodyClassName: "text-ink-soft leading-[1.9] tracking-wide",
title: "第一次使用 Codewhale",
lede:
"这一页带你从第一次打开终端开始,一直到用 Codewhale 完成一个小任务。它还会说明应该从哪个文件夹启动,因为这个文件夹决定了 Codewhale 能修改什么。",
sections: [
{
id: "what-you-need",
title: "确认你需要准备什么",
blocks: [
{
p: "你需要一台 Mac、一台 Windows x64 电脑或一台 Linux 电脑,安装时还需要联网。要得到回答,你还需要一个模型:来自 DeepSeek 等托管提供商的 API 密钥,或者用 Ollama 在你自己电脑上运行的模型。",
},
{
p: "Codewhale 免费,也不需要账号。托管提供商会按你的用量直接向你收费;你自己运行的模型不需要密钥。",
},
],
},
{
id: "open-terminal",
title: "打开终端",
blocks: [
{
p: "终端是一个用输入命令代替点击的窗口。按下回车键后,命令就会运行。",
},
{
rows: [
["macOS", "按 Cmd+空格键,输入 Terminal,然后按回车键。"],
["Windows", "打开“开始”菜单,输入 PowerShell,然后按回车键。"],
["Linux", "打开应用程序菜单并搜索“终端”。在 Ubuntu 上,按 Ctrl+Alt+T 也能打开。"],
],
},
{
note: "在 Mac 上用 Cmd+V 粘贴,在 Windows 上用 Ctrl+V 粘贴。大多数 Linux 终端用 Ctrl+Shift+V 粘贴。",
},
],
},
{
id: "install",
title: "安装 Codewhale",
blocks: [
{
p: "在 Mac 或 Linux 上,粘贴下面这条命令并按回车键。它会下载 Codewhale、校验下载内容,并把 Codewhale 加入 PATH,也就是终端查找命令时会搜索的文件夹列表。",
},
{ code: "curl -fsSL https://codewhale.net/install.sh | sh", lang: "macOS · Linux" },
{ p: "在 Windows 上,粘贴下面这条命令并按回车键:" },
{ code: "winget install --id HunterBown.CodeWhale --exact --source winget", lang: "Windows" },
{
p: "安装完成后,打开一个新的终端窗口,让它读取新的 PATH。然后确认安装成功:显示版本号就说明 Codewhale 已经安装好了。",
},
{ code: "codewhale --version", lang: "终端" },
{
p: "如果终端提示 `command not found`,请按照[安装页面上的 PATH 步骤](/install#install-path)操作。",
},
],
},
{
id: "choose-folder",
title: "选择工作的文件夹",
blocks: [
{
p: "Codewhale 在你启动它时终端所在的文件夹里工作。这个文件夹就是它的工作区:Codewhale 在这里读取和修改文件,除非你允许,它的文件工具无法访问其他文件夹。",
},
{
p: "在 Mac 上,操作系统沙箱还会阻止它的命令写入工作区以外的位置(临时文件除外)。在 Linux 上,安装 bubblewrap 后也有同样的沙箱。Windows 目前还没有这种沙箱,因此 Codewhale 依靠先征求你的同意。",
},
{
rows: [
["只放一个项目的文件夹", "大多数情况下的最佳选择。Codewhale 只看到这个项目,`/undo` 可以撤回它做的修改。"],
["一个新的空文件夹", "适合开始做新东西,比如一个网站或一个脚本。下一节会说明如何创建。"],
["包含多个项目的文件夹", "让一个任务可以读取和修改所有这些项目里的文件。请选择能装下任务所需全部内容的最小文件夹;文件夹非常大时,Codewhale 会关闭快照并告诉你。"],
["你的主文件夹、桌面、文稿或下载", "Codewhale 可以在这里运行,但在这些文件夹本身中 `/undo` 和快照是关闭的,因为它们范围太大,无法安全地做快照。它们里面的子文件夹可以正常使用。"],
],
},
{
note: "快照是 Codewhale 为它修改的文件保存的副本。它们保存在 Codewhale 的设置文件夹里,而不是你的项目里,并且不需要 git。",
},
{
p: "要让 Codewhale 多访问一个文件夹,请在 Codewhale 中输入 `/trust add` 和该文件夹的路径。Full Access 模式可以访问所有文件夹,并且在没有沙箱的情况下运行命令,所以只有在你希望 Codewhale 在整台电脑范围内工作时才使用它。",
},
{
p: "默认情况下,在 git 仓库中 Codewhale 会直接修改文件,并向你展示改了什么。在没有 git 的文件夹中,它会在每次修改前询问你。",
},
],
},
{
id: "open-in-folder",
title: "在那个文件夹中打开终端",
blocks: [
{
rows: [
["macOS 访达", "按住 Control 点按该文件夹,选择“服务”>“新建位于文件夹位置的终端窗口”。如果没有这一项,请在“系统设置”>“键盘”>“键盘快捷键”>“服务”>“文件和文件夹”中打开它。"],
["Windows 文件资源管理器", "右键点击该文件夹,选择“在终端中打开”。在 Windows 10 上,按住 Shift 再右键点击,选择“在此处打开 PowerShell 窗口”。"],
["任意终端", "输入 `cd` 和一个空格,把文件夹从访达或文件资源管理器拖进窗口,然后按回车键。"],
["不切换文件夹", "启动 Codewhale 时加上 `-C` 和文件夹路径,例如在 Mac 上输入 `codewhale -C ~/Projects/my-site`。"],
],
},
{ p: "下面这些命令可以在终端里切换和查看文件夹。它们在 Mac、Linux 和 PowerShell 中都能用。" },
{
codeTerms: true,
rows: [
["pwd", "显示你当前所在的文件夹。"],
["ls", "列出其中的内容。"],
["cd Documents", "进入当前文件夹里的 Documents 文件夹。"],
["cd ..", "回到上一级文件夹。"],
["mkdir my-site", "新建一个名为 my-site 的文件夹。"],
],
},
{
note: "输入文件夹名的前几个字母后按 Tab 键,终端会帮你补全名字。",
},
],
},
{
id: "connect",
title: "启动 Codewhale 并连接模型",
blocks: [
{ code: "codewhale", lang: "终端" },
{
p: "Codewhale 会立即打开一个输入框。你发送第一条消息时,它会询问模型在哪里运行:选择一个托管提供商并粘贴它的 API 密钥,或者在你的电脑上运行着 Ollama 时选择 Ollama。",
},
{
p: "你输入的密钥保存在你自己的电脑上,位于 Codewhale 的设置文件夹 `~/.codewhale` 中。之后可以用 `/provider` 更换提供商。",
},
{
p: "如果 Codewhale 询问是否使用这个文件夹中的说明和文件,对于你自己创建的文件夹,回答“是”即可。",
},
],
},
{
id: "first-task",
title: "交给它第一个任务",
blocks: [
{
p: "用平常的话写下你想做的事,然后按回车键。安全的第一个任务不修改任何东西,例如:`说明这个文件夹里有什么,以及每个文件的用途。不要修改任何内容。`",
},
{
p: "然后试一个会创建文件的任务,例如:`创建一个名为 packing-list.md 的文件,写一份周末出游的行李清单。`",
},
{
p: "默认情况下,Codewhale 在运行非只读命令前会先询问你。在允许之前,请先看清审批里写明要运行什么。",
},
],
},
{
id: "stop-undo-quit",
title: "停止、撤回和退出",
blocks: [
{
codeTerms: true,
rows: [
["Esc", "停止正在运行的任务。Ctrl+C 的作用相同。"],
["/undo", "撤回你的上一个请求及其造成的文件修改。"],
["/diff", "显示这个文件夹中改了什么。"],
["/receipts", "列出 Codewhale 做过的事:文件、命令和审批。"],
["/exit", "退出 Codewhale。输入框为空时,按 Ctrl+D 也会退出。"],
["codewhale --continue", "在同一个文件夹中运行它,可以继续你在那里的上一次对话。"],
],
},
],
},
{
id: "help",
title: "获取帮助",
blocks: [
{
p: "在 Codewhale 中输入 `/help` 可以列出它的命令。你也可以[在 Codewhale Discord 中提问](https://discord.gg/37gfS3ksug),或阅读[出现问题时怎么办](/docs/troubleshooting)。",
},
],
},
],
next: [
{
href: "/docs/guide",
label: "开始第一个任务",
note: "更多第一个提示词的示例,以及如何检查回答。",
},
{
href: "/docs/auth",
label: "连接模型提供商",
note: "保存密钥、查看当前使用的密钥,或运行本地模型。",
},
{
href: "/docs/modes",
label: "设置模式与审批",
note: "决定哪些命令和修改需要你批准。",
},
],
sourceNote:
"来源文档:docs/GUIDE.md、docs/INSTALL.md、docs/MODES.md、docs/SANDBOX.md、docs/KEYBINDINGS.md · 修改时请同步更新 docs-map.ts。",
};
-1
View File
@@ -16,7 +16,6 @@ export const DOCS_TOPICS = [
"computers",
"configuration",
"constitution",
"first-time",
"fleet",
"guide",
"hooks",
-17
View File
@@ -96,23 +96,6 @@ function installLines(facts: RepoFacts): string[] {
"",
...link(I.scriptLink, `${SITE_URL}/install.sh`),
...(version ? p(I.releaseLine) : []),
...h(I.newTitle),
...p(I.newLead),
...h(I.newMacTitle, 3),
`1. ${t(I.newMac1)}`,
`2. ${t(I.newMac2)}`,
"",
...code(INSTALL_COMMANDS.shell),
`3. ${t(I.newMac3)}`,
"",
...h(I.newWinTitle, 3),
`1. ${t(I.newWin1)}`,
`2. ${t(I.newWin2)}`,
"",
...code(INSTALL_COMMANDS.windows),
`3. ${t(I.newWin3)}`,
"",
...link(I.newNext, `${SITE_URL}/en/docs/first-time`),
...h(I.pathTitle),
...p(I.pathPlaces),
...bullets([I.pathZsh, I.pathBashLinux, I.pathBashMac, I.pathOther, I.pathFish]),
+1 -1
View File
@@ -42,7 +42,7 @@ export const RELEASE_CONTRIBUTORS: string[] = [
* one. scripts/check-contributor-credit.py requires them here now; the release
* cut moves them into RELEASE_CONTRIBUTORS with that release's changelog block.
*/
export const UNRELEASED_CONTRIBUTORS: string[] = ["@luochen211", "@Lstarsky0", "@SamhandsomeLee", "@gaord", "@MannXo"];
export const UNRELEASED_CONTRIBUTORS: string[] = ["@luochen211", "@Lstarsky0", "@SamhandsomeLee", "@gaord"];
/**
* Contributors who helped with reports, reproductions, and verification.
-1
View File
@@ -49,7 +49,6 @@ const STEMS = {
"docs-web": { exportName: "docsWeb", typeName: "DocsWebDict" },
"docs-auth": { exportName: "docsAuth", typeName: "DocsAuthDict" },
"docs-trust": { exportName: "docsTrust", typeName: "DocsTrustDict" },
"docs-first-time": { exportName: "docsFirstTime", typeName: "DocsFirstTimeDict" },
states: { exportName: "states", typeName: "StatesDict" },
changelog: { exportName: "changelog", typeName: "ChangelogDict" },
"computer-use": { exportName: "computerUse", typeName: "ComputerUseDict" },