mirror of
https://github.com/codewhale-hq/Codewhale.git
synced 2026-11-11 23:56:38 -10:00
Compare commits
24
Commits
No files matched your search
@@ -1,2 +1,8 @@
|
||||
# Default owner for everything in the repo.
|
||||
* @Hmbown
|
||||
|
||||
# AI code review is advisory and not wired through CODEOWNERS: GitHub CODEOWNERS
|
||||
# only accepts users and teams, not bots. @Hmbown stays the human code owner.
|
||||
# - Claude: .github/workflows/claude-review.yml (GitHub Actions).
|
||||
# - Codex/ChatGPT: the ChatGPT Codex cloud integration (chatgpt.com/codex ->
|
||||
# connect GitHub -> enable Code review), authed by the ChatGPT subscription.
|
||||
@@ -1,19 +0,0 @@
|
||||
# Getting help with Codewhale
|
||||
|
||||
- **A question about using Codewhale:** ask in
|
||||
[Discussions Q&A](https://github.com/codewhale-hq/Codewhale/discussions/categories/q-a)
|
||||
or on [Discord](https://discord.gg/37gfS3ksug). The
|
||||
[user guide](../docs/GUIDE.md) and the [documentation index](../docs/README.md)
|
||||
answer many questions already.
|
||||
- **A bug, a crash or behavior that surprised you:** open a
|
||||
[bug report](https://github.com/codewhale-hq/Codewhale/issues/new?template=bug_report.yml).
|
||||
`codewhale doctor` prints the version and setup details a report needs.
|
||||
- **An idea or feedback:** open an
|
||||
[issue](https://github.com/codewhale-hq/Codewhale/issues/new/choose), or talk it
|
||||
through first in
|
||||
[Ideas](https://github.com/codewhale-hq/Codewhale/discussions/categories/ideas).
|
||||
- **A security problem:** report it privately as described in the
|
||||
[security policy](SECURITY.md), never in a public issue.
|
||||
|
||||
The [contributing guide](../CONTRIBUTING.md) explains where each kind of help
|
||||
goes and how a pull request lands.
|
||||
@@ -76,8 +76,9 @@ jobs:
|
||||
- name: Audit website with verified build dependency mitigation
|
||||
if: matrix.name == 'web'
|
||||
run: npm run audit:dependencies -- --report "${{ runner.temp }}/npm-audit.json"
|
||||
# The VS Code extension's packager dependency override is recorded in
|
||||
# docs/dependency-maintenance.md; any reported vulnerability fails the job.
|
||||
# The VS Code extension's packager toolchain carries one unfixed high
|
||||
# advisory that never runs for users (DEPENDENCY_SECURITY.md at the repo
|
||||
# root). It is admitted by name; anything else fails the job.
|
||||
- name: Audit extension with reviewed packager advisory acceptance
|
||||
if: matrix.name == 'vscode'
|
||||
run: npm run audit:dependencies -- --report "${{ runner.temp }}/npm-audit.json"
|
||||
|
||||
+11
-15
@@ -7,7 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
- **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.
|
||||
- **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.
|
||||
- **Paused stream:** A paused stream now shows the thinking it has already received instead of waiting for the next token.
|
||||
- **Sign-in notice:** The "Account not loaded" notice now appears once per sign-in in the footer, and Esc dismisses it; it no longer lands in the transcript.
|
||||
- **Usage reporting:** Installers now run their closing diagnostic with usage reporting and update checks off and print how to turn usage reporting off. `codewhale doctor` now says a saved opt-out comes from the config file, not the environment, and help and English notices call the feature "usage reporting".
|
||||
- **Long tool names:** Tools whose encoded name is longer than the route's limit are now left out with a visible warning instead of failing every request with a 400. The limit is 64 characters on OpenAI and on other OpenAI-compatible providers that document no larger limit, and 128 on Anthropic-style routes, DeepSeek, Ollama, Ollama Cloud, vLLM, SGLang and custom routes. Forcing such a tool gives an error that names the tool and the limit, and a session that already called one on a route with a documented limit (Anthropic, OpenAI, Codex, DeepSeek) is refused with a message saying to switch route or start a new session. A forced tool_choice in any chat form now names the encoded tool, and a tool whose real name looks like an encoded escape can no longer be mistaken for another tool.
|
||||
- **Start after a hard kill:** The Engine now also starts after a hard kill when its home was spelled through a symbolic link (for example under /tmp on macOS) or, on macOS and Windows, in a different letter case: a dead owner's record for the deleted data directory is discarded instead of refusing every start.
|
||||
- **Context window labels:** the model picker and fleet capability badges now show `1.5M` and `2.1M` for 1,500,000- and 2,100,000-token windows instead of `1.50M` and `2.10M`. Thanks [@Aboysky](https://github.com/Aboysky) (PR 7295).
|
||||
- **REPL output limit:** REPL output is now cut to its byte limit, so Chinese, Japanese, Korean or emoji output no longer comes back up to three times longer than the limit, and the cut never splits a character. Thanks [@Aboysky](https://github.com/Aboysky) (PR 7296).
|
||||
- **Git status with renamed files:** the Git and Changes views no longer fail with "git status failed" or show a phantom row when a file is renamed on the worktree side (for example a moved file marked with `git add -N`), including when the original name starts with non-ASCII text such as Chinese. Thanks [@luochen211](https://github.com/luochen211) (PR 7310).
|
||||
@@ -22,7 +30,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- **Simplified Chinese punctuation and /status:** in the zh-Hans interface, the `/router` setup and test screens, label prompts and usage lines now use full-width punctuation (`:;,()`) instead of ASCII marks next to Chinese text, and `/status` values start in one column under Chinese labels instead of starting at a different column for each label. Thanks [@luochen211](https://github.com/luochen211) (PR 7314).
|
||||
- **Git tool on older git:** the Git tool's `review_scope` action no longer passes `--no-lazy-fetch`, which git older than 2.44 rejects, so it failed on every call there. It sets `GIT_NO_LAZY_FETCH=1` instead, which keeps the same protection on newer git and is ignored by older git.
|
||||
- **Plugins from a shell:** `codewhale plugins trust <name>` prints the same capability review as `/plugin trust` with its bundle review token and, in a terminal, asks once. `--review-token <token> --yes` trusts without a prompt while that token still matches the installed content and capabilities; the token ties trust to the exact content, does not show that anyone read the review, and is not the host-service review token. A `--yes` trust is recorded as the command line and grants no host service. Every command the agent runs (shell, persistent terminal, verifier and test runs, the Python kernel, MCP servers it starts) now carries `CODEWHALE_AGENT_PROJECT` with the session's project, while hooks and configured MCP servers do not; with it, a trust or enable is recorded as the agent's, for that project only, even when a prompt is answered through a terminal, and the agent may trust only a plugin it installed from a folder inside the project. `plugins enable`, `disable` and `revoke` work the same way, each with `--json`, and `plugins status` shows who approved. The agent's shell commands that run them go through the session's approval like any other shell command, so a Full Access agent can trust and enable a plugin it wrote; the mark is a label the agent could remove in Full Access. (CW-7891)
|
||||
- **Sandbox:** a managed configuration can set `tool_launcher` (Linux) to run every sandboxed tool command as another user, in front of bubblewrap; commands then run inside the sandbox or not at all, masks are computed for that user, `/tmp` is private per command, and `GET /v1/runtime/info` reports `sandbox.kind` and `sandbox.tool_launcher`. Without the key nothing changes. A managed configuration named by `CODEWHALE_MANAGED_CONFIG_PATH` is now applied on every load; a configuration whose provider comes from a plugin no longer loads with the managed file skipped, it fails to load instead.
|
||||
- **Sandbox:** a managed configuration can set `tool_launcher` (Linux) to run every sandboxed tool command as another user, in front of bubblewrap; commands then run inside the sandbox or not at all, masks are computed for that user, `/tmp` is private per command, and `GET /v1/runtime/info` reports `sandbox.kind` and `sandbox.tool_launcher`. Without the key nothing changes. A managed configuration named by `CODEWHALE_MANAGED_CONFIG_PATH` or `managed_config_path` is applied on every load, and one that is missing or cannot be read stops the load instead of loading with no managed layer; a blank `CODEWHALE_MANAGED_CONFIG_PATH` is now ignored instead of switching the managed layer off, and a config file's `managed_config_path` can no longer point away from an existing file at the default managed path. Fleet local workers inherit the managed-config variable, and `codewhale serve --mcp` does not expose shell tools when the managed configuration it loads sets `tool_launcher`. A configuration whose provider is declared only by a plugin fails to load under a managed configuration, with an error that says to declare the provider table or pick a built-in provider.
|
||||
- **Receipts:** a terminal session's receipt (`/receipts`, `codewhale receipts`) now shows the provider's token counts and the same cost report `/cost` prints, saying "unknown" and why when the route's price is not known, never a guess. A Runtime thread's receipt shows token counts only; its cost line follows in a later release. `codewhale metrics` now counts usage saved in terminal sessions instead of reporting none. Sessions saved by 0.10.2 carry only a combined token total.
|
||||
- **ACP server on the official SDK:** The ACP server (`codewhale serve --acp`) now runs on the official Agent Client Protocol SDK. Its messages follow the official v1 types, and it no longer advertises the non-standard `modelSelection` capability or session `models` field. Session list entries carry `createdAt` and `messageCount` under `_meta.codewhale`, terminal sign-in methods are advertised only to clients that enabled terminal authentication, a notification is never answered, a permission request always carries text, and session list and configuration requests are served while a turn runs, response ids are echoed unchanged for every client (the Zed-only string rewrite is gone), and `initialize` without `protocolVersion` is refused with `-32602`.
|
||||
- **External agents over ACP:** Codewhale now drives external agents through the official Agent Client Protocol SDK. Request ids on the wire are strings, and an agent's error is treated as a sign-in problem only when it carries the protocol's auth-required code.
|
||||
@@ -660,7 +668,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.
|
||||
@@ -697,12 +705,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.
|
||||
@@ -731,12 +733,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.
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
# Extension packager dependency security
|
||||
|
||||
The VS Code extension's packaging toolchain uses `@vscode/vsce` →
|
||||
`@azure/identity` → `@azure/msal-node`. MSAL Node versions before 7.1.0 are
|
||||
affected by [GHSA-xcx6-jx8p-x3h5](https://github.com/AzureAD/microsoft-authentication-library-for-js/security/advisories/GHSA-xcx6-jx8p-x3h5).
|
||||
The extension manifest overrides Azure Identity's MSAL Node dependency to
|
||||
`^7.1.2`; its lockfile resolves 7.1.2. Keep the override until Azure Identity
|
||||
requires a patched version itself.
|
||||
|
||||
`npm run audit:dependencies -- --report <path>` audits the complete lockfile
|
||||
and keeps the raw report. Every reported vulnerability blocks the check.
|
||||
Incomplete reports, count mismatches and critical findings also fail. There
|
||||
is no advisory exception.
|
||||
|
||||
After changing this dependency, materialize the lockfile with `npm ci`, run
|
||||
the extension's existing `npm test` checks and build a local package with
|
||||
`npm run package`. Packaging uses `vsce package --no-dependencies`, which
|
||||
excludes development dependencies from the VSIX. A local package build does
|
||||
not establish that a publishing authentication flow works; verify that flow
|
||||
only when publication is separately authorized.
|
||||
@@ -1,25 +1,21 @@
|
||||
<div align="center">
|
||||
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="brand/mark-reversed.svg">
|
||||
<img src="brand/mark-mono.svg" alt="Codewhale" width="72" height="72">
|
||||
<source media="(prefers-color-scheme: dark)" srcset="brand/wordmark-inverted.svg">
|
||||
<img src="brand/wordmark.svg" alt="Codewhale" width="320">
|
||||
</picture>
|
||||
|
||||
<h1>The model can be anyone's. The agent should be yours.</h1>
|
||||
**An open-source coding agent for your terminal that works with any model.**
|
||||
|
||||
Codewhale is an open-source coding agent for your terminal, written in Rust.
|
||||
It works with any model, can split one task across several agents, and every
|
||||
line of it is yours to read.
|
||||
Give Codewhale a task and it reads the repository, edits files, and runs
|
||||
commands until the task is done or it needs input from you. It works with more
|
||||
than 40 built-in providers, DeepSeek V4.1 Flash by default, and with models running on
|
||||
your own machine. `/receipts` lists the files it changed, the commands it ran,
|
||||
and the web and MCP calls it made.
|
||||
|
||||
```bash
|
||||
curl -fsSL https://codewhale.net/install.sh | sh
|
||||
```
|
||||
|
||||
This installs the latest release on macOS or Linux, and it needs no account.
|
||||
[Windows, npm and Cargo](#install) are below.
|
||||
|
||||
[](https://github.com/codewhale-hq/Codewhale/releases/latest)
|
||||
[](https://github.com/codewhale-hq/Codewhale/actions/workflows/ci.yml)
|
||||
[](https://github.com/codewhale-hq/CodeWhale/actions/workflows/ci.yml)
|
||||
[](https://crates.io/crates/codewhale-cli)
|
||||
[](https://www.npmjs.com/package/codewhale)
|
||||
[](LICENSE)
|
||||
[](https://discord.gg/37gfS3ksug)
|
||||
|
||||
@@ -27,41 +23,63 @@ This installs the latest release on macOS or Linux, and it needs no account.
|
||||
|
||||
[简体中文](docs/i18n/README.zh-CN.md) · [日本語](docs/i18n/README.ja-JP.md) · [Tiếng Việt](docs/i18n/README.vi.md) · [Bahasa Indonesia](docs/i18n/README.id.md) · [한국어](docs/i18n/README.ko-KR.md) · [Español](docs/i18n/README.es-419.md) · [Português](docs/i18n/README.pt-BR.md) · [Русский](docs/i18n/README.ru.md) · [Українська](docs/i18n/README.uk.md) · [Français](docs/i18n/README.fr.md) · [Deutsch](docs/i18n/README.de.md) · [繁體中文](docs/i18n/README.zh-TW.md) · [हिन्दी](docs/i18n/README.hi.md) · [Türkçe](docs/i18n/README.tr.md) · [Italiano](docs/i18n/README.it.md) · [Polski](docs/i18n/README.pl.md) · [العربية](docs/i18n/README.ar.md) · [Català](docs/i18n/README.ca.md)
|
||||
|
||||
<img src="web/public/codewhale-tui-0102-demo.png" width="760" alt="Codewhale in a terminal after a demo task. It reports that it added a --json flag to export.py, that all 3 local tests passed and that a delegated review found no issues. The task list below shows three completed steps, and the status line shows Full Access and the local website-demo provider.">
|
||||
<img src="web/public/codewhale-tui-0102-demo.png" alt="A Codewhale terminal session" width="760">
|
||||
|
||||
<sub>A development build of 0.10.3, which is not released yet, with a scripted test model, in Full Access inside a throwaway workspace. The edit, three tests and a sub-agent review ran for real.</sub>
|
||||
<sub>A 0.10.2 development build with a scripted test model, in Full Access inside a throwaway workspace. The edit, three tests and a sub-agent review ran for real.</sub>
|
||||
|
||||
</div>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://www.orcarouter.ai/redeem/1a6250545fdc4e0aaccb0a4470159e67" rel="sponsored">
|
||||
<img src="web/public/codewhale-x-orcarouter.webp" alt="Codewhale × OrcaRouter — +30% DeepSeek v4.1 Flash usage" width="760">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
[View the Codewhale × OrcaRouter offer](https://www.orcarouter.ai/redeem/1a6250545fdc4e0aaccb0a4470159e67). Codewhale earns a referral fee from OrcaRouter.
|
||||
|
||||
## Install
|
||||
|
||||
The command above puts checksum-verified `codewhale` and its short alias
|
||||
`codew` in `~/.local/bin`. If your shell then says `command not found`,
|
||||
follow the PATH note the installer prints
|
||||
([details](docs/INSTALL.md#put-it-on-your-path)). `codewhale update` upgrades
|
||||
it.
|
||||
macOS and Linux:
|
||||
|
||||
```bash
|
||||
winget install --id HunterBown.CodeWhale --exact --source winget # Windows x64
|
||||
npm install -g codewhale # wraps the same release binaries
|
||||
cargo install codewhale-cli --locked # builds from source; can trail GitHub Releases
|
||||
curl -fsSL https://codewhale.net/install.sh | sh
|
||||
```
|
||||
|
||||
Prebuilt binaries cover Linux, macOS and Windows on x64 and arm64. Scoop, Nix,
|
||||
Homebrew on Linux, Android and Termux (preview) and manual downloads are in the
|
||||
[installation guide](docs/INSTALL.md), and containers are in
|
||||
[Docker](docs/DOCKER.md). Use one install method per machine; otherwise two
|
||||
copies end up on your `PATH` and whichever comes first wins.
|
||||
The installer puts checksum-verified `codewhale` and its short alias `codew`
|
||||
in `~/.local/bin`. If your shell then says `command not found`, run the PATH
|
||||
line the installer prints ([details](docs/INSTALL.md#put-it-on-your-path)).
|
||||
`codewhale update` upgrades it. These commands install the latest release.
|
||||
This page follows `main`, so a feature marked with a version arrives in that
|
||||
release; the [changelog](CHANGELOG.md) lists what each release adds.
|
||||
|
||||
<details>
|
||||
<summary><b>Windows, npm, Cargo, and other ways to install</b></summary>
|
||||
|
||||
```bash
|
||||
winget install HunterBown.CodeWhale # Windows x64 (or Scoop, or the installer from GitHub Releases)
|
||||
npm install -g codewhale # wraps the same release binaries
|
||||
cargo install codewhale-cli --locked # crates.io; can trail GitHub Releases
|
||||
```
|
||||
|
||||
Prebuilt binaries cover Linux, macOS, and Windows on x64 and arm64. Nix,
|
||||
Homebrew on Linux, Android/Termux (preview), and manual downloads are in the
|
||||
[installation guide](docs/INSTALL.md), and containers in
|
||||
[Docker](docs/DOCKER.md). Stick to one install method per machine; otherwise
|
||||
two copies end up on `PATH` and whichever comes first wins.
|
||||
|
||||
</details>
|
||||
|
||||
## Quickstart
|
||||
|
||||
1. Commit or stash your work, then run `codewhale` from the root of a git
|
||||
repository. By default it applies file edits there without asking and asks
|
||||
before each shell command that is not read-only.
|
||||
2. Run `/provider` (or press `F3`), pick a provider, paste its API key and
|
||||
choose a model. DeepSeek is the default provider. On a first launch with no
|
||||
key, Codewhale asks for a provider when you send your first message, and it
|
||||
offers a local model when a local Ollama server answers.
|
||||
repository. By default (**Accept Edits**) it edits files in the repository
|
||||
without asking and asks before each command; `Shift+Tab` steps the
|
||||
permission dial when you want it to ask more or less.
|
||||
2. Run `/provider` (or press `F3`), pick a provider, paste its API key, and
|
||||
choose a model. DeepSeek is the default provider. With no key, pick a local
|
||||
runtime instead. If no provider is configured and Ollama is already serving
|
||||
a chat model, Codewhale uses it automatically; from 0.10.2 it offers it when
|
||||
you send your first message.
|
||||
3. Type a task:
|
||||
|
||||
```text
|
||||
@@ -70,161 +88,94 @@ Fix the failing tests and explain what changed.
|
||||
|
||||
Read-only commands run straight away. For any other shell command Codewhale
|
||||
asks first: `y` runs it once, `a` allows commands like it for the rest of the
|
||||
conversation, and `n` refuses. `/undo` takes back your last request and its
|
||||
file changes, and `/help` lists commands and keys.
|
||||
conversation, `n` refuses. `/undo` takes back your last request and its file
|
||||
changes. `/help` lists commands and keys.
|
||||
|
||||
For scripts and CI, `codewhale exec` runs one task without the interface. Read
|
||||
[headless runs](docs/INSTALL.md#headless-scripts-ci) before you give it tools
|
||||
with `--auto`, because `--auto` uses Full Access unless you add a sandbox.
|
||||
For scripts and CI, `codewhale exec` runs one task without the interface. By
|
||||
default, `--auto` uses Full Access: tools run without asking, shell commands
|
||||
have no Codewhale sandbox, and trust mode lets file tools write outside the
|
||||
workspace. Add `--sandbox workspace-write` to keep the shell sandbox and
|
||||
file-tool workspace restrictions, with trust mode off:
|
||||
|
||||
```bash
|
||||
codewhale exec --auto --sandbox workspace-write "fix the failing tests and explain what changed"
|
||||
```
|
||||
|
||||
## Modes and permissions
|
||||
|
||||
A fresh install starts in Work mode with the Accept Edits permission. One dial
|
||||
switches both: `Shift+Tab` steps Ask → Accept Edits → Plan → Auto-Review →
|
||||
Full Access.
|
||||
|
||||
| | Switch with | Options |
|
||||
| --- | --- | --- |
|
||||
| Mode | the Plan stop on `Shift+Tab`, or `/mode` | **Plan** reads and searches but changes nothing. **Work** (every other stop) edits files and runs commands; start a goal with `/goal` when a request needs more than one turn. |
|
||||
| Permission | `Shift+Tab` | **Ask** asks before every file edit and every shell command that is not read-only. **Accept Edits** applies file edits in your git repository without asking and still asks before commands. **Auto-Review** lets rules and a reviewer model approve routine calls and refuses high-risk ones; switch to Ask to approve those yourself. **Full Access** never asks, and commands can reach anything on this computer; repository rules and managed policy still apply. |
|
||||
|
||||
See the [modes and permissions guide](docs/MODES.md) for the details.
|
||||
|
||||
## What it does
|
||||
|
||||
Give Codewhale a task and it reads the repository, edits files and runs
|
||||
commands until the task is done or it needs your input. The
|
||||
[latest release notes](https://github.com/codewhale-hq/Codewhale/releases/latest)
|
||||
list the newest changes, and the [changelog](CHANGELOG.md) has every
|
||||
version.
|
||||
### New in 0.10.2
|
||||
|
||||
### Any model
|
||||
- `/pet on` turns your terminal into an animated whale that shows what the
|
||||
agent is doing, while the message box stays live. Its 980 particles spell
|
||||
the current step in English, Chinese or Japanese.
|
||||
- `/workbar terminal` shows the shells the agent is running as they run. Type
|
||||
into one, or press `Ctrl+N` for a shell of your own (macOS and Linux).
|
||||
- `/undo` takes back your whole last request in one step, including every file
|
||||
it changed, and `/diff` works in folders without Git.
|
||||
- When a plan is ready, Codewhale asks whether to start Work with Ask or
|
||||
Auto-Review, or keep planning.
|
||||
- `/color` gives each chat its own composer color, saved with the session.
|
||||
- `web_search` can use DeepSeek's new Search API: set
|
||||
`[search] provider = "deepseek"` with a DeepSeek key.
|
||||
- Outside Full Access, a `curl` or `wget` that would upload a credential never
|
||||
runs without your approval, and a command that hides invisible Unicode is
|
||||
refused. `write` no longer replaces a file the agent has not read, or one
|
||||
that changed after it read it.
|
||||
|
||||
### Models
|
||||
|
||||
More than 40 providers are built in, including Anthropic, OpenAI, DeepSeek,
|
||||
Google, Mistral, Moonshot, OpenRouter and xAI. Any OpenAI-compatible endpoint
|
||||
works, and so do local models through Ollama, vLLM or SGLang. The default
|
||||
model is DeepSeek's `deepseek-flash`, which now serves DeepSeek V4.1 Flash
|
||||
Google, Mistral, Moonshot, OpenRouter, and xAI. Any OpenAI-compatible endpoint
|
||||
works, and so do local models through Ollama, vLLM, or SGLang. The default is
|
||||
DeepSeek's `deepseek-flash`, which DeepSeek moved to DeepSeek-V4.1-Flash-1017
|
||||
on 2026-10-17; 0.10.1 runs it without an update
|
||||
([details](docs/PROVIDERS.md#deepseek-v41-flash)).
|
||||
|
||||
`/model` switches models between turns, and `/model auto` picks one for each
|
||||
turn (`/router` sets up how). `/turn inspect` shows which model a turn used,
|
||||
what it cost and how long it took. See [Providers](docs/PROVIDERS.md).
|
||||
what it cost and how long it took. `codewhale auth chatgpt` lets you use a
|
||||
ChatGPT plan instead of an API key (experimental). See
|
||||
[Providers](docs/PROVIDERS.md).
|
||||
|
||||
### Receipts and undo
|
||||
|
||||
`/receipts`, or `codewhale receipts --last` after you quit, lists the files a
|
||||
session changed, the commands it ran, its web and MCP calls, the agents it
|
||||
started, each approval and who gave it, and what failed. `/undo` takes back
|
||||
your last request and the files it changed in one step. `/restore` rolls the
|
||||
whole workspace back to an earlier snapshot, and because that overwrites later
|
||||
edits it needs `/trust on` or Full Access. See [Receipts](docs/RECEIPTS.md).
|
||||
`/receipts`, or `codewhale receipts --last` after you quit, lists the files
|
||||
changed, commands run, web and MCP calls, agents started, approvals and who
|
||||
gave them, and failures. `/undo` takes back your last request and the files it
|
||||
changed, in one step and without trust mode. `/restore`
|
||||
rolls the whole workspace back to an earlier snapshot; because that overwrites
|
||||
later edits, it needs `/trust on` or Full Access. See [Receipts](docs/RECEIPTS.md).
|
||||
|
||||
### Several agents on one task
|
||||
Exec runs that offer tools are saved in every output format, so
|
||||
`codewhale receipts --last` works after `exec --auto`, including `--json`.
|
||||
Answer-only text and `--json` runs are saved only when resuming or when
|
||||
conversation compaction starts; use `--output-format stream-json` to always
|
||||
save them for receipts or `exec --continue`.
|
||||
|
||||
### Longer work
|
||||
|
||||
`/goal` sets an objective that carries across turns. Codewhale can split work
|
||||
across [sub-agents](docs/SUBAGENTS.md), run a [Fleet](docs/FLEET.md) of agents
|
||||
with their own roles and models, or follow a
|
||||
[workflow](docs/WORKFLOW_AUTHORING.md) checked into the repository.
|
||||
across [sub-agents](docs/SUBAGENTS.md), run a [fleet](docs/FLEET.md) of agents,
|
||||
or follow a [workflow](docs/WORKFLOW_AUTHORING.md) checked into the
|
||||
repository.
|
||||
|
||||
### Extensions
|
||||
|
||||
Connect [MCP servers](docs/MCP.md), add [skills](docs/SKILLS.md) and
|
||||
[plugins](docs/PLUGINS.md), and run [hooks](docs/HOOKS.md) on session and tool
|
||||
events. Code mode (experimental, on by default) lets the model write one
|
||||
`execute_tools` program that calls several MCP and plugin tools; each call
|
||||
still meets your approval rules and keeps its receipt. The Computer Use plugin
|
||||
bundled with v0.10.2 (experimental, macOS only) can observe and operate other
|
||||
apps, and it stays off until you review its access and enable it
|
||||
([guide](https://github.com/codewhale-hq/Codewhale/blob/v0.10.2/crates/tui/plugins/computer-use/README.md)).
|
||||
Coming in 0.10.3: the plugin is released
|
||||
[separately](https://github.com/codewhale-hq/codewhale-cu-plugin).
|
||||
|
||||
### Ways to run it
|
||||
|
||||
| Command | What it does |
|
||||
| --- | --- |
|
||||
| `codewhale` | The interactive terminal app |
|
||||
| `codewhale exec "…"` | One task without the interface, for scripts and CI ([headless runs](docs/INSTALL.md#headless-scripts-ci)) |
|
||||
| `codewhale web` | A local web client on `127.0.0.1` ([guide](docs/WEB.md)) |
|
||||
| `codewhale review --pr N` | An advisory review of a GitHub pull request; the [GitHub bot setup](docs/GITHUB_ACTION.md#set-up-your-own-bot) runs it on your own model key |
|
||||
| `codewhale app-server --http` | The [Runtime API](docs/RUNTIME_API.md), a local HTTP API for threads, events, approvals and each turn's file changes |
|
||||
| `codewhale serve --acp` | The Agent Client Protocol over stdio, for editors such as Zed |
|
||||
|
||||
The terminal app, `exec`, `web` and the Runtime API share one engine
|
||||
([architecture](docs/ARCHITECTURE.md)). A community-maintained
|
||||
[VS Code extension](https://marketplace.visualstudio.com/items?itemName=HengQuWorld.brotherwhale-vscode)
|
||||
connects to it from the sidebar ([source](https://github.com/HengQuWorld/CodeWhale-VSCode)).
|
||||
|
||||
<p align="center">
|
||||
<a href="https://www.orcarouter.ai/redeem/1a6250545fdc4e0aaccb0a4470159e67" rel="sponsored">
|
||||
<img src="web/public/codewhale-x-orcarouter.webp" alt="Codewhale × OrcaRouter: +30% DeepSeek V4.1 Flash usage" width="560">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<a href="https://www.orcarouter.ai/redeem/1a6250545fdc4e0aaccb0a4470159e67" rel="sponsored">Codewhale × OrcaRouter</a>
|
||||
gives 30% extra DeepSeek V4.1 Flash credit on each OrcaRouter top-up until
|
||||
November 21, 2026. This link is part of a referral campaign; Codewhale may
|
||||
earn a referral reward on top-ups made through it.
|
||||
|
||||
## How it stays safe
|
||||
|
||||
Two switches say how much Codewhale may do on its own. `Tab`, when the input
|
||||
is empty, changes the mode: **Plan** reads and searches but changes nothing,
|
||||
**Work** edits files and runs commands, and **Operate** has the same access as
|
||||
Work and keeps going until the goal is verified or it needs you. `Shift+Tab`
|
||||
changes the permission. **Ask**, the default, applies file edits in your
|
||||
repository without asking and asks before shell commands that are not
|
||||
read-only.
|
||||
**Auto-Review** lets rules and a reviewer model approve routine calls and
|
||||
refuses high-risk ones; switch to Ask to approve those yourself. **Full
|
||||
Access** never asks and turns off Codewhale's sandbox, although repository
|
||||
rules and managed policy still apply. The
|
||||
[v0.10.2 guide](https://github.com/codewhale-hq/Codewhale/blob/v0.10.2/docs/MODES.md)
|
||||
has the details.
|
||||
|
||||
Coming in 0.10.3: `Shift+Tab` becomes one dial through Ask, Accept Edits,
|
||||
Plan, Auto-Review and Full Access, `Tab` no longer changes the mode, and
|
||||
Operate is retired. Ask will ask before every file edit too, and the new
|
||||
default, Accept Edits, will apply edits inside the git repository and still ask
|
||||
before commands. The [current guide](docs/MODES.md) describes it.
|
||||
|
||||
In Ask and Auto-Review, commands run in an OS sandbox where the system has
|
||||
one: Seatbelt on macOS, and bubblewrap on Linux when it is installed. Windows
|
||||
has no OS sandbox yet, and Full Access turns the sandbox off. Inside the
|
||||
sandbox, shell commands and terminal sessions cannot read credential stores
|
||||
such as `~/.ssh`, `~/.aws` and `.env` files.
|
||||
`/preview-request` shows a redacted preview of the next request without
|
||||
sending it.
|
||||
|
||||
You need no account, and keys you add with `/provider` stay on your machine.
|
||||
With your own API key you pay the provider directly, and local models need no
|
||||
key. The footer and `/cost` show what a session has cost when the price is
|
||||
known, and an unknown price is shown as unknown, never as free.
|
||||
|
||||
Telemetry is on by default. It sends aggregate counts (version and platform,
|
||||
session length and outcome, which built-in providers you used, feature and
|
||||
error counts) with a random install ID that rotates every 90 days, and never
|
||||
conversations, code, prompts, model names, file or repository names, or
|
||||
credentials. This command turns it off:
|
||||
|
||||
```bash
|
||||
codewhale config set telemetry false
|
||||
```
|
||||
|
||||
At startup Codewhale also checks GitHub for a new release; set
|
||||
`CODEWHALE_NO_UPDATE_CHECK=1` to skip that. More in
|
||||
[authorization order](docs/AUTHORIZATION_ORDER.md),
|
||||
[sandboxing](docs/SANDBOX.md) and [telemetry](docs/TELEMETRY.md).
|
||||
|
||||
## Where it is going
|
||||
|
||||
Codewhale is becoming one environment across your terminal, a desktop app,
|
||||
the browser and cloud computers, all run by one engine. Each part says where
|
||||
it stands today.
|
||||
|
||||
| Part | State | Today |
|
||||
| --- | --- | --- |
|
||||
| Terminal app | Released | Install it on macOS, Linux or Windows and run `codewhale` in any repository. |
|
||||
| Local browser client | Released | Run `codewhale web` to read the conversation, send a task and answer approvals in your browser; it comes with the terminal and needs no account. |
|
||||
| Many agents on one task | Released | Sub-agents, Fleets and workflows let several agents share one task, each with its own role. |
|
||||
| Desktop app | In development, no download | We are building a desktop app on the same engine, and it has no public download yet. |
|
||||
| Cloud computers | Invite-only | Cloud computers are invite-only, and we are building them so a session can keep working on a machine in the cloud. |
|
||||
| One session across them | Being built | We are building one session that goes with you from the terminal to the desktop, the browser and the cloud, and stays yours the whole way. |
|
||||
|
||||
The [roadmap](docs/ROADMAP.md) says what each release covers and where you
|
||||
can help.
|
||||
|
||||
## Coming from another agent
|
||||
### Coming from another agent
|
||||
|
||||
Codewhale reads `AGENTS.md` on its own. Instruction files written for other
|
||||
tools, such as `CLAUDE.md`, `.cursorrules` or
|
||||
tools, such as `CLAUDE.md`, `.cursorrules`, or
|
||||
`.github/copilot-instructions.md`, load only when you opt in with
|
||||
`project_instruction_imports = ["claude"]` (or `"cursor"`, `"cline"`,
|
||||
`"windsurf"`, `"copilot"`, `"all"`) in your config; see
|
||||
@@ -232,45 +183,90 @@ tools, such as `CLAUDE.md`, `.cursorrules` or
|
||||
it did not load.
|
||||
|
||||
`/import-claude` prepares an import of your Claude Code settings and MCP
|
||||
servers for you to review. `/plugin install` accepts Claude Code plugins that
|
||||
contain skills, commands, agent profiles and MCP servers, and refuses plugins
|
||||
that need hooks or LSP servers
|
||||
servers for you to review. `/plugin install` accepts Claude Code plugins
|
||||
containing skills, commands, agent profiles, and MCP servers, and refuses
|
||||
plugins that need hooks or LSP servers
|
||||
([details](docs/CLAUDE_PLUGIN_COMPAT.md)).
|
||||
|
||||
### Extensions
|
||||
|
||||
Connect [MCP servers](docs/MCP.md), add [skills](docs/SKILLS.md) and
|
||||
[plugins](docs/PLUGINS.md), and run [hooks](docs/HOOKS.md) on session and tool
|
||||
events. Code mode (experimental, on by default) lets the model write one
|
||||
`execute_tools` program that calls several MCP and plugin tools; each call
|
||||
still meets your approval rules and keeps its receipt. The independently installed Computer
|
||||
Use plugin (experimental, macOS only) can observe
|
||||
and operate other apps. It stays off until you review its access and enable it
|
||||
([guide](https://github.com/codewhale-hq/codewhale-cu-plugin)).
|
||||
|
||||
## Ways to run it
|
||||
|
||||
| Command | What it does |
|
||||
| --- | --- |
|
||||
| `codewhale` | The interactive terminal app |
|
||||
| `codewhale exec "…"` | One task without the interface. On its own it answers without using tools; `--auto` enables tools with Full Access by default, `--sandbox workspace-write` restricts them, and `--output-format stream-json` streams events as JSON |
|
||||
| `codewhale web` | A local web UI on `127.0.0.1` ([guide](docs/WEB.md)) |
|
||||
| `codewhale review --pr N` | An advisory review of a GitHub pull request. The [GitHub bot setup](docs/GITHUB_ACTION.md#set-up-your-own-bot) generates a pinned workflow for automatic reviews and `@codewhale review`, using your own model key and optional App identity |
|
||||
| `codewhale app-server --http` | The [Runtime API](docs/RUNTIME_API.md), a local HTTP API for threads, events, approvals, and each turn's file changes |
|
||||
| `codewhale serve --acp` | The Agent Client Protocol over stdio, for editors such as Zed |
|
||||
|
||||
The terminal app, `exec`, `web`, and the Runtime API share one engine
|
||||
([architecture](docs/ARCHITECTURE.md)). A community-maintained
|
||||
[VS Code extension](https://marketplace.visualstudio.com/items?itemName=HengQuWorld.brotherwhale-vscode)
|
||||
connects to it from the sidebar ([source](https://github.com/HengQuWorld/CodeWhale-VSCode)).
|
||||
A native desktop app built on the Runtime API is in development; it has no
|
||||
public download yet.
|
||||
|
||||
## Cost and accounts
|
||||
|
||||
Codewhale is free and MIT licensed. With your own API key you pay the provider
|
||||
directly; local models need no key. The footer and `/cost` show what a session
|
||||
has cost when the price is known, and an unknown price is shown as unknown,
|
||||
never as free.
|
||||
|
||||
You don't need an account. Keys you add with `/provider` stay on your machine.
|
||||
|
||||
## Safety and privacy
|
||||
|
||||
In Ask and Auto-Review, commands run in an OS sandbox where the system has
|
||||
one: Seatbelt on macOS, and bubblewrap on Linux when it is installed.
|
||||
Windows has no OS sandbox yet, and Full Access turns the sandbox off. Inside
|
||||
the sandbox, shell commands cannot read credential stores such as `~/.ssh`,
|
||||
`~/.aws` and `.env` files; from 0.10.2 the same rule covers terminal sessions
|
||||
and more file tools. `/preview-request` shows a redacted preview of the next
|
||||
request without sending it.
|
||||
|
||||
Telemetry is on by default. It sends aggregate counts (version and platform,
|
||||
session length and outcome, which built-in providers you used, feature and
|
||||
error counts) with a random install ID that rotates every 90 days, and never
|
||||
conversations, code, prompts, model names, file or repository names, or
|
||||
credentials. `codewhale config set telemetry false` turns
|
||||
it off. At startup Codewhale also
|
||||
checks GitHub for a new release; set `CODEWHALE_NO_UPDATE_CHECK=1` to skip
|
||||
that. More in [authorization order](docs/AUTHORIZATION_ORDER.md),
|
||||
[sandboxing](docs/SANDBOX.md), and [telemetry](docs/TELEMETRY.md).
|
||||
|
||||
## Documentation
|
||||
|
||||
These guides follow the main branch and can describe changes that are not
|
||||
in the latest release yet.
|
||||
|
||||
| Start here | Reference |
|
||||
| --- | --- |
|
||||
| [User guide](docs/GUIDE.md) | [Configuration](docs/CONFIGURATION.md) |
|
||||
| [Installation](docs/INSTALL.md) | [Architecture](docs/ARCHITECTURE.md) |
|
||||
| [Providers and local models](docs/PROVIDERS.md) | [Runtime API](docs/RUNTIME_API.md) |
|
||||
| [Modes and permissions](docs/MODES.md) | [Plugin authoring](docs/PLUGIN_AUTHORING.md) |
|
||||
| [Keybindings](docs/KEYBINDINGS.md) | [Receipts](docs/RECEIPTS.md) |
|
||||
| [GitHub pull request review](docs/GITHUB_ACTION.md) | [All documentation](docs/README.md) |
|
||||
| [Installation](docs/INSTALL.md) | [Configuration](docs/CONFIGURATION.md) |
|
||||
| [Providers and local models](docs/PROVIDERS.md) | [Architecture](docs/ARCHITECTURE.md) |
|
||||
| [Modes and permissions](docs/MODES.md) | [Runtime API](docs/RUNTIME_API.md) |
|
||||
| [Keybindings](docs/KEYBINDINGS.md) | [Plugin authoring](docs/PLUGIN_AUTHORING.md) |
|
||||
| [GitHub PR review](docs/GITHUB_ACTION.md) | [All documentation](docs/README.md) |
|
||||
|
||||
## Contributing
|
||||
## Community
|
||||
|
||||
Codewhale is built in the open by the people who use it, in many languages.
|
||||
|
||||
<a href="docs/CONTRIBUTORS.md#everyone"><img src="docs/assets/contributors.png" width="760" alt="The Codewhale contributor wall: one avatar for each person whose code, co-authored commits, carried-forward pull requests or credited reports are in Codewhale, with the total count at the top."></a>
|
||||
|
||||
The [contributor wall](docs/CONTRIBUTORS.md) counts everyone whose code,
|
||||
co-authored commits, carried-forward pull requests or credited reports are in
|
||||
Codewhale, including people GitHub's own contributor list misses.
|
||||
`scripts/contributor-wall.py` regenerates the picture and the list from git
|
||||
history and GitHub.
|
||||
|
||||
All feedback is welcome: a bug, an idea, or something that looked or felt
|
||||
wrong. [Open an issue](https://github.com/codewhale-hq/Codewhale/issues/new/choose)
|
||||
for any of these; an issue is also the best place to start a new feature.
|
||||
Pull requests are welcome for fixes, translations, providers and planned work.
|
||||
Questions go to [Discussions](https://github.com/codewhale-hq/Codewhale/discussions).
|
||||
The [contributing guide](CONTRIBUTING.md) shows where each kind of help goes,
|
||||
how your work lands with your name on it, and
|
||||
[where things live](CONTRIBUTING.md#project-structure).
|
||||
Codewhale is what it is because of its community, and all feedback is
|
||||
welcome: a bug, an idea, or something that looked or felt wrong.
|
||||
[Open an issue](https://github.com/codewhale-hq/CodeWhale/issues/new/choose) for any
|
||||
of these; an issue is also the best place to start a new feature. Pull requests
|
||||
are welcome for fixes, translations, providers and planned work. The
|
||||
[contributing guide](CONTRIBUTING.md) shows where each kind of help goes, and
|
||||
the [roadmap](docs/ROADMAP.md) shows where the project is headed. Contributors
|
||||
keep credit for the work that lands, and the
|
||||
[repository layout](CONTRIBUTING.md#project-structure) shows where things live.
|
||||
|
||||
Chat on [Discord](https://discord.gg/37gfS3ksug), or add the maintainer,
|
||||
Hunter, on WeChat (`hunterbown`) and ask to join the Whale Brothers group.
|
||||
@@ -280,8 +276,10 @@ Everyone is welcome.
|
||||
|
||||
Codewhale started as `deepseek-tui` and still reads that project's
|
||||
configuration and sessions. It is independently maintained and not affiliated
|
||||
with DeepSeek or any other model provider.
|
||||
with DeepSeek or any other model provider. Thanks to
|
||||
[everyone who has contributed](docs/CONTRIBUTORS.md).
|
||||
|
||||
[](docs/CONTRIBUTORS.md#everyone)
|
||||
|
||||
[MIT](LICENSE). Code adapted from other open-source projects is listed in the
|
||||
third-party notices for [vendored sources](THIRD_PARTY_NOTICES.md) and
|
||||
[adapted designs](docs/THIRD_PARTY_NOTICES.md).
|
||||
[third-party notices](docs/THIRD_PARTY_NOTICES.md).
|
||||
@@ -41,6 +41,34 @@ mod platform {
|
||||
tokio::io::WriteHalf<Connection>,
|
||||
>;
|
||||
|
||||
fn resolved_store_form(path: PathBuf) -> Option<PathBuf> {
|
||||
let checked = codewhale_engine::runtime_threads::checked_runtime_store_root(path).ok()?;
|
||||
let mut tail = Vec::new();
|
||||
let mut probe = checked.as_path();
|
||||
loop {
|
||||
match probe.canonicalize() {
|
||||
Ok(real) => {
|
||||
return Some(tail.iter().rev().fold(real, |acc, part| acc.join(part)));
|
||||
}
|
||||
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
|
||||
tail.push(probe.file_name()?.to_owned());
|
||||
probe = probe.parent()?;
|
||||
}
|
||||
Err(_) => return None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn resolved_store_form_of(path: &std::path::Path) -> PathBuf {
|
||||
resolved_store_form(path.to_path_buf()).unwrap_or_else(|| path.to_path_buf())
|
||||
}
|
||||
|
||||
fn same_store_path(left: &std::path::Path, right: &std::path::Path) -> bool {
|
||||
left == right
|
||||
|| (cfg!(any(target_os = "macos", windows))
|
||||
&& left.to_string_lossy().to_lowercase() == right.to_string_lossy().to_lowercase())
|
||||
}
|
||||
|
||||
fn same_socket_selection(left: &std::path::Path, right: &std::path::Path) -> bool {
|
||||
if left == right {
|
||||
return true;
|
||||
@@ -228,15 +256,13 @@ mod platform {
|
||||
Ok(())
|
||||
}
|
||||
fn discard_if_store_gone_blocking(&self, selected_store: PathBuf) -> Result<bool> {
|
||||
let gone =
|
||||
codewhale_engine::runtime_threads::checked_runtime_store_root(selected_store)
|
||||
.is_ok_and(|selected| {
|
||||
selected == self.receipt().data_dir
|
||||
&& matches!(
|
||||
selected.symlink_metadata(),
|
||||
Err(error) if error.kind() == std::io::ErrorKind::NotFound
|
||||
)
|
||||
});
|
||||
let gone = resolved_store_form(selected_store).is_some_and(|selected| {
|
||||
same_store_path(&selected, &resolved_store_form_of(&self.receipt().data_dir))
|
||||
&& matches!(
|
||||
selected.symlink_metadata(),
|
||||
Err(error) if error.kind() == std::io::ErrorKind::NotFound
|
||||
)
|
||||
});
|
||||
if !gone || !matches!(self.revalidate_receipt(), Ok(true)) {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
@@ -76,6 +76,18 @@ impl McpServerSettings {
|
||||
}
|
||||
}
|
||||
|
||||
fn refuse_shell_tools(&mut self) -> Vec<String> {
|
||||
let (refused, kept): (Vec<String>, Vec<String>) = std::mem::take(&mut self.expose_tools)
|
||||
.into_iter()
|
||||
.partition(|name| {
|
||||
build_exposed_tools(std::slice::from_ref(name))
|
||||
.iter()
|
||||
.any(|tool| tool.internal == "exec_shell")
|
||||
});
|
||||
self.expose_tools = kept;
|
||||
refused
|
||||
}
|
||||
|
||||
fn from_toml(contents: &str) -> Result<Self> {
|
||||
let config: McpServerConfigFile = toml::from_str(contents)?;
|
||||
Ok(Self {
|
||||
@@ -98,12 +110,21 @@ pub async fn run_mcp_server(
|
||||
workspace: PathBuf,
|
||||
skills_dir: PathBuf,
|
||||
skills_config: std::sync::Arc<codewhale_runtime::config::SkillsConfig>,
|
||||
tool_launcher_configured: bool,
|
||||
) -> Result<()> {
|
||||
// Settings load is a synchronous config read; keep it off the async
|
||||
// worker per the blocking-call convention.
|
||||
let settings = tokio::task::spawn_blocking(McpServerSettings::load)
|
||||
let mut settings = tokio::task::spawn_blocking(McpServerSettings::load)
|
||||
.await
|
||||
.context("MCP server settings task failed")??;
|
||||
if tool_launcher_configured {
|
||||
for name in settings.refuse_shell_tools() {
|
||||
eprintln!(
|
||||
"codewhale mcp server: not exposing '{name}': the managed configuration sets \
|
||||
tool_launcher, and this server does not run commands through it."
|
||||
);
|
||||
}
|
||||
}
|
||||
let server = McpServer::new(workspace, settings, skills_dir, skills_config)?;
|
||||
// stdout carries the protocol; the notice goes to stderr so an operator
|
||||
// whose config lists write tools sees why they are missing.
|
||||
@@ -906,6 +927,21 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_tool_launcher_refuses_shell_tools_even_when_the_operator_allows_them() {
|
||||
let mut settings = McpServerSettings::from_toml(
|
||||
"[server]\nexpose_tools = [\"file_read\", \"shell\", \"exec_shell\", \"Bash\"]\nrequire_approval = false\n",
|
||||
)
|
||||
.expect("parse config");
|
||||
assert_eq!(settings.refuse_shell_tools(), vec!["shell", "exec_shell"]);
|
||||
let server = test_server(PathBuf::from("."), settings);
|
||||
let names: Vec<&str> = server.tools.iter().map(|tool| tool.name.as_ref()).collect();
|
||||
assert_eq!(names, vec!["file_read"]);
|
||||
for internal in ["exec_shell", "Bash", "bash"] {
|
||||
assert!(server.registry.get(internal).is_none(), "{internal}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn existing_config_without_require_approval_withholds_and_names_write_tools() {
|
||||
let settings = McpServerSettings::from_toml(
|
||||
|
||||
@@ -296,6 +296,53 @@ async fn dead_owner_whose_store_was_deleted_is_discarded_and_startup_proceeds()
|
||||
fixture.bind_replacement(None).await.unwrap();
|
||||
}
|
||||
|
||||
async fn assert_gone_store_spelled_as_is_discarded(
|
||||
label: &str,
|
||||
spell: impl FnOnce(&Path) -> PathBuf,
|
||||
) {
|
||||
let fixture = BootstrapPublicationFixture::new(label, true, false).await;
|
||||
std::fs::remove_dir(&fixture.owner.data_dir).unwrap();
|
||||
let spelled = spell(&fixture.harness.root);
|
||||
assert_ne!(spelled, fixture.owner.data_dir);
|
||||
assert!(matches!(
|
||||
codewhale_app_server::daemon_client::probe_owner_for_host_startup(
|
||||
fixture.owner.config_path.clone(),
|
||||
Some(fixture.owner.socket_path.clone()),
|
||||
&spelled,
|
||||
)
|
||||
.await
|
||||
.unwrap(),
|
||||
codewhale_app_server::daemon_client::HostOwnerProbe::Absent
|
||||
));
|
||||
assert!(
|
||||
fixture
|
||||
.parent
|
||||
.read_private_receipt("daemon.sock.owner.json", 16384)
|
||||
.unwrap()
|
||||
.is_none()
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn dead_owner_is_discarded_when_the_selected_store_is_spelled_through_a_symlink() {
|
||||
assert_gone_store_spelled_as_is_discarded("boot-gone-link", |root| {
|
||||
std::os::unix::fs::symlink(root, root.join("alias")).unwrap();
|
||||
root.join("alias").join("runtime")
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
#[tokio::test]
|
||||
async fn dead_owner_is_discarded_when_the_selected_store_is_spelled_in_another_letter_case() {
|
||||
assert_gone_store_spelled_as_is_discarded("boot-gone-case", |root| {
|
||||
let upper = PathBuf::from(root.to_string_lossy().to_uppercase());
|
||||
assert!(upper.exists(), "this test needs a case-insensitive volume");
|
||||
upper.join("RUNTIME")
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn live_owner_with_a_missing_store_is_never_discarded() {
|
||||
let fixture = BootstrapPublicationFixture::new("boot-gone-live", false, false).await;
|
||||
|
||||
+30
-2
@@ -117,7 +117,7 @@ struct Cli {
|
||||
#[arg(
|
||||
long,
|
||||
value_name = "BOOL",
|
||||
help = "Control aggregate usage counting (default on; Codewhale + PostHog; \
|
||||
help = "Control usage reporting (default on; Codewhale + PostHog; \
|
||||
durable off: config set telemetry false; CODEWHALE_TELEMETRY=0 always wins)"
|
||||
)]
|
||||
telemetry: Option<bool>,
|
||||
@@ -1957,7 +1957,7 @@ enum ConfigCommand {
|
||||
Unset {
|
||||
key: String,
|
||||
},
|
||||
/// Review aggregate usage counting by Codewhale and PostHog (default on).
|
||||
/// Review usage reporting by Codewhale and PostHog (default on).
|
||||
Telemetry {
|
||||
/// Optional compatibility form: enable future sessions under this policy version.
|
||||
#[arg(long, value_name = "VERSION")]
|
||||
@@ -6792,6 +6792,10 @@ fn apply_tui_env(cli: &Cli, resolved_runtime: &ResolvedRuntimeOptions, passthrou
|
||||
codewhale_config::TELEMETRY_FLOOR_ENV,
|
||||
if floor { "1" } else { "0" },
|
||||
);
|
||||
set_tui_env(
|
||||
codewhale_config::TELEMETRY_SOURCE_ENV,
|
||||
resolved_runtime.telemetry_source.as_str(),
|
||||
);
|
||||
if let Some(endpoint) = resolved_runtime.telemetry_endpoint.as_ref() {
|
||||
set_tui_env("CODEWHALE_TELEMETRY_ENDPOINT", endpoint);
|
||||
}
|
||||
@@ -7226,6 +7230,30 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dispatcher_hands_the_telemetry_source_to_the_tui() {
|
||||
let _lock = env_lock();
|
||||
let _guards = [
|
||||
ScopedEnvVar::remove("CODEWHALE_TELEMETRY"),
|
||||
ScopedEnvVar::remove("DEEPSEEK_TELEMETRY"),
|
||||
ScopedEnvVar::remove(codewhale_config::TELEMETRY_FLOOR_ENV),
|
||||
ScopedEnvVar::remove(codewhale_config::TELEMETRY_SOURCE_ENV),
|
||||
];
|
||||
|
||||
let cli = parse_ok(&["codewhale"]);
|
||||
let mut runtime = resolved_runtime_for_test(ProviderKind::NvidiaNim, ProviderSource::Cli);
|
||||
runtime.telemetry = false;
|
||||
runtime.telemetry_source = codewhale_config::TelemetrySource::Config;
|
||||
apply_tui_env(&cli, &runtime, &[]);
|
||||
|
||||
assert_eq!(std::env::var("CODEWHALE_TELEMETRY").as_deref(), Ok("false"));
|
||||
assert!(std::env::var(codewhale_config::TELEMETRY_FLOOR_ENV).is_ok());
|
||||
assert_eq!(
|
||||
std::env::var(codewhale_config::TELEMETRY_SOURCE_ENV).as_deref(),
|
||||
Ok("config")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clap_command_definition_is_consistent() {
|
||||
Cli::command().debug_assert();
|
||||
|
||||
@@ -605,7 +605,7 @@ pub const SETTINGS_SCHEMA: &[SettingDef] = &[
|
||||
def(
|
||||
"show_thinking",
|
||||
SettingKind::Bool(SHOW_THINKING),
|
||||
"false",
|
||||
"true",
|
||||
ui(
|
||||
TAB_APPEARANCE,
|
||||
"display",
|
||||
@@ -1546,6 +1546,12 @@ pub const SETTINGS_SCHEMA: &[SettingDef] = &[
|
||||
"false",
|
||||
None,
|
||||
),
|
||||
def(
|
||||
"account_preferences_notice_shown",
|
||||
SettingKind::String,
|
||||
"",
|
||||
None,
|
||||
),
|
||||
// Round 3 work-bar placement migration ran once (`top` → `bottom`).
|
||||
def(
|
||||
"work_surface_bottom_migrated",
|
||||
|
||||
@@ -33,6 +33,8 @@ pub const LEGACY_CLI_API_KEY_SOURCE_ENV: &str = "DEEPSEEK_API_KEY_SOURCE";
|
||||
/// [`CLI_API_KEY_SOURCE_ENV`]. Set to `1`/`0` on every delegated run.
|
||||
pub const TELEMETRY_FLOOR_ENV: &str = "CODEWHALE_TELEMETRY_FLOOR";
|
||||
|
||||
pub const TELEMETRY_SOURCE_ENV: &str = "CODEWHALE_TELEMETRY_SOURCE";
|
||||
|
||||
/// Whether an environment-level kill switch forces telemetry off here.
|
||||
///
|
||||
/// A floor is *not* the same as "telemetry resolved to false": off is the
|
||||
@@ -92,6 +94,32 @@ impl TelemetrySource {
|
||||
Self::Default => "default",
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn from_label(label: &str) -> Option<Self> {
|
||||
match label {
|
||||
"cli" => Some(Self::Cli),
|
||||
"env" => Some(Self::Env),
|
||||
"config" => Some(Self::Config),
|
||||
"default" => Some(Self::Default),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn dispatched_telemetry_source() -> Option<TelemetrySource> {
|
||||
parse_bool(&std::env::var(TELEMETRY_FLOOR_ENV).ok()?).ok()?;
|
||||
TelemetrySource::from_label(&std::env::var(TELEMETRY_SOURCE_ENV).ok()?)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn dispatched_telemetry_consent(file_telemetry: Option<bool>) -> (bool, TelemetrySource) {
|
||||
let (on, source) = resolved_telemetry_consent(file_telemetry);
|
||||
match (source, dispatched_telemetry_source()) {
|
||||
(TelemetrySource::Env, Some(declared)) => (on, declared),
|
||||
_ => (on, source),
|
||||
}
|
||||
}
|
||||
|
||||
/// Read the telemetry environment override, reporting an unreadable value
|
||||
|
||||
@@ -8763,6 +8763,7 @@ struct TelemetryEnvGuard {
|
||||
codewhale: Option<OsString>,
|
||||
deepseek: Option<OsString>,
|
||||
floor: Option<OsString>,
|
||||
source: Option<OsString>,
|
||||
codewhale_endpoint: Option<OsString>,
|
||||
deepseek_endpoint: Option<OsString>,
|
||||
_lock: std::sync::MutexGuard<'static, ()>,
|
||||
@@ -8775,6 +8776,7 @@ impl TelemetryEnvGuard {
|
||||
codewhale: env::var_os("CODEWHALE_TELEMETRY"),
|
||||
deepseek: env::var_os("DEEPSEEK_TELEMETRY"),
|
||||
floor: env::var_os(TELEMETRY_FLOOR_ENV),
|
||||
source: env::var_os(TELEMETRY_SOURCE_ENV),
|
||||
// The endpoint variables are cleared too. Resolution now has a
|
||||
// shipped default, so an ambient endpoint in the developer's or
|
||||
// CI's environment is the difference between pinning the default
|
||||
@@ -8788,6 +8790,7 @@ impl TelemetryEnvGuard {
|
||||
env::remove_var("CODEWHALE_TELEMETRY");
|
||||
env::remove_var("DEEPSEEK_TELEMETRY");
|
||||
env::remove_var(TELEMETRY_FLOOR_ENV);
|
||||
env::remove_var(TELEMETRY_SOURCE_ENV);
|
||||
env::remove_var("CODEWHALE_TELEMETRY_ENDPOINT");
|
||||
env::remove_var("DEEPSEEK_TELEMETRY_ENDPOINT");
|
||||
}
|
||||
@@ -8808,6 +8811,13 @@ impl TelemetryEnvGuard {
|
||||
}
|
||||
}
|
||||
|
||||
fn set_source(&self, value: &str) {
|
||||
// Safety: test-only environment mutation guarded by the module mutex.
|
||||
unsafe {
|
||||
env::set_var(TELEMETRY_SOURCE_ENV, value);
|
||||
}
|
||||
}
|
||||
|
||||
fn set_floor(&self, value: &str) {
|
||||
// Safety: test-only environment mutation guarded by the module mutex.
|
||||
unsafe {
|
||||
@@ -8821,6 +8831,7 @@ impl TelemetryEnvGuard {
|
||||
env::remove_var("CODEWHALE_TELEMETRY");
|
||||
env::remove_var("DEEPSEEK_TELEMETRY");
|
||||
env::remove_var(TELEMETRY_FLOOR_ENV);
|
||||
env::remove_var(TELEMETRY_SOURCE_ENV);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8841,6 +8852,10 @@ impl Drop for TelemetryEnvGuard {
|
||||
Some(value) => env::set_var(TELEMETRY_FLOOR_ENV, value),
|
||||
None => env::remove_var(TELEMETRY_FLOOR_ENV),
|
||||
}
|
||||
match self.source.take() {
|
||||
Some(value) => env::set_var(TELEMETRY_SOURCE_ENV, value),
|
||||
None => env::remove_var(TELEMETRY_SOURCE_ENV),
|
||||
}
|
||||
match self.codewhale_endpoint.take() {
|
||||
Some(value) => env::set_var("CODEWHALE_TELEMETRY_ENDPOINT", value),
|
||||
None => env::remove_var("CODEWHALE_TELEMETRY_ENDPOINT"),
|
||||
@@ -8995,6 +9010,57 @@ fn telemetry_consent_names_its_source() {
|
||||
assert_eq!(source, TelemetrySource::Env);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dispatched_consent_names_the_dispatchers_source_not_its_hand_off() {
|
||||
let guard = TelemetryEnvGuard::take();
|
||||
|
||||
guard.set("false");
|
||||
guard.set_floor("0");
|
||||
guard.set_source("config");
|
||||
let (on, source) = dispatched_telemetry_consent(Some(false));
|
||||
assert!(!on);
|
||||
assert_eq!(source, TelemetrySource::Config);
|
||||
|
||||
guard.set("false");
|
||||
guard.set_floor("1");
|
||||
guard.set_source("env");
|
||||
let (on, source) = dispatched_telemetry_consent(Some(false));
|
||||
assert!(!on);
|
||||
assert_eq!(source, TelemetrySource::Env);
|
||||
|
||||
guard.set("true");
|
||||
guard.set_floor("0");
|
||||
guard.set_source("default");
|
||||
let (on, source) = dispatched_telemetry_consent(None);
|
||||
assert!(on);
|
||||
assert_eq!(source, TelemetrySource::Default);
|
||||
|
||||
guard.set("false");
|
||||
guard.set_floor("1");
|
||||
guard.set_source("cli");
|
||||
let (on, source) = dispatched_telemetry_consent(None);
|
||||
assert!(!on);
|
||||
assert_eq!(source, TelemetrySource::Cli);
|
||||
|
||||
guard.clear();
|
||||
guard.set("0");
|
||||
guard.set_source("config");
|
||||
let (on, source) = dispatched_telemetry_consent(Some(false));
|
||||
assert!(!on);
|
||||
assert_eq!(
|
||||
source,
|
||||
TelemetrySource::Env,
|
||||
"a source with no dispatcher floor statement is not believed"
|
||||
);
|
||||
|
||||
guard.clear();
|
||||
guard.set("0");
|
||||
guard.set_floor("1");
|
||||
guard.set_source("nonsense");
|
||||
let (_, source) = dispatched_telemetry_consent(None);
|
||||
assert_eq!(source, TelemetrySource::Env);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolved_runtime_options_mints_a_route_candidate() {
|
||||
let resolved = ConfigToml::default().resolve_runtime_options(&CliRuntimeOverrides::default());
|
||||
|
||||
@@ -0,0 +1,432 @@
|
||||
import { readFileSync, writeFileSync } from 'node:fs'
|
||||
import { dirname, join } from 'node:path'
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url'
|
||||
|
||||
const here = dirname(fileURLToPath(import.meta.url))
|
||||
const root = join(here, '..')
|
||||
const SOURCES = {
|
||||
compatibility: join(root, 'src/dsh/compatibility.json'),
|
||||
hostRoot: join(root, 'src/root.ts'),
|
||||
shellHooks: join(root, 'src/dsh/shell-hooks.ts'),
|
||||
seatTable: join(root, '../../app-server/src/runtime_api/plugins.rs'),
|
||||
catalog: join(root, 'src/dsh/inspect-catalog.json'),
|
||||
}
|
||||
|
||||
const read = (path) => readFileSync(path, 'utf8').replace(/\r\n/g, '\n')
|
||||
|
||||
const SERVICES = {
|
||||
tools: {
|
||||
summary: 'The tool registry: a plugin adds model-callable tools. The Engine owns calling, approval and results.',
|
||||
availability: 'always',
|
||||
methods: [
|
||||
{ name: 'register', signature: 'register(definition: ToolDefinition): () => void', summary: 'Register one tool built with defineTool from @deepseek-ai/dsh-tools. The definition must stay inside the supported JSON Schema subset (type, oneOf, properties, required, additionalProperties, items, enum, const). A second argument is refused. Returns an idempotent disposer.' },
|
||||
{ name: 'get', signature: 'get(name: string, agent: Agent): { name: string } | undefined', summary: 'Whether the calling agent has a tool by this name. Answers only inside a live tool call, for that call\'s own agent.' },
|
||||
],
|
||||
},
|
||||
commands: {
|
||||
summary: 'Slash commands a person types. A handler runs only when the person types the command.',
|
||||
availability: 'always',
|
||||
methods: [
|
||||
{ name: 'register', signature: 'register(definition: { name: string, description: string, argumentHint?: string, handler(call: { args: string, signal: AbortSignal }): CommandResult }): () => void', summary: 'Register one slash command. Names start with a lower case letter, then lower case letters, digits, _ and -, at most 64 characters; at most 64 commands per plugin. A name that clashes with a built-in command gets the alias plugin-<plugin-slug>-<command>, and is refused if that alias is invalid or also taken. A name another plugin already registered is refused, so pick a plugin-specific name. Returns an idempotent disposer.' },
|
||||
],
|
||||
},
|
||||
storage: {
|
||||
summary: 'Owner-local JSON persistence under the data folder the Engine assigned. It holds no session history and no secrets.',
|
||||
availability: 'always',
|
||||
methods: [
|
||||
{ name: 'get', signature: 'get(key: string): Promise<Json | undefined>', summary: 'Read one stored value.' },
|
||||
{ name: 'set', signature: 'set(key: string, value: Json): Promise<void>', summary: 'Store one JSON value, within the bounded key, value and owner limits.' },
|
||||
{ name: 'delete', signature: 'delete(key: string): Promise<void>', summary: 'Remove one stored value.' },
|
||||
],
|
||||
},
|
||||
skills: {
|
||||
summary: 'Skill roots: reviewed folders of skills added to the Engine\'s skill catalog.',
|
||||
availability: 'always',
|
||||
methods: [
|
||||
{ name: 'registerRoot', signature: 'registerRoot(definition: { path: string }): () => void', summary: 'Add a bundle-relative folder of SKILL.md packages. At most 8 roots per plugin. Absolute paths, links, empty components, . and .. are refused.' },
|
||||
],
|
||||
},
|
||||
sessions: {
|
||||
summary: 'The session of the calling tool invocation.',
|
||||
availability: 'tool-invocation',
|
||||
methods: [
|
||||
{ name: 'flush', signature: 'flush(session: Session): Promise<boolean>', summary: 'Ask the Engine to persist the committed prefix of the exact session of this tool call. Nothing else on sessions is available.' },
|
||||
],
|
||||
},
|
||||
userQuestions: {
|
||||
summary: 'Questions put to the person through the Engine\'s own question card.',
|
||||
availability: 'tool-invocation',
|
||||
methods: [
|
||||
{ name: 'ask', signature: 'ask(request: { agent: Agent, questions: Question[], signal?: AbortSignal }): Promise<Answers>', summary: 'Ask the person typed questions during a live root tool call. Timed or continued questions (wait) are not supported.' },
|
||||
],
|
||||
},
|
||||
subagents: {
|
||||
summary: 'One independent child agent a root tool call may start.',
|
||||
availability: 'tool-invocation',
|
||||
methods: [
|
||||
{ name: 'getProvider', signature: 'getProvider(id: \'spawn\'): Provider', summary: 'Describe the one provider: a fresh child with no inherited conversation.' },
|
||||
{ name: 'startActivation', signature: 'startActivation(options: { provider: \'spawn\', childId: string, request: { parent: Agent, prompt: TextBlock[], agentOptions?: { model?: string } }, signal?: AbortSignal, delivery?: \'caller\' | \'parent\' }): Promise<{ childId: string, messageId: string, result: Promise<ChildResult>, dispose(): Promise<void> }>', summary: 'Start the child through the Engine\'s approval gate. Resolves to the activation handle { childId, messageId, result, dispose }. Prompts are text only; only the model option is accepted. Needs the live calling agent as parent and is not available inside a child.' },
|
||||
{ name: 'result', signature: 'activation.result: Promise<{ stopReason: string, output: { type: \'text\', text: string }[], diagnostic?: string }>', summary: 'A getter, not a method: read the property, do not call it. Resolves when the child finishes, with its stop reason and its text output blocks; diagnostic notes dropped non-text blocks.' },
|
||||
{ name: 'dispose', signature: 'activation.dispose(): Promise<void>', summary: 'Stop that exact child and its own descendants.' },
|
||||
],
|
||||
},
|
||||
agents: {
|
||||
summary: 'Reserved. The host offers no methods on agents.',
|
||||
availability: 'none',
|
||||
methods: [],
|
||||
},
|
||||
systemPrompt: {
|
||||
summary: 'The child agent\'s persona slot, reachable only inside an agent/created listener.',
|
||||
availability: 'child-agent-hook',
|
||||
methods: [
|
||||
{ name: 'section', signature: 'agent.ctx.systemPrompt.section({ name: \'deployment:persona-prefix\', order: agent.ctx.systemPrompt.getSectionOrder(\'DEPLOYMENT_PERSONA_PREFIX\'), text: string }): () => void', summary: 'Set the child\'s persona text, at most 32 KiB, once. name must be the string deployment:persona-prefix (the PERSONA_PREFIX_SECTION value) and order must be what getSectionOrder(\'DEPLOYMENT_PERSONA_PREFIX\') returns, which is 0. Anything else is refused. Called anywhere else it fails. The Engine\'s own system prompt cannot be replaced.' },
|
||||
],
|
||||
},
|
||||
logger: {
|
||||
summary: 'Cordis logging. Every line becomes a host log notification attributed to the plugin.',
|
||||
availability: 'always',
|
||||
cordis: true,
|
||||
methods: [
|
||||
{ name: 'logger', signature: 'ctx.logger(name?: string): Logger', summary: 'A named logger with debug, info, warn and error. The host reports error and warn as such and everything else as info.' },
|
||||
],
|
||||
},
|
||||
events: {
|
||||
summary: 'Cordis event dispatch. Query the host Event provider for the events this host fires.',
|
||||
availability: 'always',
|
||||
cordis: true,
|
||||
methods: [
|
||||
{ name: 'on', signature: 'ctx.on(name: string, listener: Function, options?: boolean | { prepend?: boolean, global?: boolean }): () => boolean', summary: 'Listen to an event. The host refuses five lifecycle events and limits the options of two (see the Event provider).' },
|
||||
{ name: 'once', signature: 'ctx.once(name: string, listener: Function, options?: boolean | { prepend?: boolean, global?: boolean }): () => boolean', summary: 'Listen to an event once.' },
|
||||
{ name: 'emit', signature: 'ctx.emit(name: string, ...args: any[]): void', summary: 'Dispatch an event to its listeners.' },
|
||||
],
|
||||
},
|
||||
reflect: {
|
||||
summary: 'Cordis service reflection. A plugin may provide its own services but not the Engine\'s.',
|
||||
availability: 'always',
|
||||
cordis: true,
|
||||
methods: [
|
||||
{ name: 'provide', signature: 'ctx.reflect.provide(name: string, value?: any): () => void', summary: 'Provide a service under a name. Names in refusedServices are refused: the Engine owns them.' },
|
||||
{ name: 'get', signature: 'ctx.reflect.get(name: string, strict?: boolean): any', summary: 'Read a provided service.' },
|
||||
],
|
||||
},
|
||||
registry: {
|
||||
summary: 'Cordis plugin registry.',
|
||||
availability: 'always',
|
||||
cordis: true,
|
||||
methods: [
|
||||
{ name: 'get', signature: 'ctx.registry.get(plugin: Plugin): Runtime | undefined', summary: 'Look up the runtime record of a plugin.' },
|
||||
{ name: 'resolve', signature: 'ctx.registry.resolve(plugin: Plugin): Function | undefined', summary: 'Resolve a plugin shape to its callback.' },
|
||||
],
|
||||
},
|
||||
prompt: {
|
||||
summary: 'Attributed sections of the Engine\'s runtime prompt. A plugin cannot replace the system prompt.',
|
||||
availability: 'always',
|
||||
methods: [
|
||||
{ name: 'registerSection', signature: 'registerSection(definition: { id: string, text: string, interpolate?: \'model-cwd\' }): () => void', summary: 'Contribute a plain-text section of at most 4 KiB; at most 128 sections per plugin. The id is lower case letters, digits, _ and -, starts with a letter, at most 64 characters. With interpolate set, only {{model}} and {{cwd}} are expanded.' },
|
||||
],
|
||||
},
|
||||
avatars: {
|
||||
summary: 'Avatar packs for the terminal.',
|
||||
availability: 'always',
|
||||
methods: [
|
||||
{ name: 'registerPack', signature: 'registerPack(definition: { path: string }): () => void', summary: 'Register a reviewed bundle-relative avatar pack: a path ending in .json, at most 4 packs per plugin.' },
|
||||
],
|
||||
},
|
||||
mcp: {
|
||||
summary: 'Proposals of MCP servers. The Engine owns connection, credentials and tool calls.',
|
||||
availability: 'native-review',
|
||||
methods: [
|
||||
{ name: 'registerServer', signature: 'registerServer(definition: { serverName: string, server: Record<string, unknown> }): () => void', summary: 'Propose one server. serverName is a letter or digit followed by up to 31 letters, digits, _ or -. headers and env must stay empty and the url may carry no credentials, query or fragment. At most 64 servers per plugin; at most 64 KiB per definition.' },
|
||||
],
|
||||
},
|
||||
shellHooks: {
|
||||
summary: 'Claude Code and Codex style command hooks, run by the Engine.',
|
||||
availability: 'native-review',
|
||||
methods: [
|
||||
{ name: 'register', signature: 'register(definition: { dialect: \'claude-code\' | \'codex\', point: string, matcher?: string, hook: { event: string, command: string, timeout_secs: number, background: false, continue_on_error: false, name: string } }): () => void', summary: 'Register one command hook for a reviewed native plugin. Forced continuation, observer steering, non-command and asynchronous hooks are not supported.' },
|
||||
],
|
||||
},
|
||||
remote: {
|
||||
summary: 'Remote pairing, served by the Engine.',
|
||||
availability: 'review-grant',
|
||||
methods: [
|
||||
{ name: 'status', signature: 'status(): Promise<Status>', summary: 'Whether remote access is on.' },
|
||||
{ name: 'pair', signature: 'pair(options?: { label?: string }): Promise<Pairing>', summary: 'Start a pairing.' },
|
||||
{ name: 'clients', signature: 'clients(): Promise<Client[]>', summary: 'List paired clients.' },
|
||||
{ name: 'revoke', signature: 'revoke(id: string): Promise<void>', summary: 'Revoke one grant by the id pair returned.' },
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
const AVAILABILITY = {
|
||||
always: 'Available to every plugin that injects it.',
|
||||
'tool-invocation': 'Answers only inside a live tool call of the plugin.',
|
||||
'child-agent-hook': 'Answers only inside an agent/created listener, on the child agent.',
|
||||
'native-review': 'Available to a reviewed native plugin.',
|
||||
'review-grant': 'Served by the Engine only when the plugin\'s review granted it.',
|
||||
none: 'No methods.',
|
||||
}
|
||||
|
||||
const EVENTS_SUPPORTED = [
|
||||
{
|
||||
name: 'tools/pre-execute',
|
||||
status: 'supported',
|
||||
mode: 'ordered listeners, native hooks first, then plugin listeners in registration order',
|
||||
signature: 'ctx.on(\'tools/pre-execute\', async (exec: { name: string, callId: string, arguments: object, signal: AbortSignal, workspace: string, mode: string, model: string }, next: () => Promise<Abstention>) => Verdict | undefined)',
|
||||
summary: 'Programmable tool admission for a reviewed native plugin. The frozen call view has no session, agent, tool or approval handle. Return { kind: \'deny\', reason }, { kind: \'ask\', reason? }, { kind: \'revise\', input }, { kind: \'annotate\', text } or { kind: \'abstain\' }; undefined abstains. A malformed answer, error or timeout fails the call closed. The listener batch has a five second deadline.',
|
||||
options: 'prepend and global are refused.',
|
||||
},
|
||||
{
|
||||
name: 'agent/created',
|
||||
status: 'supported',
|
||||
mode: 'listeners of the owning plugin only',
|
||||
signature: 'ctx.on(\'agent/created\', async ({ agent }: { agent: { id: string, ctx: { systemPrompt: { getSectionOrder(name), section(section) }, tools: { restrict(filter: { deny: string[] }) } } } }) => void)',
|
||||
summary: 'Fires for a child agent that this plugin\'s own subagents.startActivation creates, before the child starts. The listener may set the child\'s persona and deny tools to it; the window closes when the listeners return.',
|
||||
options: 'global is refused.',
|
||||
},
|
||||
]
|
||||
|
||||
const REFUSED_EVENT_REASON = 'Not projected by this host: no core projection exists, so a listener would never fire.'
|
||||
|
||||
const ROW_PACKAGES = {
|
||||
'@deepseek-ai/dsh-mcp-client': {
|
||||
summary: 'An MCP server row. The Engine proposes the server and owns the connection.',
|
||||
schema: {
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
required: ['transport', 'serverName'],
|
||||
properties: {
|
||||
transport: { type: 'string', enum: ['stdio', 'streamable-http', 'sse'] },
|
||||
serverName: { type: 'string', pattern: '^[A-Za-z0-9][A-Za-z0-9_-]{0,31}$' },
|
||||
command: { type: 'string', description: 'stdio only: the program to run.' },
|
||||
args: { type: 'array', items: { type: 'string' }, description: 'stdio only.' },
|
||||
env: { type: 'object', maxProperties: 0, description: 'Must be empty or absent: credentials stay with the Engine.' },
|
||||
cwd: { type: 'string', description: 'stdio only: a bundle-relative directory, no .. components.' },
|
||||
url: { type: 'string', description: 'streamable-http and sse: the endpoint, with no credentials, query or fragment.' },
|
||||
headers: { type: 'object', maxProperties: 0, description: 'Must be empty or absent: credentials stay with the Engine.' },
|
||||
toolCallTimeoutMs: { type: 'integer', minimum: 1000, maximum: 86400000, multipleOf: 1000 },
|
||||
failOnStartupError: { type: 'boolean', const: false, description: 'Only false: the Engine connects lazily and reports failures through /mcp.' },
|
||||
},
|
||||
},
|
||||
limits: ['stdio rows name a command; streamable-http and sse rows name a url.', 'Reconnect and instruction bounds belong to the Engine and are not configurable here.'],
|
||||
example: { transport: 'stdio', serverName: 'docs', command: 'node', args: ['server.js'], cwd: '.' },
|
||||
},
|
||||
'@deepseek-ai/dsh-skill-filesystem': {
|
||||
summary: 'A skill folder row, limited to reviewed roots inside the bundle.',
|
||||
schema: {
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
required: ['includeDefaultRoots', 'watch', 'customSkillDirs'],
|
||||
properties: {
|
||||
providerName: { type: 'string', maxLength: 64 },
|
||||
includeDefaultRoots: { type: 'boolean', const: false, description: 'Must be false: ambient roots are not read.' },
|
||||
watch: { type: 'boolean', const: false, description: 'Must be false: there is no file watcher.' },
|
||||
customSkillDirs: { type: 'array', minItems: 1, maxItems: 8, items: { type: 'string' }, description: 'Bundle-relative skill roots, no duplicates.' },
|
||||
},
|
||||
},
|
||||
limits: ['Both includeDefaultRoots and watch must be written out as false.'],
|
||||
example: { includeDefaultRoots: false, watch: false, customSkillDirs: ['skills'] },
|
||||
},
|
||||
'@deepseek-ai/dsh-hooks-claude-code': {
|
||||
summary: 'Claude Code style command hooks read from a reviewed file in the bundle.',
|
||||
schema: {
|
||||
type: 'object',
|
||||
additionalProperties: true,
|
||||
required: ['configPath'],
|
||||
properties: {
|
||||
configPath: { type: 'string', description: 'A regular file inside the bundle, at most 1 MiB, unchanged since review.' },
|
||||
pluginRoot: { type: 'string', description: 'Absent, ".", or this reviewed bundle root.' },
|
||||
defaultTimeoutMs: { type: 'integer', minimum: 1000, maximum: 86400000, multipleOf: 1000, description: 'Whole seconds, used when a hook sets none; default 600000.' },
|
||||
},
|
||||
},
|
||||
limits: ['projectDir is refused: each process uses the current workspace.', 'Only command hooks run; non-command and asynchronous hooks fail the row.', 'At most 128 commands per row.'],
|
||||
example: { configPath: 'hooks/hooks.json' },
|
||||
},
|
||||
'@deepseek-ai/dsh-hooks-codex': {
|
||||
summary: 'Codex style command hooks read from a reviewed file in the bundle.',
|
||||
schema: {
|
||||
type: 'object',
|
||||
additionalProperties: true,
|
||||
required: ['configPath'],
|
||||
properties: {
|
||||
configPath: { type: 'string', description: 'A regular file inside the bundle, at most 1 MiB, unchanged since review.' },
|
||||
pluginRoot: { type: 'string', description: 'Absent, ".", or this reviewed bundle root.' },
|
||||
defaultTimeoutMs: { type: 'integer', minimum: 1000, maximum: 86400000, multipleOf: 1000, description: 'Whole seconds, used when a hook sets none; default 600000.' },
|
||||
},
|
||||
},
|
||||
limits: ['projectDir is refused: each process uses the current workspace.', 'Only command hooks run; non-command and asynchronous hooks fail the row.', 'At most 128 commands per row.'],
|
||||
example: { configPath: 'hooks/hooks.json' },
|
||||
},
|
||||
'@deepseek-ai/dsh-persona': {
|
||||
summary: 'Text added before and after the Engine\'s own prompt for this plugin\'s entry.',
|
||||
schema: {
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
required: ['prefix'],
|
||||
properties: {
|
||||
prefix: { type: 'string', description: 'At most 4096 UTF-8 bytes, no control characters. Only {{model}} and {{cwd}} are expanded.' },
|
||||
suffix: { type: 'string', description: 'Same limits as prefix.' },
|
||||
complete: { type: 'boolean', const: false, description: 'Replacing the whole prompt belongs to the Engine.' },
|
||||
includeRuntimeContext: { type: 'boolean', const: true, description: 'Suppressing the runtime context belongs to the Engine.' },
|
||||
},
|
||||
},
|
||||
limits: ['Needs an exact Engine-selected entry scope.'],
|
||||
example: { prefix: 'Answer in short sentences.' },
|
||||
},
|
||||
}
|
||||
|
||||
const THEME = {
|
||||
tokens: [],
|
||||
colorSchemes: ['light', 'dark'],
|
||||
scheme: 'The frame sets data-theme and color-scheme on its document to the app\'s current scheme and follows changes.',
|
||||
api: [
|
||||
{ name: 'ctx.theme.getTheme', signature: 'getTheme(): { active: { colorScheme: \'light\' | \'dark\' } }', summary: 'The scheme in use now.' },
|
||||
{ name: 'ctx.theme.overrideTokens', signature: 'overrideTokens(source: string, tokens: Record<string, { light: string, dark: string }>): () => void', summary: 'Set custom properties on the frame\'s document for each scheme. source is at most 256 characters; each name starts with -- and is at most 128 characters; each value is at most 512 characters. Returns a disposer.' },
|
||||
],
|
||||
notes: [
|
||||
'The frame defines no named design tokens of its own. There are no --dsw-* tokens to read: a plugin that wants tokens defines them with overrideTokens and uses them in its own styles.',
|
||||
'The frame\'s background is transparent and its margin is zero, so the plugin sits on the app\'s own surface.',
|
||||
],
|
||||
}
|
||||
|
||||
const SEAT_NOTES = {
|
||||
'customize.plugins.detail': { drawn: true, where: 'The plugin\'s own page under Customize, Plugins.' },
|
||||
'conversation.composer.dock': { drawn: false, where: 'Under the composer. The desktop app does not place it yet.' },
|
||||
'conversation.chat.node': { drawn: false, where: 'Inside the conversation. The desktop app does not place it yet.' },
|
||||
'workpane.panel': { drawn: false, where: 'The work pane. The desktop app does not place it yet.' },
|
||||
'sidebar.section': { drawn: false, where: 'The sidebar. The desktop app does not place it yet.' },
|
||||
'command.row': { drawn: false, where: 'The command list. The desktop app does not place it yet.' },
|
||||
}
|
||||
|
||||
function between(text, open, close, what) {
|
||||
const start = text.indexOf(open)
|
||||
if (start < 0) throw new Error(`${what}: cannot find ${open}`)
|
||||
const end = text.indexOf(close, start + open.length)
|
||||
if (end < 0) throw new Error(`${what}: cannot find the end of ${open}`)
|
||||
return text.slice(start + open.length, end)
|
||||
}
|
||||
|
||||
export function parseSeatTable(source) {
|
||||
const text = source.replace(/\r\n/g, '\n')
|
||||
const constants = Object.fromEntries([...text.matchAll(/const ([A-Z_]+): &str = "([^"]+)";/g)].map((m) => [m[1], m[2]]))
|
||||
const resolve = (literal, name) => literal !== undefined ? literal : constants[name]
|
||||
const seatBlock = between(text, 'const PLUGIN_UI_SEATS: [&str; ', '];', 'seat list')
|
||||
const declaredSeats = Number(seatBlock.slice(0, seatBlock.indexOf(']')))
|
||||
const seatBody = seatBlock.slice(seatBlock.indexOf('= [') + 3)
|
||||
const seats = [...seatBody.matchAll(/"([^"]+)"|([A-Z_]+)/g)].map((m) => resolve(m[1], m[2]))
|
||||
if (seats.length !== declaredSeats || seats.some((seat) => seat === undefined)) throw new Error('the seat list does not match its declared length')
|
||||
const tableBlock = between(text, 'const DSH_SEAT_TABLE: [(&str, DshSeat); ', '];\n', 'slot table')
|
||||
const declaredSlots = Number(tableBlock.slice(0, tableBlock.indexOf(']')))
|
||||
const tableBody = tableBlock.slice(tableBlock.indexOf('= [') + 3)
|
||||
const slots = [...tableBody.matchAll(/\(\s*"([^"]+)",\s*DshSeat::(Seat|Unsupported)\(\s*(?:"([^"]*)"|([A-Z_]+))\s*\)\s*,?\s*\)/g)].map((m) => {
|
||||
const value = resolve(m[3], m[4])
|
||||
return m[2] === 'Seat' ? { slot: m[1], seat: value, supported: true } : { slot: m[1], seat: null, supported: false, reason: value }
|
||||
})
|
||||
if (slots.length !== declaredSlots) throw new Error(`the slot table declares ${declaredSlots} rows and ${slots.length} were read`)
|
||||
for (const slot of slots) if (slot.supported && !seats.includes(slot.seat)) throw new Error(`slot ${slot.slot} maps to ${slot.seat}, which is not a seat`)
|
||||
return { seats, slots }
|
||||
}
|
||||
|
||||
export function parseRefusedEvents(source) {
|
||||
const text = source.replace(/\r\n/g, '\n')
|
||||
const body = between(text, 'const unsupportedDshLifecycle = new Set([', '])', 'refused events')
|
||||
return [...body.matchAll(/'([^']+)'/g)].map((m) => m[1])
|
||||
}
|
||||
|
||||
export function parseHookEvents(source) {
|
||||
const text = source.replace(/\r\n/g, '\n')
|
||||
const body = between(text, 'const EVENTS:Record<string,string>={', '}', 'hook events')
|
||||
return [...body.matchAll(/([A-Za-z]+):'([a-z_]+)'/g)].map((m) => ({ event: m[1], point: m[2] }))
|
||||
}
|
||||
|
||||
export function buildCatalog(paths = SOURCES) {
|
||||
const compatibility = JSON.parse(read(paths.compatibility))
|
||||
const provided = Object.entries(compatibility.provided_services).flatMap(([group, names]) => names.map((name) => ({ group, name })))
|
||||
const known = new Set(Object.keys(SERVICES))
|
||||
const missing = provided.filter(({ name }) => !known.has(name)).map(({ name }) => name)
|
||||
if (missing.length) throw new Error(`compatibility.json provides services with no entry in the method table: ${missing.join(', ')}`)
|
||||
const extra = [...known].filter((name) => !provided.some((entry) => entry.name === name))
|
||||
if (extra.length) throw new Error(`the method table names services the host does not provide: ${extra.join(', ')}`)
|
||||
|
||||
const services = provided.map(({ group, name }) => {
|
||||
const entry = SERVICES[name]
|
||||
return {
|
||||
key: name,
|
||||
group,
|
||||
summary: entry.summary,
|
||||
availability: entry.availability,
|
||||
availabilityText: AVAILABILITY[entry.availability],
|
||||
methods: entry.methods,
|
||||
}
|
||||
})
|
||||
|
||||
const refusedEvents = parseRefusedEvents(read(paths.hostRoot))
|
||||
const hookEvents = parseHookEvents(read(paths.shellHooks))
|
||||
const events = [
|
||||
...EVENTS_SUPPORTED,
|
||||
...refusedEvents.map((name) => ({ name, status: 'refused', summary: REFUSED_EVENT_REASON })),
|
||||
]
|
||||
|
||||
const rowPackages = Object.keys(compatibility.row_bridges)
|
||||
const unknownRows = rowPackages.filter((name) => !ROW_PACKAGES[name])
|
||||
const staleRows = Object.keys(ROW_PACKAGES).filter((name) => !rowPackages.includes(name))
|
||||
if (unknownRows.length || staleRows.length) throw new Error(`row packages differ from compatibility.json: missing ${unknownRows.join(', ')}; stale ${staleRows.join(', ')}`)
|
||||
const rows = rowPackages.map((name) => ({
|
||||
id: `row:${name}`,
|
||||
name,
|
||||
bridge: compatibility.row_bridges[name],
|
||||
summary: ROW_PACKAGES[name].summary,
|
||||
limits: ROW_PACKAGES[name].limits,
|
||||
example: ROW_PACKAGES[name].example,
|
||||
...(name.includes('hooks-') ? { hookEvents } : {}),
|
||||
schema: { $schema: 'https://json-schema.org/draft/2020-12/schema', title: name, ...ROW_PACKAGES[name].schema },
|
||||
}))
|
||||
|
||||
const table = parseSeatTable(read(paths.seatTable))
|
||||
const unknownSeatNotes = table.seats.filter((seat) => !SEAT_NOTES[seat])
|
||||
const staleSeatNotes = Object.keys(SEAT_NOTES).filter((seat) => !table.seats.includes(seat))
|
||||
if (unknownSeatNotes.length || staleSeatNotes.length) throw new Error(`seat notes differ from the Engine seat list: missing ${unknownSeatNotes.join(', ')}; stale ${staleSeatNotes.join(', ')}`)
|
||||
const seats = table.seats.map((seat) => ({ seat, drawn: SEAT_NOTES[seat].drawn, where: SEAT_NOTES[seat].where }))
|
||||
|
||||
return {
|
||||
version: 1,
|
||||
targetDshVersion: compatibility.target_dsh_version,
|
||||
host: {
|
||||
services,
|
||||
refusedServices: compatibility.refused_services,
|
||||
events,
|
||||
cordisInternalEvents: 'Cordis fires its own internal/* lifecycle events. They are not part of this host\'s supported contract and are not listed.',
|
||||
config: rows,
|
||||
},
|
||||
client: {
|
||||
seats,
|
||||
slots: table.slots,
|
||||
theme: THEME,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export function renderCatalog(catalog) {
|
||||
return `${JSON.stringify(catalog, null, 2)}\n`
|
||||
}
|
||||
|
||||
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
||||
const text = renderCatalog(buildCatalog())
|
||||
if (process.argv.includes('--check')) {
|
||||
const current = existsOrEmpty(SOURCES.catalog)
|
||||
if (current !== text) {
|
||||
process.stderr.write('inspect-catalog.json is out of date: run node scripts/gen-inspect-catalog.mjs\n')
|
||||
process.exit(1)
|
||||
}
|
||||
} else {
|
||||
writeFileSync(SOURCES.catalog, text)
|
||||
}
|
||||
}
|
||||
|
||||
function existsOrEmpty(path) {
|
||||
try {
|
||||
return read(path)
|
||||
} catch {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,918 @@
|
||||
{
|
||||
"version": 1,
|
||||
"targetDshVersion": "0.2.2-rc.1",
|
||||
"host": {
|
||||
"services": [
|
||||
{
|
||||
"key": "tools",
|
||||
"group": "dsh",
|
||||
"summary": "The tool registry: a plugin adds model-callable tools. The Engine owns calling, approval and results.",
|
||||
"availability": "always",
|
||||
"availabilityText": "Available to every plugin that injects it.",
|
||||
"methods": [
|
||||
{
|
||||
"name": "register",
|
||||
"signature": "register(definition: ToolDefinition): () => void",
|
||||
"summary": "Register one tool built with defineTool from @deepseek-ai/dsh-tools. The definition must stay inside the supported JSON Schema subset (type, oneOf, properties, required, additionalProperties, items, enum, const). A second argument is refused. Returns an idempotent disposer."
|
||||
},
|
||||
{
|
||||
"name": "get",
|
||||
"signature": "get(name: string, agent: Agent): { name: string } | undefined",
|
||||
"summary": "Whether the calling agent has a tool by this name. Answers only inside a live tool call, for that call's own agent."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "commands",
|
||||
"group": "dsh",
|
||||
"summary": "Slash commands a person types. A handler runs only when the person types the command.",
|
||||
"availability": "always",
|
||||
"availabilityText": "Available to every plugin that injects it.",
|
||||
"methods": [
|
||||
{
|
||||
"name": "register",
|
||||
"signature": "register(definition: { name: string, description: string, argumentHint?: string, handler(call: { args: string, signal: AbortSignal }): CommandResult }): () => void",
|
||||
"summary": "Register one slash command. Names start with a lower case letter, then lower case letters, digits, _ and -, at most 64 characters; at most 64 commands per plugin. A name that clashes with a built-in command gets the alias plugin-<plugin-slug>-<command>, and is refused if that alias is invalid or also taken. A name another plugin already registered is refused, so pick a plugin-specific name. Returns an idempotent disposer."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "storage",
|
||||
"group": "dsh",
|
||||
"summary": "Owner-local JSON persistence under the data folder the Engine assigned. It holds no session history and no secrets.",
|
||||
"availability": "always",
|
||||
"availabilityText": "Available to every plugin that injects it.",
|
||||
"methods": [
|
||||
{
|
||||
"name": "get",
|
||||
"signature": "get(key: string): Promise<Json | undefined>",
|
||||
"summary": "Read one stored value."
|
||||
},
|
||||
{
|
||||
"name": "set",
|
||||
"signature": "set(key: string, value: Json): Promise<void>",
|
||||
"summary": "Store one JSON value, within the bounded key, value and owner limits."
|
||||
},
|
||||
{
|
||||
"name": "delete",
|
||||
"signature": "delete(key: string): Promise<void>",
|
||||
"summary": "Remove one stored value."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "skills",
|
||||
"group": "dsh",
|
||||
"summary": "Skill roots: reviewed folders of skills added to the Engine's skill catalog.",
|
||||
"availability": "always",
|
||||
"availabilityText": "Available to every plugin that injects it.",
|
||||
"methods": [
|
||||
{
|
||||
"name": "registerRoot",
|
||||
"signature": "registerRoot(definition: { path: string }): () => void",
|
||||
"summary": "Add a bundle-relative folder of SKILL.md packages. At most 8 roots per plugin. Absolute paths, links, empty components, . and .. are refused."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "sessions",
|
||||
"group": "dsh",
|
||||
"summary": "The session of the calling tool invocation.",
|
||||
"availability": "tool-invocation",
|
||||
"availabilityText": "Answers only inside a live tool call of the plugin.",
|
||||
"methods": [
|
||||
{
|
||||
"name": "flush",
|
||||
"signature": "flush(session: Session): Promise<boolean>",
|
||||
"summary": "Ask the Engine to persist the committed prefix of the exact session of this tool call. Nothing else on sessions is available."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "userQuestions",
|
||||
"group": "dsh",
|
||||
"summary": "Questions put to the person through the Engine's own question card.",
|
||||
"availability": "tool-invocation",
|
||||
"availabilityText": "Answers only inside a live tool call of the plugin.",
|
||||
"methods": [
|
||||
{
|
||||
"name": "ask",
|
||||
"signature": "ask(request: { agent: Agent, questions: Question[], signal?: AbortSignal }): Promise<Answers>",
|
||||
"summary": "Ask the person typed questions during a live root tool call. Timed or continued questions (wait) are not supported."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "subagents",
|
||||
"group": "dsh",
|
||||
"summary": "One independent child agent a root tool call may start.",
|
||||
"availability": "tool-invocation",
|
||||
"availabilityText": "Answers only inside a live tool call of the plugin.",
|
||||
"methods": [
|
||||
{
|
||||
"name": "getProvider",
|
||||
"signature": "getProvider(id: 'spawn'): Provider",
|
||||
"summary": "Describe the one provider: a fresh child with no inherited conversation."
|
||||
},
|
||||
{
|
||||
"name": "startActivation",
|
||||
"signature": "startActivation(options: { provider: 'spawn', childId: string, request: { parent: Agent, prompt: TextBlock[], agentOptions?: { model?: string } }, signal?: AbortSignal, delivery?: 'caller' | 'parent' }): Promise<{ childId: string, messageId: string, result: Promise<ChildResult>, dispose(): Promise<void> }>",
|
||||
"summary": "Start the child through the Engine's approval gate. Resolves to the activation handle { childId, messageId, result, dispose }. Prompts are text only; only the model option is accepted. Needs the live calling agent as parent and is not available inside a child."
|
||||
},
|
||||
{
|
||||
"name": "result",
|
||||
"signature": "activation.result: Promise<{ stopReason: string, output: { type: 'text', text: string }[], diagnostic?: string }>",
|
||||
"summary": "A getter, not a method: read the property, do not call it. Resolves when the child finishes, with its stop reason and its text output blocks; diagnostic notes dropped non-text blocks."
|
||||
},
|
||||
{
|
||||
"name": "dispose",
|
||||
"signature": "activation.dispose(): Promise<void>",
|
||||
"summary": "Stop that exact child and its own descendants."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "agents",
|
||||
"group": "dsh",
|
||||
"summary": "Reserved. The host offers no methods on agents.",
|
||||
"availability": "none",
|
||||
"availabilityText": "No methods.",
|
||||
"methods": []
|
||||
},
|
||||
{
|
||||
"key": "systemPrompt",
|
||||
"group": "dsh",
|
||||
"summary": "The child agent's persona slot, reachable only inside an agent/created listener.",
|
||||
"availability": "child-agent-hook",
|
||||
"availabilityText": "Answers only inside an agent/created listener, on the child agent.",
|
||||
"methods": [
|
||||
{
|
||||
"name": "section",
|
||||
"signature": "agent.ctx.systemPrompt.section({ name: 'deployment:persona-prefix', order: agent.ctx.systemPrompt.getSectionOrder('DEPLOYMENT_PERSONA_PREFIX'), text: string }): () => void",
|
||||
"summary": "Set the child's persona text, at most 32 KiB, once. name must be the string deployment:persona-prefix (the PERSONA_PREFIX_SECTION value) and order must be what getSectionOrder('DEPLOYMENT_PERSONA_PREFIX') returns, which is 0. Anything else is refused. Called anywhere else it fails. The Engine's own system prompt cannot be replaced."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "logger",
|
||||
"group": "cordis",
|
||||
"summary": "Cordis logging. Every line becomes a host log notification attributed to the plugin.",
|
||||
"availability": "always",
|
||||
"availabilityText": "Available to every plugin that injects it.",
|
||||
"methods": [
|
||||
{
|
||||
"name": "logger",
|
||||
"signature": "ctx.logger(name?: string): Logger",
|
||||
"summary": "A named logger with debug, info, warn and error. The host reports error and warn as such and everything else as info."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "events",
|
||||
"group": "cordis",
|
||||
"summary": "Cordis event dispatch. Query the host Event provider for the events this host fires.",
|
||||
"availability": "always",
|
||||
"availabilityText": "Available to every plugin that injects it.",
|
||||
"methods": [
|
||||
{
|
||||
"name": "on",
|
||||
"signature": "ctx.on(name: string, listener: Function, options?: boolean | { prepend?: boolean, global?: boolean }): () => boolean",
|
||||
"summary": "Listen to an event. The host refuses five lifecycle events and limits the options of two (see the Event provider)."
|
||||
},
|
||||
{
|
||||
"name": "once",
|
||||
"signature": "ctx.once(name: string, listener: Function, options?: boolean | { prepend?: boolean, global?: boolean }): () => boolean",
|
||||
"summary": "Listen to an event once."
|
||||
},
|
||||
{
|
||||
"name": "emit",
|
||||
"signature": "ctx.emit(name: string, ...args: any[]): void",
|
||||
"summary": "Dispatch an event to its listeners."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "reflect",
|
||||
"group": "cordis",
|
||||
"summary": "Cordis service reflection. A plugin may provide its own services but not the Engine's.",
|
||||
"availability": "always",
|
||||
"availabilityText": "Available to every plugin that injects it.",
|
||||
"methods": [
|
||||
{
|
||||
"name": "provide",
|
||||
"signature": "ctx.reflect.provide(name: string, value?: any): () => void",
|
||||
"summary": "Provide a service under a name. Names in refusedServices are refused: the Engine owns them."
|
||||
},
|
||||
{
|
||||
"name": "get",
|
||||
"signature": "ctx.reflect.get(name: string, strict?: boolean): any",
|
||||
"summary": "Read a provided service."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "registry",
|
||||
"group": "cordis",
|
||||
"summary": "Cordis plugin registry.",
|
||||
"availability": "always",
|
||||
"availabilityText": "Available to every plugin that injects it.",
|
||||
"methods": [
|
||||
{
|
||||
"name": "get",
|
||||
"signature": "ctx.registry.get(plugin: Plugin): Runtime | undefined",
|
||||
"summary": "Look up the runtime record of a plugin."
|
||||
},
|
||||
{
|
||||
"name": "resolve",
|
||||
"signature": "ctx.registry.resolve(plugin: Plugin): Function | undefined",
|
||||
"summary": "Resolve a plugin shape to its callback."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "prompt",
|
||||
"group": "codewhale",
|
||||
"summary": "Attributed sections of the Engine's runtime prompt. A plugin cannot replace the system prompt.",
|
||||
"availability": "always",
|
||||
"availabilityText": "Available to every plugin that injects it.",
|
||||
"methods": [
|
||||
{
|
||||
"name": "registerSection",
|
||||
"signature": "registerSection(definition: { id: string, text: string, interpolate?: 'model-cwd' }): () => void",
|
||||
"summary": "Contribute a plain-text section of at most 4 KiB; at most 128 sections per plugin. The id is lower case letters, digits, _ and -, starts with a letter, at most 64 characters. With interpolate set, only {{model}} and {{cwd}} are expanded."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "avatars",
|
||||
"group": "codewhale",
|
||||
"summary": "Avatar packs for the terminal.",
|
||||
"availability": "always",
|
||||
"availabilityText": "Available to every plugin that injects it.",
|
||||
"methods": [
|
||||
{
|
||||
"name": "registerPack",
|
||||
"signature": "registerPack(definition: { path: string }): () => void",
|
||||
"summary": "Register a reviewed bundle-relative avatar pack: a path ending in .json, at most 4 packs per plugin."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "mcp",
|
||||
"group": "codewhale",
|
||||
"summary": "Proposals of MCP servers. The Engine owns connection, credentials and tool calls.",
|
||||
"availability": "native-review",
|
||||
"availabilityText": "Available to a reviewed native plugin.",
|
||||
"methods": [
|
||||
{
|
||||
"name": "registerServer",
|
||||
"signature": "registerServer(definition: { serverName: string, server: Record<string, unknown> }): () => void",
|
||||
"summary": "Propose one server. serverName is a letter or digit followed by up to 31 letters, digits, _ or -. headers and env must stay empty and the url may carry no credentials, query or fragment. At most 64 servers per plugin; at most 64 KiB per definition."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "shellHooks",
|
||||
"group": "codewhale",
|
||||
"summary": "Claude Code and Codex style command hooks, run by the Engine.",
|
||||
"availability": "native-review",
|
||||
"availabilityText": "Available to a reviewed native plugin.",
|
||||
"methods": [
|
||||
{
|
||||
"name": "register",
|
||||
"signature": "register(definition: { dialect: 'claude-code' | 'codex', point: string, matcher?: string, hook: { event: string, command: string, timeout_secs: number, background: false, continue_on_error: false, name: string } }): () => void",
|
||||
"summary": "Register one command hook for a reviewed native plugin. Forced continuation, observer steering, non-command and asynchronous hooks are not supported."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "remote",
|
||||
"group": "codewhale",
|
||||
"summary": "Remote pairing, served by the Engine.",
|
||||
"availability": "review-grant",
|
||||
"availabilityText": "Served by the Engine only when the plugin's review granted it.",
|
||||
"methods": [
|
||||
{
|
||||
"name": "status",
|
||||
"signature": "status(): Promise<Status>",
|
||||
"summary": "Whether remote access is on."
|
||||
},
|
||||
{
|
||||
"name": "pair",
|
||||
"signature": "pair(options?: { label?: string }): Promise<Pairing>",
|
||||
"summary": "Start a pairing."
|
||||
},
|
||||
{
|
||||
"name": "clients",
|
||||
"signature": "clients(): Promise<Client[]>",
|
||||
"summary": "List paired clients."
|
||||
},
|
||||
{
|
||||
"name": "revoke",
|
||||
"signature": "revoke(id: string): Promise<void>",
|
||||
"summary": "Revoke one grant by the id pair returned."
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"refusedServices": [
|
||||
"shellHooks",
|
||||
"loader",
|
||||
"approval",
|
||||
"agents",
|
||||
"subagents",
|
||||
"sessions",
|
||||
"llm",
|
||||
"sandboxPolicy",
|
||||
"credentials",
|
||||
"fs",
|
||||
"subprocess",
|
||||
"systemPrompt",
|
||||
"runtimeLoop",
|
||||
"tools",
|
||||
"commands",
|
||||
"prompt",
|
||||
"storage",
|
||||
"skills",
|
||||
"avatars",
|
||||
"mcp",
|
||||
"logger",
|
||||
"userQuestions"
|
||||
],
|
||||
"events": [
|
||||
{
|
||||
"name": "tools/pre-execute",
|
||||
"status": "supported",
|
||||
"mode": "ordered listeners, native hooks first, then plugin listeners in registration order",
|
||||
"signature": "ctx.on('tools/pre-execute', async (exec: { name: string, callId: string, arguments: object, signal: AbortSignal, workspace: string, mode: string, model: string }, next: () => Promise<Abstention>) => Verdict | undefined)",
|
||||
"summary": "Programmable tool admission for a reviewed native plugin. The frozen call view has no session, agent, tool or approval handle. Return { kind: 'deny', reason }, { kind: 'ask', reason? }, { kind: 'revise', input }, { kind: 'annotate', text } or { kind: 'abstain' }; undefined abstains. A malformed answer, error or timeout fails the call closed. The listener batch has a five second deadline.",
|
||||
"options": "prepend and global are refused."
|
||||
},
|
||||
{
|
||||
"name": "agent/created",
|
||||
"status": "supported",
|
||||
"mode": "listeners of the owning plugin only",
|
||||
"signature": "ctx.on('agent/created', async ({ agent }: { agent: { id: string, ctx: { systemPrompt: { getSectionOrder(name), section(section) }, tools: { restrict(filter: { deny: string[] }) } } } }) => void)",
|
||||
"summary": "Fires for a child agent that this plugin's own subagents.startActivation creates, before the child starts. The listener may set the child's persona and deny tools to it; the window closes when the listeners return.",
|
||||
"options": "global is refused."
|
||||
},
|
||||
{
|
||||
"name": "agent/pre-step",
|
||||
"status": "refused",
|
||||
"summary": "Not projected by this host: no core projection exists, so a listener would never fire."
|
||||
},
|
||||
{
|
||||
"name": "agent/turn-stopping",
|
||||
"status": "refused",
|
||||
"summary": "Not projected by this host: no core projection exists, so a listener would never fire."
|
||||
},
|
||||
{
|
||||
"name": "tools/post-execute",
|
||||
"status": "refused",
|
||||
"summary": "Not projected by this host: no core projection exists, so a listener would never fire."
|
||||
},
|
||||
{
|
||||
"name": "subagent/start",
|
||||
"status": "refused",
|
||||
"summary": "Not projected by this host: no core projection exists, so a listener would never fire."
|
||||
},
|
||||
{
|
||||
"name": "subagent/end",
|
||||
"status": "refused",
|
||||
"summary": "Not projected by this host: no core projection exists, so a listener would never fire."
|
||||
}
|
||||
],
|
||||
"cordisInternalEvents": "Cordis fires its own internal/* lifecycle events. They are not part of this host's supported contract and are not listed.",
|
||||
"config": [
|
||||
{
|
||||
"id": "row:@deepseek-ai/dsh-mcp-client",
|
||||
"name": "@deepseek-ai/dsh-mcp-client",
|
||||
"bridge": "mcp",
|
||||
"summary": "An MCP server row. The Engine proposes the server and owns the connection.",
|
||||
"limits": [
|
||||
"stdio rows name a command; streamable-http and sse rows name a url.",
|
||||
"Reconnect and instruction bounds belong to the Engine and are not configurable here."
|
||||
],
|
||||
"example": {
|
||||
"transport": "stdio",
|
||||
"serverName": "docs",
|
||||
"command": "node",
|
||||
"args": [
|
||||
"server.js"
|
||||
],
|
||||
"cwd": "."
|
||||
},
|
||||
"schema": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"title": "@deepseek-ai/dsh-mcp-client",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"transport",
|
||||
"serverName"
|
||||
],
|
||||
"properties": {
|
||||
"transport": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"stdio",
|
||||
"streamable-http",
|
||||
"sse"
|
||||
]
|
||||
},
|
||||
"serverName": {
|
||||
"type": "string",
|
||||
"pattern": "^[A-Za-z0-9][A-Za-z0-9_-]{0,31}$"
|
||||
},
|
||||
"command": {
|
||||
"type": "string",
|
||||
"description": "stdio only: the program to run."
|
||||
},
|
||||
"args": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "stdio only."
|
||||
},
|
||||
"env": {
|
||||
"type": "object",
|
||||
"maxProperties": 0,
|
||||
"description": "Must be empty or absent: credentials stay with the Engine."
|
||||
},
|
||||
"cwd": {
|
||||
"type": "string",
|
||||
"description": "stdio only: a bundle-relative directory, no .. components."
|
||||
},
|
||||
"url": {
|
||||
"type": "string",
|
||||
"description": "streamable-http and sse: the endpoint, with no credentials, query or fragment."
|
||||
},
|
||||
"headers": {
|
||||
"type": "object",
|
||||
"maxProperties": 0,
|
||||
"description": "Must be empty or absent: credentials stay with the Engine."
|
||||
},
|
||||
"toolCallTimeoutMs": {
|
||||
"type": "integer",
|
||||
"minimum": 1000,
|
||||
"maximum": 86400000,
|
||||
"multipleOf": 1000
|
||||
},
|
||||
"failOnStartupError": {
|
||||
"type": "boolean",
|
||||
"const": false,
|
||||
"description": "Only false: the Engine connects lazily and reports failures through /mcp."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "row:@deepseek-ai/dsh-skill-filesystem",
|
||||
"name": "@deepseek-ai/dsh-skill-filesystem",
|
||||
"bridge": "skills",
|
||||
"summary": "A skill folder row, limited to reviewed roots inside the bundle.",
|
||||
"limits": [
|
||||
"Both includeDefaultRoots and watch must be written out as false."
|
||||
],
|
||||
"example": {
|
||||
"includeDefaultRoots": false,
|
||||
"watch": false,
|
||||
"customSkillDirs": [
|
||||
"skills"
|
||||
]
|
||||
},
|
||||
"schema": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"title": "@deepseek-ai/dsh-skill-filesystem",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"includeDefaultRoots",
|
||||
"watch",
|
||||
"customSkillDirs"
|
||||
],
|
||||
"properties": {
|
||||
"providerName": {
|
||||
"type": "string",
|
||||
"maxLength": 64
|
||||
},
|
||||
"includeDefaultRoots": {
|
||||
"type": "boolean",
|
||||
"const": false,
|
||||
"description": "Must be false: ambient roots are not read."
|
||||
},
|
||||
"watch": {
|
||||
"type": "boolean",
|
||||
"const": false,
|
||||
"description": "Must be false: there is no file watcher."
|
||||
},
|
||||
"customSkillDirs": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"maxItems": 8,
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "Bundle-relative skill roots, no duplicates."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "row:@deepseek-ai/dsh-hooks-claude-code",
|
||||
"name": "@deepseek-ai/dsh-hooks-claude-code",
|
||||
"bridge": "hooks-claude-code",
|
||||
"summary": "Claude Code style command hooks read from a reviewed file in the bundle.",
|
||||
"limits": [
|
||||
"projectDir is refused: each process uses the current workspace.",
|
||||
"Only command hooks run; non-command and asynchronous hooks fail the row.",
|
||||
"At most 128 commands per row."
|
||||
],
|
||||
"example": {
|
||||
"configPath": "hooks/hooks.json"
|
||||
},
|
||||
"hookEvents": [
|
||||
{
|
||||
"event": "SessionStart",
|
||||
"point": "session_start"
|
||||
},
|
||||
{
|
||||
"event": "UserPromptSubmit",
|
||||
"point": "message_submit"
|
||||
},
|
||||
{
|
||||
"event": "PreToolUse",
|
||||
"point": "tool_call_before"
|
||||
},
|
||||
{
|
||||
"event": "PostToolUse",
|
||||
"point": "tool_call_after"
|
||||
},
|
||||
{
|
||||
"event": "Stop",
|
||||
"point": "turn_end"
|
||||
},
|
||||
{
|
||||
"event": "SubagentStart",
|
||||
"point": "subagent_spawn"
|
||||
},
|
||||
{
|
||||
"event": "SubagentStop",
|
||||
"point": "subagent_complete"
|
||||
}
|
||||
],
|
||||
"schema": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"title": "@deepseek-ai/dsh-hooks-claude-code",
|
||||
"type": "object",
|
||||
"additionalProperties": true,
|
||||
"required": [
|
||||
"configPath"
|
||||
],
|
||||
"properties": {
|
||||
"configPath": {
|
||||
"type": "string",
|
||||
"description": "A regular file inside the bundle, at most 1 MiB, unchanged since review."
|
||||
},
|
||||
"pluginRoot": {
|
||||
"type": "string",
|
||||
"description": "Absent, \".\", or this reviewed bundle root."
|
||||
},
|
||||
"defaultTimeoutMs": {
|
||||
"type": "integer",
|
||||
"minimum": 1000,
|
||||
"maximum": 86400000,
|
||||
"multipleOf": 1000,
|
||||
"description": "Whole seconds, used when a hook sets none; default 600000."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "row:@deepseek-ai/dsh-hooks-codex",
|
||||
"name": "@deepseek-ai/dsh-hooks-codex",
|
||||
"bridge": "hooks-codex",
|
||||
"summary": "Codex style command hooks read from a reviewed file in the bundle.",
|
||||
"limits": [
|
||||
"projectDir is refused: each process uses the current workspace.",
|
||||
"Only command hooks run; non-command and asynchronous hooks fail the row.",
|
||||
"At most 128 commands per row."
|
||||
],
|
||||
"example": {
|
||||
"configPath": "hooks/hooks.json"
|
||||
},
|
||||
"hookEvents": [
|
||||
{
|
||||
"event": "SessionStart",
|
||||
"point": "session_start"
|
||||
},
|
||||
{
|
||||
"event": "UserPromptSubmit",
|
||||
"point": "message_submit"
|
||||
},
|
||||
{
|
||||
"event": "PreToolUse",
|
||||
"point": "tool_call_before"
|
||||
},
|
||||
{
|
||||
"event": "PostToolUse",
|
||||
"point": "tool_call_after"
|
||||
},
|
||||
{
|
||||
"event": "Stop",
|
||||
"point": "turn_end"
|
||||
},
|
||||
{
|
||||
"event": "SubagentStart",
|
||||
"point": "subagent_spawn"
|
||||
},
|
||||
{
|
||||
"event": "SubagentStop",
|
||||
"point": "subagent_complete"
|
||||
}
|
||||
],
|
||||
"schema": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"title": "@deepseek-ai/dsh-hooks-codex",
|
||||
"type": "object",
|
||||
"additionalProperties": true,
|
||||
"required": [
|
||||
"configPath"
|
||||
],
|
||||
"properties": {
|
||||
"configPath": {
|
||||
"type": "string",
|
||||
"description": "A regular file inside the bundle, at most 1 MiB, unchanged since review."
|
||||
},
|
||||
"pluginRoot": {
|
||||
"type": "string",
|
||||
"description": "Absent, \".\", or this reviewed bundle root."
|
||||
},
|
||||
"defaultTimeoutMs": {
|
||||
"type": "integer",
|
||||
"minimum": 1000,
|
||||
"maximum": 86400000,
|
||||
"multipleOf": 1000,
|
||||
"description": "Whole seconds, used when a hook sets none; default 600000."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "row:@deepseek-ai/dsh-persona",
|
||||
"name": "@deepseek-ai/dsh-persona",
|
||||
"bridge": "persona",
|
||||
"summary": "Text added before and after the Engine's own prompt for this plugin's entry.",
|
||||
"limits": [
|
||||
"Needs an exact Engine-selected entry scope."
|
||||
],
|
||||
"example": {
|
||||
"prefix": "Answer in short sentences."
|
||||
},
|
||||
"schema": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"title": "@deepseek-ai/dsh-persona",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"prefix"
|
||||
],
|
||||
"properties": {
|
||||
"prefix": {
|
||||
"type": "string",
|
||||
"description": "At most 4096 UTF-8 bytes, no control characters. Only {{model}} and {{cwd}} are expanded."
|
||||
},
|
||||
"suffix": {
|
||||
"type": "string",
|
||||
"description": "Same limits as prefix."
|
||||
},
|
||||
"complete": {
|
||||
"type": "boolean",
|
||||
"const": false,
|
||||
"description": "Replacing the whole prompt belongs to the Engine."
|
||||
},
|
||||
"includeRuntimeContext": {
|
||||
"type": "boolean",
|
||||
"const": true,
|
||||
"description": "Suppressing the runtime context belongs to the Engine."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"client": {
|
||||
"seats": [
|
||||
{
|
||||
"seat": "customize.plugins.detail",
|
||||
"drawn": true,
|
||||
"where": "The plugin's own page under Customize, Plugins."
|
||||
},
|
||||
{
|
||||
"seat": "conversation.composer.dock",
|
||||
"drawn": false,
|
||||
"where": "Under the composer. The desktop app does not place it yet."
|
||||
},
|
||||
{
|
||||
"seat": "conversation.chat.node",
|
||||
"drawn": false,
|
||||
"where": "Inside the conversation. The desktop app does not place it yet."
|
||||
},
|
||||
{
|
||||
"seat": "workpane.panel",
|
||||
"drawn": false,
|
||||
"where": "The work pane. The desktop app does not place it yet."
|
||||
},
|
||||
{
|
||||
"seat": "sidebar.section",
|
||||
"drawn": false,
|
||||
"where": "The sidebar. The desktop app does not place it yet."
|
||||
},
|
||||
{
|
||||
"seat": "command.row",
|
||||
"drawn": false,
|
||||
"where": "The command list. The desktop app does not place it yet."
|
||||
}
|
||||
],
|
||||
"slots": [
|
||||
{
|
||||
"slot": "plugins.bundle.config",
|
||||
"seat": "customize.plugins.detail",
|
||||
"supported": true
|
||||
},
|
||||
{
|
||||
"slot": "plugins.row.config",
|
||||
"seat": "customize.plugins.detail",
|
||||
"supported": true
|
||||
},
|
||||
{
|
||||
"slot": "plugins.tab",
|
||||
"seat": "customize.plugins.detail",
|
||||
"supported": true
|
||||
},
|
||||
{
|
||||
"slot": "plugins.detail.section",
|
||||
"seat": "customize.plugins.detail",
|
||||
"supported": true
|
||||
},
|
||||
{
|
||||
"slot": "settings.section",
|
||||
"seat": "customize.plugins.detail",
|
||||
"supported": true
|
||||
},
|
||||
{
|
||||
"slot": "settings.plugin.item",
|
||||
"seat": "customize.plugins.detail",
|
||||
"supported": true
|
||||
},
|
||||
{
|
||||
"slot": "settings.general.item",
|
||||
"seat": "customize.plugins.detail",
|
||||
"supported": true
|
||||
},
|
||||
{
|
||||
"slot": "settings.plugins.tab",
|
||||
"seat": "customize.plugins.detail",
|
||||
"supported": true
|
||||
},
|
||||
{
|
||||
"slot": "conversation.chat.node",
|
||||
"seat": "conversation.chat.node",
|
||||
"supported": true
|
||||
},
|
||||
{
|
||||
"slot": "conversation.chat.turnTail",
|
||||
"seat": "conversation.chat.node",
|
||||
"supported": true
|
||||
},
|
||||
{
|
||||
"slot": "conversation.chat.assistant-actions",
|
||||
"seat": "conversation.chat.node",
|
||||
"supported": true
|
||||
},
|
||||
{
|
||||
"slot": "conversation.chat.text-interaction",
|
||||
"seat": "conversation.chat.node",
|
||||
"supported": true
|
||||
},
|
||||
{
|
||||
"slot": "conversation.user.text.node",
|
||||
"seat": "conversation.chat.node",
|
||||
"supported": true
|
||||
},
|
||||
{
|
||||
"slot": "tool.call.toolview",
|
||||
"seat": "conversation.chat.node",
|
||||
"supported": true
|
||||
},
|
||||
{
|
||||
"slot": "conversation.view",
|
||||
"seat": "conversation.chat.node",
|
||||
"supported": true
|
||||
},
|
||||
{
|
||||
"slot": "conversation.input.dock",
|
||||
"seat": "conversation.composer.dock",
|
||||
"supported": true
|
||||
},
|
||||
{
|
||||
"slot": "conversation.input.overlay",
|
||||
"seat": "conversation.composer.dock",
|
||||
"supported": true
|
||||
},
|
||||
{
|
||||
"slot": "conversation.input.right",
|
||||
"seat": "conversation.composer.dock",
|
||||
"supported": true
|
||||
},
|
||||
{
|
||||
"slot": "conversation.composer.dock",
|
||||
"seat": "conversation.composer.dock",
|
||||
"supported": true
|
||||
},
|
||||
{
|
||||
"slot": "sidebar.right.pane.tab",
|
||||
"seat": "workpane.panel",
|
||||
"supported": true
|
||||
},
|
||||
{
|
||||
"slot": "main.conversation",
|
||||
"seat": "workpane.panel",
|
||||
"supported": true
|
||||
},
|
||||
{
|
||||
"slot": "main.page-menu",
|
||||
"seat": "workpane.panel",
|
||||
"supported": true
|
||||
},
|
||||
{
|
||||
"slot": "sidebar.panellist",
|
||||
"seat": "sidebar.section",
|
||||
"supported": true
|
||||
},
|
||||
{
|
||||
"slot": "sidebar.workspaces",
|
||||
"seat": "sidebar.section",
|
||||
"supported": true
|
||||
},
|
||||
{
|
||||
"slot": "sidebar.footer.action",
|
||||
"seat": "sidebar.section",
|
||||
"supported": true
|
||||
},
|
||||
{
|
||||
"slot": "sidebar.session.row.leading",
|
||||
"seat": "sidebar.section",
|
||||
"supported": true
|
||||
},
|
||||
{
|
||||
"slot": "sidebar.workspaces.session.row.action",
|
||||
"seat": "sidebar.section",
|
||||
"supported": true
|
||||
},
|
||||
{
|
||||
"slot": "conversation.session.header.actions",
|
||||
"seat": null,
|
||||
"supported": false,
|
||||
"reason": "no ADE seat yet"
|
||||
},
|
||||
{
|
||||
"slot": "conversation.session.header.utilities",
|
||||
"seat": null,
|
||||
"supported": false,
|
||||
"reason": "no ADE seat yet"
|
||||
},
|
||||
{
|
||||
"slot": "contextMenu.icon",
|
||||
"seat": null,
|
||||
"supported": false,
|
||||
"reason": "no ADE seat yet"
|
||||
}
|
||||
],
|
||||
"theme": {
|
||||
"tokens": [],
|
||||
"colorSchemes": [
|
||||
"light",
|
||||
"dark"
|
||||
],
|
||||
"scheme": "The frame sets data-theme and color-scheme on its document to the app's current scheme and follows changes.",
|
||||
"api": [
|
||||
{
|
||||
"name": "ctx.theme.getTheme",
|
||||
"signature": "getTheme(): { active: { colorScheme: 'light' | 'dark' } }",
|
||||
"summary": "The scheme in use now."
|
||||
},
|
||||
{
|
||||
"name": "ctx.theme.overrideTokens",
|
||||
"signature": "overrideTokens(source: string, tokens: Record<string, { light: string, dark: string }>): () => void",
|
||||
"summary": "Set custom properties on the frame's document for each scheme. source is at most 256 characters; each name starts with -- and is at most 128 characters; each value is at most 512 characters. Returns a disposer."
|
||||
}
|
||||
],
|
||||
"notes": [
|
||||
"The frame defines no named design tokens of its own. There are no --dsw-* tokens to read: a plugin that wants tokens defines them with overrideTokens and uses them in its own styles.",
|
||||
"The frame's background is transparent and its margin is zero, so the plugin sits on the app's own surface."
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,245 @@
|
||||
import { isJson } from '../json.ts'
|
||||
import { assertSupportedJsonSchema, validateJsonSchemaValue } from './dsh-tools-compat.js'
|
||||
import catalogData from './inspect-catalog.json' with { type: 'json' }
|
||||
|
||||
export type JsonValue = null | boolean | number | string | JsonValue[] | { [key: string]: JsonValue }
|
||||
export type InspectPlatform = 'host' | 'client'
|
||||
|
||||
export interface InspectMethod {
|
||||
name: string
|
||||
description: string
|
||||
inputSchema: JsonValue
|
||||
outputSchema: JsonValue
|
||||
}
|
||||
|
||||
export interface InspectProviderView {
|
||||
platform: InspectPlatform
|
||||
id: string
|
||||
description: string
|
||||
methods: InspectMethod[]
|
||||
}
|
||||
|
||||
export interface InstalledRow {
|
||||
id: string
|
||||
name: string
|
||||
state: string
|
||||
}
|
||||
|
||||
export interface ClientState {
|
||||
connected: boolean
|
||||
drawnSeats?: readonly string[]
|
||||
}
|
||||
|
||||
export interface InspectSources {
|
||||
tools?: (agent: unknown, signal: AbortSignal) => Promise<readonly JsonValue[]> | readonly JsonValue[]
|
||||
installed?: (signal: AbortSignal) => Promise<readonly InstalledRow[]> | readonly InstalledRow[]
|
||||
client?: () => ClientState
|
||||
}
|
||||
|
||||
export interface CordisInspect {
|
||||
list(): InspectProviderView[]
|
||||
query(platform: string, provider: string, method: string, input: JsonValue | undefined, agent?: unknown, signal?: AbortSignal): Promise<JsonValue>
|
||||
}
|
||||
|
||||
interface Provider {
|
||||
platform: InspectPlatform
|
||||
id: string
|
||||
description: string
|
||||
methods: InspectMethod[]
|
||||
run(method: string, input: Record<string, JsonValue>, agent: unknown, signal: AbortSignal): Promise<any> | any
|
||||
}
|
||||
|
||||
const catalog = catalogData as any
|
||||
const EMPTY_INPUT: JsonValue = { type: 'object', properties: {}, additionalProperties: false }
|
||||
const ANY_OUTPUT: JsonValue = { description: 'JSON data owned by this inspect provider.' }
|
||||
const NO_CLIENT = 'No client is connected. Plugin screens are drawn by the desktop app; in a terminal nothing is drawn, so every slot reports drawn: false.'
|
||||
|
||||
const exactInput = (field: string, description: string): JsonValue => ({
|
||||
type: 'object',
|
||||
properties: { [field]: { type: 'string', description } },
|
||||
additionalProperties: false,
|
||||
})
|
||||
|
||||
const method = (name: string, description: string, inputSchema: JsonValue = EMPTY_INPUT, outputSchema: JsonValue = ANY_OUTPUT): InspectMethod =>
|
||||
({ name, description, inputSchema, outputSchema })
|
||||
|
||||
const copy = <T>(value: T): T => JSON.parse(JSON.stringify(value))
|
||||
|
||||
function serviceView(service: any): JsonValue {
|
||||
return {
|
||||
key: service.key,
|
||||
group: service.group,
|
||||
summary: service.summary,
|
||||
availability: service.availability,
|
||||
availabilityText: service.availabilityText,
|
||||
methods: service.methods,
|
||||
}
|
||||
}
|
||||
|
||||
function serviceProvider(): Provider {
|
||||
return {
|
||||
platform: 'host', id: 'Service',
|
||||
description: 'The services this host gives plugins, with their methods, and the services a plugin may not provide. A name that is not listed does not exist here.',
|
||||
methods: [method('listService', 'Without input: the directory of services with one-line summaries and method signatures. With service: that service\'s exact contract, with availability. These are services a plugin injects, not DSH\'s full catalogue.',
|
||||
exactInput('service', 'Exact service key. Omit it for the directory.'))],
|
||||
run(_name, input) {
|
||||
const key = input.service
|
||||
if (key === undefined) {
|
||||
return {
|
||||
services: catalog.host.services.map((s: any) => ({ key: s.key, group: s.group, summary: s.summary, availability: s.availability, signatures: s.methods.map((m: any) => m.signature) })),
|
||||
refusedServices: catalog.host.refusedServices,
|
||||
note: 'refusedServices are names a plugin may not provide: the Engine owns them. Service methods named by the host Event and Config providers are separate.',
|
||||
targetDshVersion: catalog.targetDshVersion,
|
||||
}
|
||||
}
|
||||
const found = catalog.host.services.find((s: any) => s.key === key)
|
||||
if (!found) throw new Error(`Host service "${String(key)}" is not provided by this host. Call without service for the directory.`)
|
||||
return { service: serviceView(found), targetDshVersion: catalog.targetDshVersion }
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function eventProvider(): Provider {
|
||||
return {
|
||||
platform: 'host', id: 'Event',
|
||||
description: 'The events a plugin may listen to in this host, and the ones the host refuses.',
|
||||
methods: [method('listEvents', 'Without input: the directory of events with status supported or refused. With event: its exact contract.',
|
||||
exactInput('event', 'Exact event name. Omit it for the directory.'))],
|
||||
run(_name, input) {
|
||||
const key = input.event
|
||||
if (key === undefined) {
|
||||
return {
|
||||
events: catalog.host.events.map((e: any) => ({ name: e.name, status: e.status, summary: e.summary })),
|
||||
note: catalog.host.cordisInternalEvents,
|
||||
}
|
||||
}
|
||||
const found = catalog.host.events.find((e: any) => e.name === key)
|
||||
if (!found) throw new Error(`Host event "${String(key)}" is not known to this host. Call without event for the directory.`)
|
||||
return { event: found }
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const CONFIG_INPUT: JsonValue = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
entry: { type: 'string', description: 'Exact entry id from the directory; returns that entry\'s Config schema.' },
|
||||
name: { type: 'string', description: 'Exact package or plugin name; limits the directory to it.' },
|
||||
offset: { type: 'integer', description: 'Zero-based directory offset; defaults to 0.' },
|
||||
limit: { type: 'integer', description: 'Directory page size, from 1 to 100; defaults to 25.' },
|
||||
},
|
||||
additionalProperties: false,
|
||||
}
|
||||
|
||||
function configProvider(sources: InspectSources): Provider {
|
||||
return {
|
||||
platform: 'host', id: 'Config',
|
||||
description: 'Config a composition row may carry: the packages a row may name, each with the config it accepts, and the plugins installed on this host with their state.',
|
||||
methods: [method('listConfigs', 'Without entry: a page of the directory (row packages first, then installed plugins) with total and nextOffset. With entry: one row package\'s exact JSON Schema, limits and a valid example. Installed plugins are listed with their state; their own config is not described here.', CONFIG_INPUT)],
|
||||
async run(_name, input, _agent, signal) {
|
||||
const rows: any[] = catalog.host.config.map((r: any) => ({ id: r.id, name: r.name, kind: 'row-package', status: 'schema', bridge: r.bridge, summary: r.summary }))
|
||||
const installed = sources.installed ? await sources.installed(signal) : []
|
||||
for (const row of installed) rows.push({ id: `plugin:${row.id}`, name: row.name, kind: 'installed-plugin', status: row.state })
|
||||
if (input.entry !== undefined) {
|
||||
const pkg = catalog.host.config.find((r: any) => r.id === input.entry)
|
||||
if (pkg) return { entry: pkg.id, name: pkg.name, kind: 'row-package', bridge: pkg.bridge, summary: pkg.summary, limits: pkg.limits, example: pkg.example, ...pkg.hookEvents ? { hookEvents: pkg.hookEvents } : {}, schema: pkg.schema }
|
||||
const live = rows.find((r) => r.id === input.entry)
|
||||
if (live) return { entry: live.id, name: live.name, kind: live.kind, status: live.status, schema: null, note: 'An installed plugin\'s own Config is not described by this host.' }
|
||||
throw new Error(`Host Config entry "${String(input.entry)}" is not listed. Call without entry for the directory.`)
|
||||
}
|
||||
const filtered = input.name === undefined ? rows : rows.filter((r) => r.name === input.name)
|
||||
const offset = typeof input.offset === 'number' ? input.offset : 0
|
||||
const limit = typeof input.limit === 'number' ? input.limit : 25
|
||||
if (offset < 0 || limit < 1 || limit > 100) throw new Error('Host Config directory needs offset >= 0 and 1 <= limit <= 100')
|
||||
const page = filtered.slice(offset, offset + limit)
|
||||
const next = offset + page.length
|
||||
return { entries: page, total: filtered.length, ...next < filtered.length ? { nextOffset: next } : {} }
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function toolProvider(sources: InspectSources): Provider | undefined {
|
||||
const source = sources.tools
|
||||
if (!source) return undefined
|
||||
return {
|
||||
platform: 'host', id: 'Tool',
|
||||
description: 'Tools visible to the requesting agent.',
|
||||
methods: [method('listTools', 'Return every tool schema currently callable by this agent.')],
|
||||
async run(_name, _input, agent, signal) {
|
||||
return { tools: [...await source(agent, signal)] }
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const SUBTREE_INPUT = exactInput('root', 'Exact slot name to select. Omit it for every slot.')
|
||||
|
||||
function slotsProvider(sources: InspectSources): Provider {
|
||||
return {
|
||||
platform: 'client', id: 'Slots',
|
||||
description: 'The DSH slot names this host accepts, the seat each maps to, and whether the connected client draws that seat.',
|
||||
methods: [method('listSubTree', 'Return every slot with its seat and drawn state; with root, also that slot selected. Slots are static names here, not a live tree, and their props are not described by this host: a plugin registers on a slot, the Engine serves it on the mapped seat, and the client draws only the seats it places.', SUBTREE_INPUT)],
|
||||
run(_name, input) {
|
||||
const state = sources.client?.() ?? { connected: false }
|
||||
const drawn = new Set(state.connected ? state.drawnSeats ?? catalog.client.seats.filter((s: any) => s.drawn).map((s: any) => s.seat) : [])
|
||||
const slots = catalog.client.slots.map((s: any) => ({
|
||||
type: 'slot', name: s.slot, seat: s.seat, supported: s.supported,
|
||||
drawn: s.supported && drawn.has(s.seat),
|
||||
...s.reason ? { reason: s.reason } : {},
|
||||
}))
|
||||
const seats = catalog.client.seats.map((s: any) => ({ seat: s.seat, drawn: drawn.has(s.seat), where: s.where }))
|
||||
const out: Record<string, JsonValue> = {
|
||||
client: state.connected ? { connected: true } : { connected: false, message: NO_CLIENT },
|
||||
seats, trees: slots, referencedTypes: [],
|
||||
}
|
||||
if (input.root !== undefined) {
|
||||
const selected = slots.find((s: any) => s.name === input.root)
|
||||
out.requestedRoot = { name: input.root, available: selected !== undefined }
|
||||
if (selected) out.selected = selected
|
||||
}
|
||||
return out
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function themeProvider(): Provider {
|
||||
return {
|
||||
platform: 'client', id: 'Theme',
|
||||
description: 'What the plugin frame provides for theming.',
|
||||
methods: [method('listTokens', 'Return the named tokens the frame provides (none), the colour schemes, and the theme calls a plugin has.')],
|
||||
run() {
|
||||
return { tokens: catalog.client.theme.tokens, colorSchemes: catalog.client.theme.colorSchemes, scheme: catalog.client.theme.scheme, api: catalog.client.theme.api, notes: catalog.client.theme.notes, referencedTypes: [] }
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export function createCordisInspect(sources: InspectSources = {}): CordisInspect {
|
||||
const providers: Provider[] = [serviceProvider(), eventProvider(), configProvider(sources), toolProvider(sources), slotsProvider(sources), themeProvider()]
|
||||
.filter((p): p is Provider => p !== undefined)
|
||||
for (const provider of providers) {
|
||||
if (provider.id.trim() === '' || provider.description.trim() === '') throw new Error('Cordis inspect provider needs an id and a description')
|
||||
for (const m of provider.methods) {
|
||||
assertSupportedJsonSchema(m.inputSchema)
|
||||
assertSupportedJsonSchema(m.outputSchema)
|
||||
}
|
||||
}
|
||||
const label = (platform: InspectPlatform) => platform === 'host' ? 'Host' : 'Client'
|
||||
return {
|
||||
list() {
|
||||
return providers.map((p) => ({ platform: p.platform, id: p.id, description: p.description, methods: copy(p.methods) }))
|
||||
},
|
||||
async query(platform, providerId, methodName, input, agent, signal = new AbortController().signal) {
|
||||
if (platform !== 'host' && platform !== 'client') throw new Error(`Cordis inspect platform must be "host" or "client", not "${String(platform)}"`)
|
||||
const provider = providers.find((p) => p.platform === platform && p.id === providerId)
|
||||
if (!provider) throw new Error(`${label(platform)} Cordis inspect provider "${providerId}" is not registered`)
|
||||
const found = provider.methods.find((m) => m.name === methodName)
|
||||
if (!found) throw new Error(`Cordis inspect provider "${provider.id}" has no method "${methodName}"`)
|
||||
const violations = validateJsonSchemaValue(found.inputSchema, input ?? {}, 'input') as string[]
|
||||
if (violations.length > 0) throw new Error(`${label(platform)} Cordis inspect ${provider.id}.${found.name} rejected input: ${violations.join('; ')}`)
|
||||
signal.throwIfAborted()
|
||||
const data = await provider.run(methodName, (input ?? {}) as Record<string, JsonValue>, agent, signal)
|
||||
signal.throwIfAborted()
|
||||
if (!isJson(data)) throw new Error(`${label(platform)} Cordis inspect ${provider.id}.${found.name} returned a non-JSON value`)
|
||||
return copy(data)
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { mkdtempSync, readFileSync, rmSync } from 'node:fs'
|
||||
import { build } from 'esbuild'
|
||||
import { dirname, join } from 'node:path'
|
||||
import { after, test } from 'node:test'
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url'
|
||||
import { buildCatalog, parseSeatTable, renderCatalog } from '../scripts/gen-inspect-catalog.mjs'
|
||||
import { createCordisInspect } from '../src/dsh/inspect.ts'
|
||||
|
||||
const here = dirname(fileURLToPath(import.meta.url))
|
||||
const src = (path) => join(here, '..', 'src', path)
|
||||
const text = (path) => readFileSync(path, 'utf8').replace(/\r\n/g, '\n')
|
||||
const compatibility = JSON.parse(text(src('dsh/compatibility.json')))
|
||||
const catalog = JSON.parse(text(src('dsh/inspect-catalog.json')))
|
||||
const seatSource = text(join(here, '..', '..', '..', 'app-server', 'src', 'runtime_api', 'plugins.rs'))
|
||||
const rootSource = text(src('root.ts'))
|
||||
const inspect = createCordisInspect()
|
||||
const ask = (platform, provider, method, input) => inspect.query(platform, provider, method, input)
|
||||
|
||||
test('the committed catalog is what the generator writes now', () => {
|
||||
assert.equal(text(src('dsh/inspect-catalog.json')), renderCatalog(buildCatalog()))
|
||||
})
|
||||
|
||||
test('list names the providers DSH calls and leaves out Tool until a source exists', async () => {
|
||||
const ids = inspect.list().map((p) => `${p.platform}.${p.id}.${p.methods.map((m) => m.name).join(',')}`)
|
||||
assert.deepEqual(ids, ['host.Service.listService', 'host.Event.listEvents', 'host.Config.listConfigs', 'client.Slots.listSubTree', 'client.Theme.listTokens'])
|
||||
const withTools = createCordisInspect({ tools: () => [{ name: 'read_file' }] })
|
||||
assert.ok(withTools.list().some((p) => p.id === 'Tool'))
|
||||
assert.deepEqual(await withTools.query('host', 'Tool', 'listTools', undefined, {}), { tools: [{ name: 'read_file' }] })
|
||||
})
|
||||
|
||||
test('errors use DSH registry wording', async () => {
|
||||
await assert.rejects(ask('host', 'Nope', 'x'), /Host Cordis inspect provider "Nope" is not registered/)
|
||||
await assert.rejects(ask('client', 'Nope', 'x'), /Client Cordis inspect provider "Nope" is not registered/)
|
||||
await assert.rejects(ask('host', 'Service', 'nope'), /Cordis inspect provider "Service" has no method "nope"/)
|
||||
await assert.rejects(ask('host', 'Service', 'listService', { service: 3 }), /Host Cordis inspect Service.listService rejected input/)
|
||||
await assert.rejects(ask('host', 'Tool', 'listTools'), /not registered/)
|
||||
await assert.rejects(ask('page', 'Service', 'listService'), /platform must be/)
|
||||
})
|
||||
|
||||
test('every service the catalog names is provided, and no provided service is missing', () => {
|
||||
const provided = Object.values(compatibility.provided_services).flat().sort()
|
||||
assert.deepEqual(catalog.host.services.map((s) => s.key).sort(), provided)
|
||||
assert.deepEqual(catalog.host.refusedServices, compatibility.refused_services)
|
||||
})
|
||||
|
||||
test('methods match the compatibility tables where it has them', () => {
|
||||
const tables = { sessions: 'session_methods', userQuestions: 'user_question_methods', subagents: 'subagent_methods', agents: 'agents_methods', systemPrompt: 'system_prompt_methods' }
|
||||
for (const [service, table] of Object.entries(tables)) {
|
||||
const named = catalog.host.services.find((s) => s.key === service).methods.map((m) => m.name).sort()
|
||||
assert.deepEqual(named, Object.keys(compatibility[table]).sort(), service)
|
||||
}
|
||||
})
|
||||
|
||||
const classBody = (source, name) => {
|
||||
const start = source.indexOf(`class ${name} extends Service`)
|
||||
assert.ok(start >= 0, `class ${name}`)
|
||||
const rest = source.slice(start + 1)
|
||||
const next = rest.search(/\n\s*(export )?(class |function )/)
|
||||
return next < 0 ? rest : rest.slice(0, next)
|
||||
}
|
||||
const defines = (body, name) => new RegExp(`^\\s*(async\\s+)?${name}\\s*\\(`, 'm').test(body)
|
||||
|
||||
test('every curated method is defined on the class that serves it', () => {
|
||||
const served = {
|
||||
tools: ['root.ts', 'ToolsShim'], commands: ['shims/commands.ts', 'CommandsShim'], storage: ['root.ts', 'StorageShim'],
|
||||
skills: ['shims/skills.ts', 'SkillsShim'], prompt: ['shims/prompt.ts', 'PromptShim'], avatars: ['shims/avatars.ts', 'AvatarsShim'],
|
||||
mcp: ['shims/mcp.ts', 'McpShim'], shellHooks: ['shims/shell-hooks.ts', 'ShellHooks'], remote: ['dsh/remote.ts', 'RemoteService'],
|
||||
sessions: ['dsh/session-prompt.ts', 'SessionsService'], userQuestions: ['root.ts', 'UserQuestionsShim'],
|
||||
subagents: ['dsh/agents.ts', 'SubagentsShim'], systemPrompt: ['dsh/session-prompt.ts', 'SystemPromptService'],
|
||||
}
|
||||
const cordis = { events: ['events', ['on', 'once', 'emit']], reflect: ['reflect', ['provide', 'get']], registry: ['registry', ['get', 'resolve']] }
|
||||
const agentsSource = text(src('dsh/agents.ts'))
|
||||
for (const service of catalog.host.services) {
|
||||
if (service.key === 'logger' || service.key === 'agents') continue
|
||||
if (cordis[service.key]) {
|
||||
const [file, names] = cordis[service.key]
|
||||
const body = text(join(here, '..', 'node_modules', '@deepseek-ai', 'cordis', 'lib', 'types', `${file}.d.ts`))
|
||||
assert.deepEqual(service.methods.map((m) => m.name).sort(), [...names].sort(), service.key)
|
||||
for (const name of names) assert.ok(defines(body, name), `${service.key}.${name}`)
|
||||
continue
|
||||
}
|
||||
const [file, cls] = served[service.key]
|
||||
const body = classBody(text(src(file)), cls)
|
||||
for (const m of service.methods) {
|
||||
if (service.key === 'subagents' && m.name === 'result') assert.match(agentsSource, /^\s*get result\(\)/m, 'subagents.result is a getter')
|
||||
else if (service.key === 'subagents' && m.name === 'dispose') assert.match(agentsSource, /^\s*dispose\(\)/m, 'subagents.dispose')
|
||||
else assert.ok(defines(body, m.name), `${service.key}.${m.name}`)
|
||||
}
|
||||
}
|
||||
assert.match(agentsSource, /^\s*section\(section: unknown\)/m)
|
||||
assert.match(agentsSource, /^\s*getSectionOrder\(name: unknown\)/m)
|
||||
})
|
||||
|
||||
test('the catalog states the contracts the host actually enforces', () => {
|
||||
const agentsSource = text(src('dsh/agents.ts'))
|
||||
const personaName = text(src('dsh/dsh-message-compat.js')).match(/PERSONA_PREFIX_SECTION = "([^"]+)"/)[1]
|
||||
const section = catalog.host.services.find((s) => s.key === 'systemPrompt').methods[0]
|
||||
assert.ok(section.signature.includes(`name: '${personaName}'`))
|
||||
assert.ok(section.signature.includes("getSectionOrder('DEPLOYMENT_PERSONA_PREFIX')"))
|
||||
assert.match(agentsSource, /PERSONA_ORDER = 0/)
|
||||
const subagents = catalog.host.services.find((s) => s.key === 'subagents').methods
|
||||
const result = subagents.find((m) => m.name === 'result')
|
||||
assert.match(result.signature, /^activation\.result: Promise<\{ stopReason: string, output: \{ type: 'text', text: string \}\[\]/)
|
||||
assert.ok(!result.signature.includes('result()'))
|
||||
assert.match(subagents.find((m) => m.name === 'startActivation').signature, /Promise<\{ childId: string, messageId: string, result: Promise</)
|
||||
})
|
||||
|
||||
test('an output that is not JSON is refused as DSH refuses it', async () => {
|
||||
const bad = createCordisInspect({ tools: () => [{ name: 'x', extra: undefined }] })
|
||||
await assert.rejects(bad.query('host', 'Tool', 'listTools', undefined, {}), /returned a non-JSON value/)
|
||||
})
|
||||
|
||||
test('every slot and its seat equal the Engine seat table', () => {
|
||||
const block = seatSource.slice(seatSource.indexOf('const DSH_SEAT_TABLE'), seatSource.indexOf('];', seatSource.indexOf('const DSH_SEAT_TABLE')))
|
||||
const names = [...block.matchAll(/\(\s*"([^"]+)",\s*DshSeat::/g)].map((m) => m[1])
|
||||
assert.equal(names.length, 30)
|
||||
assert.deepEqual(catalog.client.slots.map((s) => s.slot), names)
|
||||
const unsupported = [...block.matchAll(/\(\s*"([^"]+)",\s*DshSeat::Unsupported/g)].map((m) => m[1])
|
||||
assert.deepEqual(catalog.client.slots.filter((s) => !s.supported).map((s) => s.slot), unsupported)
|
||||
const seats = [...seatSource.slice(seatSource.indexOf('const PLUGIN_UI_SEATS'), seatSource.indexOf('];', seatSource.indexOf('const PLUGIN_UI_SEATS'))).matchAll(/"([^"]+)"/g)].map((m) => m[1])
|
||||
for (const slot of catalog.client.slots.filter((s) => s.supported)) assert.ok(slot.seat === 'customize.plugins.detail' || seats.includes(slot.seat), slot.slot)
|
||||
assert.equal(catalog.client.seats.length, 6)
|
||||
assert.deepEqual(parseSeatTable(seatSource.replace(/\n/g, '\r\n')), parseSeatTable(seatSource))
|
||||
})
|
||||
|
||||
test('the refused events equal the list in root.ts', () => {
|
||||
const body = rootSource.slice(rootSource.indexOf('unsupportedDshLifecycle = new Set(['), rootSource.indexOf('])', rootSource.indexOf('unsupportedDshLifecycle = new Set([')))
|
||||
const refused = [...body.matchAll(/'([^']+)'/g)].map((m) => m[1])
|
||||
assert.equal(refused.length, 5)
|
||||
assert.deepEqual(catalog.host.events.filter((e) => e.status === 'refused').map((e) => e.name), refused)
|
||||
for (const name of catalog.host.events.filter((e) => e.status === 'supported').map((e) => e.name)) assert.ok(rootSource.includes(`'${name}'`) || text(src('dsh/agents.ts')).includes(`'${name}'`), name)
|
||||
})
|
||||
|
||||
test('the Service provider answers a directory, one contract, and refuses a name the host lacks', async () => {
|
||||
const directory = await ask('host', 'Service', 'listService')
|
||||
assert.equal(directory.services.length, 18)
|
||||
assert.ok(!directory.services.some((s) => s.key === 'fs' || s.key === 'llm'))
|
||||
const one = await ask('host', 'Service', 'listService', { service: 'prompt' })
|
||||
assert.equal(one.service.methods[0].name, 'registerSection')
|
||||
await assert.rejects(ask('host', 'Service', 'listService', { service: 'llm' }), /not provided by this host/)
|
||||
})
|
||||
|
||||
test('the Event provider separates supported from refused', async () => {
|
||||
const { events } = await ask('host', 'Event', 'listEvents')
|
||||
assert.deepEqual(events.map((e) => e.status), ['supported', 'supported', 'refused', 'refused', 'refused', 'refused', 'refused'])
|
||||
assert.equal((await ask('host', 'Event', 'listEvents', { event: 'tools/pre-execute' })).event.name, 'tools/pre-execute')
|
||||
await assert.rejects(ask('host', 'Event', 'listEvents', { event: 'subagent/start-x' }), /not known/)
|
||||
})
|
||||
|
||||
test('the Config provider lists exactly the row packages, pages, and shows installed rows', async () => {
|
||||
const page = await ask('host', 'Config', 'listConfigs')
|
||||
assert.deepEqual(page.entries.map((e) => e.name).sort(), Object.keys(compatibility.row_bridges).sort())
|
||||
assert.equal(page.total, 5)
|
||||
const paged = await ask('host', 'Config', 'listConfigs', { limit: 2 })
|
||||
assert.equal(paged.entries.length, 2)
|
||||
assert.equal(paged.nextOffset, 2)
|
||||
const schema = await ask('host', 'Config', 'listConfigs', { entry: 'row:@deepseek-ai/dsh-persona' })
|
||||
assert.deepEqual(schema.schema.required, ['prefix'])
|
||||
await assert.rejects(ask('host', 'Config', 'listConfigs', { limit: 500 }), /limit/)
|
||||
await assert.rejects(ask('host', 'Config', 'listConfigs', { entry: 'row:nope' }), /not listed/)
|
||||
const live = createCordisInspect({ installed: () => [{ id: 'abc', name: 'hello', state: 'active' }] })
|
||||
const after = await live.query('host', 'Config', 'listConfigs', { name: 'hello' })
|
||||
assert.deepEqual(after.entries, [{ id: 'plugin:abc', name: 'hello', kind: 'installed-plugin', status: 'active' }])
|
||||
})
|
||||
|
||||
test('each row example passes the bridge that reads it', async () => {
|
||||
const dir = mkdtempSync(join(here, '..', 'node_modules', '.cw-inspect-'))
|
||||
after(() => rmSync(dir, { recursive: true, force: true }))
|
||||
const bridges = join(dir, 'bridges.mjs')
|
||||
await build({
|
||||
stdin: { contents: "export { normalizePersonaConfig } from './src/dsh/persona.ts'\nexport { reviewedMcpModule } from './src/dsh/mcp.ts'\nexport { reviewedSkillModule } from './src/dsh/skill-filesystem.ts'", resolveDir: join(here, '..'), loader: 'ts' },
|
||||
outfile: bridges, bundle: true, platform: 'node', format: 'esm', packages: 'external', absWorkingDir: join(here, '..'), logLevel: 'silent',
|
||||
})
|
||||
const { normalizePersonaConfig, reviewedMcpModule, reviewedSkillModule } = await import(pathToFileURL(bridges).href)
|
||||
const example = (name) => catalog.host.config.find((r) => r.name === name).example
|
||||
assert.doesNotThrow(() => normalizePersonaConfig(example('@deepseek-ai/dsh-persona')))
|
||||
assert.throws(() => normalizePersonaConfig({ prefix: 'x', complete: true }))
|
||||
const servers = []
|
||||
reviewedMcpModule().apply({ mcp: { registerServer: (d) => servers.push(d) } }, example('@deepseek-ai/dsh-mcp-client'))
|
||||
assert.equal(servers[0].serverName, 'docs')
|
||||
const roots = []
|
||||
reviewedSkillModule().apply({ skills: { registerRoot: (d) => roots.push(d) } }, example('@deepseek-ai/dsh-skill-filesystem'))
|
||||
assert.equal(roots.length, 1)
|
||||
})
|
||||
|
||||
test('Slots says no client is connected in a terminal and marks no slot drawn', async () => {
|
||||
const out = await ask('client', 'Slots', 'listSubTree')
|
||||
assert.equal(out.client.connected, false)
|
||||
assert.match(out.client.message, /No client is connected/)
|
||||
assert.equal(out.trees.length, 30)
|
||||
assert.ok(out.trees.every((s) => s.drawn === false))
|
||||
assert.deepEqual((await ask('client', 'Slots', 'listSubTree', { root: 'conversation.composer.dock' })).selected.seat, 'conversation.composer.dock')
|
||||
assert.equal((await ask('client', 'Slots', 'listSubTree', { root: 'nope' })).requestedRoot.available, false)
|
||||
})
|
||||
|
||||
test('Slots marks only the drawn seats when a client is connected', async () => {
|
||||
const connected = createCordisInspect({ client: () => ({ connected: true }) })
|
||||
const out = await connected.query('client', 'Slots', 'listSubTree')
|
||||
const drawn = out.trees.filter((s) => s.drawn).map((s) => s.name)
|
||||
assert.deepEqual(drawn, ['plugins.bundle.config', 'plugins.row.config', 'plugins.tab', 'plugins.detail.section', 'settings.section', 'settings.plugin.item', 'settings.general.item', 'settings.plugins.tab'])
|
||||
const all = createCordisInspect({ client: () => ({ connected: true, drawnSeats: ['conversation.composer.dock'] }) })
|
||||
const composer = (await all.query('client', 'Slots', 'listSubTree')).trees.filter((s) => s.drawn).map((s) => s.name)
|
||||
assert.ok(composer.includes('conversation.input.dock') && !composer.includes('plugins.tab'))
|
||||
assert.ok((await ask('client', 'Slots', 'listSubTree')).trees.filter((s) => !s.supported).every((s) => s.seat === null && s.reason))
|
||||
})
|
||||
|
||||
test('Theme reports that the frame defines no named tokens', async () => {
|
||||
const out = await ask('client', 'Theme', 'listTokens')
|
||||
assert.deepEqual(out.tokens, [])
|
||||
assert.deepEqual(out.colorSchemes, ['light', 'dark'])
|
||||
assert.ok(out.api.some((a) => a.name === 'ctx.theme.overrideTokens'))
|
||||
})
|
||||
@@ -44,8 +44,9 @@ use super::CodewhaleClient;
|
||||
use super::prepared::WireDialect;
|
||||
use super::role_placement::{RolePlacement, role_placement};
|
||||
use super::wire::{
|
||||
extract_sse_data_value, from_api_tool_name, next_sse_line, push_sse_event_data,
|
||||
to_api_tool_name,
|
||||
ToolNameWire, check_history_tool_names, documented_tool_name_limit, extract_sse_data_value,
|
||||
from_api_tool_name, next_sse_line, over_limit_tool_name_message, push_sse_event_data,
|
||||
to_api_tool_name, tool_name_exceeds_limit, tool_name_limit,
|
||||
};
|
||||
|
||||
/// Maximum `cache_control` breakpoints Anthropic accepts per request.
|
||||
@@ -103,6 +104,11 @@ impl CodewhaleClient {
|
||||
repair_dangling_tool_uses(&mut messages);
|
||||
body["messages"] = Value::Array(messages);
|
||||
|
||||
let tool_name_limit = tool_name_limit(self.api_provider, ToolNameWire::AnthropicMessages);
|
||||
check_history_tool_names(
|
||||
request,
|
||||
documented_tool_name_limit(self.api_provider, ToolNameWire::AnthropicMessages),
|
||||
)?;
|
||||
let mut omitted_tool_names = Vec::new();
|
||||
let mut omitted_logical_names = Vec::new();
|
||||
if let Some(tools) = request.tools.as_ref()
|
||||
@@ -113,6 +119,11 @@ impl CodewhaleClient {
|
||||
};
|
||||
let mut wire_tools = Vec::new();
|
||||
for tool in tools {
|
||||
if tool_name_exceeds_limit(&tool.name, tool_name_limit) {
|
||||
omitted_logical_names.push(tool.name.clone());
|
||||
omitted_tool_names.push(to_api_tool_name(&tool.name));
|
||||
continue;
|
||||
}
|
||||
match project_tool_schema(route_projection, &tool.input_schema, tool.strict) {
|
||||
ToolSchemaProjection::Exact {
|
||||
parameters,
|
||||
@@ -155,6 +166,9 @@ impl CodewhaleClient {
|
||||
if let Some(name) = forced_tool_name(tool_choice)
|
||||
&& omitted_logical_names.iter().any(|omitted| omitted == name)
|
||||
{
|
||||
if tool_name_exceeds_limit(name, tool_name_limit) {
|
||||
anyhow::bail!(over_limit_tool_name_message(name, tool_name_limit));
|
||||
}
|
||||
anyhow::bail!(
|
||||
"cannot force an omitted tool because its input schema is incompatible with this route"
|
||||
);
|
||||
@@ -1305,6 +1319,75 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn over_long_encoded_tool_names_are_withheld_at_128() {
|
||||
let client = anthropic_test_client(None);
|
||||
let tool = |name: &str| Tool {
|
||||
tool_type: None,
|
||||
name: name.to_string(),
|
||||
description: "d".to_string(),
|
||||
input_schema: json!({"type": "object", "additionalProperties": false}),
|
||||
allowed_callers: None,
|
||||
defer_loading: None,
|
||||
input_examples: None,
|
||||
strict: None,
|
||||
cache_control: None,
|
||||
};
|
||||
let mid = "mcp__tracker__projects.v2.issues.list-by-assignee";
|
||||
let long = format!(
|
||||
"mcp__tracker__{}.v2.issues.list-by-assignee",
|
||||
"a".repeat(70)
|
||||
);
|
||||
assert!(to_api_tool_name(mid).len() > 64 && to_api_tool_name(mid).len() <= 128);
|
||||
assert!(to_api_tool_name(&long).len() > 128);
|
||||
|
||||
let mut request = request_with("claude-sonnet-4-6", None, None, None);
|
||||
request.tools = Some(vec![tool("read_file"), tool(mid), tool(&long)]);
|
||||
let (body, omitted) = client
|
||||
.build_anthropic_wire_body(&request, true)
|
||||
.expect("body builds");
|
||||
let sent: Vec<&str> = body["tools"]
|
||||
.as_array()
|
||||
.expect("tools")
|
||||
.iter()
|
||||
.filter_map(|t| t["name"].as_str())
|
||||
.collect();
|
||||
assert_eq!(sent, vec!["read_file".to_string(), to_api_tool_name(mid)]);
|
||||
assert_eq!(omitted, vec![to_api_tool_name(&long)]);
|
||||
|
||||
request.tool_choice = Some(json!({"type": "tool", "name": long}));
|
||||
let error = client
|
||||
.build_anthropic_wire_body(&request, true)
|
||||
.expect_err("forcing a withheld tool is refused");
|
||||
assert!(error.to_string().contains("at most 128"), "{error}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_100_character_encoded_name_is_offered_on_an_anthropic_wire_route_without_a_documented_limit()
|
||||
{
|
||||
let tool = |name: &str| Tool {
|
||||
tool_type: None,
|
||||
name: name.to_string(),
|
||||
description: "d".to_string(),
|
||||
input_schema: json!({"type": "object", "additionalProperties": false}),
|
||||
allowed_callers: None,
|
||||
defer_loading: None,
|
||||
input_examples: None,
|
||||
strict: None,
|
||||
cache_control: None,
|
||||
};
|
||||
let name = "a".repeat(100);
|
||||
assert_eq!(to_api_tool_name(&name).len(), 100);
|
||||
let client = minimax_test_client();
|
||||
let mut request = request_with("MiniMax-M2", None, None, None);
|
||||
request.tools = Some(vec![tool(&name)]);
|
||||
let (body, omitted) = client
|
||||
.build_anthropic_wire_body(&request, true)
|
||||
.expect("body builds");
|
||||
assert_eq!(body["tools"][0]["name"], name);
|
||||
assert!(omitted.is_empty());
|
||||
}
|
||||
|
||||
fn request_with(
|
||||
model: &str,
|
||||
reasoning_effort: Option<&str>,
|
||||
|
||||
@@ -41,8 +41,10 @@ use super::reasoning_effort::apply_reasoning_effort;
|
||||
use super::role_placement::{RolePlacement, role_placement};
|
||||
use super::wire::{
|
||||
ERROR_BODY_MAX_BYTES, SSE_BACKPRESSURE_HIGH_WATERMARK, SSE_BACKPRESSURE_SLEEP_MS,
|
||||
SSE_MAX_LINES_PER_CHUNK, acquire_stream_buffer, bounded_error_text, from_api_tool_name,
|
||||
parse_usage, release_stream_buffer, system_to_instructions, to_api_tool_name,
|
||||
SSE_MAX_LINES_PER_CHUNK, ToolNameWire, acquire_stream_buffer, bounded_error_text,
|
||||
check_history_tool_names, documented_tool_name_limit, from_api_tool_name,
|
||||
over_limit_tool_name_message, parse_usage, release_stream_buffer, system_to_instructions,
|
||||
to_api_tool_name, tool_name_exceeds_limit, tool_name_limit,
|
||||
};
|
||||
use super::wire::{extract_sse_data_value, flush_sse_line, push_sse_event_data, take_sse_line};
|
||||
use codewhale_config::route::RouteLimits;
|
||||
@@ -1208,11 +1210,20 @@ pub fn build_chat_wire_body_with_window(
|
||||
body["top_p"] = json!(top_p);
|
||||
}
|
||||
let mut omitted_tool_names = Vec::new();
|
||||
let tool_name_limit = tool_name_limit(provider, ToolNameWire::OpenAiCompatible);
|
||||
check_history_tool_names(
|
||||
request,
|
||||
documented_tool_name_limit(provider, ToolNameWire::OpenAiCompatible),
|
||||
)?;
|
||||
if let Some(tools) = request.tools.as_ref() {
|
||||
let route_projection =
|
||||
chat_route_tool_projection(provider, base_url, strict_tools_disabled);
|
||||
let mut chat_tools = Vec::new();
|
||||
for tool in tools {
|
||||
if tool_name_exceeds_limit(&tool.name, tool_name_limit) {
|
||||
omitted_tool_names.push(to_api_tool_name(&tool.name));
|
||||
continue;
|
||||
}
|
||||
match project_tool_schema(route_projection, &tool.input_schema, tool.strict) {
|
||||
ToolSchemaProjection::Exact {
|
||||
parameters,
|
||||
@@ -1247,6 +1258,9 @@ pub fn build_chat_wire_body_with_window(
|
||||
.iter()
|
||||
.any(|omitted| omitted == name || *omitted == to_api_tool_name(name))
|
||||
{
|
||||
if tool_name_exceeds_limit(name, tool_name_limit) {
|
||||
bail!(over_limit_tool_name_message(name, tool_name_limit));
|
||||
}
|
||||
bail!(
|
||||
"cannot force an omitted tool because its input schema is incompatible with this route"
|
||||
);
|
||||
@@ -3255,11 +3269,25 @@ fn deepseek_base_url_supports_strict_tools(base_url: &str) -> bool {
|
||||
|
||||
fn map_tool_choice_for_chat(choice: &Value) -> Option<Value> {
|
||||
if let Some(choice_str) = choice.as_str() {
|
||||
return Some(json!(choice_str));
|
||||
return Some(match forced_tool_name(choice) {
|
||||
Some(name) => json!({
|
||||
"type": "function",
|
||||
"function": { "name": to_api_tool_name(name) }
|
||||
}),
|
||||
None => json!(choice_str),
|
||||
});
|
||||
}
|
||||
let Some(choice_type) = choice.get("type").and_then(Value::as_str) else {
|
||||
return Some(choice.clone());
|
||||
};
|
||||
if choice_type == "function"
|
||||
&& let Some(name) = forced_tool_name(choice)
|
||||
{
|
||||
return Some(json!({
|
||||
"type": "function",
|
||||
"function": { "name": to_api_tool_name(name) }
|
||||
}));
|
||||
}
|
||||
|
||||
match choice_type {
|
||||
"auto" | "none" => Some(json!(choice_type)),
|
||||
@@ -6664,9 +6692,13 @@ mod image_block_wire_tests {
|
||||
//! most of them at once. The shape is fixed by OpenAI's spec: a `user`
|
||||
//! message whose `content` is an array of parts, with the image as
|
||||
//! `{"type":"image_url","image_url":{"url":…}}`.
|
||||
use super::{ProviderKind, build_chat_messages, build_chat_wire_body};
|
||||
use super::{
|
||||
ChatWireBody, ProviderKind, build_chat_messages, build_chat_wire_body,
|
||||
map_tool_choice_for_chat, to_api_tool_name,
|
||||
};
|
||||
use codewhale_models::Role;
|
||||
use codewhale_models::{ContentBlock, ImageUrlContent, Message, MessageRequest};
|
||||
use codewhale_models::{ContentBlock, ImageUrlContent, Message, MessageRequest, Tool};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
const DATA_URL: &str = "data:image/png;base64,QUJD";
|
||||
|
||||
@@ -6973,6 +7005,220 @@ mod image_block_wire_tests {
|
||||
}
|
||||
}
|
||||
|
||||
fn named_tool(name: &str) -> Tool {
|
||||
Tool {
|
||||
tool_type: None,
|
||||
name: name.to_string(),
|
||||
description: "d".to_string(),
|
||||
input_schema: json!({"type": "object", "additionalProperties": false}),
|
||||
allowed_callers: None,
|
||||
defer_loading: None,
|
||||
input_examples: None,
|
||||
strict: None,
|
||||
cache_control: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn function_object_tool_choice_encodes_the_tool_name() {
|
||||
let encoded = to_api_tool_name("web.run");
|
||||
assert_eq!(encoded, "web-x00002E-run");
|
||||
let expected = json!({"type": "function", "function": {"name": encoded}});
|
||||
for choice in [
|
||||
json!({"type": "function", "function": {"name": "web.run"}}),
|
||||
json!({"type": "function", "name": "web.run"}),
|
||||
json!({"type": "tool", "name": "web.run"}),
|
||||
json!("web.run"),
|
||||
] {
|
||||
assert_eq!(
|
||||
map_tool_choice_for_chat(&choice),
|
||||
Some(expected.clone()),
|
||||
"{choice}"
|
||||
);
|
||||
}
|
||||
assert_eq!(
|
||||
map_tool_choice_for_chat(&json!("required")),
|
||||
Some(json!("required"))
|
||||
);
|
||||
|
||||
let mut request = request_with_image();
|
||||
request.tools = Some(vec![named_tool("read_file"), named_tool("web.run")]);
|
||||
request.tool_choice = Some(json!({"type": "function", "function": {"name": "web.run"}}));
|
||||
let body = build_chat_wire_body(
|
||||
&request,
|
||||
ProviderKind::Openai,
|
||||
"https://api.openai.com/v1",
|
||||
false,
|
||||
None,
|
||||
)
|
||||
.expect("wire body");
|
||||
assert_eq!(body.body["tool_choice"], expected);
|
||||
let declared: Vec<&str> = body.body["tools"]
|
||||
.as_array()
|
||||
.expect("tools")
|
||||
.iter()
|
||||
.filter_map(|tool| tool.pointer("/function/name").and_then(Value::as_str))
|
||||
.collect();
|
||||
assert!(declared.contains(&"web-x00002E-run"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn over_long_encoded_tool_names_are_withheld_by_provider_limit() {
|
||||
let mid = "mcp__tracker__projects.v2.issues.list-by-assignee";
|
||||
let long = format!(
|
||||
"mcp__tracker__{}.v2.issues.list-by-assignee",
|
||||
"a".repeat(70)
|
||||
);
|
||||
let mid_len = to_api_tool_name(mid).len();
|
||||
assert!(mid_len > 64 && mid_len <= 128, "{mid_len}");
|
||||
assert!(to_api_tool_name(&long).len() > 128);
|
||||
|
||||
let build = |provider, base_url: &str, choice: Option<Value>| {
|
||||
let mut request = request_with_image();
|
||||
request.tools = Some(vec![
|
||||
named_tool("read_file"),
|
||||
named_tool(mid),
|
||||
named_tool(&long),
|
||||
]);
|
||||
request.tool_choice = choice;
|
||||
build_chat_wire_body(&request, provider, base_url, false, None)
|
||||
};
|
||||
let declared = |body: &ChatWireBody| -> Vec<String> {
|
||||
body.body["tools"]
|
||||
.as_array()
|
||||
.expect("tools")
|
||||
.iter()
|
||||
.filter_map(|tool| tool.pointer("/function/name").and_then(Value::as_str))
|
||||
.map(str::to_string)
|
||||
.collect()
|
||||
};
|
||||
|
||||
let openai = build(ProviderKind::Openai, "https://api.openai.com/v1", None).expect("body");
|
||||
assert_eq!(declared(&openai), vec!["read_file".to_string()]);
|
||||
assert_eq!(
|
||||
openai.omitted_tool_names,
|
||||
vec![to_api_tool_name(mid), to_api_tool_name(&long)]
|
||||
);
|
||||
|
||||
let deepseek =
|
||||
build(ProviderKind::Deepseek, "https://api.deepseek.com", None).expect("body");
|
||||
assert_eq!(
|
||||
declared(&deepseek),
|
||||
vec!["read_file".to_string(), to_api_tool_name(mid)]
|
||||
);
|
||||
assert_eq!(deepseek.omitted_tool_names, vec![to_api_tool_name(&long)]);
|
||||
|
||||
let forced = json!({"type": "function", "function": {"name": mid}});
|
||||
let error = build(
|
||||
ProviderKind::Openai,
|
||||
"https://api.openai.com/v1",
|
||||
Some(forced),
|
||||
)
|
||||
.err()
|
||||
.expect("forcing a withheld tool is refused");
|
||||
let message = error.to_string();
|
||||
assert!(
|
||||
message.contains(&format!("{mid_len} characters")),
|
||||
"{message}"
|
||||
);
|
||||
assert!(message.contains("at most 64"), "{message}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn over_long_tool_name_in_history_is_refused_with_the_limit() {
|
||||
let long = "mcp__tracker__projects.v2.issues.list-by-assignee";
|
||||
let mut request = request_with_image();
|
||||
request.messages.push(Message {
|
||||
role: Role::Assistant,
|
||||
content: vec![ContentBlock::ToolUse {
|
||||
execution_id: None,
|
||||
id: "call_1".to_string(),
|
||||
name: long.to_string(),
|
||||
input: json!({}),
|
||||
caller: None,
|
||||
thought_signature: None,
|
||||
}],
|
||||
});
|
||||
let error = build_chat_wire_body(
|
||||
&request,
|
||||
ProviderKind::Openai,
|
||||
"https://api.openai.com/v1",
|
||||
false,
|
||||
None,
|
||||
)
|
||||
.err()
|
||||
.expect("history name over the limit is refused");
|
||||
let message = error.to_string();
|
||||
assert!(
|
||||
message.contains("at most 64") && message.contains(long),
|
||||
"{message}"
|
||||
);
|
||||
build_chat_wire_body(
|
||||
&request,
|
||||
ProviderKind::Deepseek,
|
||||
"https://api.deepseek.com",
|
||||
false,
|
||||
None,
|
||||
)
|
||||
.expect("the same history fits a 128-character route");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn history_name_of_65_to_128_characters_is_sent_unchanged_on_undocumented_routes() {
|
||||
let long = "mcp__tracker__projects.v2.issues.list-by-assignee";
|
||||
assert!(to_api_tool_name(long).len() > 64);
|
||||
let mut request = request_with_image();
|
||||
request.messages.push(Message {
|
||||
role: Role::Assistant,
|
||||
content: vec![ContentBlock::ToolUse {
|
||||
execution_id: None,
|
||||
id: "call_1".to_string(),
|
||||
name: long.to_string(),
|
||||
input: json!({}),
|
||||
caller: None,
|
||||
thought_signature: None,
|
||||
}],
|
||||
});
|
||||
for (provider, base_url) in [
|
||||
(ProviderKind::Ollama, "http://localhost:11434/v1"),
|
||||
(ProviderKind::Custom, "http://localhost:8000/v1"),
|
||||
(ProviderKind::Minimax, "https://api.minimax.io/v1"),
|
||||
] {
|
||||
build_chat_wire_body(&request, provider, base_url, false, None)
|
||||
.unwrap_or_else(|error| panic!("{provider:?}: {error}"));
|
||||
}
|
||||
let error = build_chat_wire_body(
|
||||
&request,
|
||||
ProviderKind::Openai,
|
||||
"https://api.openai.com/v1",
|
||||
false,
|
||||
None,
|
||||
)
|
||||
.err()
|
||||
.expect("documented 64-character route refuses the history name");
|
||||
assert!(
|
||||
error.to_string().contains("earlier in this session"),
|
||||
"{error}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_100_character_encoded_name_is_withheld_on_a_chat_wire_route() {
|
||||
let name = "a".repeat(100);
|
||||
let mut request = request_with_image();
|
||||
request.tools = Some(vec![named_tool("read_file"), named_tool(&name)]);
|
||||
let body = build_chat_wire_body(
|
||||
&request,
|
||||
ProviderKind::Minimax,
|
||||
"https://api.minimax.io/v1",
|
||||
false,
|
||||
None,
|
||||
)
|
||||
.expect("wire body");
|
||||
assert_eq!(body.omitted_tool_names, vec![name]);
|
||||
assert_eq!(body.body["tools"].as_array().expect("tools").len(), 1);
|
||||
}
|
||||
|
||||
fn request_with_image() -> MessageRequest {
|
||||
MessageRequest {
|
||||
model: "gpt-4o".to_string(),
|
||||
|
||||
@@ -35,8 +35,10 @@ use super::chat::REASONING_REPLAY_PLACEHOLDER;
|
||||
use super::prepared::WireDialect;
|
||||
use super::role_placement::{RolePlacement, role_placement};
|
||||
use super::wire::{
|
||||
ERROR_BODY_MAX_BYTES, bounded_error_text, from_api_tool_name, system_to_instructions,
|
||||
to_api_tool_name,
|
||||
ERROR_BODY_MAX_BYTES, TOOL_NAME_LIMIT_64, ToolNameWire, bounded_error_text,
|
||||
check_history_tool_names, documented_tool_name_limit, from_api_tool_name,
|
||||
over_limit_tool_name_message, system_to_instructions, to_api_tool_name,
|
||||
tool_name_exceeds_limit, tool_name_limit,
|
||||
};
|
||||
use super::wire::{extract_sse_data_value, next_sse_line, push_sse_event_data};
|
||||
use crate::llm_client::LlmError;
|
||||
@@ -135,6 +137,7 @@ pub(super) fn build_responses_wire_body(
|
||||
.filter(|text| !text.trim().is_empty())
|
||||
.unwrap_or_else(|| "You are a helpful assistant.".to_string());
|
||||
|
||||
let provider_kind = ProviderKind::parse(context.provider_id);
|
||||
let mut input = convert_messages_to_responses_input(request, profile, context);
|
||||
if profile.request.instructions == InstructionsPlacement::LeadingSystemInput {
|
||||
input.insert(
|
||||
@@ -150,12 +153,26 @@ pub(super) fn build_responses_wire_body(
|
||||
}
|
||||
body["input"] = json!(input);
|
||||
|
||||
let tool_name_limit = provider_kind.map_or(TOOL_NAME_LIMIT_64, |provider| {
|
||||
tool_name_limit(provider, ToolNameWire::OpenAiCompatible)
|
||||
});
|
||||
check_history_tool_names(
|
||||
request,
|
||||
provider_kind.and_then(|provider| {
|
||||
documented_tool_name_limit(provider, ToolNameWire::OpenAiCompatible)
|
||||
}),
|
||||
)?;
|
||||
let mut omitted_tool_names = Vec::new();
|
||||
let mut omitted_logical_names = Vec::new();
|
||||
// Convert tools to Responses function tools.
|
||||
if let Some(tools) = request.tools.as_ref() {
|
||||
let mut responses_tools = Vec::new();
|
||||
for tool in tools {
|
||||
if tool_name_exceeds_limit(&tool.name, tool_name_limit) {
|
||||
omitted_logical_names.push(tool.name.clone());
|
||||
omitted_tool_names.push(to_api_tool_name(&tool.name));
|
||||
continue;
|
||||
}
|
||||
match project_tool_schema(
|
||||
RouteToolProjection::RelaxedNonStrict,
|
||||
&tool.input_schema,
|
||||
@@ -185,6 +202,9 @@ pub(super) fn build_responses_wire_body(
|
||||
if let Some(name) = request.tool_choice.as_ref().and_then(forced_tool_name)
|
||||
&& omitted_logical_names.iter().any(|omitted| omitted == name)
|
||||
{
|
||||
if tool_name_exceeds_limit(name, tool_name_limit) {
|
||||
anyhow::bail!(over_limit_tool_name_message(name, tool_name_limit));
|
||||
}
|
||||
anyhow::bail!(
|
||||
"cannot force an omitted tool because its input schema is incompatible with this route"
|
||||
);
|
||||
|
||||
@@ -755,6 +755,43 @@ async fn responses_stream_inserts_boundary_between_reasoning_summary_parts() {
|
||||
assert_eq!(thinking, "partA\n\npartB");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn over_long_encoded_tool_names_are_withheld_from_responses_at_64() {
|
||||
let tool = |name: &str| Tool {
|
||||
tool_type: None,
|
||||
name: name.to_string(),
|
||||
description: "d".to_string(),
|
||||
input_schema: json!({"type": "object", "additionalProperties": false}),
|
||||
allowed_callers: None,
|
||||
defer_loading: None,
|
||||
input_examples: None,
|
||||
strict: None,
|
||||
cache_control: None,
|
||||
};
|
||||
let long = "mcp__tracker__projects.v2.issues.list-by-assignee";
|
||||
assert!(to_api_tool_name(long).len() > 64);
|
||||
let mut request = minimal_responses_request();
|
||||
request.tools = Some(vec![tool("read_file"), tool(long)]);
|
||||
let (body, omitted) = build_responses_wire_body(
|
||||
&request,
|
||||
profile_for_provider(ProviderKind::Openai),
|
||||
ResponsesReplayContext {
|
||||
provider_id: ProviderKind::Openai.as_str(),
|
||||
wire_model_id: request.model.as_str(),
|
||||
reasoning_api_id: None,
|
||||
},
|
||||
)
|
||||
.expect("body builds");
|
||||
let sent: Vec<&str> = body["tools"]
|
||||
.as_array()
|
||||
.expect("tools")
|
||||
.iter()
|
||||
.filter_map(|t| t["name"].as_str())
|
||||
.collect();
|
||||
assert_eq!(sent, vec!["read_file"]);
|
||||
assert_eq!(omitted, vec![to_api_tool_name(long)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn codex_reasoning_effort_uses_responses_labels() {
|
||||
assert_eq!(codex_responses_reasoning_effort("max"), Some("max"));
|
||||
|
||||
@@ -1449,9 +1449,10 @@ fn tool_name_scenario() {
|
||||
}
|
||||
// from tool_name_decode_bare_hex_no_trailing_dash
|
||||
{
|
||||
let mangled = "foo_x00002Ebar";
|
||||
let decoded = from_api_tool_name(mangled);
|
||||
assert_eq!(decoded, "foo_.bar");
|
||||
let exact = "foo_x00002Ebar";
|
||||
assert_eq!(from_api_tool_name(exact), exact);
|
||||
let mangled = "foo_.x00002Ebar";
|
||||
assert_eq!(from_api_tool_name(mangled), "foo_..bar");
|
||||
}
|
||||
// from tool_name_bare_hex_preserves_alnum
|
||||
{
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
use anyhow::Result;
|
||||
use serde_json::Value;
|
||||
|
||||
use codewhale_models::{ServerToolUsage, SystemPrompt, Usage};
|
||||
use codewhale_config::ProviderKind;
|
||||
use codewhale_models::{ContentBlock, MessageRequest, ServerToolUsage, SystemPrompt, Usage};
|
||||
use std::sync::{Mutex as StdMutex, OnceLock};
|
||||
|
||||
pub fn saturating_u32(value: u64) -> u32 {
|
||||
@@ -315,7 +316,7 @@ pub fn to_api_tool_name(name: &str) -> String {
|
||||
out
|
||||
}
|
||||
|
||||
pub fn from_api_tool_name(name: &str) -> String {
|
||||
fn decode_delimited_tool_name(name: &str) -> String {
|
||||
let mut out = String::new();
|
||||
let mut iter = name.chars().peekable();
|
||||
while let Some(ch) = iter.next() {
|
||||
@@ -357,12 +358,87 @@ pub fn from_api_tool_name(name: &str) -> String {
|
||||
}
|
||||
out.push('-');
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
// Second pass: decode bare hex escapes (e.g. `x00002E`) that the model
|
||||
// may produce when it mangles the `-x00002E-` delimiter form. Only
|
||||
// decode when the resulting character is one that `to_api_tool_name`
|
||||
// would have encoded (not alphanumeric, not `_`, not `-`).
|
||||
decode_bare_hex_escapes(&out)
|
||||
pub fn from_api_tool_name(name: &str) -> String {
|
||||
let exact = decode_delimited_tool_name(name);
|
||||
if to_api_tool_name(&exact) == name {
|
||||
return exact;
|
||||
}
|
||||
decode_bare_hex_escapes(&exact)
|
||||
}
|
||||
|
||||
pub const TOOL_NAME_LIMIT_64: usize = 64;
|
||||
pub const TOOL_NAME_LIMIT_128: usize = 128;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ToolNameWire {
|
||||
AnthropicMessages,
|
||||
OpenAiCompatible,
|
||||
}
|
||||
|
||||
pub fn documented_tool_name_limit(provider: ProviderKind, wire: ToolNameWire) -> Option<usize> {
|
||||
match (provider, wire) {
|
||||
(ProviderKind::Deepseek | ProviderKind::DeepseekAnthropic, _) => Some(TOOL_NAME_LIMIT_128),
|
||||
(ProviderKind::Anthropic, ToolNameWire::AnthropicMessages) => Some(TOOL_NAME_LIMIT_128),
|
||||
(ProviderKind::Openai | ProviderKind::OpenaiCodex, ToolNameWire::OpenAiCompatible) => {
|
||||
Some(TOOL_NAME_LIMIT_64)
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn tool_name_limit(provider: ProviderKind, wire: ToolNameWire) -> usize {
|
||||
if let Some(limit) = documented_tool_name_limit(provider, wire) {
|
||||
return limit;
|
||||
}
|
||||
match (provider, wire) {
|
||||
(_, ToolNameWire::AnthropicMessages) => TOOL_NAME_LIMIT_128,
|
||||
(
|
||||
ProviderKind::Ollama
|
||||
| ProviderKind::OllamaCloud
|
||||
| ProviderKind::Vllm
|
||||
| ProviderKind::Sglang
|
||||
| ProviderKind::Custom,
|
||||
_,
|
||||
) => TOOL_NAME_LIMIT_128,
|
||||
(_, ToolNameWire::OpenAiCompatible) => TOOL_NAME_LIMIT_64,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn check_history_tool_names(request: &MessageRequest, documented: Option<usize>) -> Result<()> {
|
||||
let Some(limit) = documented else {
|
||||
return Ok(());
|
||||
};
|
||||
for message in &request.messages {
|
||||
for block in &message.content {
|
||||
if let ContentBlock::ToolUse { name, .. } = block
|
||||
&& tool_name_exceeds_limit(name, limit)
|
||||
{
|
||||
anyhow::bail!(over_limit_history_tool_name_message(name, limit));
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn tool_name_exceeds_limit(name: &str, limit: usize) -> bool {
|
||||
to_api_tool_name(name).len() > limit
|
||||
}
|
||||
|
||||
pub fn over_limit_tool_name_message(name: &str, limit: usize) -> String {
|
||||
format!(
|
||||
"tool name `{name}` encodes to {} characters but this provider accepts at most {limit}; rename the tool (shorter name, fewer dots, hyphens or other special characters) or remove it",
|
||||
to_api_tool_name(name).len()
|
||||
)
|
||||
}
|
||||
|
||||
pub fn over_limit_history_tool_name_message(name: &str, limit: usize) -> String {
|
||||
format!(
|
||||
"tool `{name}` was called earlier in this session and its encoded name is {} characters, but this provider accepts at most {limit}; switch to a route with a larger limit or start a new session",
|
||||
to_api_tool_name(name).len()
|
||||
)
|
||||
}
|
||||
|
||||
pub const SSE_BACKPRESSURE_HIGH_WATERMARK: usize = 1024 * 1024; // 1 MB
|
||||
@@ -465,6 +541,98 @@ mod tests {
|
||||
use super::*;
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn tool_name_encoding_is_collision_free_for_hostile_names() {
|
||||
let names = [
|
||||
"web.run",
|
||||
"web-x00002E-run",
|
||||
"web--x00002E--run",
|
||||
"webx00002Erun",
|
||||
"web_x00002Erun",
|
||||
"web_.run",
|
||||
"a-b",
|
||||
"a--b",
|
||||
"a_b",
|
||||
"a.b",
|
||||
"ax00002Eb",
|
||||
"a-x00002E-b",
|
||||
"mcp__srv__tool.name",
|
||||
"mcp__srv__tool-x00002E-name",
|
||||
];
|
||||
let mut seen = std::collections::HashMap::new();
|
||||
for name in names {
|
||||
let encoded = to_api_tool_name(name);
|
||||
assert!(
|
||||
encoded
|
||||
.chars()
|
||||
.all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-'),
|
||||
"{encoded}"
|
||||
);
|
||||
if let Some(previous) = seen.insert(encoded.clone(), name) {
|
||||
panic!("{previous} and {name} both encode to {encoded}");
|
||||
}
|
||||
assert_eq!(from_api_tool_name(&encoded), name, "round trip of {name}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tool_name_limit_follows_the_wire_unless_the_provider_documents_one() {
|
||||
use ToolNameWire::{AnthropicMessages, OpenAiCompatible};
|
||||
assert_eq!(
|
||||
tool_name_limit(ProviderKind::Anthropic, AnthropicMessages),
|
||||
128
|
||||
);
|
||||
assert_eq!(
|
||||
tool_name_limit(ProviderKind::MinimaxAnthropic, AnthropicMessages),
|
||||
128
|
||||
);
|
||||
assert_eq!(
|
||||
tool_name_limit(
|
||||
ProviderKind::ModelstudioTokenPlanAnthropic,
|
||||
AnthropicMessages
|
||||
),
|
||||
128
|
||||
);
|
||||
assert_eq!(
|
||||
tool_name_limit(ProviderKind::Deepseek, OpenAiCompatible),
|
||||
128
|
||||
);
|
||||
assert_eq!(
|
||||
tool_name_limit(ProviderKind::DeepseekAnthropic, AnthropicMessages),
|
||||
128
|
||||
);
|
||||
assert_eq!(tool_name_limit(ProviderKind::Openai, OpenAiCompatible), 64);
|
||||
assert_eq!(tool_name_limit(ProviderKind::Minimax, OpenAiCompatible), 64);
|
||||
assert_eq!(tool_name_limit(ProviderKind::Ollama, OpenAiCompatible), 128);
|
||||
assert_eq!(
|
||||
documented_tool_name_limit(ProviderKind::Ollama, OpenAiCompatible),
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
documented_tool_name_limit(ProviderKind::Custom, OpenAiCompatible),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tool_name_limit_counts_the_encoded_form() {
|
||||
let dotted = "mcp__tracker__projects.v2.issues.list-by-assignee";
|
||||
assert!(dotted.len() <= 64);
|
||||
assert!(tool_name_exceeds_limit(dotted, TOOL_NAME_LIMIT_64));
|
||||
assert!(!tool_name_exceeds_limit(dotted, TOOL_NAME_LIMIT_128));
|
||||
let plain = "a".repeat(64);
|
||||
assert!(!tool_name_exceeds_limit(&plain, TOOL_NAME_LIMIT_64));
|
||||
assert!(tool_name_exceeds_limit(
|
||||
&format!("{plain}a"),
|
||||
TOOL_NAME_LIMIT_64
|
||||
));
|
||||
let message = over_limit_tool_name_message(dotted, TOOL_NAME_LIMIT_64);
|
||||
assert!(
|
||||
message.contains("at most 64") && message.contains(dotted),
|
||||
"{message}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sse_line_limit_rejects_oversized_pending_complete_and_final_lines() {
|
||||
let mut decoder = SseLineDecoder::new();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1044,7 +1044,7 @@ pub fn tool_projection_warning_message(
|
||||
omitted_tool_count: usize,
|
||||
) -> String {
|
||||
format!(
|
||||
"Warning: {provider} omitted incompatible tools for this request: {}",
|
||||
"Warning: {provider} omitted tools for this request: {}. A tool is omitted when its input schema is incompatible with this route or when its encoded name is longer than the provider's tool-name limit (64 characters on OpenAI-style routes, 128 on Anthropic-style routes and DeepSeek); a name ending in … is cut for display. Shorten the tool's name (dots, hyphens and other special characters each add characters) or remove it.",
|
||||
tool_projection_warning_tool_list(omitted_tool_names, omitted_tool_count)
|
||||
)
|
||||
}
|
||||
@@ -1251,6 +1251,15 @@ mod tool_projection_warning_tests {
|
||||
bounded_tool_projection_warning_names,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn projection_warning_message_names_the_tool_and_the_name_limit() {
|
||||
let names = vec!["web-x00002E-run".to_string()];
|
||||
let message = tool_projection_warning_message("OpenAI", &names, 1);
|
||||
assert!(message.contains("web-x00002E-run"), "{message}");
|
||||
assert!(message.contains("64 characters"), "{message}");
|
||||
assert!(message.contains("128 on Anthropic-style"), "{message}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn projection_warning_names_are_count_and_length_bounded() {
|
||||
let names: Vec<String> = (0..12)
|
||||
|
||||
@@ -1727,9 +1727,10 @@ fn ssh_client_env() -> BTreeMap<String, String> {
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn process_base_env() -> BTreeMap<String, String> {
|
||||
base_env_from(std::env::vars_os())
|
||||
}
|
||||
const MANAGED_CONFIG_ENV: [&str; 2] = [
|
||||
"CODEWHALE_MANAGED_CONFIG_PATH",
|
||||
"DEEPSEEK_MANAGED_CONFIG_PATH",
|
||||
];
|
||||
|
||||
/// The worker's inherited environment: the repository's scrubbed child-env
|
||||
/// allowlist (`child_env`), so proxy routing, CA bundles, temp directories,
|
||||
@@ -1742,11 +1743,22 @@ where
|
||||
K: AsRef<std::ffi::OsStr>,
|
||||
V: AsRef<std::ffi::OsStr>,
|
||||
{
|
||||
let parent: Vec<(std::ffi::OsString, std::ffi::OsString)> = parent
|
||||
.into_iter()
|
||||
.map(|(key, value)| (key.as_ref().to_os_string(), value.as_ref().to_os_string()))
|
||||
.collect();
|
||||
let mut env: BTreeMap<String, String> =
|
||||
codewhale_runtime::child_env::sanitized_runtime_env_from(parent)
|
||||
codewhale_runtime::child_env::sanitized_runtime_env_from(parent.iter().cloned())
|
||||
.into_iter()
|
||||
.filter_map(|(key, value)| Some((key.into_string().ok()?, value.into_string().ok()?)))
|
||||
.collect();
|
||||
for (key, value) in &parent {
|
||||
if MANAGED_CONFIG_ENV.iter().any(|name| key == name)
|
||||
&& let (Some(key), Some(value)) = (key.to_str(), value.to_str())
|
||||
{
|
||||
env.insert(key.to_string(), value.to_string());
|
||||
}
|
||||
}
|
||||
force_worker_telemetry_off(&mut env);
|
||||
env
|
||||
}
|
||||
@@ -1754,7 +1766,7 @@ where
|
||||
/// Fleet workers are an implementation detail of the parent session, not
|
||||
/// sessions of their own — the parent already accounts for the dispatch.
|
||||
///
|
||||
/// The spawn path `env_clear()`s and rebuilds from [`process_base_env`], so an
|
||||
/// The spawn path `env_clear()`s and rebuilds from [`base_env_from`], so an
|
||||
/// operator's opt-out would otherwise never reach a worker at all. Hard-off
|
||||
/// here means a worker can never emit, can never inherit an ambient "on", and
|
||||
/// can never write telemetry state into the operator's home.
|
||||
@@ -1772,8 +1784,24 @@ fn worker_env(
|
||||
request_env: &BTreeMap<String, String>,
|
||||
allowlist: &BTreeSet<String>,
|
||||
) -> FleetHostResult<BTreeMap<String, String>> {
|
||||
let mut env = process_base_env();
|
||||
env.extend(filtered_env(request_env, allowlist)?);
|
||||
worker_env_from(std::env::vars_os(), request_env, allowlist)
|
||||
}
|
||||
|
||||
fn worker_env_from<K, V>(
|
||||
parent: impl IntoIterator<Item = (K, V)>,
|
||||
request_env: &BTreeMap<String, String>,
|
||||
allowlist: &BTreeSet<String>,
|
||||
) -> FleetHostResult<BTreeMap<String, String>>
|
||||
where
|
||||
K: AsRef<std::ffi::OsStr>,
|
||||
V: AsRef<std::ffi::OsStr>,
|
||||
{
|
||||
let mut env = base_env_from(parent);
|
||||
let mut requested = filtered_env(request_env, allowlist)?;
|
||||
for key in MANAGED_CONFIG_ENV {
|
||||
requested.remove(key);
|
||||
}
|
||||
env.extend(requested);
|
||||
force_worker_telemetry_off(&mut env);
|
||||
Ok(env)
|
||||
}
|
||||
@@ -2770,11 +2798,47 @@ mod tests {
|
||||
assert_eq!(config.codewhale_binary, "/usr/local/bin/codewhale");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn worker_env_keeps_the_parent_managed_config_path_and_a_request_cannot_replace_it() {
|
||||
let parent = [
|
||||
("CODEWHALE_MANAGED_CONFIG_PATH", "/etc/example/managed.toml"),
|
||||
(
|
||||
"DEEPSEEK_MANAGED_CONFIG_PATH",
|
||||
"/etc/deepseek/managed_config.toml",
|
||||
),
|
||||
("USER", "fleet"),
|
||||
];
|
||||
let request_env = BTreeMap::from([
|
||||
(
|
||||
"CODEWHALE_MANAGED_CONFIG_PATH".to_string(),
|
||||
"/nonexistent/managed.toml".to_string(),
|
||||
),
|
||||
(
|
||||
"DEEPSEEK_MANAGED_CONFIG_PATH".to_string(),
|
||||
"/nonexistent/managed.toml".to_string(),
|
||||
),
|
||||
]);
|
||||
let allowlist: BTreeSet<String> = request_env.keys().cloned().collect();
|
||||
let env = worker_env_from(parent, &request_env, &allowlist).expect("worker env");
|
||||
for (key, value) in &parent[..2] {
|
||||
assert_eq!(env.get(*key).map(String::as_str), Some(*value), "{key}");
|
||||
}
|
||||
|
||||
let env =
|
||||
worker_env_from([("USER", "fleet")], &request_env, &allowlist).expect("worker env");
|
||||
for key in MANAGED_CONFIG_ENV {
|
||||
assert!(
|
||||
!env.contains_key(key),
|
||||
"{key} must come only from the parent"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn worker_env_forces_telemetry_off_even_when_the_allowlist_says_otherwise() {
|
||||
// The spawn path env_clear()s and rebuilds from this map, so anything
|
||||
// absent here simply does not exist inside the worker.
|
||||
let base = process_base_env();
|
||||
let base = base_env_from(std::env::vars_os());
|
||||
assert_eq!(
|
||||
base.get("CODEWHALE_TELEMETRY").map(String::as_str),
|
||||
Some("false")
|
||||
|
||||
@@ -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(
|
||||
{
|
||||
|
||||
@@ -4988,14 +4988,16 @@ mod tests {
|
||||
Arc::new(MockExecutor),
|
||||
)
|
||||
.await?;
|
||||
sleep(STORE_IDLE_POLL_INTERVAL + Duration::from_millis(300)).await;
|
||||
settle_queue_fixture(&manager).await?;
|
||||
inspect_settled_workers(&manager).await?;
|
||||
manager.fingerprint_reads.store(0, Ordering::Relaxed);
|
||||
manager.store_loads.store(0, Ordering::Relaxed);
|
||||
|
||||
let window = Duration::from_secs(3);
|
||||
sleep(window).await;
|
||||
let started = Instant::now();
|
||||
sleep(Duration::from_secs(3)).await;
|
||||
let reads = manager.fingerprint_reads.load(Ordering::Relaxed);
|
||||
let loads = manager.store_loads.load(Ordering::Relaxed);
|
||||
let window = started.elapsed();
|
||||
let ticks = (window.as_millis() / STORE_SETTLED_NAP.as_millis()) as usize + 1;
|
||||
assert!(
|
||||
reads <= workers * ticks,
|
||||
|
||||
@@ -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!(
|
||||
|
||||
@@ -326,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.
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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) --
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
"TerminalDockLive": "actiu",
|
||||
"ExtensionPromptUnavailable": "Les instruccions de les extensions no estan disponibles; consulta /plugin show.",
|
||||
"ProfileConstitutionUnavailableLocal": "No s'han pogut carregar les preferències del compte de Codewhale; aquest torn fa servir la configuració local. La clau del proveïdor continua funcionant; executa codewhale login per recuperar les preferències del compte.",
|
||||
"AccountPreferencesNotLoaded": "Compte no carregat. Executa {command}. La clau del proveïdor continua funcionant.",
|
||||
"ComposerOversizedSubmitHeld": "No s’ha enviat: el missatge supera els {limit} caràcters i no s’ha pogut desar en un fitxer de text enganxat ({error}). El text complet continua al camp d’entrada: escurça’l o habilita l’escriptura a .codewhale/pastes i torna a prémer Enter.",
|
||||
"ComposerOversizedSubmitHeldShort": "No s’ha enviat: supera els {limit} caràcters; no s’ha desat el fitxer d’enganxat.",
|
||||
"CmdPluginOwnerReport": "Amfitrió d’extensions:\n Estat: {state}\n Eines actives ({count}): {tools}",
|
||||
@@ -1667,7 +1668,7 @@
|
||||
"PhaseIdle": "inactiu",
|
||||
"PhaseDraft": "esborrany",
|
||||
"PhaseWorking": "treballant",
|
||||
"PhaseReasoning": "raonant",
|
||||
"PhaseReasoning": "pensant",
|
||||
"PhaseReading": "llegint",
|
||||
"PhaseUsingTool": "usant una eina",
|
||||
"PhaseSubagents": "subagents en execució",
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
"TerminalDockLive": "aktiv",
|
||||
"ExtensionPromptUnavailable": "Anweisungen von Erweiterungen sind nicht verfügbar; prüfe /plugin show.",
|
||||
"ProfileConstitutionUnavailableLocal": "Die Codewhale-Kontoeinstellungen konnten nicht geladen werden; dieser Durchlauf nutzt deine lokalen Einstellungen. Dein Anbieter-Schlüssel funktioniert weiter; führe codewhale login aus, um die Kontoeinstellungen wiederherzustellen.",
|
||||
"AccountPreferencesNotLoaded": "Konto nicht geladen. Führe {command} aus. Dein Anbieter-Schlüssel funktioniert weiter.",
|
||||
"ComposerOversizedSubmitHeld": "Nicht gesendet: Diese Nachricht enthält mehr als {limit} Zeichen und konnte nicht als Einfügedatei gespeichert werden ({error}). Der vollständige Text steht noch im Eingabefeld: Kürze ihn oder mache .codewhale/pastes beschreibbar und drücke dann erneut Enter.",
|
||||
"ComposerOversizedSubmitHeldShort": "Nicht gesendet: über {limit} Zeichen; Einfügedatei nicht gespeichert.",
|
||||
"CmdPluginOwnerReport": "Erweiterungshost:\n Status: {state}\n Aktive Werkzeuge ({count}): {tools}",
|
||||
@@ -1667,7 +1668,7 @@
|
||||
"PhaseIdle": "ruhend",
|
||||
"PhaseDraft": "entwurf",
|
||||
"PhaseWorking": "arbeitet",
|
||||
"PhaseReasoning": "überlegt",
|
||||
"PhaseReasoning": "denkt nach",
|
||||
"PhaseReading": "liest",
|
||||
"PhaseUsingTool": "nutzt Tool",
|
||||
"PhaseSubagents": "Subagenten laufen",
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
"TerminalDockLive": "live",
|
||||
"ExtensionPromptUnavailable": "Extension instructions are unavailable; inspect /plugin show.",
|
||||
"ProfileConstitutionUnavailableLocal": "Codewhale account preferences could not be loaded, so this turn uses your local settings. Your provider key still works; run codewhale login to restore account preferences.",
|
||||
"AccountPreferencesNotLoaded": "Account not loaded. Run {command}. Your provider key still works.",
|
||||
"ComposerOversizedSubmitHeld": "Not sent: this message is over {limit} characters and could not be saved as a paste file ({error}). The full text is still in the composer: shorten it, or make .codewhale/pastes writable, then press Enter again.",
|
||||
"ComposerOversizedSubmitHeldShort": "Not sent: over {limit} characters; paste file not saved.",
|
||||
"CmdPluginOwnerReport": "Extension host:\n State: {state}\n Live tools ({count}): {tools}",
|
||||
@@ -235,14 +236,14 @@
|
||||
"TelemetryNoticeActionChoose": "choose",
|
||||
"TelemetryNoticeActionConfirm": "confirm",
|
||||
"TelemetryNoticeActionExit": "exit",
|
||||
"TelemetryNoticeReceiptEnabled": "Anonymous usage counts are on. Conversations and code are never collected. Change this in /settings; schema: docs/TELEMETRY.md",
|
||||
"TelemetryNoticeReceiptDisabled": "Anonymous usage counting is off. You will not be asked again.",
|
||||
"TelemetryNoticeReceiptEnabledUnsaved": "Anonymous usage counts are on for this session. Codewhale could not save this notice, so it will appear again. Change this in /settings; schema: docs/TELEMETRY.md",
|
||||
"TelemetryNoticeReceiptDisabledUnsaved": "Anonymous usage counting is off for this session. Codewhale could not save the choice, so it will ask again next launch.",
|
||||
"TelemetryPreferenceEnabledNextLaunch": "Anonymous usage counts will be on next launch. Conversations and code are never collected.",
|
||||
"TelemetryPreferenceDisabled": "Anonymous usage counts are off. Local telemetry state was erased.",
|
||||
"TelemetryPreferenceDisabledWithWarning": "Anonymous usage counts are off, but part of the cleanup needs attention: {detail}",
|
||||
"TelemetryPreferenceDisabledForSession": "Anonymous usage counts are off for this session, but the preference was not saved and may return next launch: {detail}",
|
||||
"TelemetryNoticeReceiptEnabled": "Usage reporting is on. Conversations and code are never collected. Change this in /settings; schema: docs/TELEMETRY.md",
|
||||
"TelemetryNoticeReceiptDisabled": "Usage reporting is off. You will not be asked again.",
|
||||
"TelemetryNoticeReceiptEnabledUnsaved": "Usage reporting is on for this session. Codewhale could not save this notice, so it will appear again. Change this in /settings; schema: docs/TELEMETRY.md",
|
||||
"TelemetryNoticeReceiptDisabledUnsaved": "Usage reporting is off for this session. Codewhale could not save the choice, so it will ask again next launch.",
|
||||
"TelemetryPreferenceEnabledNextLaunch": "Usage reporting will be on next launch. Conversations and code are never collected.",
|
||||
"TelemetryPreferenceDisabled": "Usage reporting is off. Local telemetry state was erased.",
|
||||
"TelemetryPreferenceDisabledWithWarning": "Usage reporting is off, but part of the cleanup needs attention: {detail}",
|
||||
"TelemetryPreferenceDisabledForSession": "Usage reporting is off for this session, but the preference was not saved and may return next launch: {detail}",
|
||||
"TelemetryPreferenceSaveFailed": "The telemetry preference could not be changed safely: {detail}",
|
||||
"StatusPickerTitle": " Status line ",
|
||||
"StatusPickerInstruction": "Choose what the footer shows:",
|
||||
@@ -373,7 +374,7 @@
|
||||
"ConfigLabelDefaultMode": "Startup mode",
|
||||
"ConfigLabelAllowShell": "Shell access",
|
||||
"ConfigLabelManagedAllowShell": "Shell access (managed)",
|
||||
"ConfigLabelTelemetry": "Anonymous usage counts",
|
||||
"ConfigLabelTelemetry": "Usage reporting",
|
||||
"ConfigHintTelemetry": "Aggregate version, session, feature and error counts to Codewhale and PostHog; never content or IP. docs/TELEMETRY.md",
|
||||
"ConfigValueTelemetryOn": "On",
|
||||
"ConfigValueTelemetryOff": "Off",
|
||||
@@ -1690,7 +1691,7 @@
|
||||
"PhaseIdle": "idle",
|
||||
"PhaseDraft": "draft",
|
||||
"PhaseWorking": "working",
|
||||
"PhaseReasoning": "reasoning",
|
||||
"PhaseReasoning": "thinking",
|
||||
"PhaseReading": "reading",
|
||||
"PhaseUsingTool": "using tool",
|
||||
"PhaseSubagents": "agents underway",
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
"TerminalDockLive": "activo",
|
||||
"ExtensionPromptUnavailable": "Las instrucciones de las extensiones no están disponibles; revisa /plugin show.",
|
||||
"ProfileConstitutionUnavailableLocal": "No se pudieron cargar las preferencias de la cuenta de Codewhale; este turno usa tu configuración local. Tu clave del proveedor sigue funcionando; ejecuta codewhale login para restaurar las preferencias de la cuenta.",
|
||||
"AccountPreferencesNotLoaded": "Cuenta no cargada. Ejecuta {command}. Tu clave del proveedor sigue funcionando.",
|
||||
"ComposerOversizedSubmitHeld": "No se envió: este mensaje supera los {limit} caracteres y no se pudo guardar en un archivo de texto pegado ({error}). El texto completo sigue en el campo de entrada: acórtalo o habilita la escritura en .codewhale/pastes y vuelve a presionar Enter.",
|
||||
"ComposerOversizedSubmitHeldShort": "No se envió: supera los {limit} caracteres; no se guardó el archivo pegado.",
|
||||
"CmdPluginOwnerReport": "Host de extensiones:\n Estado: {state}\n Herramientas activas ({count}): {tools}",
|
||||
@@ -1690,7 +1691,7 @@
|
||||
"PhaseIdle": "inactivo",
|
||||
"PhaseDraft": "borrador",
|
||||
"PhaseWorking": "trabajando",
|
||||
"PhaseReasoning": "razonando",
|
||||
"PhaseReasoning": "pensando",
|
||||
"PhaseReading": "leyendo",
|
||||
"PhaseUsingTool": "usando herramienta",
|
||||
"PhaseSubagents": "subagentes en curso",
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
"TerminalDockLive": "actif",
|
||||
"ExtensionPromptUnavailable": "Les instructions des extensions sont indisponibles ; consultez /plugin show.",
|
||||
"ProfileConstitutionUnavailableLocal": "Les préférences du compte Codewhale n'ont pas pu être chargées ; ce tour utilise vos réglages locaux. Votre clé de fournisseur fonctionne toujours ; lancez codewhale login pour rétablir les préférences du compte.",
|
||||
"AccountPreferencesNotLoaded": "Compte non chargé. Lancez {command}. Votre clé de fournisseur fonctionne toujours.",
|
||||
"ComposerOversizedSubmitHeld": "Non envoyé : ce message dépasse {limit} caractères et n’a pas pu être enregistré dans un fichier de texte collé ({error}). Le texte intégral reste dans la zone de saisie : raccourcissez-le ou autorisez l’écriture dans .codewhale/pastes, puis appuyez à nouveau sur Enter.",
|
||||
"ComposerOversizedSubmitHeldShort": "Non envoyé : plus de {limit} caractères ; fichier collé non enregistré.",
|
||||
"CmdPluginOwnerReport": "Hôte des extensions :\n État : {state}\n Outils actifs ({count}) : {tools}",
|
||||
@@ -1667,7 +1668,7 @@
|
||||
"PhaseIdle": "inactif",
|
||||
"PhaseDraft": "brouillon",
|
||||
"PhaseWorking": "travail en cours",
|
||||
"PhaseReasoning": "raisonnement",
|
||||
"PhaseReasoning": "réflexion",
|
||||
"PhaseReading": "lecture",
|
||||
"PhaseUsingTool": "utilisation d'outil",
|
||||
"PhaseSubagents": "sous-agents en cours",
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
"TerminalDockLive": "सक्रिय",
|
||||
"ExtensionPromptUnavailable": "एक्सटेंशन के निर्देश उपलब्ध नहीं हैं; /plugin show से जाँचें।",
|
||||
"ProfileConstitutionUnavailableLocal": "Codewhale खाते की प्राथमिकताएँ लोड नहीं हो सकीं, इसलिए यह टर्न आपकी स्थानीय सेटिंग्स का उपयोग करता है। आपकी प्रदाता कुंजी काम करती रहेगी; खाते की प्राथमिकताएँ वापस लाने के लिए codewhale login चलाएँ।",
|
||||
"AccountPreferencesNotLoaded": "खाता लोड नहीं हुआ। {command} चलाएँ। आपकी प्रदाता कुंजी काम करती रहेगी।",
|
||||
"ComposerOversizedSubmitHeld": "नहीं भेजा गया: इस संदेश में {limit} से अधिक वर्ण हैं और इसे पेस्ट फ़ाइल के रूप में सहेजा नहीं जा सका ({error})। पूरा पाठ अभी भी इनपुट बॉक्स में है: इसे छोटा करें या .codewhale/pastes में लिखने की अनुमति दें, फिर Enter दबाएँ।",
|
||||
"ComposerOversizedSubmitHeldShort": "नहीं भेजा गया: {limit} से अधिक वर्ण; पेस्ट फ़ाइल सहेजी नहीं गई।",
|
||||
"CmdPluginOwnerReport": "एक्सटेंशन होस्ट:\n स्थिति: {state}\n सक्रिय टूल ({count}): {tools}",
|
||||
@@ -1667,7 +1668,7 @@
|
||||
"PhaseIdle": "निष्क्रिय",
|
||||
"PhaseDraft": "मसौदा",
|
||||
"PhaseWorking": "काम चल रहा",
|
||||
"PhaseReasoning": "तर्क चल रहा",
|
||||
"PhaseReasoning": "सोच रहा",
|
||||
"PhaseReading": "पढ़ रहा",
|
||||
"PhaseUsingTool": "टूल इस्तेमाल हो रहा",
|
||||
"PhaseSubagents": "सबएजेंट चल रहे हैं",
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
"TerminalDockLive": "aktif",
|
||||
"ExtensionPromptUnavailable": "Instruksi ekstensi tidak tersedia; periksa /plugin show.",
|
||||
"ProfileConstitutionUnavailableLocal": "Preferensi akun Codewhale tidak dapat dimuat, jadi giliran ini memakai pengaturan lokal. Kunci penyedia tetap berfungsi; jalankan codewhale login untuk memulihkan preferensi akun.",
|
||||
"AccountPreferencesNotLoaded": "Akun tidak dimuat. Jalankan {command}. Kunci penyedia tetap berfungsi.",
|
||||
"ComposerOversizedSubmitHeld": "Belum terkirim: pesan ini melebihi {limit} karakter dan tidak dapat disimpan sebagai berkas teks tempelan ({error}). Teks lengkap masih ada di kolom input: persingkat teks atau izinkan penulisan ke .codewhale/pastes, lalu tekan Enter lagi.",
|
||||
"ComposerOversizedSubmitHeldShort": "Belum terkirim: melebihi {limit} karakter; berkas tempelan tidak tersimpan.",
|
||||
"CmdPluginOwnerReport": "Host ekstensi:\n Status: {state}\n Alat aktif ({count}): {tools}",
|
||||
@@ -1667,7 +1668,7 @@
|
||||
"PhaseIdle": "diam",
|
||||
"PhaseDraft": "draf",
|
||||
"PhaseWorking": "bekerja",
|
||||
"PhaseReasoning": "menalar",
|
||||
"PhaseReasoning": "berpikir",
|
||||
"PhaseReading": "membaca",
|
||||
"PhaseUsingTool": "memakai tool",
|
||||
"PhaseSubagents": "subagen berjalan",
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
"TerminalDockLive": "稼働中",
|
||||
"ExtensionPromptUnavailable": "拡張機能の指示を利用できません。/plugin show で確認してください。",
|
||||
"ProfileConstitutionUnavailableLocal": "Codewhale アカウントの設定を読み込めなかったため、このターンはローカル設定を使います。プロバイダーキーはそのまま使えます。アカウント設定を戻すには codewhale login を実行してください。",
|
||||
"AccountPreferencesNotLoaded": "アカウントを読み込めませんでした。{command} を実行してください。プロバイダーキーはそのまま使えます。",
|
||||
"ComposerOversizedSubmitHeld": "未送信:メッセージが {limit} 文字を超えており、貼り付けファイルに保存できませんでした({error})。全文は入力欄に残っています。短くするか .codewhale/pastes を書き込み可能にしてから、もう一度 Enter を押してください。",
|
||||
"ComposerOversizedSubmitHeldShort": "未送信:{limit} 文字超。貼り付けファイルは未保存です。",
|
||||
"CmdPluginOwnerReport": "拡張機能ホスト:\n 状態: {state}\n 有効なツール ({count}): {tools}",
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
"TerminalDockLive": "실행 중",
|
||||
"ExtensionPromptUnavailable": "확장 기능의 지침을 사용할 수 없습니다. /plugin show에서 확인하세요.",
|
||||
"ProfileConstitutionUnavailableLocal": "Codewhale 계정 설정을 불러오지 못해 이번 턴은 로컬 설정을 사용합니다. 공급자 키는 계속 사용할 수 있으며, 계정 설정을 복원하려면 codewhale login을 실행하세요.",
|
||||
"AccountPreferencesNotLoaded": "계정을 불러오지 못했습니다. {command}을 실행하세요. 공급자 키는 계속 사용할 수 있습니다.",
|
||||
"ComposerOversizedSubmitHeld": "전송하지 못했습니다. 메시지가 {limit}자를 초과하며 붙여넣기 파일로 저장할 수 없습니다({error}). 전체 텍스트는 입력란에 그대로 있습니다. 내용을 줄이거나 .codewhale/pastes에 쓰기 권한을 부여한 뒤 Enter를 다시 누르세요.",
|
||||
"ComposerOversizedSubmitHeldShort": "전송 안 됨: {limit}자 초과, 붙여넣기 파일 저장 실패.",
|
||||
"CmdPluginOwnerReport": "확장 호스트:\n 상태: {state}\n 활성 도구 ({count}): {tools}",
|
||||
@@ -1690,7 +1691,7 @@
|
||||
"PhaseIdle": "유휴",
|
||||
"PhaseDraft": "초안",
|
||||
"PhaseWorking": "작업 중",
|
||||
"PhaseReasoning": "추론 중",
|
||||
"PhaseReasoning": "생각 중",
|
||||
"PhaseReading": "읽는 중",
|
||||
"PhaseUsingTool": "도구 사용 중",
|
||||
"PhaseSubagents": "서브에이전트 실행 중",
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
"TerminalDockLive": "ativo",
|
||||
"ExtensionPromptUnavailable": "As instruções das extensões não estão disponíveis; confira /plugin show.",
|
||||
"ProfileConstitutionUnavailableLocal": "Não foi possível carregar as preferências da conta Codewhale; este turno usa suas configurações locais. Sua chave do provedor continua funcionando; execute codewhale login para restaurar as preferências da conta.",
|
||||
"AccountPreferencesNotLoaded": "Conta não carregada. Execute {command}. Sua chave do provedor continua funcionando.",
|
||||
"ComposerOversizedSubmitHeld": "Não enviado: esta mensagem excede {limit} caracteres e não pôde ser salva em um arquivo de texto colado ({error}). O texto completo continua no campo de entrada: reduza-o ou permita a gravação em .codewhale/pastes e pressione Enter novamente.",
|
||||
"ComposerOversizedSubmitHeldShort": "Não enviado: acima de {limit} caracteres; arquivo colado não salvo.",
|
||||
"CmdPluginOwnerReport": "Host de extensões:\n Estado: {state}\n Ferramentas ativas ({count}): {tools}",
|
||||
@@ -1690,7 +1691,7 @@
|
||||
"PhaseIdle": "ocioso",
|
||||
"PhaseDraft": "rascunho",
|
||||
"PhaseWorking": "trabalhando",
|
||||
"PhaseReasoning": "raciocinando",
|
||||
"PhaseReasoning": "pensando",
|
||||
"PhaseReading": "lendo",
|
||||
"PhaseUsingTool": "usando ferramenta",
|
||||
"PhaseSubagents": "subagentes em andamento",
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
"TerminalDockLive": "активен",
|
||||
"ExtensionPromptUnavailable": "Инструкции расширений недоступны; проверьте /plugin show.",
|
||||
"ProfileConstitutionUnavailableLocal": "Не удалось загрузить настройки аккаунта Codewhale, поэтому этот ход использует локальные настройки. Ключ провайдера продолжает работать; выполните codewhale login, чтобы вернуть настройки аккаунта.",
|
||||
"AccountPreferencesNotLoaded": "Аккаунт не загружен. Выполните {command}. Ключ провайдера продолжает работать.",
|
||||
"ComposerOversizedSubmitHeld": "Не отправлено: сообщение длиннее {limit} символов, и его не удалось сохранить в файл вставленного текста ({error}). Полный текст остался в поле ввода: сократите его или разрешите запись в .codewhale/pastes, затем снова нажмите Enter.",
|
||||
"ComposerOversizedSubmitHeldShort": "Не отправлено: больше {limit} символов; файл вставки не сохранён.",
|
||||
"CmdPluginOwnerReport": "Хост расширений:\n Состояние: {state}\n Активные инструменты ({count}): {tools}",
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
"TerminalDockLive": "активний",
|
||||
"ExtensionPromptUnavailable": "Інструкції розширень недоступні; перевірте /plugin show.",
|
||||
"ProfileConstitutionUnavailableLocal": "Не вдалося завантажити налаштування облікового запису Codewhale, тому цей хід використовує локальні налаштування. Ключ провайдера й далі працює; виконайте codewhale login, щоб повернути налаштування облікового запису.",
|
||||
"AccountPreferencesNotLoaded": "Обліковий запис не завантажено. Виконайте {command}. Ключ провайдера й далі працює.",
|
||||
"ComposerOversizedSubmitHeld": "Не надіслано: повідомлення перевищує {limit} символів, і його не вдалося зберегти у файл вставленого тексту ({error}). Повний текст залишається в полі введення: скоротіть його або дозвольте запис у .codewhale/pastes, а потім знову натисніть Enter.",
|
||||
"ComposerOversizedSubmitHeldShort": "Не надіслано: понад {limit} символів; файл вставки не збережено.",
|
||||
"CmdPluginOwnerReport": "Хост розширень:\n Стан: {state}\n Активні інструменти ({count}): {tools}",
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
"TerminalDockLive": "đang chạy",
|
||||
"ExtensionPromptUnavailable": "Không thể tải hướng dẫn của tiện ích mở rộng; kiểm tra /plugin show.",
|
||||
"ProfileConstitutionUnavailableLocal": "Không thể tải tùy chọn tài khoản Codewhale nên lượt này dùng cài đặt cục bộ. Khóa nhà cung cấp của bạn vẫn hoạt động; chạy codewhale login để khôi phục tùy chọn tài khoản.",
|
||||
"AccountPreferencesNotLoaded": "Chưa tải được tài khoản. Chạy {command}. Khóa nhà cung cấp của bạn vẫn hoạt động.",
|
||||
"ComposerOversizedSubmitHeld": "Chưa gửi: tin nhắn vượt quá {limit} ký tự và không thể lưu thành tệp văn bản đã dán ({error}). Toàn bộ nội dung vẫn còn trong ô nhập: hãy rút ngắn hoặc cho phép ghi vào .codewhale/pastes, rồi nhấn Enter lần nữa.",
|
||||
"ComposerOversizedSubmitHeldShort": "Chưa gửi: vượt quá {limit} ký tự; chưa lưu tệp dán.",
|
||||
"CmdPluginOwnerReport": "Máy chủ tiện ích mở rộng:\n Trạng thái: {state}\n Công cụ đang hoạt động ({count}): {tools}",
|
||||
@@ -1690,7 +1691,7 @@
|
||||
"PhaseIdle": "rảnh",
|
||||
"PhaseDraft": "nháp",
|
||||
"PhaseWorking": "đang xử lý",
|
||||
"PhaseReasoning": "đang suy luận",
|
||||
"PhaseReasoning": "đang suy nghĩ",
|
||||
"PhaseReading": "đang đọc",
|
||||
"PhaseUsingTool": "đang dùng công cụ",
|
||||
"PhaseSubagents": "đang chạy tác tử con",
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
"TerminalDockLive": "运行中",
|
||||
"ExtensionPromptUnavailable": "扩展指令不可用;请通过 /plugin show 查看。",
|
||||
"ProfileConstitutionUnavailableLocal": "无法加载 Codewhale 账号偏好,本轮改用本地设置。你的提供商密钥仍可正常使用;运行 codewhale login 可恢复账号偏好。",
|
||||
"AccountPreferencesNotLoaded": "账号未加载。请运行 {command}。你的提供商密钥仍可正常使用。",
|
||||
"ComposerOversizedSubmitHeld": "未发送:消息超过 {limit} 个字符,无法保存为粘贴文本文件({error})。完整文本仍保留在输入框中:请缩短内容,或允许写入 .codewhale/pastes,然后再次按 Enter。",
|
||||
"ComposerOversizedSubmitHeldShort": "未发送:超过 {limit} 个字符;粘贴文件未保存。",
|
||||
"CmdPluginOwnerReport": "扩展宿主:\n 状态:{state}\n 活动工具({count}):{tools}",
|
||||
@@ -1690,7 +1691,7 @@
|
||||
"PhaseIdle": "空闲",
|
||||
"PhaseDraft": "草稿",
|
||||
"PhaseWorking": "工作中",
|
||||
"PhaseReasoning": "推理中",
|
||||
"PhaseReasoning": "思考中",
|
||||
"PhaseReading": "读取中",
|
||||
"PhaseUsingTool": "使用工具中",
|
||||
"PhaseSubagents": "智能体工作中",
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
"TerminalDockLive": "執行中",
|
||||
"ExtensionPromptUnavailable": "擴充功能指令無法使用;請透過 /plugin show 查看。",
|
||||
"ProfileConstitutionUnavailableLocal": "無法載入 Codewhale 帳號偏好,本輪改用本機設定。你的服務商金鑰仍可正常使用;執行 codewhale login 可恢復帳號偏好。",
|
||||
"AccountPreferencesNotLoaded": "帳號未載入。請執行 {command}。你的服務商金鑰仍可正常使用。",
|
||||
"ComposerOversizedSubmitHeld": "未傳送:訊息超過 {limit} 個字元,無法儲存為貼上文字檔案({error})。完整文字仍保留在輸入框中:請縮短內容,或允許寫入 .codewhale/pastes,然後再次按 Enter。",
|
||||
"ComposerOversizedSubmitHeldShort": "未傳送:超過 {limit} 個字元;貼上檔案未儲存。",
|
||||
"CmdPluginOwnerReport": "擴充功能主機:\n 狀態:{state}\n 活動工具({count}):{tools}",
|
||||
@@ -1688,7 +1689,7 @@
|
||||
"PhaseIdle": "閒置",
|
||||
"PhaseDraft": "草稿",
|
||||
"PhaseWorking": "工作中",
|
||||
"PhaseReasoning": "推理中",
|
||||
"PhaseReasoning": "思考中",
|
||||
"PhaseReading": "讀取中",
|
||||
"PhaseUsingTool": "使用工具中",
|
||||
"PhaseSubagents": "代理程式工作中",
|
||||
|
||||
@@ -714,6 +714,7 @@ pub enum MessageId {
|
||||
ExtensionsInventorySkills,
|
||||
ExtensionPromptUnavailable,
|
||||
ProfileConstitutionUnavailableLocal,
|
||||
AccountPreferencesNotLoaded,
|
||||
ExtensionsMarketplaceDetail,
|
||||
ExtensionsMarketplaceUnavailable,
|
||||
ExtensionsMcpEmpty,
|
||||
@@ -3651,6 +3652,7 @@ pub const ALL_MESSAGE_IDS: &[MessageId] = &[
|
||||
MessageId::ExtensionsInventorySkills,
|
||||
MessageId::ExtensionPromptUnavailable,
|
||||
MessageId::ProfileConstitutionUnavailableLocal,
|
||||
MessageId::AccountPreferencesNotLoaded,
|
||||
MessageId::ExtensionsMarketplaceDetail,
|
||||
MessageId::ExtensionsMarketplaceUnavailable,
|
||||
MessageId::ExtensionsMcpEmpty,
|
||||
|
||||
@@ -3446,15 +3446,10 @@ impl Config {
|
||||
return Some("project runtime configuration");
|
||||
}
|
||||
|
||||
let managed_path = self
|
||||
.managed_config_path
|
||||
.as_deref()
|
||||
.map(expand_path)
|
||||
.or_else(default_managed_config_path);
|
||||
if let Some(path) = managed_path
|
||||
&& path.exists()
|
||||
{
|
||||
match load_single_config_file(&path) {
|
||||
match managed_config_file(self.managed_config_path.as_deref()) {
|
||||
Err(_) => return Some("an unreadable managed runtime configuration"),
|
||||
Ok(None) => {}
|
||||
Ok(Some(path)) => match load_single_config_file(&path) {
|
||||
Ok(managed)
|
||||
if managed.approval_policy.is_some()
|
||||
|| managed.sandbox_mode.is_some()
|
||||
@@ -3464,7 +3459,7 @@ impl Config {
|
||||
}
|
||||
Err(_) => return Some("an unreadable managed runtime configuration"),
|
||||
Ok(_) => {}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
let env_controls_runtime = || {
|
||||
@@ -3550,21 +3545,16 @@ impl Config {
|
||||
}
|
||||
}
|
||||
|
||||
let managed_path = self
|
||||
.managed_config_path
|
||||
.as_deref()
|
||||
.map(expand_path)
|
||||
.or_else(default_managed_config_path);
|
||||
if let Some(path) = managed_path
|
||||
&& path.exists()
|
||||
{
|
||||
match load_single_config_file(&path) {
|
||||
match managed_config_file(self.managed_config_path.as_deref()) {
|
||||
Err(_) => return ApprovalPolicyControl::Ambiguous,
|
||||
Ok(None) => {}
|
||||
Ok(Some(path)) => match load_single_config_file(&path) {
|
||||
Ok(managed) if managed.approval_policy.is_some() => {
|
||||
return ApprovalPolicyControl::ManagedConfig;
|
||||
}
|
||||
Err(_) => return ApprovalPolicyControl::Ambiguous,
|
||||
Ok(_) => {}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
if approval_policy_env_is_set() {
|
||||
@@ -3630,21 +3620,16 @@ impl Config {
|
||||
return ShellAccessControl::ProjectConfig;
|
||||
}
|
||||
|
||||
let managed_path = self
|
||||
.managed_config_path
|
||||
.as_deref()
|
||||
.map(expand_path)
|
||||
.or_else(default_managed_config_path);
|
||||
if let Some(path) = managed_path
|
||||
&& path.exists()
|
||||
{
|
||||
match load_single_config_file(&path) {
|
||||
match managed_config_file(self.managed_config_path.as_deref()) {
|
||||
Err(_) => return ShellAccessControl::Ambiguous,
|
||||
Ok(None) => {}
|
||||
Ok(Some(path)) => match load_single_config_file(&path) {
|
||||
Ok(managed) if managed.allow_shell.is_some() => {
|
||||
return ShellAccessControl::ManagedConfig;
|
||||
}
|
||||
Err(_) => return ShellAccessControl::Ambiguous,
|
||||
Ok(_) => {}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
if allow_shell_env_is_set() {
|
||||
@@ -8932,9 +8917,10 @@ fn apply_env_overrides_unlocked(config: &mut Config, policy: ConfigEnvironmentPo
|
||||
{
|
||||
config.sandbox_api_key = Some(value);
|
||||
}
|
||||
if let Ok(value) = std::env::var("CODEWHALE_MANAGED_CONFIG_PATH")
|
||||
.or_else(|_| std::env::var("DEEPSEEK_MANAGED_CONFIG_PATH"))
|
||||
{
|
||||
if let Ok(value) = codewhale_env_var(
|
||||
"CODEWHALE_MANAGED_CONFIG_PATH",
|
||||
"DEEPSEEK_MANAGED_CONFIG_PATH",
|
||||
) {
|
||||
config.managed_config_path = Some(value);
|
||||
}
|
||||
if policy.permits_secret_bearing_values()
|
||||
@@ -10718,26 +10704,25 @@ fn warn_on_misplaced_top_level_keys(raw: &str) -> Option<String> {
|
||||
}
|
||||
|
||||
fn apply_managed_overrides(config: &mut Config) -> Result<()> {
|
||||
let path = managed_path_from_env()
|
||||
.or_else(|| config.managed_config_path.as_deref().map(expand_path))
|
||||
.or_else(default_managed_config_path);
|
||||
let Some(path) = path else {
|
||||
let Some(path) = managed_config_file(config.managed_config_path.as_deref())? else {
|
||||
return Ok(());
|
||||
};
|
||||
if !path.exists() {
|
||||
return Ok(());
|
||||
}
|
||||
let mut managed = load_single_config_file(&path)?;
|
||||
let mut managed = load_single_config_file(&path).with_context(|| {
|
||||
format!(
|
||||
"the managed configuration {} cannot be loaded, and Codewhale does not start without a managed configuration it was told to use",
|
||||
codewhale_config::quote_os_path(&path)
|
||||
)
|
||||
})?;
|
||||
strip_external_credential_consent(&mut managed);
|
||||
managed.tool_launcher = managed_tool_launcher(&path)?;
|
||||
let prior_route = config
|
||||
.active_provider_identity()
|
||||
.map_err(anyhow::Error::msg)?;
|
||||
.map_err(|error| managed_route_error(config, &path, error))?;
|
||||
let mut merged = merge_config(config.clone(), managed.clone());
|
||||
apply_layer_root_model(&mut merged, &managed);
|
||||
let merged_route = merged
|
||||
.active_provider_identity()
|
||||
.map_err(anyhow::Error::msg)?;
|
||||
.map_err(|error| managed_route_error(&merged, &path, error))?;
|
||||
if managed.provider.is_some()
|
||||
|| managed.default_text_model.is_some()
|
||||
|| managed.legacy_model.is_some()
|
||||
@@ -10888,10 +10873,62 @@ fn managed_tool_launcher(path: &Path) -> Result<Option<Vec<String>>> {
|
||||
}
|
||||
|
||||
fn managed_path_from_env() -> Option<PathBuf> {
|
||||
std::env::var("CODEWHALE_MANAGED_CONFIG_PATH")
|
||||
.or_else(|_| std::env::var("DEEPSEEK_MANAGED_CONFIG_PATH"))
|
||||
.ok()
|
||||
.map(|value| expand_path(&value))
|
||||
codewhale_env_var(
|
||||
"CODEWHALE_MANAGED_CONFIG_PATH",
|
||||
"DEEPSEEK_MANAGED_CONFIG_PATH",
|
||||
)
|
||||
.ok()
|
||||
.map(|value| expand_path(&value))
|
||||
}
|
||||
|
||||
fn managed_config_file(configured: Option<&str>) -> Result<Option<PathBuf>> {
|
||||
if let Some(path) = managed_path_from_env() {
|
||||
return Ok(Some(path));
|
||||
}
|
||||
let default = default_managed_config_path().filter(|path| {
|
||||
fs::symlink_metadata(path).map_or_else(
|
||||
|error| error.kind() != std::io::ErrorKind::NotFound,
|
||||
|_| true,
|
||||
)
|
||||
});
|
||||
let Some(configured) = configured
|
||||
.filter(|value| !value.trim().is_empty())
|
||||
.map(expand_path)
|
||||
else {
|
||||
return Ok(default);
|
||||
};
|
||||
if let Some(default) = default
|
||||
&& default != configured
|
||||
{
|
||||
anyhow::bail!(
|
||||
"`managed_config_path` names {}, but the default managed configuration {} exists, and a config file cannot replace it; name another managed file with CODEWHALE_MANAGED_CONFIG_PATH instead",
|
||||
codewhale_config::quote_os_path(&configured),
|
||||
codewhale_config::quote_os_path(&default)
|
||||
);
|
||||
}
|
||||
Ok(Some(configured))
|
||||
}
|
||||
|
||||
fn managed_route_error(config: &Config, path: &Path, error: String) -> anyhow::Error {
|
||||
let selected = config.provider.as_deref().unwrap_or_default();
|
||||
let undeclared = !selected.is_empty()
|
||||
&& ProviderKind::parse(selected).is_none()
|
||||
&& config
|
||||
.providers
|
||||
.as_ref()
|
||||
.and_then(|providers| providers.custom_provider_config(selected))
|
||||
.is_none();
|
||||
if undeclared {
|
||||
anyhow::anyhow!(
|
||||
"provider '{selected}' has no `[providers.{selected}]` table in the configuration, and the managed configuration {} applies: a provider that only a plugin declares cannot be used under a managed configuration. Declare `[providers.{selected}]` in the config file, or select a built-in provider",
|
||||
codewhale_config::quote_os_path(path)
|
||||
)
|
||||
} else {
|
||||
anyhow::anyhow!(
|
||||
"{error} (while applying the managed configuration {})",
|
||||
codewhale_config::quote_os_path(path)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn managed_path_is_named_by_host(path: &Path) -> bool {
|
||||
|
||||
@@ -588,12 +588,11 @@ pub fn session_custom_provider_config(name: &str, kind: &str, base_url: &str) ->
|
||||
// provider is contacted: the assertions are about which host string a route
|
||||
// resolves to, and about the classifications derived from it.
|
||||
|
||||
/// A managed-config guard pointing at a path that does not exist, so an
|
||||
/// A managed-config guard pointing at an empty managed file, so an
|
||||
/// operator-installed managed file on the developer's machine cannot leak into
|
||||
/// these route assertions.
|
||||
pub fn no_managed_config(root: &std::path::Path) -> EnvVarGuard {
|
||||
EnvVarGuard::set(
|
||||
"DEEPSEEK_MANAGED_CONFIG_PATH",
|
||||
root.join("absent-managed.toml"),
|
||||
)
|
||||
let path = root.join("empty-managed.toml");
|
||||
std::fs::write(&path, "").expect("write empty managed config");
|
||||
EnvVarGuard::set("CODEWHALE_MANAGED_CONFIG_PATH", path)
|
||||
}
|
||||
@@ -3108,7 +3108,8 @@ fn policy_control_waits_for_foreign_test_env_overrides_to_restore() {
|
||||
let temp = tempfile::tempdir().expect("tempdir");
|
||||
let config_path = temp.path().join("config.toml");
|
||||
let workspace = temp.path().join("workspace");
|
||||
let managed_config_path = temp.path().join("missing-managed.toml");
|
||||
let managed_config_path = temp.path().join("empty-managed.toml");
|
||||
fs::write(&managed_config_path, "").expect("empty managed config");
|
||||
let requirements_path = temp.path().join("missing-requirements.toml");
|
||||
let (started_tx, started_rx) = mpsc::channel();
|
||||
let (tx, rx) = mpsc::channel();
|
||||
@@ -6476,10 +6477,7 @@ fn deepseek_model_env_overrides_default_text_model() -> Result<()> {
|
||||
fn retired_deepseek_aliases_from_env_are_migrated_before_runtime() -> Result<()> {
|
||||
let _lock = lock_test_env();
|
||||
let temp_root = tempfile::tempdir()?;
|
||||
let _managed_config = crate::test_support::EnvVarGuard::set(
|
||||
"DEEPSEEK_MANAGED_CONFIG_PATH",
|
||||
temp_root.path().join("missing-managed.toml"),
|
||||
);
|
||||
let _managed_config = no_managed_config(temp_root.path());
|
||||
|
||||
for (provider, alias, expected_effort) in [
|
||||
("deepseek", "deepseek-chat", "off"),
|
||||
@@ -12198,10 +12196,7 @@ fn tool_launcher_is_refused_outside_the_managed_layer() -> Result<()> {
|
||||
let _lock = lock_test_env();
|
||||
let temp_root = tempfile::tempdir()?;
|
||||
let _guard = EnvGuard::new(temp_root.path());
|
||||
let _managed = EnvVarGuard::set(
|
||||
"CODEWHALE_MANAGED_CONFIG_PATH",
|
||||
temp_root.path().join("absent-managed.toml"),
|
||||
);
|
||||
let _managed = no_managed_config(temp_root.path());
|
||||
let config_path = temp_root.path().join("config.toml");
|
||||
for document in [
|
||||
"tool_launcher = [\"/usr/bin/sudo\", \"-n\", \"-u\", \"agent\", \"--\"]\n",
|
||||
@@ -12372,17 +12367,62 @@ fn the_managed_file_named_by_the_host_survives_a_provider_that_resolves_late() -
|
||||
ConfigEnvironmentPolicy::Runtime,
|
||||
&mut late_provider,
|
||||
);
|
||||
if let Ok(config) = loaded {
|
||||
assert_eq!(
|
||||
config.tool_launcher.as_deref(),
|
||||
Some(["/usr/bin/sudo", "-n"].map(str::to_string).as_slice()),
|
||||
"{user_file}"
|
||||
);
|
||||
}
|
||||
let error = loaded.expect_err("a plugin provider does not load under a managed file");
|
||||
assert!(
|
||||
format!("{error:#}").contains("a provider that only a plugin declares"),
|
||||
"{user_file}: {error:#}"
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_named_managed_file_that_cannot_be_read_is_a_load_error() -> Result<()> {
|
||||
let _lock = lock_test_env();
|
||||
let temp_root = tempfile::tempdir()?;
|
||||
let _guard = EnvGuard::new(temp_root.path());
|
||||
let _legacy_managed = EnvVarGuard::remove("DEEPSEEK_MANAGED_CONFIG_PATH");
|
||||
let config_path = temp_root.path().join("config.toml");
|
||||
fs::write(&config_path, "")?;
|
||||
let missing = temp_root.path().join("missing-managed.toml");
|
||||
for named in [missing.clone(), temp_root.path().to_path_buf()] {
|
||||
let _managed = EnvVarGuard::set("CODEWHALE_MANAGED_CONFIG_PATH", &named);
|
||||
let error = Config::load(Some(config_path.clone()), None).expect_err("closed");
|
||||
assert!(
|
||||
format!("{error:#}").contains("does not start without a managed configuration"),
|
||||
"{error:#}"
|
||||
);
|
||||
}
|
||||
|
||||
let _managed = EnvVarGuard::remove("CODEWHALE_MANAGED_CONFIG_PATH");
|
||||
fs::write(
|
||||
&config_path,
|
||||
format!(
|
||||
"managed_config_path = {}\n",
|
||||
toml::Value::String(missing.display().to_string())
|
||||
),
|
||||
)?;
|
||||
let error = Config::load(Some(config_path.clone()), None).expect_err("closed");
|
||||
assert!(
|
||||
format!("{error:#}").contains("does not start without a managed configuration"),
|
||||
"{error:#}"
|
||||
);
|
||||
|
||||
fs::write(&config_path, "")?;
|
||||
let empty = temp_root.path().join("empty-managed.toml");
|
||||
fs::write(&empty, "")?;
|
||||
let _blank = EnvVarGuard::set("CODEWHALE_MANAGED_CONFIG_PATH", "");
|
||||
let _legacy = EnvVarGuard::set("DEEPSEEK_MANAGED_CONFIG_PATH", &missing);
|
||||
let error = Config::load(Some(config_path.clone()), None).expect_err("blank defers");
|
||||
assert!(
|
||||
format!("{error:#}").contains("missing-managed.toml"),
|
||||
"{error:#}"
|
||||
);
|
||||
let _legacy = EnvVarGuard::set("DEEPSEEK_MANAGED_CONFIG_PATH", &empty);
|
||||
Config::load(Some(config_path), None)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(all(target_os = "linux", not(target_env = "ohos")))]
|
||||
#[test]
|
||||
fn tool_launcher_is_refused_from_a_managed_file_the_user_config_names() -> Result<()> {
|
||||
|
||||
@@ -492,6 +492,8 @@ pub struct Settings {
|
||||
/// One-time Operate-retirement toast has been shown. Same suppression
|
||||
/// contract as `yolo_deprecation_shown`.
|
||||
pub operate_deprecation_shown: bool,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub account_preferences_notice_shown: Option<String>,
|
||||
/// Round 3 (2026-09-01) moved the work bar under the composer. Every
|
||||
/// settings.toml saved before that carries `work_surface_placement =
|
||||
/// "top"` — the old default, persisted verbatim by ordinary saves, not a
|
||||
@@ -650,9 +652,7 @@ impl Default for Settings {
|
||||
mention_menu_limit: 128,
|
||||
mention_walk_depth: 10,
|
||||
mention_menu_behavior: "fuzzy".to_string(),
|
||||
// Reasoning is useful when explicitly requested, but it should
|
||||
// never displace the actual conversation in the default TUI.
|
||||
show_thinking: false,
|
||||
show_thinking: true,
|
||||
thinking_default_expanded: false,
|
||||
thinking_preview_lines: default_thinking_preview_lines(),
|
||||
thinking_highlight: true,
|
||||
@@ -694,6 +694,7 @@ impl Default for Settings {
|
||||
feature_intro_shown: false,
|
||||
yolo_deprecation_shown: false,
|
||||
operate_deprecation_shown: false,
|
||||
account_preferences_notice_shown: None,
|
||||
legacy_operate_default: false,
|
||||
work_surface_bottom_migrated: false,
|
||||
behavioral_tip_impressions: std::collections::BTreeMap::new(),
|
||||
@@ -3747,6 +3748,36 @@ mod tests {
|
||||
assert_eq!(reloaded.permission_posture.as_deref(), Some("ask"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn thinking_shows_by_default_and_a_saved_choice_survives_reload_and_other_saves() {
|
||||
let _g = config_path_test_guard();
|
||||
let tmp = tempfile::tempdir().expect("tempdir");
|
||||
let settings_path = tmp.path().join("settings.toml");
|
||||
let _config_override =
|
||||
EnvVarRestore::set("DEEPSEEK_CONFIG_PATH", tmp.path().join("config.toml"));
|
||||
|
||||
let absent = Settings::load().expect("load without a settings file");
|
||||
assert!(absent.show_thinking);
|
||||
assert!(!absent.thinking_default_expanded);
|
||||
assert!(!absent.is_set("show_thinking"));
|
||||
|
||||
std::fs::write(&settings_path, "theme = \"light\"\n").expect("settings without the key");
|
||||
let unnamed = Settings::load().expect("load settings without the key");
|
||||
assert!(unnamed.show_thinking);
|
||||
assert!(!unnamed.is_set("show_thinking"));
|
||||
|
||||
for saved in [false, true] {
|
||||
std::fs::write(&settings_path, format!("show_thinking = {saved}\n"))
|
||||
.expect("saved choice");
|
||||
let loaded = Settings::load().expect("load the saved choice");
|
||||
assert_eq!(loaded.show_thinking, saved);
|
||||
assert!(loaded.is_set("show_thinking"));
|
||||
Settings::transact(|settings| settings.set("locale", "en")).expect("another save");
|
||||
let reloaded = Settings::load().expect("reload after another save");
|
||||
assert_eq!(reloaded.show_thinking, saved);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn settings_load_keeps_top_placement_chosen_after_the_bottom_migration() {
|
||||
let _g = config_path_test_guard();
|
||||
@@ -3886,7 +3917,7 @@ mod tests {
|
||||
let mut settings = Settings::default();
|
||||
// Density is calm by default; motion is an independent axis.
|
||||
assert!(settings.calm_mode);
|
||||
assert!(!settings.show_thinking);
|
||||
settings.show_thinking = false;
|
||||
|
||||
let changed = settings.apply_preset("CALM").expect("calm preset applies");
|
||||
assert_eq!(
|
||||
@@ -3916,8 +3947,8 @@ mod tests {
|
||||
assert!(settings.fancy_animations);
|
||||
assert_eq!(settings.transcript_spacing, "spacious");
|
||||
assert_eq!(settings.tool_collapse_mode, "compact");
|
||||
// Thinking is opt-in so the transcript stays focused on the chat.
|
||||
assert!(!settings.show_thinking);
|
||||
assert!(settings.show_thinking);
|
||||
assert!(!settings.thinking_default_expanded);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
"in_model_catalogue": "true when the skill renders as an ambient catalogue line",
|
||||
"shadowed_aliases": "aliases that collide with another canonical bundled name; the canonical skill wins resolution"
|
||||
},
|
||||
"generation": "21",
|
||||
"generation": "22",
|
||||
"skills": [
|
||||
{
|
||||
"name": "skill-creator",
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
---
|
||||
name: plugin-creator
|
||||
description: Scaffold a local Codewhale plugin bundle with a versioned manifest, namespaced Skills, and an explicit trust review.
|
||||
---
|
||||
|
||||
# Plugin Creator
|
||||
|
||||
Use this skill when a user wants a local Codewhale plugin bundle. Trusted and
|
||||
enabled bundles may add declarative Skills, commands, agents, hooks, and MCP
|
||||
servers (stdio and remote) through the existing engines. LSP, filesystem
|
||||
roots, and lifecycle mutation are inventory-only. Native extensions (host
|
||||
code) are inventory-only unless the user has turned on the experimental
|
||||
`[features] extension_host` flag.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. Pick a Codewhale-owned location:
|
||||
- User bundle: `~/.codewhale/plugins/<plugin-name>/`
|
||||
- Workspace bundle: `<workspace>/.codewhale/plugins/<plugin-name>/`
|
||||
2. Normalize the bundle name to lowercase hyphen-case.
|
||||
3. Create `plugin.json` (Agent Plugins v1.0.0; a legacy `plugin.toml` stays
|
||||
readable, but new bundles use `plugin.json`):
|
||||
|
||||
```json
|
||||
{
|
||||
"$schema": "https://agent-plugins.org/schemas/plugin.json",
|
||||
"name": "my-plugin",
|
||||
"version": "0.1.0",
|
||||
"description": "What this bundle provides"
|
||||
}
|
||||
```
|
||||
|
||||
4. Put each Skill under `skills/<skill-name>/SKILL.md`; Codewhale finds
|
||||
`skills/` automatically and exposes each as `my-plugin:<skill-name>`,
|
||||
never as an unqualified command.
|
||||
5. Add MCP servers in a sibling `mcp.json` only when the bundle needs an
|
||||
existing MCP engine. Keep stdio commands and paths inside the bundle. Map local
|
||||
environment values only as exact `${SOURCE_ENV}` references. For remote MCP,
|
||||
use HTTPS (or loopback HTTP), forbid URL user information/query/fragment,
|
||||
use only environment-backed headers or bearer tokens, and declare the exact
|
||||
normalized endpoint host set in `capabilities.network_hosts` under
|
||||
`extensions["net.codewhale"]`. Never place credentials in the manifest.
|
||||
6. Commands (`commands/*.md`), agents (`agents/*.toml`), and hooks
|
||||
(`hooks/*.toml`), declared under `extensions["net.codewhale"]`, activate
|
||||
under the current policy — workspace bundles win same-name collisions over
|
||||
user and built-in bundles. LSP, filesystem roots, and lifecycle mutation
|
||||
are inventory-only: declare them only when inventorying future work. A
|
||||
`native` entry runs only under the experimental extension host; there it
|
||||
must be one `.mjs`, `.js` or `.mts` ES module file, `/plugin validate` rejects
|
||||
anything else, and its tools always use `Required` approval, never a
|
||||
plugin's read-only hint. Full Access, Bypass, or an exact session grant
|
||||
for the reviewed build can satisfy that gate without a prompt. A bundle
|
||||
that declares only unsupported surfaces cannot be enabled.
|
||||
7. Validate and review without executing bundle content:
|
||||
- `/plugin validate <plugin-name>`
|
||||
- `/plugin show <plugin-name>`
|
||||
- stop and present these results; the person runs `/plugin enable <plugin-name>`
|
||||
to open the content/capability review, reviews it, runs the exact
|
||||
`/plugin trust ...` confirmation shown, then enables the bundle
|
||||
8. Verify `/skills inspect` reports plugin provenance and `/plugin list`
|
||||
reports the expected trust and activation state. Trust stages the reviewed
|
||||
content but does not activate it. After enablement, follow the host's
|
||||
reload notice: use `/reload` or a new session to apply changes to a live
|
||||
session's pinned skills and tools.
|
||||
|
||||
Every user and workspace bundle starts untrusted and disabled. Reuse the
|
||||
existing `/plugin marketplace`, install, update, review and reload surfaces;
|
||||
do not add a parallel installer, registry or automatic trust flow. Catalog
|
||||
membership alone never installs, trusts or enables a plugin.
|
||||
|
||||
## Experimental host code
|
||||
|
||||
Only scaffold host code when the person explicitly uses the experimental
|
||||
extension-host feature. Start from the tested `hello-extension` example and
|
||||
`docs/EXTENSIONS.md` in the Codewhale repository. A typed `.mts` entry may use
|
||||
Node's erasable TypeScript syntax; bundle dependencies locally. Register tools
|
||||
with a plugin-specific prefix and an object input schema, propagate
|
||||
`exec.signal`, and use `ctx.effect` for bounded asynchronous cleanup. The
|
||||
current execution context exposes `signal`, `callId` and `args`; it does not
|
||||
expose the calling workspace path. Do not change the shared process cwd.
|
||||
|
||||
Stop after install, validate and show; never automate the trust token. A
|
||||
person reviews, trusts and enables the bundle. `/plugin show <name>` reports
|
||||
owner state, live tools and recent attributed diagnostics. Recovery may create
|
||||
fresh registrations, but never replays an interrupted tool call. Explain the
|
||||
shared-process and current platform sandbox limits without claiming isolation.
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: plugin-creator
|
||||
description: Scaffold a local Codewhale plugin bundle with a versioned manifest, namespaced Skills, and an explicit trust review.
|
||||
description: Scaffold a local Codewhale plugin bundle with a versioned manifest, namespaced Skills, and a trust review that goes through the session's approval.
|
||||
---
|
||||
|
||||
# Plugin Creator
|
||||
@@ -54,9 +54,24 @@ code) are inventory-only unless the user has turned on the experimental
|
||||
7. Validate and review without executing bundle content:
|
||||
- `/plugin validate <plugin-name>`
|
||||
- `/plugin show <plugin-name>`
|
||||
- stop and present these results; the person runs `/plugin enable <plugin-name>`
|
||||
- present these results; the person runs `/plugin enable <plugin-name>`
|
||||
to open the content/capability review, reviews it, runs the exact
|
||||
`/plugin trust ...` confirmation shown, then enables the bundle
|
||||
- exception: in Full Access, for a plugin you wrote yourself, you may
|
||||
trust and enable it without asking the person. Write it in an ordinary
|
||||
folder inside the current project (for example
|
||||
`<workspace>/plugins/<plugin-name>/`), not under `.codewhale/plugins/`
|
||||
as in step 1, because `codewhale install` copies it into place and
|
||||
refuses a name that is already in use. Then run, in this order:
|
||||
`codewhale install <folder>`, `codewhale plugins trust <name>` (it prints
|
||||
the review and a review token), `codewhale plugins trust <name>
|
||||
--review-token <token> --yes` with the token from your own run, and
|
||||
`codewhale plugins enable <name>`. These are shell commands, so the
|
||||
session's approval applies to them like any other (Ask asks the person,
|
||||
Auto-Review's reviewer judges, Full Access runs them). The trust is
|
||||
recorded as approved by the agent, for this project only. A bundle that
|
||||
was not installed this way from inside the project is refused. Never do
|
||||
this for a plugin you did not write.
|
||||
8. Verify `/skills inspect` reports plugin provenance and `/plugin list`
|
||||
reports the expected trust and activation state. Trust stages the reviewed
|
||||
content but does not activate it. After enablement, follow the host's
|
||||
@@ -65,22 +80,18 @@ code) are inventory-only unless the user has turned on the experimental
|
||||
|
||||
Every user and workspace bundle starts untrusted and disabled. Reuse the
|
||||
existing `/plugin marketplace`, install, update, review and reload surfaces;
|
||||
do not add a parallel installer, registry or automatic trust flow. Catalog
|
||||
membership alone never installs, trusts or enables a plugin.
|
||||
do not add a parallel installer or registry. Catalog membership alone never
|
||||
installs, trusts or enables a plugin.
|
||||
|
||||
## Experimental host code
|
||||
## Plugins with code or a screen
|
||||
|
||||
Only scaffold host code when the person explicitly uses the experimental
|
||||
extension-host feature. Start from the tested `hello-extension` example and
|
||||
`docs/EXTENSIONS.md` in the Codewhale repository. A typed `.mts` entry may use
|
||||
Node's erasable TypeScript syntax; bundle dependencies locally. Register tools
|
||||
with a plugin-specific prefix and an object input schema, propagate
|
||||
`exec.signal`, and use `ctx.effect` for bounded asynchronous cleanup. The
|
||||
current execution context exposes `signal`, `callId` and `args`; it does not
|
||||
expose the calling workspace path. Do not change the shared process cwd.
|
||||
|
||||
Stop after install, validate and show; never automate the trust token. A
|
||||
person reviews, trusts and enables the bundle. `/plugin show <name>` reports
|
||||
owner state, live tools and recent attributed diagnostics. Recovery may create
|
||||
fresh registrations, but never replays an interrupted tool call. Explain the
|
||||
shared-process and current platform sandbox limits without claiming isolation.
|
||||
For a plugin that runs code, adds tools of its own, or shows a screen, use the
|
||||
`cordis-plugin-development` skill when it is offered: it teaches the format
|
||||
that also loads in DeepSeek Harness. Host code runs only under the
|
||||
experimental `[features] extension_host` flag; start from the tested
|
||||
`hello-extension` example and `docs/EXTENSIONS.md` in the Codewhale repository,
|
||||
add a `plugin.json` beside the package for Codewhale's own kinds, and never
|
||||
change the shared process cwd. A person reviews and turns on a plugin as step 7
|
||||
says. `/plugin show <name>` reports owner state, live tools and recent
|
||||
attributed diagnostics. Explain the shared-process and current platform sandbox
|
||||
limits without claiming isolation.
|
||||
@@ -42,7 +42,7 @@ use sha2::{Digest, Sha256};
|
||||
/// person; the exact generation-16 body allows a safe upgrade.
|
||||
/// Generation 18 recognizes previously shipped bodies by SHA-256 as well as by
|
||||
/// retained bytes, so an untouched copy from any earlier generation upgrades.
|
||||
const BUNDLED_SKILL_VERSION: &str = "21";
|
||||
const BUNDLED_SKILL_VERSION: &str = "22";
|
||||
|
||||
mod dsh_creator;
|
||||
use dsh_creator::{BundledFile, CREATOR_SKILLS};
|
||||
@@ -176,6 +176,10 @@ const SUPERSEDED_BODIES: &[(&str, &str)] = &[
|
||||
"plugin-creator",
|
||||
include_str!("../assets/skills/plugin-creator/SKILL.generation-16.md"),
|
||||
),
|
||||
(
|
||||
"plugin-creator",
|
||||
include_str!("../assets/skills/plugin-creator/SKILL.generation-19.md"),
|
||||
),
|
||||
(
|
||||
"help",
|
||||
include_str!("../assets/skills/help/SKILL.generation-14.md"),
|
||||
|
||||
@@ -70,6 +70,7 @@ plugin-creator 935a64e72137a2420910a3365f8d4000038987fa0753648e9751dd79eae5cc30
|
||||
plugin-creator a22e9b25e4ba8eac43007899547bf4471e79a18d3b55f60df4cf073271529104
|
||||
plugin-creator b150b79446d876a2cd572aacd7be7712fd9914c340b51969a29186f85db4e18a
|
||||
plugin-creator c20a792055a4b5601e58b2fb721cd896a34f572a4acdabd1a706d46ce77eb639
|
||||
plugin-creator c761d6774b751a985ffed2894f595d8f871430b0a05a8e94bb7245399d811de7
|
||||
plugin-creator ce156ea9c6e2896b9c6c0da08f3d78c27bb793f685bcd2afbbffd712a5f64387
|
||||
plugin-creator e91aa6dfde62abd9548dda7a43bc097a82aa19385335117e86cb204edc9e294d
|
||||
presentations 2b6d20a00280f3de7814ebd8870e6e1af08c11bd115603b265fc31b9004f9d35
|
||||
|
||||
@@ -874,7 +874,8 @@ fn generation_17_refreshes_previous_plugin_creator_bodies() {
|
||||
.find(|skill| skill.name == "plugin-creator")
|
||||
.unwrap();
|
||||
assert!(skill.body.contains("Create `plugin.json`"));
|
||||
assert!(skill.body.contains("## Experimental host code"));
|
||||
assert!(skill.body.contains("cordis-plugin-development"));
|
||||
assert!(!skill.body.contains("never automate the trust token"));
|
||||
for (generation, old) in [
|
||||
(
|
||||
"15",
|
||||
@@ -884,9 +885,16 @@ fn generation_17_refreshes_previous_plugin_creator_bodies() {
|
||||
"16",
|
||||
include_str!("../../assets/skills/plugin-creator/SKILL.generation-16.md"),
|
||||
),
|
||||
(
|
||||
"19",
|
||||
include_str!("../../assets/skills/plugin-creator/SKILL.generation-19.md"),
|
||||
),
|
||||
] {
|
||||
assert!(is_superseded_shipped_body("plugin-creator", old));
|
||||
assert!(!old.contains("## Experimental host code"));
|
||||
assert_eq!(
|
||||
old.contains("## Experimental host code"),
|
||||
generation == "19"
|
||||
);
|
||||
for customized in [false, true] {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
fs::create_dir_all(skill_dir(&tmp, "plugin-creator")).unwrap();
|
||||
|
||||
+11
-15
@@ -7,7 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
- **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.
|
||||
- **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.
|
||||
- **Paused stream:** A paused stream now shows the thinking it has already received instead of waiting for the next token.
|
||||
- **Sign-in notice:** The "Account not loaded" notice now appears once per sign-in in the footer, and Esc dismisses it; it no longer lands in the transcript.
|
||||
- **Usage reporting:** Installers now run their closing diagnostic with usage reporting and update checks off and print how to turn usage reporting off. `codewhale doctor` now says a saved opt-out comes from the config file, not the environment, and help and English notices call the feature "usage reporting".
|
||||
- **Long tool names:** Tools whose encoded name is longer than the route's limit are now left out with a visible warning instead of failing every request with a 400. The limit is 64 characters on OpenAI and on other OpenAI-compatible providers that document no larger limit, and 128 on Anthropic-style routes, DeepSeek, Ollama, Ollama Cloud, vLLM, SGLang and custom routes. Forcing such a tool gives an error that names the tool and the limit, and a session that already called one on a route with a documented limit (Anthropic, OpenAI, Codex, DeepSeek) is refused with a message saying to switch route or start a new session. A forced tool_choice in any chat form now names the encoded tool, and a tool whose real name looks like an encoded escape can no longer be mistaken for another tool.
|
||||
- **Start after a hard kill:** The Engine now also starts after a hard kill when its home was spelled through a symbolic link (for example under /tmp on macOS) or, on macOS and Windows, in a different letter case: a dead owner's record for the deleted data directory is discarded instead of refusing every start.
|
||||
- **Context window labels:** the model picker and fleet capability badges now show `1.5M` and `2.1M` for 1,500,000- and 2,100,000-token windows instead of `1.50M` and `2.10M`. Thanks [@Aboysky](https://github.com/Aboysky) (PR 7295).
|
||||
- **REPL output limit:** REPL output is now cut to its byte limit, so Chinese, Japanese, Korean or emoji output no longer comes back up to three times longer than the limit, and the cut never splits a character. Thanks [@Aboysky](https://github.com/Aboysky) (PR 7296).
|
||||
- **Git status with renamed files:** the Git and Changes views no longer fail with "git status failed" or show a phantom row when a file is renamed on the worktree side (for example a moved file marked with `git add -N`), including when the original name starts with non-ASCII text such as Chinese. Thanks [@luochen211](https://github.com/luochen211) (PR 7310).
|
||||
@@ -22,7 +30,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- **Simplified Chinese punctuation and /status:** in the zh-Hans interface, the `/router` setup and test screens, label prompts and usage lines now use full-width punctuation (`:;,()`) instead of ASCII marks next to Chinese text, and `/status` values start in one column under Chinese labels instead of starting at a different column for each label. Thanks [@luochen211](https://github.com/luochen211) (PR 7314).
|
||||
- **Git tool on older git:** the Git tool's `review_scope` action no longer passes `--no-lazy-fetch`, which git older than 2.44 rejects, so it failed on every call there. It sets `GIT_NO_LAZY_FETCH=1` instead, which keeps the same protection on newer git and is ignored by older git.
|
||||
- **Plugins from a shell:** `codewhale plugins trust <name>` prints the same capability review as `/plugin trust` with its bundle review token and, in a terminal, asks once. `--review-token <token> --yes` trusts without a prompt while that token still matches the installed content and capabilities; the token ties trust to the exact content, does not show that anyone read the review, and is not the host-service review token. A `--yes` trust is recorded as the command line and grants no host service. Every command the agent runs (shell, persistent terminal, verifier and test runs, the Python kernel, MCP servers it starts) now carries `CODEWHALE_AGENT_PROJECT` with the session's project, while hooks and configured MCP servers do not; with it, a trust or enable is recorded as the agent's, for that project only, even when a prompt is answered through a terminal, and the agent may trust only a plugin it installed from a folder inside the project. `plugins enable`, `disable` and `revoke` work the same way, each with `--json`, and `plugins status` shows who approved. The agent's shell commands that run them go through the session's approval like any other shell command, so a Full Access agent can trust and enable a plugin it wrote; the mark is a label the agent could remove in Full Access. (CW-7891)
|
||||
- **Sandbox:** a managed configuration can set `tool_launcher` (Linux) to run every sandboxed tool command as another user, in front of bubblewrap; commands then run inside the sandbox or not at all, masks are computed for that user, `/tmp` is private per command, and `GET /v1/runtime/info` reports `sandbox.kind` and `sandbox.tool_launcher`. Without the key nothing changes. A managed configuration named by `CODEWHALE_MANAGED_CONFIG_PATH` is now applied on every load; a configuration whose provider comes from a plugin no longer loads with the managed file skipped, it fails to load instead.
|
||||
- **Sandbox:** a managed configuration can set `tool_launcher` (Linux) to run every sandboxed tool command as another user, in front of bubblewrap; commands then run inside the sandbox or not at all, masks are computed for that user, `/tmp` is private per command, and `GET /v1/runtime/info` reports `sandbox.kind` and `sandbox.tool_launcher`. Without the key nothing changes. A managed configuration named by `CODEWHALE_MANAGED_CONFIG_PATH` or `managed_config_path` is applied on every load, and one that is missing or cannot be read stops the load instead of loading with no managed layer; a blank `CODEWHALE_MANAGED_CONFIG_PATH` is now ignored instead of switching the managed layer off, and a config file's `managed_config_path` can no longer point away from an existing file at the default managed path. Fleet local workers inherit the managed-config variable, and `codewhale serve --mcp` does not expose shell tools when the managed configuration it loads sets `tool_launcher`. A configuration whose provider is declared only by a plugin fails to load under a managed configuration, with an error that says to declare the provider table or pick a built-in provider.
|
||||
- **Receipts:** a terminal session's receipt (`/receipts`, `codewhale receipts`) now shows the provider's token counts and the same cost report `/cost` prints, saying "unknown" and why when the route's price is not known, never a guess. A Runtime thread's receipt shows token counts only; its cost line follows in a later release. `codewhale metrics` now counts usage saved in terminal sessions instead of reporting none. Sessions saved by 0.10.2 carry only a combined token total.
|
||||
- **ACP server on the official SDK:** The ACP server (`codewhale serve --acp`) now runs on the official Agent Client Protocol SDK. Its messages follow the official v1 types, and it no longer advertises the non-standard `modelSelection` capability or session `models` field. Session list entries carry `createdAt` and `messageCount` under `_meta.codewhale`, terminal sign-in methods are advertised only to clients that enabled terminal authentication, a notification is never answered, a permission request always carries text, and session list and configuration requests are served while a turn runs, response ids are echoed unchanged for every client (the Zed-only string rewrite is gone), and `initialize` without `protocolVersion` is refused with `-32602`.
|
||||
- **External agents over ACP:** Codewhale now drives external agents through the official Agent Client Protocol SDK. Request ids on the wire are strings, and an agent's error is treated as a sign-in problem only when it carries the protocol's auth-required code.
|
||||
@@ -660,7 +668,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.
|
||||
@@ -697,12 +705,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.
|
||||
@@ -731,12 +733,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.
|
||||
|
||||
+30
-1
@@ -3216,6 +3216,7 @@ async fn run_async_main_dispatch(
|
||||
workspace,
|
||||
skills_dir,
|
||||
skills_config,
|
||||
config.tool_launcher.is_some(),
|
||||
)
|
||||
.await
|
||||
} else if http_selected {
|
||||
@@ -7867,7 +7868,7 @@ fn doctor_control_socket_posture_line(config: &Config) -> String {
|
||||
/// Report the durable opt-out and environment preference without arming or
|
||||
/// touching telemetry state. Notice presentation is not a permission gate.
|
||||
fn doctor_runtime_telemetry(config: &Config) -> (bool, &'static str) {
|
||||
let (on, source) = codewhale_config::resolved_telemetry_consent(config.telemetry);
|
||||
let (on, source) = codewhale_config::dispatched_telemetry_consent(config.telemetry);
|
||||
if !on {
|
||||
return (false, source.as_str());
|
||||
}
|
||||
@@ -16558,6 +16559,8 @@ mod doctor_setup_state_tests {
|
||||
let _telemetry_alias_env = crate::test_support::EnvVarGuard::remove("DEEPSEEK_TELEMETRY");
|
||||
let _telemetry_floor =
|
||||
crate::test_support::EnvVarGuard::remove("CODEWHALE_TELEMETRY_FLOOR");
|
||||
let _telemetry_source =
|
||||
crate::test_support::EnvVarGuard::remove("CODEWHALE_TELEMETRY_SOURCE");
|
||||
let workspace = tmp.path().join("workspace");
|
||||
fs::create_dir_all(&workspace).expect("workspace");
|
||||
|
||||
@@ -16608,6 +16611,32 @@ mod doctor_setup_state_tests {
|
||||
let report = doctor_setup_report_json(&config, &workspace);
|
||||
assert_eq!(report["runtime_posture"]["telemetry"]["value"], false);
|
||||
assert_eq!(report["runtime_posture"]["telemetry"]["source"], "config");
|
||||
|
||||
{
|
||||
let _handed_down =
|
||||
crate::test_support::EnvVarGuard::set("CODEWHALE_TELEMETRY", "false");
|
||||
let _floor = crate::test_support::EnvVarGuard::set("CODEWHALE_TELEMETRY_FLOOR", "0");
|
||||
let _source =
|
||||
crate::test_support::EnvVarGuard::set("CODEWHALE_TELEMETRY_SOURCE", "config");
|
||||
let line = doctor_runtime_posture_line(&config, &workspace);
|
||||
assert!(
|
||||
line.contains("telemetry=off (config)"),
|
||||
"a saved opt-out the dispatcher handed down is not the environment: {line}"
|
||||
);
|
||||
let report = doctor_setup_report_json(&config, &workspace);
|
||||
assert_eq!(report["runtime_posture"]["telemetry"]["source"], "config");
|
||||
}
|
||||
{
|
||||
let _by_hand = crate::test_support::EnvVarGuard::set("CODEWHALE_TELEMETRY", "0");
|
||||
let _floor = crate::test_support::EnvVarGuard::set("CODEWHALE_TELEMETRY_FLOOR", "1");
|
||||
let _source =
|
||||
crate::test_support::EnvVarGuard::set("CODEWHALE_TELEMETRY_SOURCE", "env");
|
||||
let line = doctor_runtime_posture_line(&config, &workspace);
|
||||
assert!(
|
||||
line.contains("telemetry=off (env)"),
|
||||
"a kill switch the person set is the environment: {line}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -87,6 +87,27 @@ pub(crate) use types::{
|
||||
WORKFLOW_DRAFT_INSTRUCTION_PREFIX,
|
||||
};
|
||||
|
||||
const ACCOUNT_PREFERENCES_NOTICE: &str = "account-preferences-notice";
|
||||
|
||||
fn account_sign_in_marker(profile: Option<&str>) -> String {
|
||||
use sha2::{Digest, Sha256};
|
||||
let api_base = codewhale_engine::profile_constitution::runtime_account_api_base();
|
||||
let session = codewhale_secrets::account::AccountSessionStore::new(
|
||||
codewhale_secrets::Secrets::file_backed_read_only(),
|
||||
profile,
|
||||
&api_base,
|
||||
)
|
||||
.runtime_info_at(Utc::now())
|
||||
.ok()
|
||||
.and_then(|info| info.session_id)
|
||||
.unwrap_or_default();
|
||||
Sha256::digest(session.as_bytes())
|
||||
.iter()
|
||||
.take(8)
|
||||
.map(|byte| format!("{byte:02x}"))
|
||||
.collect()
|
||||
}
|
||||
|
||||
// === Types ===
|
||||
|
||||
/// One login owns one mailbox. A cancelled task can only write its abandoned
|
||||
@@ -2448,6 +2469,7 @@ pub struct App {
|
||||
/// re-render per character. `None` means "no bump since the last
|
||||
/// finalize" so the first chunk of a block always renders immediately.
|
||||
pub thinking_revision_last_bump_at: Option<Instant>,
|
||||
pub(crate) thinking_revision_owed: bool,
|
||||
/// Newline-gated streaming collector state.
|
||||
pub streaming_state: StreamingState,
|
||||
/// Live approximate output tokens for the current assistant stream.
|
||||
@@ -3791,6 +3813,38 @@ impl App {
|
||||
);
|
||||
}
|
||||
|
||||
pub(crate) fn notify_account_preferences_not_loaded(&mut self) {
|
||||
let sign_in = account_sign_in_marker(self.config_profile.as_deref());
|
||||
self.notify_account_preferences_not_loaded_for(&sign_in);
|
||||
}
|
||||
|
||||
fn notify_account_preferences_not_loaded_for(&mut self, sign_in: &str) {
|
||||
let first_for_sign_in = Settings::transact_opt(|settings| {
|
||||
if settings.account_preferences_notice_shown.as_deref() == Some(sign_in) {
|
||||
return Ok(None);
|
||||
}
|
||||
settings.account_preferences_notice_shown = Some(sign_in.to_string());
|
||||
Ok(Some(()))
|
||||
});
|
||||
if matches!(first_for_sign_in, Ok(None)) {
|
||||
return;
|
||||
}
|
||||
let text = self
|
||||
.tr(MessageId::AccountPreferencesNotLoaded)
|
||||
.replace("{command}", "codewhale login");
|
||||
self.push_status_toast_record(StatusToast::standing(
|
||||
text,
|
||||
StatusToastLevel::Warning,
|
||||
ACCOUNT_PREFERENCES_NOTICE,
|
||||
));
|
||||
}
|
||||
|
||||
pub(crate) fn dismiss_account_preferences_notice(&mut self) -> bool {
|
||||
let before = self.status_toasts.len();
|
||||
self.retire_event_notices(ACCOUNT_PREFERENCES_NOTICE);
|
||||
self.status_toasts.len() != before
|
||||
}
|
||||
|
||||
/// One-release migration notice for the Shift+Tab/Ctrl+T rebinding: users
|
||||
/// pressing Shift+Tab expecting the old thinking cycle land here first.
|
||||
fn notify_keybinding_migration_once(&mut self) {
|
||||
|
||||
@@ -1142,6 +1142,7 @@ impl App {
|
||||
streaming_thinking_active_entry: None,
|
||||
pending_thinking_translation_cells: HashMap::new(),
|
||||
thinking_revision_last_bump_at: None,
|
||||
thinking_revision_owed: false,
|
||||
streaming_state: StreamingState::new(),
|
||||
streaming_output_token_estimate: 0,
|
||||
reasoning_buffer: String::new(),
|
||||
|
||||
@@ -184,6 +184,46 @@ fn feature_intro_shows_once_persists_then_is_idempotent() {
|
||||
let _ = std::fs::remove_dir_all(&tmp);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn account_preferences_notice_shows_once_per_sign_in_and_can_be_dismissed() {
|
||||
let _env_lock = lock_test_env();
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let config_path = tmp.path().join("config.toml");
|
||||
let _env = EnvVarGuard::set(
|
||||
"DEEPSEEK_CONFIG_PATH",
|
||||
config_path.to_string_lossy().as_ref(),
|
||||
);
|
||||
let shown = |app: &App| {
|
||||
let notice = app
|
||||
.tr(MessageId::AccountPreferencesNotLoaded)
|
||||
.replace("{command}", "codewhale login");
|
||||
app.status_toasts
|
||||
.iter()
|
||||
.filter(|toast| toast.text == notice && toast.kind == StatusToastKind::Standing)
|
||||
.count()
|
||||
};
|
||||
|
||||
let mut app = App::new(test_options(false), &Config::default());
|
||||
app.notify_account_preferences_not_loaded_for("first-sign-in");
|
||||
app.notify_account_preferences_not_loaded_for("first-sign-in");
|
||||
assert_eq!(shown(&app), 1);
|
||||
assert_eq!(
|
||||
Settings::load()
|
||||
.expect("settings should load")
|
||||
.account_preferences_notice_shown
|
||||
.as_deref(),
|
||||
Some("first-sign-in")
|
||||
);
|
||||
assert!(app.dismiss_account_preferences_notice());
|
||||
assert_eq!(shown(&app), 0);
|
||||
|
||||
let mut later_session = App::new(test_options(false), &Config::default());
|
||||
later_session.notify_account_preferences_not_loaded_for("first-sign-in");
|
||||
assert_eq!(shown(&later_session), 0, "the same sign-in is told once");
|
||||
later_session.notify_account_preferences_not_loaded_for("second-sign-in");
|
||||
assert_eq!(shown(&later_session), 1, "a later sign-in is told again");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn initial_input_scenario() {
|
||||
// Scenario consolidation of: initial_input_prefill_waits_for_manual_submit, initial_input_submit_marks_startup_dispatch
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
│ Background (default) ✎ │current terminal
|
||||
│ Pet mode Off [ ] │saved terminal
|
||||
│ Quiet transcript On [x] │startup terminal
|
||||
│ Model reasoning in chat Off [ ] │source settings.toml
|
||||
│ Model reasoning in chat On [x] │source settings.toml
|
||||
│ Thinking Default Expanded Off [ ] │scope SAVED
|
||||
│ Thinking Preview Lines 2 ✎ │apply applies on save
|
||||
│ Thinking background highlight On [x] │kind choice
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
Background (default) ✎ SAVED █
|
||||
Pet mode Off [ ] SAVED █
|
||||
Quiet transcript On [x] SAVED █
|
||||
Model reasoning in chat Off [ ] SAVED █
|
||||
Model reasoning in chat On [x] SAVED █
|
||||
Thinking Default Expanded Off [ ] SAVED █
|
||||
Thinking Preview Lines 2 ✎ SAVED │
|
||||
Thinking background highlight On [x] SAVED │
|
||||
|
||||
@@ -416,15 +416,15 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn a_notice_sheds_whole_clauses_and_never_dangles() {
|
||||
const NOTICE: &str = "Anonymous usage counts are on. Conversations and code are never collected. Change this in /settings; schema: docs/TELEMETRY.md";
|
||||
const NOTICE: &str = "Usage reporting is on. Conversations and code are never collected. Change this in /settings; schema: docs/TELEMETRY.md";
|
||||
assert_eq!(fit_notice(NOTICE, 200, false).as_deref(), Some(NOTICE));
|
||||
assert_eq!(
|
||||
fit_notice(NOTICE, 80, false).as_deref(),
|
||||
Some("Anonymous usage counts are on. Conversations and code are never collected.")
|
||||
Some("Usage reporting is on. Conversations and code are never collected.")
|
||||
);
|
||||
assert_eq!(
|
||||
fit_notice(NOTICE, 40, false).as_deref(),
|
||||
Some("Anonymous usage counts are on.")
|
||||
Some("Usage reporting is on.")
|
||||
);
|
||||
assert_eq!(fit_notice(" ", 40, false), None);
|
||||
}
|
||||
|
||||
@@ -30,15 +30,17 @@ use crate::tui::history::HistoryCell;
|
||||
/// frame cap (see `frame_rate_limiter`) yet imperceptible as lag.
|
||||
///
|
||||
/// Correctness: this only skips *intermediate* repaints. Appended content is
|
||||
/// never dropped — it lands in the cell immediately — and finalize always
|
||||
/// forces a bump so the final reasoning text is fully rendered.
|
||||
/// never dropped — it lands in the cell immediately — the display beat renders
|
||||
/// a skipped bump once the window has passed, and finalize always forces a
|
||||
/// bump so the final reasoning text is fully rendered.
|
||||
const THINKING_REVISION_THROTTLE: Duration = Duration::from_millis(100);
|
||||
|
||||
/// Bump the active-cell revision for a streaming thinking mutation, but at
|
||||
/// most once per [`THINKING_REVISION_THROTTLE`] window. Returns whether a bump
|
||||
/// was actually emitted. Skipped bumps coalesce into the next one (or into the
|
||||
/// forced finalize bump), so no content is ever lost — only redundant
|
||||
/// intermediate re-wraps are dropped.
|
||||
/// was actually emitted. Skipped bumps coalesce into the next one, into
|
||||
/// [`render_owed_revision`] on a later display beat, or into the forced
|
||||
/// finalize bump, so no content is ever lost — only redundant intermediate
|
||||
/// re-wraps are dropped.
|
||||
fn bump_thinking_revision_throttled(app: &mut App, now: Instant) -> bool {
|
||||
let due = app
|
||||
.thinking_revision_last_bump_at
|
||||
@@ -47,9 +49,17 @@ fn bump_thinking_revision_throttled(app: &mut App, now: Instant) -> bool {
|
||||
app.thinking_revision_last_bump_at = Some(now);
|
||||
app.bump_active_cell_revision();
|
||||
}
|
||||
app.thinking_revision_owed = !due;
|
||||
due
|
||||
}
|
||||
|
||||
pub(super) fn render_owed_revision(app: &mut App, now: Instant) -> bool {
|
||||
if app.streaming_thinking_active_entry.is_none() {
|
||||
app.thinking_revision_owed = false;
|
||||
}
|
||||
app.thinking_revision_owed && bump_thinking_revision_throttled(app, now)
|
||||
}
|
||||
|
||||
/// Ensure an in-flight Thinking entry exists in `active_cell` and return its
|
||||
/// entry index. If no thinking entry is currently streaming, push a fresh one.
|
||||
/// P2.3: thinking shares the active cell with subsequent tool calls so the
|
||||
@@ -76,7 +86,8 @@ pub(super) fn ensure_active_entry(app: &mut App) -> usize {
|
||||
/// committed to the cell immediately; the active-cell revision bump that
|
||||
/// triggers a re-wrap of the live tail is debounced to at most one per
|
||||
/// [`THINKING_REVISION_THROTTLE`] window (#1620). Skipped bumps coalesce into
|
||||
/// the next append or the forced finalize bump, so no content is ever lost.
|
||||
/// the next append, a later display beat or the forced finalize bump, so no
|
||||
/// content is ever lost.
|
||||
pub(super) fn append(app: &mut App, entry_idx: usize, text: &str) {
|
||||
append_at(app, entry_idx, text, Instant::now());
|
||||
}
|
||||
@@ -308,6 +319,7 @@ pub(super) fn finalize_active_entry(app: &mut App, duration: Option<f32>, remain
|
||||
// the debounce window so the next thinking block's first chunk renders
|
||||
// immediately rather than being coalesced into a stale window.
|
||||
app.thinking_revision_last_bump_at = None;
|
||||
app.thinking_revision_owed = false;
|
||||
app.bump_active_cell_revision();
|
||||
true
|
||||
}
|
||||
@@ -426,6 +438,34 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_paused_stream_renders_the_reasoning_it_already_received() {
|
||||
let mut app = test_app();
|
||||
let entry = ensure_active_entry(&mut app);
|
||||
app.thinking_revision_last_bump_at = None;
|
||||
let t0 = Instant::now();
|
||||
append_at(&mut app, entry, "first slice ", t0);
|
||||
let shown = app.active_cell_revision;
|
||||
append_at(&mut app, entry, "held back", t0 + Duration::from_millis(16));
|
||||
assert_eq!(app.active_cell_revision, shown);
|
||||
|
||||
assert!(!render_owed_revision(
|
||||
&mut app,
|
||||
t0 + Duration::from_millis(32)
|
||||
));
|
||||
assert_eq!(app.active_cell_revision, shown);
|
||||
assert!(render_owed_revision(
|
||||
&mut app,
|
||||
t0 + THINKING_REVISION_THROTTLE
|
||||
));
|
||||
assert_eq!(app.active_cell_revision, shown.wrapping_add(1));
|
||||
assert!(!render_owed_revision(
|
||||
&mut app,
|
||||
t0 + THINKING_REVISION_THROTTLE * 3
|
||||
));
|
||||
assert_eq!(app.active_cell_revision, shown.wrapping_add(1));
|
||||
}
|
||||
|
||||
/// #7123: a reasoning block that opens while the display beat still holds
|
||||
/// an uncommitted slice of prose must not drop that slice. `start_block`
|
||||
/// resets `StreamingState`, so the tail has to be delivered first.
|
||||
|
||||
@@ -7232,14 +7232,16 @@ pub(crate) async fn run_event_loop(
|
||||
continue;
|
||||
}
|
||||
// An idle operator can dismiss the persistent context warning
|
||||
// without affecting vim or attachment handling. While a turn
|
||||
// is active Esc retains its cancellation meaning.
|
||||
// or the account preferences notice without affecting vim or
|
||||
// attachment handling. While a turn is active Esc retains its
|
||||
// cancellation meaning.
|
||||
KeyCode::Esc
|
||||
if !app.is_loading
|
||||
&& app.input.is_empty()
|
||||
&& !slash_menu_open
|
||||
&& !mention_menu_open
|
||||
&& app.dismiss_context_pressure_warning() =>
|
||||
&& (app.dismiss_context_pressure_warning()
|
||||
|| app.dismiss_account_preferences_notice()) =>
|
||||
{
|
||||
continue;
|
||||
}
|
||||
@@ -9326,14 +9328,10 @@ pub(super) fn apply_engine_status(app: &mut App, message: String) -> bool {
|
||||
app.add_message(HistoryCell::System { content: message });
|
||||
return true;
|
||||
}
|
||||
// The account-profile fallback notice is sent once per engine; keep it in
|
||||
// the transcript so the next turn's status line cannot erase it.
|
||||
let retain = app.tr(MessageId::ProfileConstitutionUnavailableLocal) == message.as_str();
|
||||
if retain {
|
||||
app.add_message(HistoryCell::System {
|
||||
content: message.clone(),
|
||||
});
|
||||
if app.tr(MessageId::ProfileConstitutionUnavailableLocal) == message.as_str() {
|
||||
app.notify_account_preferences_not_loaded();
|
||||
return false;
|
||||
}
|
||||
app.status_message = Some(message);
|
||||
retain
|
||||
false
|
||||
}
|
||||
@@ -1555,7 +1555,10 @@ pub(crate) fn commit_streaming_display_tick(
|
||||
updated = true;
|
||||
}
|
||||
|
||||
if app.streaming_state.has_pending_stream_text(0) {
|
||||
if streaming_thinking::render_owed_revision(app, now) {
|
||||
updated = true;
|
||||
}
|
||||
if app.streaming_state.has_pending_stream_text(0) || app.thinking_revision_owed {
|
||||
stream_display_clock.note_delta(now);
|
||||
}
|
||||
|
||||
|
||||
@@ -8950,10 +8950,9 @@ fn setup_runtime_presets_preserve_environment_authority_and_state() {
|
||||
"DEEPSEEK_SANDBOX_MODE",
|
||||
]
|
||||
.map(EnvVarGuard::remove);
|
||||
let _managed = EnvVarGuard::set(
|
||||
"CODEWHALE_MANAGED_CONFIG_PATH",
|
||||
home._tmp.path().join("absent-managed.toml"),
|
||||
);
|
||||
let empty_managed = home._tmp.path().join("empty-managed.toml");
|
||||
std::fs::write(&empty_managed, "").expect("empty managed config");
|
||||
let _managed = EnvVarGuard::set("CODEWHALE_MANAGED_CONFIG_PATH", &empty_managed);
|
||||
let _requirements = EnvVarGuard::set(
|
||||
"CODEWHALE_REQUIREMENTS_PATH",
|
||||
home._tmp.path().join("absent-requirements.toml"),
|
||||
@@ -27344,7 +27343,7 @@ fn turn_inspector_switches_isolated_full_turn_pages_with_tagged_reasoning_and_ou
|
||||
|
||||
let latest = pager.body_text();
|
||||
assert!(latest.contains("SECOND-PROMPT"), "{latest}");
|
||||
assert!(latest.contains("[∿ reasoning 1/1 · running]"), "{latest}");
|
||||
assert!(latest.contains("[∿ thinking 1/1 · running]"), "{latest}");
|
||||
assert!(latest.contains("SECOND-THOUGHT"), "{latest}");
|
||||
assert!(latest.contains("[◆ · running]"), "{latest}");
|
||||
assert!(latest.contains("SECOND-ANSWER"), "{latest}");
|
||||
@@ -27356,7 +27355,7 @@ fn turn_inspector_switches_isolated_full_turn_pages_with_tagged_reasoning_and_ou
|
||||
for expected in [
|
||||
"FIRST-PROMPT",
|
||||
"keep this input line",
|
||||
"[∿ reasoning 1/1 · Done]",
|
||||
"[∿ thinking 1/1 · Done]",
|
||||
"FIRST-THOUGHT",
|
||||
"full reasoning tail",
|
||||
"[⚙ using tool]",
|
||||
@@ -35489,23 +35488,41 @@ fn extension_prompt_origin_keeps_denials_and_other_policy_holds() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn account_profile_fallback_notice_stays_in_the_transcript() {
|
||||
fn account_profile_fallback_notice_is_a_standing_notice_outside_the_transcript() {
|
||||
let _home = SettingsHomeGuard::new();
|
||||
let mut app = App::new(create_test_options(), &Config::default());
|
||||
let notice = app
|
||||
let engine_notice = app
|
||||
.tr(MessageId::ProfileConstitutionUnavailableLocal)
|
||||
.to_string();
|
||||
let before = app.history.len();
|
||||
assert!(super::event_loop::apply_engine_status(
|
||||
let status_before = app.status_message.clone();
|
||||
assert!(!super::event_loop::apply_engine_status(
|
||||
&mut app,
|
||||
notice.clone()
|
||||
engine_notice
|
||||
));
|
||||
assert_eq!(app.history.len(), before);
|
||||
assert_eq!(app.status_message, status_before);
|
||||
assert!(!super::event_loop::apply_engine_status(
|
||||
&mut app,
|
||||
"Executing tools sequentially".into()
|
||||
));
|
||||
assert_eq!(app.history.len(), before + 1);
|
||||
assert!(matches!(&app.history[before], HistoryCell::System { content } if *content == notice));
|
||||
assert_eq!(app.history.len(), before);
|
||||
let notice = app
|
||||
.tr(MessageId::AccountPreferencesNotLoaded)
|
||||
.replace("{command}", "codewhale login");
|
||||
let standing = |app: &App| {
|
||||
app.status_toasts
|
||||
.iter()
|
||||
.find(|toast| toast.text == notice)
|
||||
.cloned()
|
||||
};
|
||||
app.sync_status_message_to_toasts();
|
||||
let shown = standing(&app).expect("a later routine status does not erase the notice");
|
||||
assert_eq!(shown.level, StatusToastLevel::Warning);
|
||||
assert_eq!(shown.ttl_ms, None);
|
||||
assert!(app.dismiss_account_preferences_notice());
|
||||
assert!(standing(&app).is_none());
|
||||
assert!(!app.dismiss_account_preferences_notice());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -32,6 +32,8 @@ mod plugin_e2e_acceptance;
|
||||
mod screen_mode_inline_pty;
|
||||
#[cfg(all(unix, feature = "long-running-tests"))]
|
||||
mod search_text_pty;
|
||||
#[cfg(all(unix, feature = "long-running-tests"))]
|
||||
mod thinking_stream_pty;
|
||||
mod tool_lifecycle_acceptance;
|
||||
#[cfg(all(unix, feature = "long-running-tests"))]
|
||||
mod work_bar_keys_pty;
|
||||
@@ -0,0 +1,585 @@
|
||||
use std::io::{BufRead, BufReader, Read, Write};
|
||||
use std::net::{SocketAddr, TcpListener, TcpStream};
|
||||
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
|
||||
use std::sync::{Arc, Mutex, mpsc};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use super::qa_harness::{
|
||||
harness::{Harness, SealedWorkspace, make_sealed_workspace},
|
||||
keys,
|
||||
};
|
||||
|
||||
const WAIT: Duration = Duration::from_secs(20);
|
||||
const MODEL: &str = "deepseek-v4-pro";
|
||||
const PROMPTS: [&str; 2] = ["first slow fixture prompt", "second slow fixture prompt"];
|
||||
const ANSWER: &str = "Fixture answer after thinking.";
|
||||
const REASONING_LINES: usize = 6;
|
||||
const NOTICE: &str = "Account not loaded. Run codewhale login.";
|
||||
const RETIRED_TRANSCRIPT_NOTICE: &str = "could not be loaded";
|
||||
|
||||
fn reasoning_line(index: usize) -> String {
|
||||
format!("thinking fixture line {index} of {REASONING_LINES}")
|
||||
}
|
||||
|
||||
struct PacedProvider {
|
||||
address: SocketAddr,
|
||||
gate: mpsc::Sender<()>,
|
||||
stop: Arc<AtomicBool>,
|
||||
turns: Arc<AtomicUsize>,
|
||||
profile_requests: Arc<AtomicUsize>,
|
||||
acceptor: Option<std::thread::JoinHandle<()>>,
|
||||
}
|
||||
|
||||
struct Served {
|
||||
gates: Mutex<mpsc::Receiver<()>>,
|
||||
stop: Arc<AtomicBool>,
|
||||
turns: Arc<AtomicUsize>,
|
||||
profile_requests: Arc<AtomicUsize>,
|
||||
}
|
||||
|
||||
impl PacedProvider {
|
||||
fn start() -> Self {
|
||||
let listener = TcpListener::bind("127.0.0.1:0").expect("loopback provider");
|
||||
let address = listener.local_addr().expect("loopback address");
|
||||
let (gate, gates) = mpsc::channel();
|
||||
let stop = Arc::new(AtomicBool::new(false));
|
||||
let turns = Arc::new(AtomicUsize::new(0));
|
||||
let profile_requests = Arc::new(AtomicUsize::new(0));
|
||||
let served = Arc::new(Served {
|
||||
gates: Mutex::new(gates),
|
||||
stop: Arc::clone(&stop),
|
||||
turns: Arc::clone(&turns),
|
||||
profile_requests: Arc::clone(&profile_requests),
|
||||
});
|
||||
let acceptor = std::thread::spawn(move || {
|
||||
for stream in listener.incoming() {
|
||||
if served.stop.load(Ordering::Acquire) {
|
||||
break;
|
||||
}
|
||||
let Ok(stream) = stream else { continue };
|
||||
let served = Arc::clone(&served);
|
||||
std::thread::spawn(move || {
|
||||
let _ = serve(stream, &served);
|
||||
});
|
||||
}
|
||||
});
|
||||
Self {
|
||||
address,
|
||||
gate,
|
||||
stop,
|
||||
turns,
|
||||
profile_requests,
|
||||
acceptor: Some(acceptor),
|
||||
}
|
||||
}
|
||||
|
||||
fn model_base(&self) -> String {
|
||||
format!("http://{}/v1", self.address)
|
||||
}
|
||||
|
||||
fn account_base(&self) -> String {
|
||||
format!("http://{}", self.address)
|
||||
}
|
||||
|
||||
fn release(&self, gates: usize) {
|
||||
for _ in 0..gates {
|
||||
self.gate.send(()).expect("provider gate");
|
||||
}
|
||||
}
|
||||
|
||||
fn turns(&self) -> usize {
|
||||
self.turns.load(Ordering::Acquire)
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for PacedProvider {
|
||||
fn drop(&mut self) {
|
||||
self.stop.store(true, Ordering::Release);
|
||||
let _ = TcpStream::connect(self.address);
|
||||
if let Some(acceptor) = self.acceptor.take() {
|
||||
let _ = acceptor.join();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn serve(stream: TcpStream, served: &Served) -> std::io::Result<()> {
|
||||
stream.set_read_timeout(Some(Duration::from_secs(10)))?;
|
||||
let mut reader = BufReader::new(stream.try_clone()?);
|
||||
let mut stream = stream;
|
||||
let mut request_line = String::new();
|
||||
if reader.read_line(&mut request_line)? == 0 {
|
||||
return Ok(());
|
||||
}
|
||||
let mut content_length = 0usize;
|
||||
loop {
|
||||
let mut header = String::new();
|
||||
if reader.read_line(&mut header)? == 0 {
|
||||
break;
|
||||
}
|
||||
let header = header.trim_end();
|
||||
if header.is_empty() {
|
||||
break;
|
||||
}
|
||||
if let Some((name, value)) = header.split_once(':')
|
||||
&& name.eq_ignore_ascii_case("content-length")
|
||||
{
|
||||
content_length = value.trim().parse().unwrap_or(0);
|
||||
}
|
||||
}
|
||||
let mut body = vec![0u8; content_length.min(8 * 1024 * 1024)];
|
||||
reader.read_exact(&mut body)?;
|
||||
let mut parts = request_line.split_whitespace();
|
||||
let method = parts.next().unwrap_or_default();
|
||||
let path = parts.next().unwrap_or_default();
|
||||
if method == "GET" && path.ends_with("/models") {
|
||||
return respond_json(
|
||||
&mut stream,
|
||||
"200 OK",
|
||||
&serde_json::json!({"object":"list","data":[{"id":MODEL,"object":"model"}]}),
|
||||
);
|
||||
}
|
||||
if path.starts_with("/api/") {
|
||||
served.profile_requests.fetch_add(1, Ordering::AcqRel);
|
||||
return respond_json(
|
||||
&mut stream,
|
||||
"401 Unauthorized",
|
||||
&serde_json::json!({"error":"loopback fixture account is signed out"}),
|
||||
);
|
||||
}
|
||||
if method != "POST" {
|
||||
return respond_json(
|
||||
&mut stream,
|
||||
"404 Not Found",
|
||||
&serde_json::json!({"error":"not served by the loopback fixture"}),
|
||||
);
|
||||
}
|
||||
let request: serde_json::Value = serde_json::from_slice(&body).unwrap_or_default();
|
||||
let streaming = request["stream"].as_bool().unwrap_or(false);
|
||||
let with_tools = request["tools"]
|
||||
.as_array()
|
||||
.is_some_and(|tools| !tools.is_empty());
|
||||
let text = String::from_utf8_lossy(&body);
|
||||
let turn = PROMPTS
|
||||
.iter()
|
||||
.rposition(|prompt| text.contains(prompt))
|
||||
.map(|index| index + 1);
|
||||
let paced = streaming
|
||||
&& with_tools
|
||||
&& turn.is_some_and(|turn| {
|
||||
served
|
||||
.turns
|
||||
.compare_exchange(turn - 1, turn, Ordering::AcqRel, Ordering::Acquire)
|
||||
.is_ok()
|
||||
});
|
||||
if !streaming {
|
||||
return respond_json(
|
||||
&mut stream,
|
||||
"200 OK",
|
||||
&serde_json::json!({
|
||||
"id":"loopback","object":"chat.completion","model":MODEL,
|
||||
"choices":[{"index":0,"message":{"role":"assistant","content":"ok"},"finish_reason":"stop"}],
|
||||
"usage":{"prompt_tokens":1,"completion_tokens":1,"total_tokens":2}
|
||||
}),
|
||||
);
|
||||
}
|
||||
stream.write_all(
|
||||
b"HTTP/1.1 200 OK\r\ncontent-type: text/event-stream\r\ncache-control: no-cache\r\nconnection: close\r\n\r\n",
|
||||
)?;
|
||||
stream.flush()?;
|
||||
if paced {
|
||||
wait_for_gate(served)?;
|
||||
for index in 1..=REASONING_LINES {
|
||||
send_delta(
|
||||
&mut stream,
|
||||
serde_json::json!({"reasoning_content": format!("{}\n", reasoning_line(index))}),
|
||||
)?;
|
||||
}
|
||||
wait_for_gate(served)?;
|
||||
send_delta(&mut stream, serde_json::json!({"content": ANSWER}))?;
|
||||
wait_for_gate(served)?;
|
||||
} else {
|
||||
send_delta(&mut stream, serde_json::json!({"content": "ok"}))?;
|
||||
}
|
||||
send_chunk(
|
||||
&mut stream,
|
||||
&serde_json::json!({
|
||||
"id":"loopback","object":"chat.completion.chunk","model":MODEL,
|
||||
"choices":[{"index":0,"delta":{},"finish_reason":"stop"}],
|
||||
"usage":{"prompt_tokens":8,"completion_tokens":4,"total_tokens":12}
|
||||
}),
|
||||
)?;
|
||||
stream.write_all(b"data: [DONE]\n\n")?;
|
||||
stream.flush()
|
||||
}
|
||||
|
||||
fn wait_for_gate(served: &Served) -> std::io::Result<()> {
|
||||
let gates = served.gates.lock().expect("provider gates");
|
||||
loop {
|
||||
if served.stop.load(Ordering::Acquire) {
|
||||
return Err(std::io::ErrorKind::Interrupted.into());
|
||||
}
|
||||
match gates.recv_timeout(Duration::from_millis(100)) {
|
||||
Ok(()) => return Ok(()),
|
||||
Err(mpsc::RecvTimeoutError::Timeout) => {}
|
||||
Err(mpsc::RecvTimeoutError::Disconnected) => {
|
||||
return Err(std::io::ErrorKind::BrokenPipe.into());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn send_delta(stream: &mut TcpStream, delta: serde_json::Value) -> std::io::Result<()> {
|
||||
send_chunk(
|
||||
stream,
|
||||
&serde_json::json!({
|
||||
"id":"loopback","object":"chat.completion.chunk","model":MODEL,
|
||||
"choices":[{"index":0,"delta":delta,"finish_reason":null}]
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
fn send_chunk(stream: &mut TcpStream, chunk: &serde_json::Value) -> std::io::Result<()> {
|
||||
stream.write_all(format!("data: {chunk}\n\n").as_bytes())?;
|
||||
stream.flush()
|
||||
}
|
||||
|
||||
fn respond_json(
|
||||
stream: &mut TcpStream,
|
||||
status: &str,
|
||||
body: &serde_json::Value,
|
||||
) -> std::io::Result<()> {
|
||||
let body = body.to_string();
|
||||
stream.write_all(
|
||||
format!(
|
||||
"HTTP/1.1 {status}\r\ncontent-type: application/json\r\ncontent-length: {}\r\nconnection: close\r\n\r\n{body}",
|
||||
body.len()
|
||||
)
|
||||
.as_bytes(),
|
||||
)?;
|
||||
stream.flush()
|
||||
}
|
||||
|
||||
fn prepare(workspace: &SealedWorkspace, provider: &PacedProvider, settings: Option<&str>) {
|
||||
let state = workspace.home().join(".codewhale");
|
||||
std::fs::write(state.join(".onboarded"), "").unwrap();
|
||||
std::fs::write(
|
||||
state.join("config.toml"),
|
||||
format!(
|
||||
"provider = \"deepseek\"\n[providers.deepseek]\nbase_url = {:?}\napi_key = \"loopback-only\"\nmodel = {MODEL:?}\n[notifications]\nmethod = \"off\"\ncompletion_sound = \"off\"\n",
|
||||
provider.model_base()
|
||||
),
|
||||
)
|
||||
.unwrap();
|
||||
if let Some(settings) = settings {
|
||||
std::fs::write(state.join("settings.toml"), settings).unwrap();
|
||||
}
|
||||
let trust = workspace.workspace().join(".deepseek");
|
||||
std::fs::create_dir_all(&trust).unwrap();
|
||||
std::fs::write(trust.join("trusted"), "").unwrap();
|
||||
}
|
||||
|
||||
fn launch(
|
||||
workspace: &SealedWorkspace,
|
||||
provider: &PacedProvider,
|
||||
(rows, cols): (u16, u16),
|
||||
session: &str,
|
||||
animated: bool,
|
||||
) -> Harness {
|
||||
let mut tui = Harness::builder(Harness::codewhale_binary())
|
||||
.cwd(workspace.workspace())
|
||||
.clear_env()
|
||||
.seal_home(workspace.home())
|
||||
.env("PATH", "/usr/bin:/bin:/usr/sbin:/sbin")
|
||||
.env("SHELL", "/bin/sh")
|
||||
.env("CODEWHALE_CLOUD_API_BASE", provider.account_base())
|
||||
.env("CODEWHALE_DISABLE_MODELS_DEV_FETCH", "1")
|
||||
.env("CODEWHALE_NO_UPDATE_CHECK", "1")
|
||||
.env("NO_ANIMATIONS", if animated { "0" } else { "1" })
|
||||
.args([
|
||||
"--workspace",
|
||||
workspace.workspace().to_str().unwrap(),
|
||||
"--no-project-config",
|
||||
"--skip-onboarding",
|
||||
session,
|
||||
])
|
||||
.size(rows, cols)
|
||||
.spawn()
|
||||
.unwrap();
|
||||
tui.wait_for_text("accept edits", WAIT)
|
||||
.unwrap_or_else(|error| panic!("the dial chip never painted: {error}"));
|
||||
tui
|
||||
}
|
||||
|
||||
fn expect(tui: &mut Harness, what: &str, predicate: impl FnMut(&super::qa_harness::Frame) -> bool) {
|
||||
if let Err(error) = tui.wait_for(predicate, WAIT) {
|
||||
panic!("waiting for {what}: {error}");
|
||||
}
|
||||
}
|
||||
|
||||
fn settle(tui: &mut Harness) {
|
||||
let deadline = Instant::now() + Duration::from_secs(2);
|
||||
let mut quiet_since = Instant::now();
|
||||
while Instant::now() < deadline && quiet_since.elapsed() < Duration::from_millis(300) {
|
||||
if tui.pump() {
|
||||
quiet_since = Instant::now();
|
||||
}
|
||||
std::thread::sleep(Duration::from_millis(20));
|
||||
}
|
||||
}
|
||||
|
||||
fn report(tui: &mut Harness, label: &str) {
|
||||
let frame = tui.frame();
|
||||
println!(
|
||||
"--- {label} at {}x{} ---\n{}",
|
||||
frame.cols(),
|
||||
frame.rows(),
|
||||
frame.debug_dump()
|
||||
);
|
||||
}
|
||||
|
||||
fn footer_row(tui: &mut Harness) -> (u16, String) {
|
||||
let frame = tui.frame();
|
||||
(0..frame.rows())
|
||||
.rev()
|
||||
.map(|row| (row, frame.row(row)))
|
||||
.find(|(_, text)| text.contains("accept edits"))
|
||||
.unwrap_or_else(|| panic!("no dial chip on screen:\n{}", frame.debug_dump()))
|
||||
}
|
||||
|
||||
fn assert_status_and_water(tui: &mut Harness, label: &str) {
|
||||
let (footer, status) = footer_row(tui);
|
||||
let frame = tui.frame();
|
||||
for retired in ["operate", "Operate", "Work bar"] {
|
||||
assert!(
|
||||
!frame.contains(retired),
|
||||
"{label}: `{retired}` is on screen:\n{}",
|
||||
frame.debug_dump()
|
||||
);
|
||||
}
|
||||
assert!(status.contains("accept edits"), "{label}: {status}");
|
||||
for row in 0..footer.saturating_sub(3) {
|
||||
let text = frame.row(row);
|
||||
let cells: Vec<char> = text.chars().collect();
|
||||
let school = cells.windows(2).any(|pair| {
|
||||
pair.iter()
|
||||
.all(|cell| ('\u{2801}'..='\u{28FF}').contains(cell))
|
||||
});
|
||||
assert!(
|
||||
!school && !text.contains(['˚', '°', '≈', '~']),
|
||||
"{label}: ambient art on transcript row {row} `{text}`:\n{}",
|
||||
frame.debug_dump()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn thinking_rows(tui: &mut Harness) -> usize {
|
||||
let frame = tui.frame();
|
||||
(0..frame.rows())
|
||||
.filter(|&row| frame.row(row).contains("Thinking…"))
|
||||
.count()
|
||||
}
|
||||
|
||||
fn stream_first_turn(tui: &mut Harness, provider: &PacedProvider, thinking_shown: bool) {
|
||||
tui.type_line(PROMPTS[0]).unwrap();
|
||||
expect(tui, "the first request and its interrupt hint", |frame| {
|
||||
provider.turns() == 1 && frame.contains("Esc to interrupt")
|
||||
});
|
||||
std::thread::sleep(Duration::from_millis(900));
|
||||
report(tui, "loading");
|
||||
assert_status_and_water(tui, "loading");
|
||||
|
||||
provider.release(1);
|
||||
let tail = reasoning_line(REASONING_LINES);
|
||||
if thinking_shown {
|
||||
expect(tui, "the live thinking block", |frame| {
|
||||
frame.contains("Thinking…") && frame.contains(&tail)
|
||||
});
|
||||
} else {
|
||||
expect(tui, "the thinking activity line", |frame| {
|
||||
frame.contains("Thinking…")
|
||||
});
|
||||
}
|
||||
std::thread::sleep(Duration::from_millis(1300));
|
||||
report(tui, "thinking");
|
||||
assert_status_and_water(tui, "thinking");
|
||||
assert_eq!(thinking_rows(tui), 1, "one Thinking line");
|
||||
let (_, status) = footer_row(tui);
|
||||
assert!(
|
||||
status.contains("thinking ") && !status.contains("reasoning"),
|
||||
"the clock names the phase: {status}"
|
||||
);
|
||||
let frame = tui.frame();
|
||||
assert!(!frame.contains("reasoning..."), "{}", frame.debug_dump());
|
||||
assert_eq!(frame.contains(&tail), thinking_shown);
|
||||
assert!(
|
||||
!frame.contains(&reasoning_line(1)),
|
||||
"a long trace stays inside its window:\n{}",
|
||||
frame.debug_dump()
|
||||
);
|
||||
|
||||
provider.release(1);
|
||||
expect(tui, "the streamed answer", |frame| frame.contains(ANSWER));
|
||||
report(tui, "answer");
|
||||
let frame = tui.frame();
|
||||
assert!(!frame.contains("Thinking…"), "{}", frame.debug_dump());
|
||||
assert_eq!(frame.contains("Thought for "), thinking_shown);
|
||||
assert!(!frame.contains(&tail), "{}", frame.debug_dump());
|
||||
|
||||
provider.release(1);
|
||||
expect(tui, "the settled turn", |frame| {
|
||||
!frame.contains("Esc to interrupt")
|
||||
});
|
||||
settle(tui);
|
||||
report(tui, "settled");
|
||||
assert_status_and_water(tui, "settled");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn thinking_streams_stops_and_restores_at_the_shipped_widths() {
|
||||
for size in [(24, 80), (32, 100), (32, 120), (40, 140)] {
|
||||
let workspace = make_sealed_workspace().unwrap();
|
||||
let provider = PacedProvider::start();
|
||||
prepare(&workspace, &provider, None);
|
||||
let mut tui = launch(&workspace, &provider, size, "--fresh", true);
|
||||
stream_first_turn(&mut tui, &provider, true);
|
||||
|
||||
tui.type_line(PROMPTS[1]).unwrap();
|
||||
expect(&mut tui, "the second request", |_| provider.turns() == 2);
|
||||
provider.release(1);
|
||||
let tail = reasoning_line(REASONING_LINES);
|
||||
expect(&mut tui, "thinking before the stop", |frame| {
|
||||
frame.contains("Thinking…") && frame.contains(&tail)
|
||||
});
|
||||
tui.send(keys::key::esc()).unwrap();
|
||||
expect(&mut tui, "the stopped turn", |frame| {
|
||||
!frame.contains("Esc to interrupt") && !frame.contains("Thinking…")
|
||||
});
|
||||
settle(&mut tui);
|
||||
report(&mut tui, "stopped");
|
||||
assert_status_and_water(&mut tui, "stopped");
|
||||
provider.release(2);
|
||||
|
||||
tui.send(keys::key::ctrl('u')).unwrap();
|
||||
settle(&mut tui);
|
||||
tui.send(keys::key::ctrl_d()).unwrap();
|
||||
tui.wait_for_exit(Duration::from_secs(10));
|
||||
tui.shutdown();
|
||||
|
||||
let mut tui = launch(&workspace, &provider, size, "--continue", false);
|
||||
expect(&mut tui, "the restored conversation", |frame| {
|
||||
frame.contains(ANSWER)
|
||||
});
|
||||
settle(&mut tui);
|
||||
report(&mut tui, "restored");
|
||||
assert_status_and_water(&mut tui, "restored");
|
||||
let frame = tui.frame();
|
||||
assert!(!frame.contains("Thinking…"), "{}", frame.debug_dump());
|
||||
tui.shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_saved_choice_to_hide_thinking_survives_the_new_default() {
|
||||
let workspace = make_sealed_workspace().unwrap();
|
||||
let provider = PacedProvider::start();
|
||||
prepare(&workspace, &provider, Some("show_thinking = false\n"));
|
||||
let mut tui = launch(&workspace, &provider, (24, 80), "--fresh", false);
|
||||
stream_first_turn(&mut tui, &provider, false);
|
||||
let frame = tui.frame();
|
||||
assert!(!frame.contains("Thought"), "{}", frame.debug_dump());
|
||||
tui.shutdown();
|
||||
let saved: toml::Value = toml::from_str(
|
||||
&std::fs::read_to_string(workspace.home().join(".codewhale/settings.toml")).unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(saved["show_thinking"].as_bool(), Some(false));
|
||||
}
|
||||
|
||||
fn sign_in(workspace: &SealedWorkspace, provider: &PacedProvider, session: &str) {
|
||||
let path = workspace.home().join(".codewhale/secrets/secrets.json");
|
||||
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
|
||||
let secrets =
|
||||
codewhale_secrets::Secrets::new(Arc::new(codewhale_secrets::FileKeyringStore::new(path)));
|
||||
let bundle = serde_json::from_value(serde_json::json!({
|
||||
"tokenType": "Bearer",
|
||||
"accessToken": "loopback-access",
|
||||
"refreshToken": "loopback-refresh",
|
||||
"session": {"id": session},
|
||||
"user": {"id": "loopback-account"}
|
||||
}))
|
||||
.unwrap();
|
||||
codewhale_secrets::account::AccountSessionStore::new(secrets, None, &provider.account_base())
|
||||
.save(bundle)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
fn run_ungated_turn(tui: &mut Harness, provider: &PacedProvider) {
|
||||
provider.release(3);
|
||||
tui.type_line(PROMPTS[0]).unwrap();
|
||||
expect(tui, "the answer", |frame| frame.contains(ANSWER));
|
||||
expect(tui, "the settled turn", |frame| {
|
||||
!frame.contains("Esc to interrupt")
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn account_preferences_notice_is_shown_once_per_sign_in_and_esc_dismisses_it() {
|
||||
let workspace = make_sealed_workspace().unwrap();
|
||||
let provider = PacedProvider::start();
|
||||
prepare(&workspace, &provider, None);
|
||||
sign_in(&workspace, &provider, "first-sign-in");
|
||||
|
||||
for cols in [80, 120] {
|
||||
let workspace = make_sealed_workspace().unwrap();
|
||||
let provider = PacedProvider::start();
|
||||
prepare(&workspace, &provider, None);
|
||||
sign_in(&workspace, &provider, "first-sign-in");
|
||||
let mut tui = launch(&workspace, &provider, (32, cols), "--fresh", false);
|
||||
run_ungated_turn(&mut tui, &provider);
|
||||
expect(&mut tui, "the standing notice", |frame| {
|
||||
frame.contains(NOTICE)
|
||||
});
|
||||
report(&mut tui, "account notice");
|
||||
assert!(provider.profile_requests.load(Ordering::Acquire) > 0);
|
||||
let (_, status) = footer_row(&mut tui);
|
||||
assert!(
|
||||
status.contains(NOTICE),
|
||||
"the notice is footer chrome: {status}"
|
||||
);
|
||||
assert!(!tui.frame().contains(RETIRED_TRANSCRIPT_NOTICE));
|
||||
tui.send(keys::key::esc()).unwrap();
|
||||
expect(&mut tui, "the dismissed notice", |frame| {
|
||||
!frame.contains(NOTICE)
|
||||
});
|
||||
report(&mut tui, "account notice dismissed");
|
||||
tui.shutdown();
|
||||
}
|
||||
|
||||
let mut tui = launch(&workspace, &provider, (32, 120), "--fresh", false);
|
||||
run_ungated_turn(&mut tui, &provider);
|
||||
expect(&mut tui, "the first notice", |frame| frame.contains(NOTICE));
|
||||
tui.shutdown();
|
||||
|
||||
let mut tui = launch(&workspace, &provider, (32, 120), "--fresh", false);
|
||||
provider.turns.store(0, Ordering::Release);
|
||||
run_ungated_turn(&mut tui, &provider);
|
||||
tui.wait_for_idle(Duration::from_secs(6), Duration::from_secs(30))
|
||||
.unwrap();
|
||||
report(&mut tui, "same sign-in, later session");
|
||||
assert!(
|
||||
!tui.frame().contains(NOTICE),
|
||||
"the same sign-in is told once"
|
||||
);
|
||||
assert!(!tui.frame().contains(RETIRED_TRANSCRIPT_NOTICE));
|
||||
tui.shutdown();
|
||||
|
||||
sign_in(&workspace, &provider, "second-sign-in");
|
||||
let mut tui = launch(&workspace, &provider, (32, 120), "--fresh", false);
|
||||
provider.turns.store(0, Ordering::Release);
|
||||
run_ungated_turn(&mut tui, &provider);
|
||||
expect(&mut tui, "the notice for a later sign-in", |frame| {
|
||||
frame.contains(NOTICE)
|
||||
});
|
||||
report(&mut tui, "later sign-in");
|
||||
tui.shutdown();
|
||||
}
|
||||
@@ -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"}}}
|
||||
+1
-1
@@ -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"}}}
|
||||
+1
-1
@@ -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"}}}
|
||||
@@ -2230,7 +2230,10 @@ Common settings keys:
|
||||
`@`-mention completions are populated. `fuzzy` searches the workspace and
|
||||
applies mention frecency. `browser` lists only the immediate children of the
|
||||
currently typed directory segment in deterministic alphabetical order.
|
||||
- `show_thinking` (on/off)
|
||||
- `show_thinking` (on/off, default on): shows model thinking in the transcript,
|
||||
live while it streams and collapsed under a `Thought for …` header once the
|
||||
answer starts. Off hides it; answers and tools stay visible. A value already
|
||||
saved in `settings.toml` is kept.
|
||||
- `thinking_default_expanded` (on/off, default off): renders thinking blocks
|
||||
expanded initially when `show_thinking` is enabled. Space still toggles the
|
||||
selected block, and it decides only the blocks you have not touched: a block
|
||||
@@ -2693,7 +2696,7 @@ reasoning contract, and all four membership ids omit generic sampling fields.
|
||||
audit logging is enabled. Future PreToolUse/PostToolUse hooks can add
|
||||
observer input around this layer, but the configured auto-review policy is
|
||||
evaluated before a tool call is allowed to proceed.
|
||||
- `managed_config_path` (string, optional): managed config file loaded after user/env config.
|
||||
- `managed_config_path` (string, optional): managed config file loaded after user/env config. `CODEWHALE_MANAGED_CONFIG_PATH` outranks it. A named file that is missing or unreadable is a load error, and it cannot replace an existing file at the default managed path.
|
||||
- `requirements_path` (string, optional): requirements file used to enforce allowed approval/sandbox values.
|
||||
- `max_subagents` (int, optional): defaults to `64` and is clamped to `1..=128`.
|
||||
- `subagents.*` (optional compatibility table): manual per-role model pins
|
||||
|
||||
@@ -14,13 +14,13 @@ and [CHANGELOG.md](../CHANGELOG.md).
|
||||
<!-- contributor-wall:start -->
|
||||
## Everyone
|
||||
|
||||
[](#everyone)
|
||||
[](#everyone)
|
||||
|
||||
433 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.
|
||||
421 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 203 of them. If your commits are missing there, add your commit email to your GitHub account and they will link.
|
||||
|
||||
[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.com/DieMoe233) · [DingYong4223](https://github.com/DingYong4223) · [djairjr](https://github.com/djairjr) · [dmitri-0](https://github.com/dmitri-0) · [dmt4](https://github.com/dmt4) · [donglovejava](https://github.com/donglovejava) · [douglarek](https://github.com/douglarek) · [Dr3259](https://github.com/Dr3259) · [DracheTek](https://github.com/DracheTek) · [drpars](https://github.com/drpars) · [dsingal0](https://github.com/dsingal0) · [dst1213](https://github.com/dst1213) · [duanchao-lab](https://github.com/duanchao-lab) · [Duducoco](https://github.com/Duducoco) · [dumbjack](https://github.com/dumbjack) · [dxfq](https://github.com/dxfq) · [dzyuan](https://github.com/dzyuan) · [e792a8](https://github.com/e792a8) · [Edouard-Legoupil](https://github.com/Edouard-Legoupil) · [elowen53](https://github.com/elowen53) · [eltociear](https://github.com/eltociear) · [EmiyaKiritsugu3](https://github.com/EmiyaKiritsugu3) · [encyc](https://github.com/encyc) · [eng2007](https://github.com/eng2007) · [eugenicum](https://github.com/eugenicum) ·Line truncated
|
||||
[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) · [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) · [angus-guo](https://github.com/angus-guo) · [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) · [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.com/DieMoe233) · [DingYong4223](https://github.com/DingYong4223) · [djairjr](https://github.com/djairjr) · [dmitri-0](https://github.com/dmitri-0) · [dmt4](https://github.com/dmt4) · [donglovejava](https://github.com/donglovejava) · [douglarek](https://github.com/douglarek) · [Dr3259](https://github.com/Dr3259) · [DracheTek](https://github.com/DracheTek) · [drpars](https://github.com/drpars) · [dst1213](https://github.com/dst1213) · [duanchao-lab](https://github.com/duanchao-lab) · [Duducoco](https://github.com/Duducoco) · [dumbjack](https://github.com/dumbjack) · [dxfq](https://github.com/dxfq) · [dzyuan](https://github.com/dzyuan) · [e792a8](https://github.com/e792a8) · [Edouard-Legoupil](https://github.com/Edouard-Legoupil) · [elowen53](https://github.com/elowen53) · [eltociear](https://github.com/eltociear) · [EmiyaKiritsugu3](https://github.com/EmiyaKiritsugu3) · [encyc](https://github.com/encyc) · [eng2007](https://github.com/eng2007) · [eugenicum](https://github.com/eugenicum) · [EvanProgramming](https://github.com/EvanProgramming) · [F1LT3R](https:/Line truncated
|
||||
|
||||
Also with us, from commits whose email is not linked to an account: AccMoment · AmosLeeeeeeee · artanis · Assassin-D007 · bluth · codgo · kurise · LeHaiDang · lei · liushiao · liuyang · markchang · ningjingkun · ornn.ban · pinvou3-dev · sanbo · ts25504 · v_wanghuanping01 · wangfeng · xiedongsheng · zero · Zhiping · Zhuoran Deng · ZZHAsus · 浩淼 · 郭闯.
|
||||
Also with us, from commits whose email is not linked to an account: AccMoment · AmosLeeeeeeee · artanis · Assassin-D007 · bluth · codgo · kurise · LeHaiDang · lei · liushiao · liuyang · Lê Hải Đăng · markchang · ningjingkun · ornn.ban · pinvou3-dev · sanbo · ts25504 · v_wanghuanping01 · wangfeng · xiedongsheng · z27014 · zero · Zhiping · Zhuoran Deng · ZZHAsus · 浩淼 · 郭闯.
|
||||
<!-- contributor-wall:end -->
|
||||
|
||||
## Organizational thanks
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -129,12 +129,19 @@ the `agent/created` hook fires only for children the same plugin starts.
|
||||
|
||||
## Plugin UI (`dsh.client`)
|
||||
|
||||
Imported DSH client UIs are not supported. The Engine refuses their client
|
||||
descriptor and asset grants when the reviewed conversion report identifies a
|
||||
DSH import; the plugin's supported Native and MCP contributions keep their
|
||||
existing lifecycle. Code Review 1.0.8's client decorates global command
|
||||
candidates through DSH services that the current detail-slot renderer does
|
||||
not provide.
|
||||
An imported DSH plugin's client UI is served, on the seats the Engine admits.
|
||||
The importer retains the reviewed client closure; at serve time the Engine
|
||||
re-verifies it byte for byte against the reviewed hashes, maps each declared
|
||||
slot key to a seat, and loads one Engine-generated bootstrap built from those
|
||||
bytes. A native client is admitted only on `customize.plugins.detail`. A slot
|
||||
key with no seat answers `plugin_ui_seat_unsupported`; several keys on one seat
|
||||
need the caller to name one (`plugin_ui_dsh_slot_required`). A closure that needs
|
||||
a host module the frame does not provide, uses ES module syntax or is not valid
|
||||
JSON answers `plugin_ui_unsupported`. Code Review 1.0.8's client decorates
|
||||
global command candidates through DSH services that the current detail-slot
|
||||
renderer does not provide, so it is not qualified.
|
||||
The route that forwards a client's requests to a plugin web server answers
|
||||
`plugin_route_not_served` (501): no plugin web server exists yet.
|
||||
|
||||
The selected Electron host's reviewed asset transport and isolated frame do
|
||||
not establish compatibility with those services. Its real network containment
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
# Hooks
|
||||
> 阅读简体中文版:[zh_hans/HOOKS.md](zh_hans/HOOKS.md)
|
||||
|
||||
Everything you add to Codewhale is a plugin. A hook in `config.toml` or a
|
||||
project `hooks.toml` is a plugin of the kind **Hook**, and a plugin bundle can
|
||||
also carry hooks. Hooks are still managed under Extensions → Hooks in the
|
||||
terminal and Customize › Hooks on the desktop; one Plugins list that shows every
|
||||
kind together is planned for 0.10.3 and is not in this build. Configuration and
|
||||
the `/hooks` commands are unchanged.
|
||||
|
||||
Hooks run a shell command when the Codewhale **TUI** reaches a lifecycle
|
||||
point. They are plain processes: they receive context through environment
|
||||
variables, some receive a JSON payload on stdin, and three of them can steer
|
||||
|
||||
@@ -5,6 +5,15 @@ In the terminal, `/mcp` (also `/mcps`) opens **Extensions → MCP**. Enter opens
|
||||
|
||||
> 阅读简体中文版:[zh_hans/MCP.md](zh_hans/MCP.md)
|
||||
|
||||
Everything you add to Codewhale is a plugin. An MCP server you configure is a
|
||||
plugin of the kind **Connector**, and a plugin bundle can also carry connectors.
|
||||
Connectors are still managed under Extensions → MCP in the terminal and
|
||||
Customize › Connectors on the desktop; one Plugins list that shows every kind
|
||||
together is planned for 0.10.3 and is not in this build. The configuration
|
||||
files, sign-in and the `/mcp` commands are unchanged. "Connection" on this page
|
||||
means the live link to a server; as a product noun it is the word for a model
|
||||
provider or another agent.
|
||||
|
||||
Codewhale can load additional tools via MCP (Model Context Protocol). MCP servers can be local stdio processes that Codewhale starts, or remote URL-based servers that speak Streamable HTTP with legacy SSE fallback.
|
||||
|
||||
## Version scope
|
||||
|
||||
+19
-7
@@ -12,13 +12,25 @@ format (`plugin.json`, compatible `kimi.plugin.json` or
|
||||
the trust/enable lifecycle — this document covers how bits get onto disk in
|
||||
the first place.
|
||||
|
||||
## Choose a plugin, skill, or MCP connection
|
||||
Everything you add to Codewhale is a plugin. A skill, an MCP connector and a hook
|
||||
are each a plugin, or part of one, and each has a kind: Skill, Connector, Hook,
|
||||
Command. A skill you set up by hand is a plugin of the kind Skill, a server you
|
||||
configure is a plugin of the kind Connector, and a hook you configure is a plugin
|
||||
of the kind Hook. They are still managed where they are today: Extensions →
|
||||
Skills, MCP and Hooks in the terminal, and Customize › Skills, Connectors and
|
||||
Hooks on the desktop. Their files, trust rules and commands (`/skills`, `/mcp`,
|
||||
`/hooks`) do not change. One Plugins list that shows every kind together is
|
||||
planned for 0.10.3 and is not in this build; today the kind labels appear on the
|
||||
parts inside a plugin bundle's detail. "Connection" is the word for a model
|
||||
provider or another agent; it is not a kind of plugin.
|
||||
|
||||
## Choose what to add
|
||||
|
||||
| What you need | Use | Starting point |
|
||||
| --- | --- | --- |
|
||||
| Reusable instructions that work through your existing tools | A skill | `/skills`, then `/skill <name>`; [Skills](SKILLS.md) |
|
||||
| A reviewed bundle of workflows, tools, commands or agent profiles | A plugin | `/plugin marketplace list`; [first-party catalog](https://github.com/codewhale-hq/codewhale-plugin-marketplace) |
|
||||
| Tools or context from a particular local process or remote service | A direct MCP connection | `/mcp`; [MCP setup](MCP.md) |
|
||||
| Reusable instructions that work through your existing tools | A plugin that adds a skill | `/skills`, then `/skill <name>`; [Skills](SKILLS.md) |
|
||||
| A reviewed bundle of workflows, tools, commands or agent profiles | A plugin bundle | `/plugin marketplace list`; [first-party catalog](https://github.com/codewhale-hq/codewhale-plugin-marketplace) |
|
||||
| Tools or context from a particular local process or remote service | A plugin that adds an MCP connector | `/mcp`; [MCP setup](MCP.md) |
|
||||
|
||||
A plugin can combine skills, Markdown commands, Fleet agent profiles, hooks,
|
||||
MCP servers and AI-provider declarations. Tools come from an MCP server or an
|
||||
@@ -26,12 +38,12 @@ experimental native extension; there is no generic application-UI component in
|
||||
the bundle manifest. Computer Use's Mac helper and Chromewhale's browser
|
||||
extension have their own setup after plugin review.
|
||||
|
||||
MCP has two connection kinds: a local process over stdio, or a remote URL over
|
||||
An MCP connector is one of two kinds: a local process over stdio, or a remote URL over
|
||||
Streamable HTTP. Legacy SSE is available when the server still supports it.
|
||||
User-configured remote servers can use environment-backed credentials or the
|
||||
host's OAuth login. Plugin-contributed remote servers require explicit
|
||||
environment-backed credentials when authentication is needed; their interactive
|
||||
OAuth path is not supported. Installing a skill or plugin does not connect an
|
||||
OAuth path is not supported. Installing a plugin does not connect an
|
||||
account or establish that a service works.
|
||||
|
||||
A local stdio server runs with your operating-system user's access. Requested
|
||||
@@ -63,7 +75,7 @@ Native TypeScript extensions require `extension_host = true` under `[features]`
|
||||
and fresh capability review. LSP, requested filesystem roots and lifecycle mutation
|
||||
remain inventory-only. Current DSH adapters include work beyond the published
|
||||
v0.10.2 CLI; third-party DSH plugins' complete workflows are not established by
|
||||
an import succeeding. DSH plugin UI is unsupported. Other applications' plugin
|
||||
an import succeeding. An imported DSH plugin's UI is served only on the seats the Engine admits ([details](DSH_PLUGIN_COMPAT.md#plugin-ui-dshclient)). Other applications' plugin
|
||||
directories and credentials are not borrowed.
|
||||
|
||||
`/plugin suggest <task>` is a local, read-only companion: it ranks already
|
||||
|
||||
+1
-2
@@ -91,8 +91,7 @@ is the source. Translations live beside their English page (`*.id.md`) or in
|
||||
[lifecycle outbox changelog](changelog-lifecycle-outbox.md)
|
||||
- [Rebrand: DeepSeek TUI to Codewhale](REBRAND.md) and
|
||||
[legacy `.deepseek/` paths](LEGACY_PATHS.md)
|
||||
- Third-party notices for [adapted designs](THIRD_PARTY_NOTICES.md) and
|
||||
[vendored sources](../THIRD_PARTY_NOTICES.md)
|
||||
- [Third-party notices](THIRD_PARTY_NOTICES.md)
|
||||
- Historical plans: [TUI modularization](TUI_MODULARIZATION.md),
|
||||
[post-0.9.1 seams](POST_0_9_1_SEAMS.md),
|
||||
[runtime simplification](RUNTIME_SIMPLIFICATION_DESIGN.md),
|
||||
|
||||
@@ -882,6 +882,8 @@ foreground shell tools, plus reviewed plugin tools (`extension_host` feature) an
|
||||
the tools of MCP servers configured in Codewhale (`mcp` feature), under the same
|
||||
permission setting. Shell requires both editor terminal support and operator
|
||||
`allow_shell`; a requested external sandbox that is unavailable removes shell.
|
||||
Plugin tools and configured MCP servers on this transport are on main after
|
||||
0.10.2; 0.10.2 itself does not have them.
|
||||
Client-supplied MCP servers, dynamic tools, task/PTY/background shell,
|
||||
interpreter, subagent and RLM lifecycles are unavailable on this transport.
|
||||
Built-in overrides remove the
|
||||
|
||||
+35
-22
@@ -145,20 +145,27 @@ The key is read only from the managed configuration file named by
|
||||
Linux, and only when neither that file nor any directory above it can be
|
||||
changed by the Engine's own user. A managed file at any other path (for
|
||||
example one a user config points at through `managed_config_path`) is refused.
|
||||
In a user `config.toml` or a profile the key is a load error. The managed file must also set `sandbox_mode` itself; a sandboxed mode
|
||||
from a user config does not count. Without the key, nothing on this page
|
||||
changes.
|
||||
In a user `config.toml` or a profile the key is a load error. The managed
|
||||
file must also set `sandbox_mode` itself; a sandboxed mode from a user config
|
||||
does not count. Without the key, nothing on this page changes.
|
||||
|
||||
A host that relies on the launcher puts the managed file at the default
|
||||
managed path, owned by root, and also sets `CODEWHALE_MANAGED_CONFIG_PATH` to
|
||||
that same path for the Engine's service. The variable stops a user config from
|
||||
pointing `managed_config_path` at a missing file; the default path is needed
|
||||
because a Fleet local worker (a child `codewhale exec`) is started with a
|
||||
cleaned environment and does not get the variable; it reads the user config's
|
||||
`managed_config_path` and then the default path, so a user config can still
|
||||
point a worker at a missing file. A file in another ordinary user's home also
|
||||
passes the "Engine's user cannot change it" check, so the file should be
|
||||
root-owned. Check `sandbox.tool_launcher` in `GET /v1/runtime/info`: the route
|
||||
A host that relies on the launcher puts the managed file in a root-owned
|
||||
place and sets `CODEWHALE_MANAGED_CONFIG_PATH` to it for the Engine's service.
|
||||
The variable outranks a user config's `managed_config_path`, and a Fleet local
|
||||
worker (a child `codewhale exec`) gets the parent's variable even though its
|
||||
environment is otherwise cleaned; a worker request cannot replace it. Any
|
||||
other `codewhale` process sees a file named only by the variable when it is
|
||||
started with the variable. An MCP server entry that runs `codewhale serve
|
||||
--mcp` is started with a cleaned environment and does not get it. A host that
|
||||
relies on the launcher therefore also keeps the file at the default managed
|
||||
path, or sets the variable in every such entry. A file named by the variable
|
||||
or by `managed_config_path` that is missing or cannot be read is a load error,
|
||||
not "no managed layer", and a user config cannot point `managed_config_path`
|
||||
away from an existing file at the default managed path.
|
||||
Only a missing file at the default path, with nothing naming another, means no
|
||||
managed layer. A file in another ordinary user's home also passes the
|
||||
"Engine's user cannot change it" check, so the file should be root-owned.
|
||||
Check `sandbox.tool_launcher` in `GET /v1/runtime/info`: the route
|
||||
reads the live configuration, but a check made once at start does not cover a
|
||||
later reload, so ask again after a reload.
|
||||
|
||||
@@ -193,18 +200,24 @@ With a launcher configured:
|
||||
|
||||
Limits: only commands the sandbox manager prepares (shell commands, terminal
|
||||
sessions, workspace runners) go through the launcher. In-process file tools,
|
||||
MCP servers, hooks, LSP servers, the extension host, Fleet local workers,
|
||||
external ACP agents and the Engine's own `git`, formatter and diagnostic
|
||||
helpers still run as the Engine's user. `codewhale serve --mcp` builds its own
|
||||
shell manager with no launcher and no deny list, so do not expose `exec_shell`
|
||||
there. With `sandbox_backend = "opensandbox"` a shell command goes to the
|
||||
remote backend before the launcher is consulted, so a launcher host pins
|
||||
MCP servers, hooks, LSP servers, the extension host, external ACP agents and
|
||||
the Engine's own `git`, formatter and diagnostic helpers still run as the
|
||||
Engine's user. A Fleet local worker is its own Engine, so its shell commands
|
||||
go through the launcher only because it loads the same managed file.
|
||||
`codewhale serve --mcp` does not run commands through the launcher. When the
|
||||
managed file it loads sets a launcher, it does not expose `shell` or
|
||||
`exec_shell`, whatever its own `expose_tools` lists; its file tools use the
|
||||
built-in read deny list only. Model-written Python in the session REPL kernel
|
||||
(` ```repl ` blocks and the `rlm` tool) runs in a local interpreter as the
|
||||
Engine's user. With `sandbox_backend = "opensandbox"` a shell command goes to
|
||||
the remote backend before the launcher is consulted, so a launcher host pins
|
||||
`sandbox_backend = "none"`, `prefer_bwrap = true` and `sandbox_network_access`
|
||||
in the managed file. A `tool_launcher` in a project config stops an
|
||||
interactive launch with an error; `serve` and `exec` do not read project
|
||||
config, so there the key is ignored. Deny-list exceptions are not applied under a launcher. Search
|
||||
rights come from mode bits and group membership, not ACLs. Processes keep
|
||||
running after the Engine itself dies, until bubblewrap's parent exits.
|
||||
config, so there the key is ignored. Deny-list exceptions are not applied
|
||||
under a launcher. Search rights come from mode bits and group membership, not
|
||||
ACLs. Processes keep running after the Engine itself dies, until bubblewrap's
|
||||
parent exits.
|
||||
|
||||
## Windows: no advertised OS sandbox
|
||||
|
||||
|
||||
@@ -5,6 +5,13 @@ In the terminal, `/skills` opens **Extensions → Skills**. Enter on a skill ope
|
||||
|
||||
> 阅读简体中文版:[zh_hans/SKILLS.md](zh_hans/SKILLS.md)
|
||||
|
||||
Everything you add to Codewhale is a plugin: a skill you set up by hand is a
|
||||
plugin of the kind **Skill**, and a skill inside a plugin bundle belongs to that
|
||||
plugin. Skills are still managed here, under Extensions → Skills and Customize ›
|
||||
Skills; one Plugins list that shows every kind together is planned for 0.10.3 and
|
||||
is not in this build. This page keeps the word "skill" for the kind; files, trust
|
||||
and the `/skills` commands are unchanged.
|
||||
|
||||
Skills are reusable `SKILL.md` instruction packs. Codewhale discovers them from
|
||||
several roots, but **only Codewhale-owned directories are writable**. The unified
|
||||
`/skills manage` manager is the interactive surface for audit and mutation; slash
|
||||
|
||||
@@ -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
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 337 KiB After Width: | Height: | Size: 326 KiB |
+13
-126
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"generated_by": "scripts/contributor-wall.py refresh",
|
||||
"github_contributor_list": 212,
|
||||
"github_contributor_list": 203,
|
||||
"people": [
|
||||
{
|
||||
"login": "0gl20shk0sbt36",
|
||||
@@ -67,15 +67,6 @@
|
||||
"merged pull request"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "Aboysky",
|
||||
"credited_for": [
|
||||
"changelog credit",
|
||||
"commit",
|
||||
"contributor record",
|
||||
"merged pull request"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "adity982",
|
||||
"credited_for": [
|
||||
@@ -185,6 +176,15 @@
|
||||
"merged pull request"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "angus-guo",
|
||||
"credited_for": [
|
||||
"changelog credit",
|
||||
"co-author",
|
||||
"commit",
|
||||
"merged pull request"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "angziii",
|
||||
"credited_for": [
|
||||
@@ -274,7 +274,6 @@
|
||||
"co-author",
|
||||
"commit",
|
||||
"contributor record",
|
||||
"harvested pull request",
|
||||
"merged pull request"
|
||||
]
|
||||
},
|
||||
@@ -535,7 +534,6 @@
|
||||
{
|
||||
"login": "cmdcorp6534",
|
||||
"credited_for": [
|
||||
"changelog credit",
|
||||
"contributor record"
|
||||
]
|
||||
},
|
||||
@@ -639,15 +637,6 @@
|
||||
"merged pull request"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "ddlw",
|
||||
"credited_for": [
|
||||
"changelog credit",
|
||||
"commit",
|
||||
"contributor record",
|
||||
"merged pull request"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "demian-welt",
|
||||
"credited_for": [
|
||||
@@ -749,15 +738,6 @@
|
||||
"changelog credit"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "dsingal0",
|
||||
"credited_for": [
|
||||
"changelog credit",
|
||||
"commit",
|
||||
"contributor record",
|
||||
"merged pull request"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "dst1213",
|
||||
"credited_for": [
|
||||
@@ -878,15 +858,6 @@
|
||||
"contributor record"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "Felixkw12",
|
||||
"credited_for": [
|
||||
"changelog credit",
|
||||
"commit",
|
||||
"contributor record",
|
||||
"merged pull request"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "Final527",
|
||||
"credited_for": [
|
||||
@@ -954,15 +925,6 @@
|
||||
"merged pull request"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "gannp",
|
||||
"credited_for": [
|
||||
"changelog credit",
|
||||
"commit",
|
||||
"contributor record",
|
||||
"merged pull request"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "gaord",
|
||||
"credited_for": [
|
||||
@@ -1161,12 +1123,6 @@
|
||||
"merged pull request"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "heydryft",
|
||||
"credited_for": [
|
||||
"changelog credit"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "hhhaiai",
|
||||
"credited_for": [
|
||||
@@ -1280,15 +1236,6 @@
|
||||
"co-author"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "imgusguo",
|
||||
"credited_for": [
|
||||
"changelog credit",
|
||||
"co-author",
|
||||
"commit",
|
||||
"merged pull request"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "imkingjh999",
|
||||
"credited_for": [
|
||||
@@ -1317,13 +1264,6 @@
|
||||
"merged pull request"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "isaacs",
|
||||
"credited_for": [
|
||||
"changelog credit",
|
||||
"contributor record"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "ivysrono",
|
||||
"credited_for": [
|
||||
@@ -1544,15 +1484,6 @@
|
||||
"merged pull request"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "laozidabo",
|
||||
"credited_for": [
|
||||
"changelog credit",
|
||||
"commit",
|
||||
"contributor record",
|
||||
"merged pull request"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "lawrencewzen",
|
||||
"credited_for": [
|
||||
@@ -1719,13 +1650,6 @@
|
||||
"merged pull request"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "liuyy2",
|
||||
"credited_for": [
|
||||
"changelog credit",
|
||||
"contributor record"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "lixiasky-back",
|
||||
"credited_for": [
|
||||
@@ -1804,15 +1728,6 @@
|
||||
"contributor record"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "luochen211",
|
||||
"credited_for": [
|
||||
"changelog credit",
|
||||
"commit",
|
||||
"contributor record",
|
||||
"merged pull request"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "M-Maciej",
|
||||
"credited_for": [
|
||||
@@ -2148,12 +2063,6 @@
|
||||
"merged pull request"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "paulocrusius",
|
||||
"credited_for": [
|
||||
"commit"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "pengyou200902",
|
||||
"credited_for": [
|
||||
@@ -2346,15 +2255,6 @@
|
||||
"contributor record"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "Ricardo-M-L",
|
||||
"credited_for": [
|
||||
"changelog credit",
|
||||
"commit",
|
||||
"contributor record",
|
||||
"merged pull request"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "RinZ27",
|
||||
"credited_for": [
|
||||
@@ -2573,13 +2473,6 @@
|
||||
"contributor record"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "soarkit",
|
||||
"credited_for": [
|
||||
"changelog credit",
|
||||
"contributor record"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "sockerch",
|
||||
"credited_for": [
|
||||
@@ -3088,13 +2981,6 @@
|
||||
"harvested pull request"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "ychu824",
|
||||
"credited_for": [
|
||||
"changelog credit",
|
||||
"contributor record"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "yekern",
|
||||
"credited_for": [
|
||||
@@ -3261,6 +3147,7 @@
|
||||
"liuhq",
|
||||
"liushiao",
|
||||
"liuyang",
|
||||
"Lê Hải Đăng",
|
||||
"markchang",
|
||||
"ningjingkun",
|
||||
"ornn.ban",
|
||||
@@ -3271,6 +3158,7 @@
|
||||
"wangfeng",
|
||||
"xiedongsheng",
|
||||
"yekern",
|
||||
"z27014",
|
||||
"zero",
|
||||
"Zhiping",
|
||||
"Zhuoran Deng",
|
||||
@@ -3306,7 +3194,6 @@
|
||||
"Horace Liu": "liuhq",
|
||||
"Leo Douglas": "douglarek",
|
||||
"maple": "yekern",
|
||||
"Alexander Kireev": "chatman-media",
|
||||
"Lê Hải Đăng": "LeHaiDang"
|
||||
"Alexander Kireev": "chatman-media"
|
||||
}
|
||||
}
|
||||
@@ -43,24 +43,3 @@ executable replaced at that path by the same local user.
|
||||
Local fixtures cover native modern Git and an emulated executable that ignores
|
||||
runtime overrides. They do not qualify a real older-Git installation, partial
|
||||
clones, or all platform-specific Git behavior.
|
||||
|
||||
## VS Code extension packager
|
||||
|
||||
The VS Code extension's packaging toolchain uses `@vscode/vsce` →
|
||||
`@azure/identity` → `@azure/msal-node`. MSAL Node versions before 7.1.0 are
|
||||
affected by [GHSA-xcx6-jx8p-x3h5](https://github.com/AzureAD/microsoft-authentication-library-for-js/security/advisories/GHSA-xcx6-jx8p-x3h5).
|
||||
The extension manifest overrides Azure Identity's MSAL Node dependency to
|
||||
`^7.1.2`; its lockfile resolves 7.1.2. Keep the override until Azure Identity
|
||||
requires a patched version itself.
|
||||
|
||||
`npm run audit:dependencies -- --report <path>` audits the complete lockfile
|
||||
and keeps the raw report. Every reported vulnerability blocks the check.
|
||||
Incomplete reports, count mismatches and critical findings also fail. There
|
||||
is no advisory exception.
|
||||
|
||||
After changing this dependency, materialize the lockfile with `npm ci`, run
|
||||
the extension's existing `npm test` checks and build a local package with
|
||||
`npm run package`. Packaging uses `vsce package --no-dependencies`, which
|
||||
excludes development dependencies from the VSIX. A local package build does
|
||||
not establish that a publishing authentication flow works; verify that flow
|
||||
only when publication is separately authorized.
|
||||
+8
-8
@@ -141,8 +141,8 @@ owner = "crates/engine/src/tools/apply_patch.rs"
|
||||
|
||||
[[feature]]
|
||||
id = "mcp-client"
|
||||
name = "MCP client"
|
||||
summary = "Connects MCP servers, exposes permitted tools and records connection status for the model."
|
||||
name = "MCP connectors"
|
||||
summary = "A plugin kind: connects MCP servers, exposes permitted tools and records connection status for the model."
|
||||
status = "stable"
|
||||
surfaces = { tui = "stable", api = "stable" }
|
||||
since = "0.1.0"
|
||||
@@ -532,8 +532,8 @@ owner = "crates/memory"
|
||||
|
||||
[[feature]]
|
||||
id = "skills"
|
||||
name = "Skills"
|
||||
summary = "Trusted instruction packages with bundle policy, invocation controls and shared, configurable index limits."
|
||||
name = "Skill plugins"
|
||||
summary = "A plugin kind: trusted instruction packages with bundle policy, invocation controls and configurable index limits."
|
||||
status = "stable"
|
||||
surfaces = { tui = "stable", api = "stable" }
|
||||
docs = "docs/SKILLS.md"
|
||||
@@ -542,7 +542,7 @@ owner = "crates/skills/src"
|
||||
[[feature]]
|
||||
id = "plugins"
|
||||
name = "Plugins"
|
||||
summary = "Installs, reviews and trusts plugin bundles, reports their status, and restores missing reviewed runtime snapshots."
|
||||
summary = "Everything you add is a plugin: installs, reviews and trusts bundles, shows status, restores missing reviewed snapshots."
|
||||
status = "stable"
|
||||
surfaces = { tui = "stable", api = "stable" }
|
||||
docs = "docs/PLUGINS.md"
|
||||
@@ -585,8 +585,8 @@ owner = "crates/net/src/import_claude.rs"
|
||||
|
||||
[[feature]]
|
||||
id = "hooks"
|
||||
name = "Hooks"
|
||||
summary = "Runs user commands on session and tool events, with an execution receipt after shell tools."
|
||||
name = "Hook plugins"
|
||||
summary = "A plugin kind: runs user commands on session and tool events, with an execution receipt after shell tools."
|
||||
status = "stable"
|
||||
surfaces = { tui = "stable", api = "stable" }
|
||||
docs = "docs/HOOKS.md"
|
||||
@@ -805,7 +805,7 @@ owner = "crates/tui/src/commands/groups/session/export.rs"
|
||||
[[feature]]
|
||||
id = "live-thinking"
|
||||
name = "Live thinking"
|
||||
summary = "/verbose toggles live model thinking in the transcript."
|
||||
summary = "Model thinking shows live in the transcript, then collapses under a Thought header; /verbose shows it in full."
|
||||
status = "stable"
|
||||
surfaces = { tui = "stable" }
|
||||
owner = "crates/tui/src/commands/groups/config/mod.rs"
|
||||
|
||||
+195
-93
@@ -1,23 +1,21 @@
|
||||
<!-- source: README.md sha256:fd76d718e3a2 -->
|
||||
<!-- source: README.md sha256:a9ec5fb3f3dd -->
|
||||
<div align="center">
|
||||
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="../../brand/mark-reversed.svg">
|
||||
<img src="../../brand/mark-mono.svg" alt="Codewhale" width="72" height="72">
|
||||
<source media="(prefers-color-scheme: dark)" srcset="../../brand/wordmark-inverted.svg">
|
||||
<img src="../../brand/wordmark.svg" alt="Codewhale" width="320">
|
||||
</picture>
|
||||
|
||||
<h1>يمكن أن يكون النموذج ملكًا لأي أحد. أما الوكيل فينبغي أن يكون ملكًا لك.</h1>
|
||||
**وكيل برمجة مفتوح المصدر لطرفيتك يعمل مع أي نموذج.**
|
||||
|
||||
Codewhale وكيل برمجة مفتوح المصدر يعمل في الطرفية لديك، ومكتوب بلغة Rust. يعمل مع أي نموذج، ويستطيع توزيع مهمة واحدة على عدة وكلاء، وكل سطر فيه متاح لك لتقرأه.
|
||||
أعطِ Codewhale مهمة فيقرأ المستودع ويعدّل الملفات ويشغّل الأوامر إلى أن تكتمل
|
||||
المهمة أو يحتاج إلى مُدخل منك. يعمل مع أكثر من 40 مزوّدًا مدمجًا، وDeepSeek V4.1 Flash هو
|
||||
الخيار الافتراضي، ومع نماذج تعمل على جهازك. ويسرد `/receipts` الملفات التي غيّرها
|
||||
والأوامر التي شغّلها واستدعاءات الويب وMCP التي أجراها.
|
||||
|
||||
```bash
|
||||
curl -fsSL https://codewhale.net/install.sh | sh
|
||||
```
|
||||
|
||||
يثبّت هذا الأمر أحدث إصدار على macOS أو Linux، ولا يحتاج إلى حساب. طرق التثبيت على [Windows وnpm وCargo](#التثبيت) في الأسفل.
|
||||
|
||||
[](https://github.com/codewhale-hq/Codewhale/releases/latest)
|
||||
[](https://github.com/codewhale-hq/Codewhale/actions/workflows/ci.yml)
|
||||
[](https://github.com/codewhale-hq/CodeWhale/actions/workflows/ci.yml)
|
||||
[](https://crates.io/crates/codewhale-cli)
|
||||
[](https://www.npmjs.com/package/codewhale)
|
||||
[](../../LICENSE)
|
||||
[](https://discord.gg/37gfS3ksug)
|
||||
|
||||
@@ -25,141 +23,245 @@ curl -fsSL https://codewhale.net/install.sh | sh
|
||||
|
||||
[English](../../README.md) · [简体中文](README.zh-CN.md) · [日本語](README.ja-JP.md) · [Tiếng Việt](README.vi.md) · [Bahasa Indonesia](README.id.md) · [한국어](README.ko-KR.md) · [Español](README.es-419.md) · [Português](README.pt-BR.md) · [Русский](README.ru.md) · [Українська](README.uk.md) · [Français](README.fr.md) · [Deutsch](README.de.md) · [繁體中文](README.zh-TW.md) · [हिन्दी](README.hi.md) · [Türkçe](README.tr.md) · [Italiano](README.it.md) · [Polski](README.pl.md) · [Català](README.ca.md)
|
||||
|
||||
<sub>هذه الترجمة مسودة آلية ولم يراجعها بعد متحدث أصلي.</sub>
|
||||
<img src="../../web/public/codewhale-tui-0102-demo.png" alt="جلسة Codewhale في الطرفية" width="760">
|
||||
|
||||
<img src="../../web/public/codewhale-tui-0102-demo.png" width="760" alt="Codewhale في الطرفية بعد مهمة تجريبية. يذكر أنه أضاف الخيار --json إلى export.py، وأن الاختبارات المحلية الثلاثة نجحت، وأن المراجعة المفوَّضة لم تجد مشكلات. تعرض قائمة المهام في الأسفل ثلاث خطوات مكتملة، ويعرض سطر الحالة Full Access والمزوّد المحلي website-demo.">
|
||||
|
||||
<sub>بنية تطوير من الإصدار 0.10.3، الذي لم يصدر بعد، مع نموذج اختبار مكتوب مسبقًا، في وضع Full Access داخل مساحة عمل مؤقتة. التعديل والاختبارات الثلاثة ومراجعة الوكيل الفرعي جرت فعلًا.</sub>
|
||||
<sub>نسخة تطوير من 0.10.2 مع نموذج اختبار ذي ردود مُعدّة مسبقًا، بإذن Full Access داخل مساحة عمل مؤقتة. أما التعديل والاختبارات الثلاثة ومراجعة الوكيل الفرعي فقد جرت فعلًا.</sub>
|
||||
|
||||
</div>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://www.orcarouter.ai/redeem/1a6250545fdc4e0aaccb0a4470159e67" rel="sponsored">
|
||||
<img src="../../web/public/codewhale-x-orcarouter.webp" alt="Codewhale × OrcaRouter — استخدام إضافي بنسبة 30% لـ DeepSeek v4.1 Flash" width="760">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
[اطّلع على عرض Codewhale × OrcaRouter](https://www.orcarouter.ai/redeem/1a6250545fdc4e0aaccb0a4470159e67). تحصل Codewhale على عمولة إحالة من OrcaRouter.
|
||||
|
||||
## التثبيت
|
||||
|
||||
يضع الأمر أعلاه `codewhale` الذي جرى التحقق من مجموعه الاختباري واسمه المختصر `codew` في `~/.local/bin`. إذا قالت الصدفة بعد ذلك `command not found`، فاتبع ملاحظة PATH التي يطبعها المثبّت ([التفاصيل](../INSTALL.md#put-it-on-your-path)). يحدّثه الأمر `codewhale update`.
|
||||
على macOS وLinux:
|
||||
|
||||
```bash
|
||||
winget install --id HunterBown.CodeWhale --exact --source winget # Windows x64
|
||||
npm install -g codewhale # wraps the same release binaries
|
||||
cargo install codewhale-cli --locked # builds from source; can trail GitHub Releases
|
||||
curl -fsSL https://codewhale.net/install.sh | sh
|
||||
```
|
||||
|
||||
تتوفر ملفات ثنائية جاهزة لأنظمة Linux وmacOS وWindows على x64 وarm64. تجد Scoop وNix وHomebrew على Linux وAndroid وTermux (معاينة) والتنزيل اليدوي في [دليل التثبيت](../INSTALL.md)، والحاويات في [Docker](../DOCKER.md). استخدم طريقة تثبيت واحدة لكل جهاز؛ وإلا ستوجد نسختان في `PATH` وتعمل التي تُعثر عليها أولًا.
|
||||
يضع المثبّت `codewhale` واسمه البديل المختصر `codew` في `~/.local/bin` بعد التحقق
|
||||
منهما بمجموع الاختبار (checksum). إذا أظهرت الصدفة بعد ذلك `command not found`،
|
||||
فشغّل سطر PATH الذي يطبعه المثبّت ([التفاصيل](../INSTALL.md#put-it-on-your-path)).
|
||||
ويرقّيه الأمر `codewhale update`. تثبّت هذه الأوامر أحدث إصدار. أما هذه الصفحة
|
||||
فتتبع `main`، لذا فالميزة المعلَّمة برقم إصدار تصل في ذلك الإصدار؛ ويسرد
|
||||
[سجل التغييرات](../../CHANGELOG.md) ما يضيفه كل إصدار.
|
||||
|
||||
<details>
|
||||
<summary><b>Windows وnpm وCargo وطرق تثبيت أخرى</b></summary>
|
||||
|
||||
```bash
|
||||
winget install HunterBown.CodeWhale # Windows x64 (or Scoop, or the installer from GitHub Releases)
|
||||
npm install -g codewhale # wraps the same release binaries
|
||||
cargo install codewhale-cli --locked # crates.io; can trail GitHub Releases
|
||||
```
|
||||
|
||||
تتوفر ملفات ثنائية جاهزة لأنظمة Linux وmacOS وWindows على x64 وarm64. وتجد Nix
|
||||
وHomebrew على Linux وAndroid/Termux (معاينة) والتنزيل اليدوي في
|
||||
[دليل التثبيت](../INSTALL.md)، والحاويات في [Docker](../DOCKER.md). التزم بطريقة
|
||||
تثبيت واحدة لكل جهاز؛ وإلا ستوجد نسختان على `PATH` وتُستخدم النسخة التي تأتي أولًا.
|
||||
|
||||
</details>
|
||||
|
||||
## البدء السريع
|
||||
|
||||
1. احفظ عملك بـ commit أو stash، ثم شغّل `codewhale` من جذر مستودع git. افتراضيًا يطبّق تعديلات الملفات في المستودع دون أن يسأل، ويسأل قبل كل أمر shell لا يقتصر على القراءة.
|
||||
2. شغّل `/provider` (أو اضغط `F3`)، واختر مزوّدًا، والصق مفتاح API الخاص به، واختر نموذجًا. المزوّد الافتراضي هو DeepSeek. عند أول تشغيل دون مفتاح، يسألك Codewhale عن مزوّد حين ترسل رسالتك الأولى، ويقترح نموذجًا محليًا إذا استجاب خادم Ollama محلي.
|
||||
1. احفظ عملك بعمل commit أو stash، ثم شغّل `codewhale` من جذر مستودع git.
|
||||
افتراضيًا (**Accept Edits**) يعدّل الملفات في المستودع دون أن يسأل، ويسأل قبل كل أمر؛ اضغط `Shift+Tab` للتنقل بين الأذونات خطوة خطوة عندما تريده أن يسأل أكثر أو أقل.
|
||||
2. شغّل `/provider` (أو اضغط `F3`)، واختر مزوّدًا، والصق مفتاح API الخاص به، ثم
|
||||
اختر نموذجًا. DeepSeek هو المزوّد الافتراضي. إن لم يكن لديك مفتاح، فاختر بيئة
|
||||
تشغيل محلية بدلًا من ذلك. وإذا لم يكن أي مزوّد مُعدًّا وكان Ollama يقدّم نموذج
|
||||
محادثة بالفعل، يستخدمه Codewhale تلقائيًا؛ وبدءًا من 0.10.2 يعرضه عليك عندما
|
||||
ترسل رسالتك الأولى.
|
||||
3. اكتب مهمة:
|
||||
|
||||
```text
|
||||
Fix the failing tests and explain what changed.
|
||||
```
|
||||
|
||||
تعمل أوامر القراءة فقط فورًا. أما أي أمر آخر في الصدفة فيسأل عنه Codewhale أولًا: `y` يشغّله مرة واحدة، و`a` يسمح بالأوامر المشابهة طوال بقية المحادثة، و`n` يرفضه. يتراجع `/undo` عن طلبك الأخير وتغييرات ملفاته، ويعرض `/help` الأوامر والمفاتيح.
|
||||
تعمل الأوامر المخصصة للقراءة فقط فورًا. أما أي أمر صدفة آخر فيسأل Codewhale عنه
|
||||
أولًا: `y` يشغّله مرة واحدة، و`a` يسمح بالأوامر المشابهة له لبقية المحادثة، و`n`
|
||||
يرفضه. ويُلغي `/undo` آخر طلب لك وتغييرات الملفات الناتجة عنه. ويسرد `/help` الأوامر واختصارات لوحة المفاتيح.
|
||||
|
||||
في السكربتات وCI، يشغّل `codewhale exec` مهمة واحدة دون الواجهة. اقرأ [التشغيل دون واجهة](../INSTALL.md#headless-scripts-ci) قبل أن تمنحه الأدوات عبر `--auto`، لأن `--auto` يستخدم Full Access ما لم تضف بيئة معزولة.
|
||||
في السكربتات وCI، يشغّل `codewhale exec` مهمة واحدة دون الواجهة. افتراضيًا،
|
||||
يستخدم `--auto` وضع Full Access: تعمل الأدوات دون سؤال، وتعمل أوامر الصدفة دون
|
||||
صندوق حماية Codewhale، ويتيح وضع الثقة لأدوات الملفات الكتابة خارج مساحة العمل.
|
||||
أضف `--sandbox workspace-write` للإبقاء على صندوق حماية الصدفة وقيود مساحة العمل
|
||||
على أدوات الملفات، مع تعطيل وضع الثقة:
|
||||
|
||||
```bash
|
||||
codewhale exec --auto --sandbox workspace-write "fix the failing tests and explain what changed"
|
||||
```
|
||||
|
||||
## الأوضاع والأذونات
|
||||
|
||||
يبدأ التثبيت الجديد في وضع Work مع إذن Accept Edits. قرص واحد يبدّل الاثنين: يتنقّل `Shift+Tab` عبر Ask → Accept Edits → Plan → Auto-Review → Full Access.
|
||||
|
||||
| | التبديل عبر | الخيارات |
|
||||
| --- | --- | --- |
|
||||
| الوضع (Mode) | محطة Plan على `Shift+Tab`، أو `/mode` | في **Plan** يقرأ ويبحث لكنه لا يغيّر شيئًا. وفي **Work** (كل المحطات الأخرى) يعدّل الملفات ويشغّل الأوامر. وابدأ هدفًا باستخدام `/goal` عندما يحتاج الطلب إلى أكثر من دورة. |
|
||||
| الإذن (Permission) | `Shift+Tab` | مع **Ask** يسأل قبل كل تعديل على ملف وقبل كل أمر صدفة ليس للقراءة فقط. ومع **Accept Edits** تُطبَّق تعديلات الملفات في مستودع git دون سؤال، ويظل يسأل قبل الأوامر. ومع **Auto-Review** تعتمد القواعد ونموذج مراجِع الاستدعاءات الروتينية، ويرفض هذا الإذن الاستدعاءات عالية الخطورة؛ انتقل إلى Ask لتوافق عليها بنفسك. أما **Full Access** فلا يسأل أبدًا، ويمكن للأوامر الوصول إلى أي شيء على هذا الحاسوب؛ وتظل قواعد المستودع والسياسة المُدارة سارية. |
|
||||
|
||||
راجع [دليل الأوضاع والأذونات](../MODES.md) للتفاصيل.
|
||||
|
||||
## ما الذي يفعله
|
||||
|
||||
أعطِ Codewhale مهمة، فيقرأ المستودع ويعدّل الملفات وينفّذ الأوامر حتى تكتمل المهمة أو يحتاج إلى ردّك. تسرد [ملاحظات أحدث إصدار](https://github.com/codewhale-hq/Codewhale/releases/latest) أحدث التغييرات، ويضم [سجل التغييرات](../../CHANGELOG.md) كل إصدار.
|
||||
### الجديد في 0.10.2
|
||||
|
||||
### أي نموذج
|
||||
- يحوّل `/pet on` طرفيتك إلى حوت متحرك يُظهر ما يفعله الوكيل، بينما يبقى مربع
|
||||
الرسائل قابلًا للاستخدام. وتكتب جسيماته البالغ عددها 980 الخطوة الحالية
|
||||
بالإنجليزية أو الصينية أو اليابانية.
|
||||
- يعرض `/workbar terminal` الصدفات التي يشغّلها الوكيل أثناء عملها. اكتب في
|
||||
إحداها، أو اضغط `Ctrl+N` لتفتح صدفة خاصة بك (macOS وLinux).
|
||||
- يتراجع `/undo` عن آخر طلب لك كاملًا في خطوة واحدة، بما في ذلك كل ملف غيّره،
|
||||
ويعمل `/diff` في المجلدات التي لا تستخدم Git.
|
||||
- عندما تكون الخطة جاهزة، يسألك Codewhale هل تبدأ Work بإذن Ask أو Auto-Review،
|
||||
أم تواصل التخطيط.
|
||||
- يمنح `/color` كل محادثة لونًا خاصًا بها لمربع الكتابة، ويُحفظ مع الجلسة.
|
||||
- يمكن أن يستخدم `web_search` واجهة Search API الجديدة من DeepSeek: اضبط
|
||||
`[search] provider = "deepseek"` مع مفتاح DeepSeek.
|
||||
- خارج Full Access، لا يعمل أبدًا أمر `curl` أو `wget` قد يرفع بيانات اعتماد دون
|
||||
موافقتك، ويُرفض الأمر الذي يُخفي محارف Unicode غير مرئية. ولم يعد `write`
|
||||
يستبدل ملفًا لم يقرأه الوكيل، أو ملفًا تغيّر بعد أن قرأه.
|
||||
|
||||
يأتي مدمجًا مع أكثر من 40 مزوّدًا، منها Anthropic وOpenAI وDeepSeek وGoogle وMistral وMoonshot وOpenRouter وxAI. تعمل أي نقطة نهاية متوافقة مع OpenAI، وكذلك النماذج المحلية عبر Ollama أو vLLM أو SGLang. النموذج الافتراضي هو `deepseek-flash` من DeepSeek، ويخدمه الآن DeepSeek V4.1 Flash ([التفاصيل](../PROVIDERS.md#deepseek-v41-flash)).
|
||||
### النماذج
|
||||
|
||||
يبدّل `/model` النموذج بين الأدوار، ويختار `/model auto` نموذجًا لكل دور (ويحدد `/router` طريقة الاختيار). يعرض `/turn inspect` النموذج الذي استخدمه دور ما وكلفته والمدة التي استغرقها. راجع [المزوّدون](../PROVIDERS.md).
|
||||
يأتي Codewhale بأكثر من 40 مزوّدًا مدمجًا، منها Anthropic وOpenAI وDeepSeek
|
||||
وGoogle وMistral وMoonshot وOpenRouter وxAI. وتعمل أي نقطة نهاية متوافقة مع
|
||||
OpenAI، وكذلك النماذج المحلية عبر Ollama أو vLLM أو SGLang. والنموذج الافتراضي
|
||||
هو `deepseek-flash` من DeepSeek، الذي نقلته DeepSeek إلى DeepSeek-V4.1-Flash-1017
|
||||
في 2026-10-17؛ ويشغّله 0.10.1 دون تحديث
|
||||
([التفاصيل](../PROVIDERS.md#deepseek-v41-flash)).
|
||||
|
||||
ويبدّل `/model` النموذج بين دورات المحادثة، ويختار `/model auto` نموذجًا لكل دورة
|
||||
(ويضبط `/router` طريقة الاختيار). ويعرض `/turn inspect` النموذج الذي استخدمته
|
||||
الدورة وتكلفتها والمدة التي استغرقتها. ويتيح لك `codewhale auth chatgpt` استخدام
|
||||
اشتراك ChatGPT بدلًا من مفتاح API (تجريبي). راجع [المزوّدين](../PROVIDERS.md).
|
||||
|
||||
### الإيصالات والتراجع
|
||||
|
||||
يسرد `/receipts`، أو `codewhale receipts --last` بعد الخروج، الملفات التي غيّرتها الجلسة، والأوامر التي شغّلتها، واستدعاءات الويب وMCP، والوكلاء الذين بدأتهم، وكل موافقة ومن أعطاها، وما فشل. يتراجع `/undo` عن طلبك الأخير والملفات التي غيّرها في خطوة واحدة. يعيد `/restore` مساحة العمل كلها إلى لقطة سابقة، ولأن ذلك يكتب فوق التعديلات اللاحقة فهو يحتاج إلى `/trust on` أو Full Access. راجع [الإيصالات](../RECEIPTS.md).
|
||||
يسرد `/receipts`، أو `codewhale receipts --last` بعد الخروج، الملفات التي تغيّرت،
|
||||
والأوامر التي شُغّلت، واستدعاءات الويب وMCP، والوكلاء الذين شُغّلوا، والموافقات
|
||||
ومن منحها، وحالات الفشل. ويُلغي `/undo` آخر طلب لك والملفات التي غيّرها، في خطوة واحدة ومن دون وضع الثقة. ويعيد `/restore` مساحة العمل
|
||||
كلها إلى لقطة سابقة؛ ولأن ذلك يكتب فوق التعديلات اللاحقة، فإنه يتطلب `/trust on`
|
||||
أو Full Access. راجع [الإيصالات](../RECEIPTS.md).
|
||||
|
||||
### عدة وكلاء على مهمة واحدة
|
||||
تُحفظ عمليات تشغيل exec التي تتيح أدوات بكل صيغ الإخراج، لذا يعمل
|
||||
`codewhale receipts --last` بعد `exec --auto`، بما في ذلك مع `--json`.
|
||||
أما عمليات التشغيل التي تقتصر على الإجابة بإخراج نصي أو `--json` فلا تُحفظ إلا
|
||||
عند الاستئناف أو عند بدء ضغط المحادثة؛ استخدم `--output-format stream-json`
|
||||
لحفظها دائمًا من أجل الإيصالات أو `exec --continue`.
|
||||
|
||||
يحدد `/goal` هدفًا يستمر عبر الأدوار. يستطيع Codewhale توزيع العمل على [وكلاء فرعيين](../SUBAGENTS.md)، أو تشغيل [Fleet](../FLEET.md) من الوكلاء لكل منهم دوره ونموذجه، أو اتباع [سير عمل](../WORKFLOW_AUTHORING.md) محفوظ في المستودع.
|
||||
### الأعمال الأطول
|
||||
|
||||
### الإضافات
|
||||
يحدد `/goal` هدفًا يستمر عبر دورات المحادثة. ويستطيع Codewhale توزيع العمل على
|
||||
[وكلاء فرعيين](../SUBAGENTS.md)، أو تشغيل [فريق وكلاء](../FLEET.md)، أو اتباع
|
||||
[سير عمل](../WORKFLOW_AUTHORING.md) مضمَّن في المستودع.
|
||||
|
||||
صِل [خوادم MCP](../MCP.md)، وأضف [المهارات](../SKILLS.md) و[الإضافات البرمجية](../PLUGINS.md)، وشغّل [الخطافات](../HOOKS.md) عند أحداث الجلسة والأدوات. يتيح وضع الشيفرة (تجريبي، ومفعّل افتراضيًا) للنموذج كتابة برنامج `execute_tools` واحد يستدعي عدة أدوات MCP وأدوات إضافات؛ ويظل كل استدعاء خاضعًا لقواعد الموافقة لديك ويحتفظ بإيصاله. إضافة Computer Use المضمّنة في v0.10.2 (تجريبية، على macOS فقط) تستطيع مراقبة التطبيقات الأخرى وتشغيلها، وتبقى معطّلة حتى تراجع صلاحياتها وتفعّلها ([الدليل](https://github.com/codewhale-hq/Codewhale/blob/v0.10.2/crates/tui/plugins/computer-use/README.md)). قادم في 0.10.3: تصدر الإضافة [بشكل منفصل](https://github.com/codewhale-hq/codewhale-cu-plugin).
|
||||
### إن كنت قادمًا من وكيل آخر
|
||||
|
||||
### طرق التشغيل
|
||||
يقرأ Codewhale ملف `AGENTS.md` من تلقاء نفسه. أما ملفات التعليمات المكتوبة لأدوات
|
||||
أخرى، مثل `CLAUDE.md` أو `.cursorrules` أو `.github/copilot-instructions.md`، فلا
|
||||
تُحمَّل إلا عندما تختار ذلك بإضافة `project_instruction_imports = ["claude"]` (أو
|
||||
`"cursor"` أو `"cline"` أو `"windsurf"` أو `"copilot"` أو `"all"`) إلى ملف
|
||||
إعداداتك؛ راجع [config.example.toml](../../config.example.toml). ويُصدر Codewhale تحذيرًا
|
||||
عندما يعثر على ملف منها لم يحمّله.
|
||||
|
||||
يجهّز `/import-claude` استيرادًا لإعدادات Claude Code وخوادم MCP الخاصة بك لتراجعه.
|
||||
ويقبل `/plugin install` إضافات Claude Code التي تحتوي على مهارات وأوامر وملفات تعريف
|
||||
وكلاء وخوادم MCP، ويرفض الإضافات التي تحتاج إلى خطافات أو خوادم LSP
|
||||
([التفاصيل](../CLAUDE_PLUGIN_COMPAT.md)).
|
||||
|
||||
### الامتدادات
|
||||
|
||||
اربط [خوادم MCP](../MCP.md)، وأضف [المهارات](../SKILLS.md) و[الإضافات](../PLUGINS.md)،
|
||||
وشغّل [الخطافات](../HOOKS.md) عند أحداث الجلسة والأدوات. ويتيح وضع الشيفرة
|
||||
(Code mode، تجريبي ومفعّل افتراضيًا) للنموذج كتابة برنامج `execute_tools` واحد
|
||||
يستدعي عدة أدوات من MCP والإضافات؛ ويظل كل استدعاء خاضعًا لقواعد الموافقة لديك
|
||||
ويحتفظ بإيصاله. وتستطيع إضافة Computer Use
|
||||
المثبّتة بشكل مستقل (تجريبية، لنظام macOS فقط) مراقبة التطبيقات الأخرى والتحكم فيها. وتبقى
|
||||
معطّلة إلى أن تراجع صلاحيات وصولها وتفعّلها
|
||||
([الدليل](https://github.com/codewhale-hq/codewhale-cu-plugin)).
|
||||
|
||||
## طرق التشغيل
|
||||
|
||||
| الأمر | ما يفعله |
|
||||
| --- | --- |
|
||||
| `codewhale` | تطبيق الطرفية التفاعلي |
|
||||
| `codewhale exec "…"` | مهمة واحدة دون الواجهة، للسكربتات وCI ([التشغيل دون واجهة](../INSTALL.md#headless-scripts-ci)) |
|
||||
| `codewhale web` | عميل ويب محلي على `127.0.0.1` ([الدليل](../WEB.md)) |
|
||||
| `codewhale review --pr N` | مراجعة استشارية لطلب سحب على GitHub؛ يشغّلها [إعداد بوت GitHub](../GITHUB_ACTION.md#set-up-your-own-bot) بمفتاح النموذج الخاص بك |
|
||||
| `codewhale app-server --http` | [Runtime API](../RUNTIME_API.md)، واجهة HTTP محلية للخيوط والأحداث والموافقات وتغييرات الملفات في كل دور |
|
||||
| `codewhale exec "…"` | مهمة واحدة دون الواجهة. دون خيارات إضافية يجيب دون استخدام الأدوات؛ ويفعّل `--auto` الأدوات مع Full Access افتراضيًا، ويقيّدها `--sandbox workspace-write`، ويبث `--output-format stream-json` الأحداث بصيغة JSON |
|
||||
| `codewhale web` | واجهة ويب محلية على `127.0.0.1` ([الدليل](../WEB.md)) |
|
||||
| `codewhale review --pr N` | مراجعة استشارية لطلب pull request على GitHub. يُنشئ [إعداد بوت GitHub](../GITHUB_ACTION.md#set-up-your-own-bot) سير عمل مثبّت الإصدار للمراجعات التلقائية و`@codewhale review`، باستخدام مفتاح النموذج الخاص بك وهوية App اختيارية |
|
||||
| `codewhale app-server --http` | واجهة [Runtime API](../RUNTIME_API.md)، وهي واجهة HTTP محلية للخيوط والأحداث والموافقات وتغييرات الملفات في كل دورة |
|
||||
| `codewhale serve --acp` | بروتوكول Agent Client Protocol عبر stdio، لمحررات مثل Zed |
|
||||
|
||||
يتشارك تطبيق الطرفية و`exec` و`web` وRuntime API محركًا واحدًا ([البنية](../ARCHITECTURE.md)). تتصل به [إضافة VS Code](https://marketplace.visualstudio.com/items?itemName=HengQuWorld.brotherwhale-vscode) التي يصونها المجتمع من الشريط الجانبي ([المصدر](https://github.com/HengQuWorld/CodeWhale-VSCode)).
|
||||
يتشارك تطبيق الطرفية و`exec` و`web` وRuntime API محركًا واحدًا
|
||||
([البنية](../ARCHITECTURE.md)). ويتصل به من الشريط الجانبي
|
||||
[امتداد VS Code](https://marketplace.visualstudio.com/items?itemName=HengQuWorld.brotherwhale-vscode)
|
||||
الذي يصونه المجتمع ([المصدر](https://github.com/HengQuWorld/CodeWhale-VSCode)).
|
||||
ويجري تطوير تطبيق أصلي لسطح المكتب مبني على Runtime API؛ ولا يتوفر له تنزيل عام
|
||||
بعد.
|
||||
|
||||
## كيف يبقى آمنًا
|
||||
## التكلفة والحسابات
|
||||
|
||||
مفتاحان يحددان ما يستطيع Codewhale فعله من تلقاء نفسه. عندما يكون حقل الإدخال فارغًا، يغيّر `Tab` الوضع: **Plan** يقرأ ويبحث ولا يغيّر شيئًا، و **Work** يعدّل الملفات وينفّذ الأوامر، و **Operate** له صلاحيات Work نفسها ويواصل العمل حتى يتحقق من الهدف أو يحتاج إليك. ويغيّر `Shift+Tab` الصلاحية. **Ask**، وهو الافتراضي، يطبّق تعديلات الملفات في مستودعك دون أن يسأل، ويسأل قبل أوامر shell التي لا تقتصر على القراءة. **Auto-Review** يترك للقواعد ولنموذج مراجِع الموافقة على الاستدعاءات الروتينية ويرفض العالية الخطورة؛ انتقل إلى Ask لتوافق عليها بنفسك. **Full Access** لا يسأل أبدًا ويعطّل sandbox الخاص بـ Codewhale، مع بقاء قواعد المستودع والسياسة المُدارة سارية. التفاصيل في [دليل v0.10.2](https://github.com/codewhale-hq/Codewhale/blob/v0.10.2/docs/MODES.md).
|
||||
يُتاح Codewhale مجانًا بترخيص MIT. عندما تستخدم مفتاح API الخاص بك تدفع للمزوّد
|
||||
مباشرة؛ ولا تحتاج النماذج المحلية إلى مفتاح. ويعرض الشريط السفلي و`/cost` تكلفة
|
||||
الجلسة عندما يكون السعر معروفًا، ويُعرض السعر غير المعروف على أنه غير معروف، ولا
|
||||
يُعرض أبدًا على أنه مجاني.
|
||||
|
||||
قادم في 0.10.3: يصبح `Shift+Tab` قرصًا واحدًا يمر بـ Ask و Accept Edits و Plan و Auto-Review و Full Access، ولن يغيّر `Tab` الوضع بعد ذلك، ويُلغى وضع Operate. سيسأل Ask قبل كل تعديل على الملفات أيضًا، وسيطبّق الافتراضي الجديد Accept Edits التعديلات داخل مستودع git ويواصل السؤال قبل الأوامر. يشرح [الدليل الحالي](../MODES.md) ذلك.
|
||||
لا تحتاج إلى حساب. وتبقى المفاتيح التي تضيفها عبر `/provider` على جهازك.
|
||||
|
||||
في Ask وAuto-Review تعمل الأوامر داخل بيئة معزولة من نظام التشغيل إن وُجدت: Seatbelt على macOS، وbubblewrap على Linux إذا كان مثبتًا. لا يملك Windows بيئة معزولة من النظام بعد، ويوقف Full Access البيئة المعزولة. داخل البيئة المعزولة لا تستطيع أوامر الصدفة وجلسات الطرفية قراءة مخازن بيانات الاعتماد مثل `~/.ssh` و`~/.aws` وملفات `.env`. يعرض `/preview-request` معاينة محجوبة البيانات الحساسة للطلب التالي دون إرساله.
|
||||
## الأمان والخصوصية
|
||||
|
||||
لا تحتاج إلى حساب، وتبقى المفاتيح التي تضيفها عبر `/provider` على جهازك. حين تستخدم مفتاح API الخاص بك تدفع للمزوّد مباشرة، ولا تحتاج النماذج المحلية إلى مفتاح. يعرض التذييل و`/cost` كلفة الجلسة حين يكون السعر معروفًا، ويظهر السعر غير المعروف على أنه غير معروف، لا على أنه مجاني أبدًا.
|
||||
في Ask وAuto-Review تعمل الأوامر داخل صندوق حماية نظام التشغيل حيثما يوفّر النظام
|
||||
واحدًا: Seatbelt على macOS، وbubblewrap على Linux عندما يكون مثبّتًا. لا يوجد على
|
||||
Windows صندوق حماية لنظام التشغيل بعد، ويوقف Full Access صندوق الحماية. وداخل
|
||||
صندوق الحماية لا تستطيع أوامر الصدفة قراءة مخازن بيانات الاعتماد مثل `~/.ssh`
|
||||
و`~/.aws` وملفات `.env`؛ وبدءًا من 0.10.2 تشمل القاعدة نفسها جلسات الطرفية ومزيدًا
|
||||
من أدوات الملفات. ويعرض `/preview-request` معاينة للطلب التالي بعد حجب بياناته الحساسة (redacted)، دون
|
||||
إرساله.
|
||||
|
||||
القياس عن بُعد مفعّل افتراضيًا. يرسل أعدادًا مجمّعة (الإصدار والمنصة، ومدة الجلسة ونتيجتها، والمزوّدين المدمجين الذين استخدمتهم، وأعداد الميزات والأخطاء) مع معرّف تثبيت عشوائي يتغير كل 90 يومًا، ولا يرسل أبدًا المحادثات أو الشيفرة أو الموجّهات أو أسماء النماذج أو أسماء الملفات أو المستودعات أو بيانات الاعتماد. يوقفه هذا الأمر:
|
||||
|
||||
```bash
|
||||
codewhale config set telemetry false
|
||||
```
|
||||
|
||||
عند البدء يتحقق Codewhale أيضًا من GitHub بحثًا عن إصدار جديد؛ اضبط `CODEWHALE_NO_UPDATE_CHECK=1` لتخطي ذلك. المزيد في [ترتيب التفويض](../AUTHORIZATION_ORDER.md) و[البيئة المعزولة](../SANDBOX.md) و[القياس عن بُعد](../TELEMETRY.md).
|
||||
|
||||
## إلى أين يتجه
|
||||
|
||||
يتحول Codewhale إلى بيئة واحدة تمتد عبر طرفيتك وتطبيق سطح المكتب والمتصفح وحواسيب السحابة، يديرها كلها محرك واحد. يبيّن كل جزء أدناه أين يقف اليوم.
|
||||
|
||||
| الجزء | الحالة | اليوم |
|
||||
| --- | --- | --- |
|
||||
| تطبيق الطرفية | منشور | ثبّته على macOS أو Linux أو Windows وشغّل `codewhale` في أي مستودع. |
|
||||
| عميل المتصفح المحلي | منشور | شغّل `codewhale web` لقراءة المحادثة وإرسال مهمة والرد على طلبات الموافقة في متصفحك؛ يأتي مع الطرفية ولا يحتاج إلى حساب. |
|
||||
| عدة وكلاء على مهمة واحدة | منشور | الوكلاء الفرعيون وFleet وسير العمل تتيح لعدة وكلاء تقاسم مهمة واحدة، لكل منهم دوره. |
|
||||
| تطبيق سطح المكتب | قيد التطوير، دون تنزيل | نبني تطبيقًا لسطح المكتب على المحرك نفسه، وليس له تنزيل عام بعد. |
|
||||
| حواسيب السحابة | بالدعوة فقط | حواسيب السحابة بالدعوة فقط، ونحن نبنيها لتتمكن الجلسة من مواصلة العمل على جهاز في السحابة. |
|
||||
| جلسة واحدة عبرها كلها | قيد البناء | نبني جلسة واحدة ترافقك من الطرفية إلى سطح المكتب والمتصفح والسحابة، وتبقى ملكًا لك طوال الطريق. |
|
||||
|
||||
توضح [خريطة الطريق](../ROADMAP.md) ما يشمله كل إصدار وأين يمكنك المساعدة.
|
||||
|
||||
## إن كنت قادمًا من وكيل آخر
|
||||
|
||||
يقرأ Codewhale ملف `AGENTS.md` من تلقاء نفسه. أما ملفات التعليمات المكتوبة لأدوات أخرى، مثل `CLAUDE.md` و`.cursorrules` و`.github/copilot-instructions.md`، فلا تُحمَّل إلا إذا فعّلتها بنفسك عبر `project_instruction_imports = ["claude"]` (أو `"cursor"` و`"cline"` و`"windsurf"` و`"copilot"` و`"all"`) في إعداداتك؛ راجع [config.example.toml](../../config.example.toml). ينبّهك Codewhale حين يجد ملفًا من هذا النوع لم يحمّله.
|
||||
|
||||
يجهّز `/import-claude` استيرادًا لإعدادات Claude Code وخوادم MCP لديك لتراجعه. يقبل `/plugin install` إضافات Claude Code التي تحتوي على مهارات وأوامر وملفات تعريف وكلاء وخوادم MCP، ويرفض الإضافات التي تحتاج إلى خطافات أو خوادم LSP ([التفاصيل](../CLAUDE_PLUGIN_COMPAT.md)).
|
||||
القياس عن بُعد مفعّل افتراضيًا. يرسل أعدادًا مجمّعة (الإصدار والمنصة، ومدة الجلسة
|
||||
ونتيجتها، والمزوّدين المدمجين الذين استخدمتهم، وأعداد الميزات والأخطاء) مع معرّف
|
||||
تثبيت عشوائي يتغيّر كل 90 يومًا، ولا يرسل أبدًا المحادثات أو الشيفرة أو الموجّهات
|
||||
(prompts) أو أسماء النماذج أو أسماء الملفات أو المستودعات
|
||||
أو بيانات الاعتماد. ويوقفه الأمر `codewhale config set telemetry false`. وعند بدء
|
||||
التشغيل يتحقق Codewhale أيضًا من GitHub بحثًا عن إصدار جديد؛ اضبط
|
||||
`CODEWHALE_NO_UPDATE_CHECK=1` لتخطي ذلك. المزيد في
|
||||
[ترتيب التفويض](../AUTHORIZATION_ORDER.md) و[صندوق الحماية](../SANDBOX.md)
|
||||
و[القياس عن بُعد](../TELEMETRY.md).
|
||||
|
||||
## التوثيق
|
||||
|
||||
تتبع هذه الأدلة الفرع الرئيسي، لذا قد تصف تغييرات لم تصل إلى أحدث إصدار بعد.
|
||||
|
||||
| ابدأ من هنا | المرجع |
|
||||
| --- | --- |
|
||||
| [دليل المستخدم](../GUIDE.md) | [الإعدادات](../CONFIGURATION.md) |
|
||||
| [التثبيت](../INSTALL.md) | [البنية](../ARCHITECTURE.md) |
|
||||
| [المزوّدون والنماذج المحلية](../PROVIDERS.md) | [Runtime API](../RUNTIME_API.md) |
|
||||
| [الأوضاع والأذونات](../MODES.md) | [كتابة الإضافات](../PLUGIN_AUTHORING.md) |
|
||||
| [اختصارات المفاتيح](../KEYBINDINGS.md) | [الإيصالات](../RECEIPTS.md) |
|
||||
| [مراجعة طلبات السحب على GitHub](../GITHUB_ACTION.md) | [كل التوثيق](../README.md) |
|
||||
| [التثبيت](../INSTALL.md) | [الإعدادات](../CONFIGURATION.md) |
|
||||
| [المزوّدون والنماذج المحلية](../PROVIDERS.md) | [البنية](../ARCHITECTURE.md) |
|
||||
| [الأوضاع والأذونات](../MODES.md) | [Runtime API](../RUNTIME_API.md) |
|
||||
| [اختصارات لوحة المفاتيح](../KEYBINDINGS.md) | [تأليف الإضافات](../PLUGIN_AUTHORING.md) |
|
||||
| [مراجعة PR على GitHub](../GITHUB_ACTION.md) | [كل التوثيق](../README.md) |
|
||||
|
||||
## المساهمة
|
||||
## المجتمع
|
||||
|
||||
يبني Codewhale في العلن الأشخاص الذين يستخدمونه، بلغات كثيرة.
|
||||
Codewhale هو ما هو عليه اليوم بفضل مجتمعه، ونرحّب بكل الملاحظات: خلل، أو فكرة، أو شيء بدا أو أحسست أنه غير صحيح.
|
||||
[افتح issue](https://github.com/codewhale-hq/CodeWhale/issues/new/choose) لأيٍّ منها؛ وهو أيضًا أفضل مكان لبدء ميزة جديدة.
|
||||
نرحّب بطلبات السحب للإصلاحات والترجمات والمزوّدين والعمل المخطَّط له.
|
||||
يوضّح [دليل المساهمة](../../CONTRIBUTING.md) أين يذهب كل نوع من المساعدة، وتوضّح [خارطة الطريق](../ROADMAP.md) إلى أين يتجه المشروع.
|
||||
يحتفظ المساهمون بنسبة العمل الذي يُدمج إليهم، ويبيّن [هيكل المستودع](../../CONTRIBUTING.md#project-structure) مكان كل شيء.
|
||||
|
||||
<a href="../CONTRIBUTORS.md#everyone"><img src="../assets/contributors.png" width="760" alt="جدار المساهمين في Codewhale: صورة رمزية لكل شخص له في Codewhale شيفرة أو commit مشترك في التأليف أو طلب سحب جرى استكماله أو بلاغ نُسب إليه، والعدد الإجمالي في الأعلى."></a>
|
||||
|
||||
يعدّ [جدار المساهمين](../CONTRIBUTORS.md) كل من له في Codewhale شيفرة أو commit مشترك في التأليف أو طلب سحب جرى استكماله أو بلاغ نُسب إليه، بمن فيهم من تُغفلهم قائمة المساهمين في GitHub نفسها. يعيد `scripts/contributor-wall.py` إنشاء الصورة والقائمة من سجل git ومن GitHub.
|
||||
|
||||
كل ملاحظة مرحّب بها: خطأ، أو فكرة، أو شيء بدا أو بدا استخدامه غير صحيح. [افتح issue](https://github.com/codewhale-hq/Codewhale/issues/new/choose) لأي من ذلك؛ والـ issue أيضًا أفضل مكان لبدء ميزة جديدة. طلبات السحب مرحّب بها للإصلاحات والترجمات والمزوّدين والعمل المخطط له. الأسئلة مكانها [Discussions](https://github.com/codewhale-hq/Codewhale/discussions). يوضح [دليل المساهمة](../../CONTRIBUTING.md) أين يذهب كل نوع من المساعدة، وكيف يُدمج عملك باسمك، و[أين يوجد كل شيء](../../CONTRIBUTING.md#project-structure).
|
||||
|
||||
تحدّث على [Discord](https://discord.gg/37gfS3ksug)، أو أضف المشرف Hunter على WeChat (`hunterbown`) واطلب الانضمام إلى مجموعة Whale Brothers. الجميع مرحّب بهم.
|
||||
تواصل معنا على [Discord](https://discord.gg/37gfS3ksug)، أو أضف القائم على صيانة
|
||||
المشروع، Hunter، على WeChat (`hunterbown`) واطلب الانضمام إلى مجموعة Whale Brothers. الجميع مرحَّب بهم.
|
||||
|
||||
## التاريخ والترخيص
|
||||
|
||||
بدأ Codewhale باسم `deepseek-tui` وما زال يقرأ إعدادات ذلك المشروع وجلساته. يُصان على نحو مستقل، ولا ينتمي إلى DeepSeek أو إلى أي مزوّد نماذج آخر.
|
||||
بدأ Codewhale باسم `deepseek-tui` ولا يزال يقرأ إعدادات ذلك المشروع وجلساته. ويُصان
|
||||
باستقلال، وليس تابعًا لـ DeepSeek ولا لأي مزوّد نماذج آخر. شكرًا
|
||||
لـ[كل من ساهم](../CONTRIBUTORS.md).
|
||||
|
||||
[MIT](../../LICENSE). الشيفرة المقتبسة من مشاريع أخرى مفتوحة المصدر مدرجة في إشعارات الأطراف الثالثة الخاصة بـ[المصادر المضمّنة](../../THIRD_PARTY_NOTICES.md) و[التصاميم المقتبسة](../THIRD_PARTY_NOTICES.md).
|
||||
[](../CONTRIBUTORS.md#everyone)
|
||||
|
||||
الترخيص: [MIT](../../LICENSE). والشيفرة المقتبسة من مشاريع مفتوحة المصدر أخرى مُدرجة في
|
||||
[إشعارات الأطراف الثالثة](../THIRD_PARTY_NOTICES.md).
|
||||
+215
-92
@@ -1,23 +1,22 @@
|
||||
<!-- source: README.md sha256:fd76d718e3a2 -->
|
||||
<!-- source: README.md sha256:a9ec5fb3f3dd -->
|
||||
<div align="center">
|
||||
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="../../brand/mark-reversed.svg">
|
||||
<img src="../../brand/mark-mono.svg" alt="Codewhale" width="72" height="72">
|
||||
<source media="(prefers-color-scheme: dark)" srcset="../../brand/wordmark-inverted.svg">
|
||||
<img src="../../brand/wordmark.svg" alt="Codewhale" width="320">
|
||||
</picture>
|
||||
|
||||
<h1>El model pot ser de qualsevol. L’agent hauria de ser teu.</h1>
|
||||
**Un agent de programació de codi obert per al teu terminal que funciona amb qualsevol model.**
|
||||
|
||||
Codewhale és un agent de programació de codi obert per al teu terminal, escrit en Rust. Funciona amb qualsevol model, pot repartir una tasca entre diversos agents i en pots llegir cada línia.
|
||||
Dona-li una tasca a Codewhale i llegirà el repositori, editarà fitxers i
|
||||
executarà ordres fins que la tasca estigui feta o necessiti alguna resposta teva.
|
||||
Funciona amb més de 40 proveïdors integrats, DeepSeek V4.1 Flash per defecte, i amb models
|
||||
que s’executen a la teva pròpia màquina. `/receipts` llista els fitxers que ha
|
||||
canviat, les ordres que ha executat i les crides web i MCP que ha fet.
|
||||
|
||||
```bash
|
||||
curl -fsSL https://codewhale.net/install.sh | sh
|
||||
```
|
||||
|
||||
Això instal·la l’última versió a macOS o Linux, i no necessita cap compte. [Windows, npm i Cargo](#installació) són més avall.
|
||||
|
||||
[](https://github.com/codewhale-hq/Codewhale/releases/latest)
|
||||
[](https://github.com/codewhale-hq/Codewhale/actions/workflows/ci.yml)
|
||||
[](https://github.com/codewhale-hq/CodeWhale/actions/workflows/ci.yml)
|
||||
[](https://crates.io/crates/codewhale-cli)
|
||||
[](https://www.npmjs.com/package/codewhale)
|
||||
[](../../LICENSE)
|
||||
[](https://discord.gg/37gfS3ksug)
|
||||
|
||||
@@ -25,141 +24,265 @@ Això instal·la l’última versió a macOS o Linux, i no necessita cap compte.
|
||||
|
||||
[English](../../README.md) · [简体中文](README.zh-CN.md) · [日本語](README.ja-JP.md) · [Tiếng Việt](README.vi.md) · [Bahasa Indonesia](README.id.md) · [한국어](README.ko-KR.md) · [Español](README.es-419.md) · [Português](README.pt-BR.md) · [Русский](README.ru.md) · [Українська](README.uk.md) · [Français](README.fr.md) · [Deutsch](README.de.md) · [繁體中文](README.zh-TW.md) · [हिन्दी](README.hi.md) · [Türkçe](README.tr.md) · [Italiano](README.it.md) · [Polski](README.pl.md) · [العربية](README.ar.md)
|
||||
|
||||
<sub>Aquesta traducció és un esborrany automàtic que encara no ha revisat cap parlant nadiu.</sub>
|
||||
<img src="../../web/public/codewhale-tui-0102-demo.png" alt="Una sessió de terminal de Codewhale" width="760">
|
||||
|
||||
<img src="../../web/public/codewhale-tui-0102-demo.png" width="760" alt="Codewhale en un terminal després d’una tasca de demostració. Informa que ha afegit una opció --json a export.py, que han passat les 3 proves locals i que una revisió delegada no ha trobat cap problema. La llista de tasques de sota mostra tres passos completats, i la línia d’estat mostra Full Access i el proveïdor local website-demo.">
|
||||
|
||||
<sub>Una compilació de desenvolupament de la 0.10.3, que encara no s’ha publicat, amb un model de prova amb guió, en Full Access dins d’un espai de treball d’un sol ús. L’edició, tres proves i una revisió d’un subagent es van executar de debò.</sub>
|
||||
<sub>Una compilació de desenvolupament de la 0.10.2 amb un model de prova amb guió, en Full Access dins d’un espai de treball d’un sol ús. L’edició, tres proves i una revisió d’un subagent es van executar de debò.</sub>
|
||||
|
||||
</div>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://www.orcarouter.ai/redeem/1a6250545fdc4e0aaccb0a4470159e67" rel="sponsored">
|
||||
<img src="../../web/public/codewhale-x-orcarouter.webp" alt="Codewhale × OrcaRouter — un 30% més d’ús de DeepSeek v4.1 Flash" width="760">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
[Consulta l’oferta de Codewhale × OrcaRouter](https://www.orcarouter.ai/redeem/1a6250545fdc4e0aaccb0a4470159e67). Codewhale rep una comissió per recomanació d’OrcaRouter.
|
||||
|
||||
## Instal·lació
|
||||
|
||||
L’ordre de dalt posa `codewhale`, verificat amb suma de comprovació, i el seu àlies curt `codew` a `~/.local/bin`. Si després la shell diu `command not found`, segueix la nota sobre el PATH que imprimeix l’instal·lador ([detalls](../INSTALL.md#put-it-on-your-path)). `codewhale update` l’actualitza.
|
||||
macOS i Linux:
|
||||
|
||||
```bash
|
||||
winget install --id HunterBown.CodeWhale --exact --source winget # Windows x64
|
||||
npm install -g codewhale # wraps the same release binaries
|
||||
cargo install codewhale-cli --locked # builds from source; can trail GitHub Releases
|
||||
curl -fsSL https://codewhale.net/install.sh | sh
|
||||
```
|
||||
|
||||
Hi ha binaris precompilats per a Linux, macOS i Windows en x64 i arm64. Scoop, Nix, Homebrew a Linux, Android i Termux (vista prèvia) i les baixades manuals són a la [guia d’instal·lació](../INSTALL.md), i els contenidors a [Docker](../DOCKER.md). Fes servir un sol mètode d’instal·lació per màquina; si no, hi haurà dues còpies al teu `PATH` i guanyarà la primera que es trobi.
|
||||
L’instal·lador desa `codewhale` i el seu àlies curt `codew` a `~/.local/bin`,
|
||||
després de comprovar-ne la suma de verificació. Si després el teu shell diu
|
||||
`command not found`, executa la línia de PATH que mostra l’instal·lador
|
||||
([detalls](../INSTALL.md#put-it-on-your-path)). `codewhale update`
|
||||
l’actualitza. Aquestes ordres instal·len la darrera versió. Aquesta pàgina
|
||||
segueix `main`, de manera que una funcionalitat marcada amb una versió arriba en
|
||||
aquella versió; el [registre de canvis](../../CHANGELOG.md) llista què afegeix cada
|
||||
versió.
|
||||
|
||||
<details>
|
||||
<summary><b>Windows, npm, Cargo i altres maneres d’instal·lar-lo</b></summary>
|
||||
|
||||
```bash
|
||||
winget install HunterBown.CodeWhale # Windows x64 (or Scoop, or the installer from GitHub Releases)
|
||||
npm install -g codewhale # wraps the same release binaries
|
||||
cargo install codewhale-cli --locked # crates.io; can trail GitHub Releases
|
||||
```
|
||||
|
||||
Hi ha binaris precompilats per a Linux, macOS i Windows en x64 i arm64. Nix,
|
||||
Homebrew a Linux, Android/Termux (versió preliminar) i les descàrregues manuals
|
||||
s’expliquen a la [guia d’instal·lació](../INSTALL.md), i els contenidors a
|
||||
[Docker](../DOCKER.md). Fes servir un sol mètode d’instal·lació per màquina;
|
||||
si no, acabaràs amb dues còpies al `PATH` i s’executarà la que hi aparegui
|
||||
primer.
|
||||
|
||||
</details>
|
||||
|
||||
## Inici ràpid
|
||||
|
||||
1. Fes commit o stash de la teva feina i executa `codewhale` des de l’arrel d’un repositori git. Per defecte aplica els canvis de fitxers del repositori sense preguntar i pregunta abans de cada ordre de shell que no sigui només de lectura.
|
||||
2. Executa `/provider` (o prem `F3`), tria un proveïdor, enganxa la seva clau d’API i tria un model. DeepSeek és el proveïdor per defecte. En el primer inici sense clau, Codewhale et demana un proveïdor quan envies el primer missatge, i ofereix un model local quan respon un servidor Ollama local.
|
||||
1. Fes un commit o un stash de la teva feina i després executa `codewhale` des
|
||||
de l’arrel d’un repositori git. Per defecte (**Accept Edits**), edita fitxers al repositori sense preguntar
|
||||
i pregunta abans de cada ordre; `Shift+Tab` recorre el selector de permisos quan vols que pregunti més o menys.
|
||||
2. Executa `/provider` (o prem `F3`), tria un proveïdor, enganxa’n la clau API
|
||||
i escull un model. DeepSeek és el proveïdor per defecte. Si no tens cap
|
||||
clau, tria un entorn local. Si no hi ha cap proveïdor configurat i Ollama ja
|
||||
serveix un model de xat, Codewhale el fa servir automàticament; a partir de
|
||||
la 0.10.2 te l’ofereix quan envies el primer missatge.
|
||||
3. Escriu una tasca:
|
||||
|
||||
```text
|
||||
Fix the failing tests and explain what changed.
|
||||
```
|
||||
|
||||
Les ordres de només lectura s’executen de seguida. Per a qualsevol altra ordre de shell, Codewhale pregunta primer: `y` l’executa una vegada, `a` permet ordres semblants durant la resta de la conversa i `n` la rebutja. `/undo` desfà la teva darrera petició i els seus canvis de fitxers, i `/help` mostra les ordres i les tecles.
|
||||
Les ordres de només lectura s’executen directament. Per a qualsevol altra ordre
|
||||
de shell, Codewhale pregunta primer: `y` l’executa una vegada, `a` permet ordres
|
||||
com aquesta durant la resta de la conversa, `n` la rebutja. `/undo` desfà la
|
||||
teva última petició i els canvis que va fer als fitxers. `/help` llista les ordres i les tecles.
|
||||
|
||||
Per a scripts i CI, `codewhale exec` executa una tasca sense la interfície. Llegeix [execucions sense interfície](../INSTALL.md#headless-scripts-ci) abans de donar-li eines amb `--auto`, perquè `--auto` fa servir Full Access tret que hi afegeixis un sandbox.
|
||||
Per a scripts i CI, `codewhale exec` executa una sola tasca sense la interfície.
|
||||
Per defecte, `--auto` fa servir Full Access: les eines s’executen sense preguntar,
|
||||
les ordres de shell no tenen cap sandbox de Codewhale i el mode de confiança
|
||||
permet que les eines de fitxers escriguin fora de l’espai de treball. Afegeix
|
||||
`--sandbox workspace-write` per mantenir el sandbox de shell i les restriccions
|
||||
de les eines de fitxers a l’espai de treball, amb el mode de confiança desactivat:
|
||||
|
||||
```bash
|
||||
codewhale exec --auto --sandbox workspace-write "fix the failing tests and explain what changed"
|
||||
```
|
||||
|
||||
## Modes i permisos
|
||||
|
||||
Una instal·lació nova comença en mode Work amb el permís Accept Edits. Un sol selector canvia tots dos: `Shift+Tab` recorre Ask → Accept Edits → Plan → Auto-Review → Full Access.
|
||||
|
||||
| | Es canvia amb | Opcions |
|
||||
| --- | --- | --- |
|
||||
| Mode | la parada Plan de `Shift+Tab`, o `/mode` | **Plan** llegeix i cerca però no canvia res. **Work** (totes les altres parades) edita fitxers i executa ordres; inicia un objectiu amb `/goal` quan una petició necessiti més d’un torn. |
|
||||
| Permís | `Shift+Tab` | **Ask** pregunta abans de cada edició de fitxer i de cada ordre de shell que no és de només lectura. **Accept Edits** aplica les edicions de fitxers al teu repositori git sense preguntar i continua preguntant abans de les ordres. **Auto-Review** deixa que unes regles i un model revisor aprovin les crides rutinàries i rebutja les d’alt risc; canvia a Ask per aprovar-les tu mateix. **Full Access** no pregunta mai, i les ordres poden accedir a tot el que hi ha en aquest ordinador; les regles del repositori i la política gestionada continuen aplicant-se. |
|
||||
|
||||
Consulta la [guia de modes i permisos](../MODES.md) per als detalls.
|
||||
|
||||
## Què fa
|
||||
|
||||
Dona una tasca a Codewhale i llegirà el repositori, editarà fitxers i executarà ordres fins que la tasca estigui feta o necessiti la teva resposta. Les [notes de la darrera versió](https://github.com/codewhale-hq/Codewhale/releases/latest) recullen els canvis més nous, i el [registre de canvis](../../CHANGELOG.md) inclou cada versió.
|
||||
### Novetats de la 0.10.2
|
||||
|
||||
### Qualsevol model
|
||||
- `/pet on` converteix el teu terminal en una balena animada que mostra què fa
|
||||
l’agent, mentre el quadre de missatges continua actiu. Les seves 980
|
||||
partícules escriuen el pas actual en anglès, xinès o japonès.
|
||||
- `/workbar terminal` mostra els shells que executa l’agent mentre s’executen.
|
||||
Escriu en un d’ells, o prem `Ctrl+N` per obrir un shell propi (macOS i
|
||||
Linux).
|
||||
- `/undo` desfà tota la teva última petició d’un sol cop, inclosos tots els
|
||||
fitxers que va canviar, i `/diff` funciona en carpetes sense Git.
|
||||
- Quan un pla està llest, Codewhale et pregunta si vols començar
|
||||
Work amb Ask o Auto-Review, o continuar planificant.
|
||||
- `/color` dona a cada xat el seu propi color al quadre de missatges, desat amb
|
||||
la sessió.
|
||||
- `web_search` pot fer servir la nova Search API de DeepSeek: defineix
|
||||
`[search] provider = "deepseek"` amb una clau de DeepSeek.
|
||||
- Fora de Full Access, un `curl` o `wget` que pujaria una credencial no
|
||||
s’executa mai sense la teva aprovació, i es rebutja qualsevol ordre que amagui
|
||||
Unicode invisible. `write` ja no substitueix un fitxer que l’agent no ha
|
||||
llegit, ni un que ha canviat després que el llegís.
|
||||
|
||||
Porta més de 40 proveïdors integrats, entre ells Anthropic, OpenAI, DeepSeek, Google, Mistral, Moonshot, OpenRouter i xAI. Funciona qualsevol endpoint compatible amb OpenAI, i també els models locals amb Ollama, vLLM o SGLang. El model per defecte és `deepseek-flash` de DeepSeek, que ara serveix DeepSeek V4.1 Flash ([detalls](../PROVIDERS.md#deepseek-v41-flash)).
|
||||
### Models
|
||||
|
||||
`/model` canvia de model entre torns, i `/model auto` en tria un per a cada torn (`/router` en defineix la manera). `/turn inspect` mostra quin model ha fet servir un torn, quant ha costat i quant ha trigat. Consulta [Proveïdors](../PROVIDERS.md).
|
||||
Hi ha més de 40 proveïdors integrats, entre els quals Anthropic, OpenAI,
|
||||
DeepSeek, Google, Mistral, Moonshot, OpenRouter i xAI. Funciona qualsevol
|
||||
endpoint compatible amb OpenAI, i també els models locals mitjançant Ollama,
|
||||
vLLM o SGLang. El model per defecte és `deepseek-flash` de DeepSeek, que
|
||||
DeepSeek va passar a DeepSeek-V4.1-Flash-1017 el 2026-10-17; la 0.10.1
|
||||
l’executa sense cap actualització
|
||||
([detalls](../PROVIDERS.md#deepseek-v41-flash)).
|
||||
|
||||
`/model` canvia de model entre torns, i `/model auto` en tria un per a cada torn
|
||||
(`/router` configura com ho fa). `/turn inspect` mostra quin model ha fet servir
|
||||
un torn, quant ha costat i quant ha trigat. `codewhale auth chatgpt` et permet
|
||||
fer servir un pla de ChatGPT en lloc d’una clau API (experimental). Consulta
|
||||
[Proveïdors](../PROVIDERS.md).
|
||||
|
||||
### Rebuts i desfer
|
||||
|
||||
`/receipts`, o `codewhale receipts --last` després de sortir, llista els fitxers que ha canviat una sessió, les ordres que ha executat, les crides web i MCP, els agents que ha iniciat, cada aprovació i qui l’ha donada, i el que ha fallat. `/undo` desfà la teva darrera petició i els fitxers que ha canviat en un sol pas. `/restore` torna tot l’espai de treball a una instantània anterior i, com que això sobreescriu les edicions posteriors, necessita `/trust on` o Full Access. Consulta [Rebuts](../RECEIPTS.md).
|
||||
`/receipts`, o `codewhale receipts --last` després de sortir, llista els fitxers
|
||||
canviats, les ordres executades, les crides web i MCP, els agents iniciats, les
|
||||
aprovacions i qui les va donar, i els errors. `/undo` desfà la teva última petició i els fitxers que va canviar, d’un sol cop i sense el mode de confiança. `/restore` torna tot l’espai de treball a una
|
||||
instantània anterior; com que això sobreescriu les edicions posteriors,
|
||||
necessita `/trust on` o Full Access. Consulta [Rebuts](../RECEIPTS.md).
|
||||
|
||||
### Diversos agents en una tasca
|
||||
Les execucions d’exec que ofereixen eines es desen en tots els formats de
|
||||
sortida, de manera que `codewhale receipts --last` funciona després
|
||||
d’`exec --auto`, també amb `--json`. Les execucions que només donen una resposta
|
||||
en text o amb `--json` només es desen quan es reprèn una conversa o quan
|
||||
comença la compactació de la conversa; fes servir `--output-format stream-json`
|
||||
per desar-les sempre per als rebuts o per a `exec --continue`.
|
||||
|
||||
`/goal` fixa un objectiu que continua d’un torn a l’altre. Codewhale pot repartir la feina entre [subagents](../SUBAGENTS.md), fer anar una [Fleet](../FLEET.md) d’agents amb rols i models propis, o seguir un [flux de treball](../WORKFLOW_AUTHORING.md) desat al repositori.
|
||||
### Feines més llargues
|
||||
|
||||
`/goal` defineix un objectiu que es manté entre torns. Codewhale pot repartir la
|
||||
feina entre [subagents](../SUBAGENTS.md), executar una [flota](../FLEET.md)
|
||||
d’agents o seguir un [flux de treball](../WORKFLOW_AUTHORING.md) versionat al
|
||||
repositori.
|
||||
|
||||
### Si véns d’un altre agent
|
||||
|
||||
Codewhale llegeix `AGENTS.md` pel seu compte. Els fitxers d’instruccions escrits
|
||||
per a altres eines, com ara `CLAUDE.md`, `.cursorrules` o
|
||||
`.github/copilot-instructions.md`, només es carreguen si ho actives amb
|
||||
`project_instruction_imports = ["claude"]` (o `"cursor"`, `"cline"`,
|
||||
`"windsurf"`, `"copilot"`, `"all"`) a la configuració; consulta
|
||||
[config.example.toml](../../config.example.toml). Codewhale avisa quan en troba un que
|
||||
no ha carregat.
|
||||
|
||||
`/import-claude` prepara una importació de la teva configuració i dels servidors
|
||||
MCP de Claude Code perquè la revisis. `/plugin install` accepta plugins de
|
||||
Claude Code que continguin skills, ordres, perfils d’agent i servidors MCP, i
|
||||
rebutja els plugins que necessiten hooks o servidors LSP
|
||||
([detalls](../CLAUDE_PLUGIN_COMPAT.md)).
|
||||
|
||||
### Extensions
|
||||
|
||||
Connecta [servidors MCP](../MCP.md), afegeix [skills](../SKILLS.md) i [connectors](../PLUGINS.md), i executa [hooks](../HOOKS.md) en esdeveniments de sessió i d’eines. El mode codi (experimental, activat per defecte) permet que el model escrigui un programa `execute_tools` que crida diverses eines MCP i de connectors; cada crida continua respectant les teves regles d’aprovació i en conserva el rebut. El connector Computer Use inclòs a v0.10.2 (experimental, només macOS) pot observar i fer servir altres aplicacions, i resta apagat fins que en revises l’accés i l’actives ([guia](https://github.com/codewhale-hq/Codewhale/blob/v0.10.2/crates/tui/plugins/computer-use/README.md)). Arriba amb 0.10.3: el connector es publica [per separat](https://github.com/codewhale-hq/codewhale-cu-plugin).
|
||||
Connecta [servidors MCP](../MCP.md), afegeix [skills](../SKILLS.md) i
|
||||
[plugins](../PLUGINS.md), i executa [hooks](../HOOKS.md) en esdeveniments de
|
||||
sessió i d’eines. Code mode (experimental, activat per defecte) permet que el
|
||||
model escrigui un sol programa `execute_tools` que crida diverses eines de MCP i
|
||||
de plugins; cada crida continua subjecta a les teves regles d’aprovació i
|
||||
conserva el seu rebut. El plugin Computer Use instal·lat independentment (experimental, només
|
||||
macOS) pot observar i operar altres aplicacions. Roman desactivat fins que en revises
|
||||
l’accés i l’actives ([guia](https://github.com/codewhale-hq/codewhale-cu-plugin)).
|
||||
|
||||
### Maneres d’executar-lo
|
||||
## Maneres d’executar-lo
|
||||
|
||||
| Ordre | Què fa |
|
||||
| --- | --- |
|
||||
| `codewhale` | L’aplicació interactiva de terminal |
|
||||
| `codewhale exec "…"` | Una tasca sense la interfície, per a scripts i CI ([execucions sense interfície](../INSTALL.md#headless-scripts-ci)) |
|
||||
| `codewhale web` | Un client web local a `127.0.0.1` ([guia](../WEB.md)) |
|
||||
| `codewhale review --pr N` | Una revisió orientativa d’una pull request de GitHub; la [configuració del bot de GitHub](../GITHUB_ACTION.md#set-up-your-own-bot) l’executa amb la teva pròpia clau de model |
|
||||
| `codewhale exec "…"` | Una tasca sense la interfície. Per si sol respon sense fer servir eines; `--auto` activa les eines amb Full Access per defecte, `--sandbox workspace-write` les restringeix, i `--output-format stream-json` emet els esdeveniments en streaming com a JSON |
|
||||
| `codewhale web` | Una interfície web local a `127.0.0.1` ([guia](../WEB.md)) |
|
||||
| `codewhale review --pr N` | Una revisió orientativa d’un pull request de GitHub. La [configuració del bot de GitHub](../GITHUB_ACTION.md#set-up-your-own-bot) genera un workflow amb la versió fixada per a revisions automàtiques i `@codewhale review`, amb la teva pròpia clau de model i una identitat d’App opcional |
|
||||
| `codewhale app-server --http` | La [Runtime API](../RUNTIME_API.md), una API HTTP local per a fils, esdeveniments, aprovacions i els canvis de fitxers de cada torn |
|
||||
| `codewhale serve --acp` | L’Agent Client Protocol sobre stdio, per a editors com Zed |
|
||||
| `codewhale serve --acp` | L’Agent Client Protocol sobre stdio, per a editors com ara Zed |
|
||||
|
||||
L’aplicació de terminal, `exec`, `web` i la Runtime API comparteixen un sol motor ([arquitectura](../ARCHITECTURE.md)). Una [extensió per a VS Code](https://marketplace.visualstudio.com/items?itemName=HengQuWorld.brotherwhale-vscode) mantinguda per la comunitat s’hi connecta des de la barra lateral ([codi font](https://github.com/HengQuWorld/CodeWhale-VSCode)).
|
||||
L’aplicació de terminal, `exec`, `web` i la Runtime API comparteixen un mateix
|
||||
motor ([arquitectura](../ARCHITECTURE.md)). Una
|
||||
[extensió de VS Code](https://marketplace.visualstudio.com/items?itemName=HengQuWorld.brotherwhale-vscode)
|
||||
mantinguda per la comunitat s’hi connecta des de la barra lateral
|
||||
([codi font](https://github.com/HengQuWorld/CodeWhale-VSCode)).
|
||||
Hi ha en desenvolupament una aplicació d’escriptori nativa basada en la Runtime
|
||||
API; encara no té cap descàrrega pública.
|
||||
|
||||
## Com es manté segur
|
||||
## Cost i comptes
|
||||
|
||||
Dos interruptors diuen quant pot fer Codewhale pel seu compte. Amb l’entrada buida, `Tab` canvia el mode: **Plan** llegeix i cerca però no canvia res, **Work** edita fitxers i executa ordres, i **Operate** té el mateix accés que Work i continua fins que l’objectiu queda verificat o et necessita. `Shift+Tab` canvia el permís. **Ask**, el predeterminat, aplica els canvis de fitxers del teu repositori sense preguntar i pregunta abans de les ordres de shell que no són només de lectura. **Auto-Review** deixa que regles i un model revisor aprovin les crides rutinàries i rebutja les d’alt risc; canvia a Ask per aprovar-les tu. **Full Access** no pregunta mai i desactiva el sandbox de Codewhale, tot i que les regles del repositori i la política gestionada continuen vigents. La [guia de v0.10.2](https://github.com/codewhale-hq/Codewhale/blob/v0.10.2/docs/MODES.md) té els detalls.
|
||||
Codewhale és gratuït i té llicència MIT. Amb la teva pròpia clau API pagues
|
||||
directament al proveïdor; els models locals no necessiten clau. La barra
|
||||
inferior i `/cost` mostren quant ha costat una sessió quan se’n coneix el preu,
|
||||
i un preu desconegut es mostra com a desconegut, mai com a gratuït.
|
||||
|
||||
Arriba amb 0.10.3: `Shift+Tab` esdevé un únic selector que passa per Ask, Accept Edits, Plan, Auto-Review i Full Access, `Tab` deixa de canviar el mode i Operate es retira. Ask preguntarà també abans de cada canvi de fitxer, i el nou predeterminat, Accept Edits, aplicarà els canvis dins del repositori git i continuarà preguntant abans de les ordres. La [guia actual](../MODES.md) ho descriu.
|
||||
No necessites cap compte. Les claus que afegeixes amb `/provider` es queden a la
|
||||
teva màquina.
|
||||
|
||||
A Ask i Auto-Review, les ordres s’executen en un sandbox del sistema operatiu quan el sistema en té un: Seatbelt a macOS, i bubblewrap a Linux quan està instal·lat. Windows encara no té sandbox del sistema, i Full Access apaga el sandbox. Dins del sandbox, les ordres de shell i les sessions de terminal no poden llegir magatzems de credencials com `~/.ssh`, `~/.aws` i els fitxers `.env`. `/preview-request` mostra una previsualització censurada de la propera petició sense enviar-la.
|
||||
## Seguretat i privadesa
|
||||
|
||||
No necessites cap compte, i les claus que afegeixes amb `/provider` es queden a la teva màquina. Amb la teva pròpia clau d’API pagues directament el proveïdor, i els models locals no necessiten clau. El peu de pàgina i `/cost` mostren quant ha costat una sessió quan se’n coneix el preu, i un preu desconegut es mostra com a desconegut, mai com a gratuït.
|
||||
En Ask i Auto-Review, les ordres s’executen dins d’un sandbox del sistema
|
||||
operatiu allà on el sistema en té: Seatbelt a macOS, i bubblewrap a Linux quan
|
||||
està instal·lat. Windows encara no té sandbox del sistema operatiu, i Full
|
||||
Access desactiva el sandbox. Dins del sandbox, les ordres de shell no poden
|
||||
llegir magatzems de credencials com ara `~/.ssh`, `~/.aws` i els fitxers `.env`;
|
||||
a partir de la 0.10.2, la mateixa regla s’aplica a les sessions de terminal i a
|
||||
més eines de fitxers. `/preview-request` mostra una previsualització de la
|
||||
propera petició, amb les dades sensibles ocultades, sense enviar-la.
|
||||
|
||||
La telemetria està activada per defecte. Envia recomptes agregats (versió i plataforma, durada i resultat de la sessió, quins proveïdors integrats has fet servir, recomptes de funcions i d’errors) amb un identificador d’instal·lació aleatori que canvia cada 90 dies, i mai converses, codi, prompts, noms de models, noms de fitxers o repositoris, ni credencials. Aquesta ordre la desactiva:
|
||||
|
||||
```bash
|
||||
codewhale config set telemetry false
|
||||
```
|
||||
|
||||
En arrencar, Codewhale també comprova a GitHub si hi ha una versió nova; defineix `CODEWHALE_NO_UPDATE_CHECK=1` per ometre-ho. Més informació a [ordre d’autorització](../AUTHORIZATION_ORDER.md), [sandbox](../SANDBOX.md) i [telemetria](../TELEMETRY.md).
|
||||
|
||||
## Cap on va
|
||||
|
||||
Codewhale s’està convertint en un únic entorn que abasta el teu terminal, una aplicació d’escriptori, el navegador i ordinadors al núvol, tot mogut per un sol motor. Cada part de sota diu on es troba avui.
|
||||
|
||||
| Part | Estat | Avui |
|
||||
| --- | --- | --- |
|
||||
| Aplicació de terminal | Publicat | Instal·la-la a macOS, Linux o Windows i executa `codewhale` a qualsevol repositori. |
|
||||
| Client de navegador local | Publicat | Executa `codewhale web` per llegir la conversa, enviar una tasca i respondre a les aprovacions al navegador; ve amb el terminal i no necessita cap compte. |
|
||||
| Molts agents en una tasca | Publicat | Els subagents, les Fleets i els fluxos de treball permeten que diversos agents comparteixin una tasca, cadascun amb el seu rol. |
|
||||
| Aplicació d’escriptori | En desenvolupament, sense descàrrega | Estem construint una aplicació d’escriptori sobre el mateix motor, i encara no té cap descàrrega pública. |
|
||||
| Ordinadors al núvol | Només per invitació | Els ordinadors al núvol són només per invitació, i els estem construint perquè una sessió pugui continuar treballant en una màquina al núvol. |
|
||||
| Una sessió a través de tots | En construcció | Estem construint una sessió que t’acompanya del terminal a l’escriptori, al navegador i al núvol, i continua sent teva tot el camí. |
|
||||
|
||||
El [full de ruta](../ROADMAP.md) diu què cobreix cada versió i on pots ajudar.
|
||||
|
||||
## Si vens d’un altre agent
|
||||
|
||||
Codewhale llegeix `AGENTS.md` pel seu compte. Els fitxers d’instruccions escrits per a altres eines, com ara `CLAUDE.md`, `.cursorrules` o `.github/copilot-instructions.md`, només es carreguen si els actives amb `project_instruction_imports = ["claude"]` (o `"cursor"`, `"cline"`, `"windsurf"`, `"copilot"`, `"all"`) a la configuració; consulta [config.example.toml](../../config.example.toml). Codewhale t’avisa quan en troba un que no ha carregat.
|
||||
|
||||
`/import-claude` prepara una importació de la teva configuració de Claude Code i dels servidors MCP perquè la revisis. `/plugin install` accepta connectors de Claude Code que contenen skills, ordres, perfils d’agent i servidors MCP, i rebutja els que necessiten hooks o servidors LSP ([detalls](../CLAUDE_PLUGIN_COMPAT.md)).
|
||||
La telemetria està activada per defecte. Envia recomptes agregats (versió i
|
||||
plataforma, durada i resultat de la sessió, quins proveïdors integrats has fet
|
||||
servir, recomptes de funcionalitats i d’errors) amb un identificador
|
||||
d’instal·lació aleatori que canvia cada 90 dies, i mai converses, codi, prompts,
|
||||
noms de models, noms de fitxers o de repositoris, ni credencials. `codewhale config set telemetry false` la desactiva. En iniciar-se,
|
||||
Codewhale també comprova a GitHub si hi ha una versió nova; defineix
|
||||
`CODEWHALE_NO_UPDATE_CHECK=1` per ometre-ho. Més informació a
|
||||
[ordre d’autorització](../AUTHORIZATION_ORDER.md),
|
||||
[sandboxing](../SANDBOX.md) i [telemetria](../TELEMETRY.md).
|
||||
|
||||
## Documentació
|
||||
|
||||
Aquestes guies segueixen la branca principal i poden descriure canvis que encara no són a l’última versió.
|
||||
|
||||
| Comença aquí | Referència |
|
||||
| --- | --- |
|
||||
| [Guia d’usuari](../GUIDE.md) | [Configuració](../CONFIGURATION.md) |
|
||||
| [Instal·lació](../INSTALL.md) | [Arquitectura](../ARCHITECTURE.md) |
|
||||
| [Proveïdors i models locals](../PROVIDERS.md) | [Runtime API](../RUNTIME_API.md) |
|
||||
| [Modes i permisos](../MODES.md) | [Escriure connectors](../PLUGIN_AUTHORING.md) |
|
||||
| [Dreceres de teclat](../KEYBINDINGS.md) | [Rebuts](../RECEIPTS.md) |
|
||||
| [Revisió de pull requests a GitHub](../GITHUB_ACTION.md) | [Tota la documentació](../README.md) |
|
||||
| [Instal·lació](../INSTALL.md) | [Configuració](../CONFIGURATION.md) |
|
||||
| [Proveïdors i models locals](../PROVIDERS.md) | [Arquitectura](../ARCHITECTURE.md) |
|
||||
| [Modes i permisos](../MODES.md) | [Runtime API](../RUNTIME_API.md) |
|
||||
| [Dreceres de teclat](../KEYBINDINGS.md) | [Creació de plugins](../PLUGIN_AUTHORING.md) |
|
||||
| [Revisió de PR a GitHub](../GITHUB_ACTION.md) | [Tota la documentació](../README.md) |
|
||||
|
||||
## Contribuir
|
||||
## Comunitat
|
||||
|
||||
Codewhale es construeix en obert per les persones que l’utilitzen, en moltes llengües.
|
||||
Codewhale és el que és gràcies a la seva comunitat, i tots els comentaris són benvinguts: un error, una idea o alguna cosa que s’ha vist o s’ha sentit estranya.
|
||||
[Obre una issue](https://github.com/codewhale-hq/CodeWhale/issues/new/choose) per a qualsevol d’aquests casos; una issue també és el millor lloc per començar una funció nova.
|
||||
Les pull requests són benvingudes per a correccions, traduccions, proveïdors i feina ja planificada.
|
||||
La [guia de contribució](../../CONTRIBUTING.md) indica on va cada tipus d’ajuda, i el [full de ruta](../ROADMAP.md) indica cap on va el projecte.
|
||||
Qui contribueix conserva el crèdit per la feina integrada, i l’[estructura del repositori](../../CONTRIBUTING.md#project-structure) mostra on és cada cosa.
|
||||
|
||||
<a href="../CONTRIBUTORS.md#everyone"><img src="../assets/contributors.png" width="760" alt="El mur de contribuïdors de Codewhale: un avatar per cada persona el codi, els commits en coautoria, les pull requests represes o els informes acreditats de la qual són a Codewhale, amb el total a dalt."></a>
|
||||
|
||||
El [mur de contribuïdors](../CONTRIBUTORS.md) compta totes les persones el codi, els commits en coautoria, les pull requests represes o els informes acreditats de les quals són a Codewhale, incloses les que la llista de contribuïdors de GitHub no mostra. `scripts/contributor-wall.py` torna a generar la imatge i la llista a partir de l’historial de git i de GitHub.
|
||||
|
||||
Tota opinió és benvinguda: un error, una idea o alguna cosa que semblava o feia l’efecte d’estar malament. [Obre una issue](https://github.com/codewhale-hq/Codewhale/issues/new/choose) per a qualsevol d’aquestes coses; una issue també és el millor lloc per començar una funció nova. Les pull requests són benvingudes per a correccions, traduccions, proveïdors i feina planificada. Les preguntes van a [Discussions](https://github.com/codewhale-hq/Codewhale/discussions). La [guia de contribució](../../CONTRIBUTING.md) mostra on va cada tipus d’ajuda, com entra la teva feina amb el teu nom i [on és cada cosa](../../CONTRIBUTING.md#project-structure).
|
||||
|
||||
Xerra a [Discord](https://discord.gg/37gfS3ksug), o afegeix el mantenidor, Hunter, a WeChat (`hunterbown`) i demana entrar al grup Whale Brothers. Tothom hi és benvingut.
|
||||
Xateja al [Discord](https://discord.gg/37gfS3ksug), o afegeix el mantenidor,
|
||||
Hunter, a WeChat (`hunterbown`) i demana entrar al grup Whale Brothers. Tothom hi és benvingut.
|
||||
|
||||
## Història i llicència
|
||||
|
||||
Codewhale va començar com a `deepseek-tui` i encara llegeix la configuració i les sessions d’aquell projecte. Es manté de manera independent i no està afiliat a DeepSeek ni a cap altre proveïdor de models.
|
||||
Codewhale va començar com a `deepseek-tui` i encara llegeix la configuració i
|
||||
les sessions d’aquell projecte. Es manté de manera independent i no està afiliat
|
||||
a DeepSeek ni a cap altre proveïdor de models. Gràcies a
|
||||
[tothom qui hi ha contribuït](../CONTRIBUTORS.md).
|
||||
|
||||
[MIT](../../LICENSE). El codi adaptat d’altres projectes de codi obert apareix als avisos de tercers per a les [fonts incloses](../../THIRD_PARTY_NOTICES.md) i els [dissenys adaptats](../THIRD_PARTY_NOTICES.md).
|
||||
[](../CONTRIBUTORS.md#everyone)
|
||||
|
||||
[MIT](../../LICENSE). El codi adaptat d’altres projectes de codi obert es recull als
|
||||
[avisos de tercers](../THIRD_PARTY_NOTICES.md).
|
||||
+228
-95
@@ -1,23 +1,23 @@
|
||||
<!-- source: README.md sha256:fd76d718e3a2 -->
|
||||
<!-- source: README.md sha256:a9ec5fb3f3dd -->
|
||||
<div align="center">
|
||||
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="../../brand/mark-reversed.svg">
|
||||
<img src="../../brand/mark-mono.svg" alt="Codewhale" width="72" height="72">
|
||||
<source media="(prefers-color-scheme: dark)" srcset="../../brand/wordmark-inverted.svg">
|
||||
<img src="../../brand/wordmark.svg" alt="Codewhale" width="320">
|
||||
</picture>
|
||||
|
||||
<h1>Das Modell kann jedem gehören. Der Agent sollte dir gehören.</h1>
|
||||
**Ein Open-Source-Coding-Agent für dein Terminal, der mit jedem Modell arbeitet.**
|
||||
|
||||
Codewhale ist ein quelloffener Coding-Agent für dein Terminal, geschrieben in Rust. Er arbeitet mit jedem Modell, kann eine Aufgabe auf mehrere Agenten verteilen, und du kannst jede Zeile davon lesen.
|
||||
Gib Codewhale eine Aufgabe, und es liest das Repository, bearbeitet Dateien und
|
||||
führt Befehle aus, bis die Aufgabe erledigt ist oder es eine Eingabe von dir
|
||||
braucht. Es arbeitet mit mehr als 40 eingebauten Anbietern, standardmäßig mit
|
||||
DeepSeek V4.1 Flash, und mit Modellen, die auf deinem eigenen Rechner laufen.
|
||||
`/receipts` listet die Dateien auf, die es geändert hat, die Befehle, die es
|
||||
ausgeführt hat, und die Web- und MCP-Aufrufe, die es gemacht hat.
|
||||
|
||||
```bash
|
||||
curl -fsSL https://codewhale.net/install.sh | sh
|
||||
```
|
||||
|
||||
Das installiert die neueste Version unter macOS oder Linux und braucht kein Konto. [Windows, npm und Cargo](#installation) stehen weiter unten.
|
||||
|
||||
[](https://github.com/codewhale-hq/Codewhale/releases/latest)
|
||||
[](https://github.com/codewhale-hq/Codewhale/actions/workflows/ci.yml)
|
||||
[](https://github.com/codewhale-hq/CodeWhale/actions/workflows/ci.yml)
|
||||
[](https://crates.io/crates/codewhale-cli)
|
||||
[](https://www.npmjs.com/package/codewhale)
|
||||
[](../../LICENSE)
|
||||
[](https://discord.gg/37gfS3ksug)
|
||||
|
||||
@@ -25,141 +25,274 @@ Das installiert die neueste Version unter macOS oder Linux und braucht kein Kont
|
||||
|
||||
[English](../../README.md) · [简体中文](README.zh-CN.md) · [日本語](README.ja-JP.md) · [Tiếng Việt](README.vi.md) · [Bahasa Indonesia](README.id.md) · [한국어](README.ko-KR.md) · [Español](README.es-419.md) · [Português](README.pt-BR.md) · [Русский](README.ru.md) · [Українська](README.uk.md) · [Français](README.fr.md) · [繁體中文](README.zh-TW.md) · [हिन्दी](README.hi.md) · [Türkçe](README.tr.md) · [Italiano](README.it.md) · [Polski](README.pl.md) · [العربية](README.ar.md) · [Català](README.ca.md)
|
||||
|
||||
<sub>Diese Übersetzung ist ein maschineller Entwurf und wurde noch nicht von Muttersprachlern geprüft.</sub>
|
||||
<img src="../../web/public/codewhale-tui-0102-demo.png" alt="Eine Codewhale-Terminalsitzung" width="760">
|
||||
|
||||
<img src="../../web/public/codewhale-tui-0102-demo.png" width="760" alt="Codewhale in einem Terminal nach einer Demo-Aufgabe. Es meldet, dass es export.py ein --json-Flag hinzugefügt hat, dass alle 3 lokalen Tests bestanden sind und dass ein delegiertes Review keine Probleme gefunden hat. Die Aufgabenliste darunter zeigt drei erledigte Schritte, und die Statuszeile zeigt Full Access und den lokalen Anbieter website-demo.">
|
||||
|
||||
<sub>Ein Entwicklungs-Build von Version 0.10.3, die noch nicht veröffentlicht ist, mit einem geskripteten Testmodell, in Full Access in einem Wegwerf-Arbeitsbereich. Die Änderung, drei Tests und ein Review durch einen Sub-Agenten liefen wirklich.</sub>
|
||||
<sub>Ein Entwicklungs-Build von 0.10.2 mit einem geskripteten Testmodell, in Full Access in einem Wegwerf-Arbeitsbereich. Die Änderung, drei Tests und ein Review durch einen Sub-Agenten liefen wirklich.</sub>
|
||||
|
||||
</div>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://www.orcarouter.ai/redeem/1a6250545fdc4e0aaccb0a4470159e67" rel="sponsored">
|
||||
<img src="../../web/public/codewhale-x-orcarouter.webp" alt="Codewhale × OrcaRouter — 30 % mehr Nutzung von DeepSeek v4.1 Flash" width="760">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
[Angebot von Codewhale × OrcaRouter ansehen](https://www.orcarouter.ai/redeem/1a6250545fdc4e0aaccb0a4470159e67). Codewhale erhält eine Empfehlungsprovision von OrcaRouter.
|
||||
|
||||
## Installation
|
||||
|
||||
Der Befehl oben legt das per Prüfsumme verifizierte `codewhale` und seinen Kurznamen `codew` in `~/.local/bin` ab. Wenn deine Shell danach `command not found` meldet, folge dem PATH-Hinweis, den der Installer ausgibt ([Details](../INSTALL.md#put-it-on-your-path)). `codewhale update` aktualisiert es.
|
||||
macOS und Linux:
|
||||
|
||||
```bash
|
||||
winget install --id HunterBown.CodeWhale --exact --source winget # Windows x64
|
||||
npm install -g codewhale # wraps the same release binaries
|
||||
cargo install codewhale-cli --locked # builds from source; can trail GitHub Releases
|
||||
curl -fsSL https://codewhale.net/install.sh | sh
|
||||
```
|
||||
|
||||
Vorkompilierte Binärdateien gibt es für Linux, macOS und Windows auf x64 und arm64. Scoop, Nix, Homebrew unter Linux, Android und Termux (Vorschau) und manuelle Downloads stehen in der [Installationsanleitung](../INSTALL.md), Container unter [Docker](../DOCKER.md). Nutze pro Rechner nur eine Installationsart; sonst landen zwei Kopien auf deinem `PATH`, und die zuerst gefundene gewinnt.
|
||||
Das Installationsskript legt `codewhale` und den kurzen Alias `codew` nach
|
||||
Prüfsummenkontrolle in `~/.local/bin` ab. Meldet deine Shell danach
|
||||
`command not found`, führe die PATH-Zeile aus, die das Skript ausgibt
|
||||
([Details](../INSTALL.md#put-it-on-your-path)).
|
||||
`codewhale update` aktualisiert es. Diese Befehle installieren das neueste
|
||||
Release. Diese Seite folgt `main`; eine mit einer Version markierte Funktion
|
||||
kommt also mit diesem Release, und der [Changelog](../../CHANGELOG.md) listet, was
|
||||
jedes Release hinzufügt.
|
||||
|
||||
<details>
|
||||
<summary><b>Windows, npm, Cargo und weitere Installationswege</b></summary>
|
||||
|
||||
```bash
|
||||
winget install HunterBown.CodeWhale # Windows x64 (or Scoop, or the installer from GitHub Releases)
|
||||
npm install -g codewhale # wraps the same release binaries
|
||||
cargo install codewhale-cli --locked # crates.io; can trail GitHub Releases
|
||||
```
|
||||
|
||||
Vorkompilierte Binärdateien gibt es für Linux, macOS und Windows auf x64 und
|
||||
arm64. Nix, Homebrew unter Linux, Android/Termux (Vorschau) und manuelle
|
||||
Downloads beschreibt die [Installationsanleitung](../INSTALL.md), Container
|
||||
die Seite [Docker](../DOCKER.md). Bleib pro Rechner bei einer
|
||||
Installationsmethode; sonst landen zwei Kopien im `PATH`, und es gewinnt die,
|
||||
die zuerst kommt.
|
||||
|
||||
</details>
|
||||
|
||||
## Schnellstart
|
||||
|
||||
1. Committe oder stashe deine Arbeit und starte dann `codewhale` im Wurzelverzeichnis eines Git-Repositorys. Standardmäßig übernimmt es Dateiänderungen im Repository ohne Rückfrage und fragt vor jedem Shell-Befehl, der nicht nur liest.
|
||||
2. Führe `/provider` aus (oder drücke `F3`), wähle einen Anbieter, füge seinen API-Schlüssel ein und wähle ein Modell. DeepSeek ist der Standardanbieter. Beim ersten Start ohne Schlüssel fragt Codewhale nach einem Anbieter, sobald du deine erste Nachricht sendest, und bietet ein lokales Modell an, wenn ein lokaler Ollama-Server antwortet.
|
||||
1. Committe oder stashe deine Arbeit und starte dann `codewhale` im
|
||||
Wurzelverzeichnis eines Git-Repositorys. Standardmäßig (**Accept Edits**) ändert es Dateien im Repository ohne Nachfrage
|
||||
und fragt vor jedem Befehl; `Shift+Tab` schaltet die Berechtigung schrittweise weiter, wenn es mehr oder weniger fragen soll.
|
||||
2. Führe `/provider` aus (oder drücke `F3`), wähle einen Anbieter, füge seinen
|
||||
API-Schlüssel ein und wähle ein Modell. DeepSeek ist der Standardanbieter.
|
||||
Ohne Schlüssel wählst du stattdessen eine lokale Runtime. Ist kein Anbieter
|
||||
konfiguriert und stellt Ollama bereits ein Chat-Modell bereit, verwendet
|
||||
Codewhale es automatisch; ab 0.10.2 bietet es dir das Modell an, sobald du
|
||||
deine erste Nachricht sendest.
|
||||
3. Gib eine Aufgabe ein:
|
||||
|
||||
```text
|
||||
Fix the failing tests and explain what changed.
|
||||
```
|
||||
|
||||
Befehle, die nur lesen, laufen sofort. Bei jedem anderen Shell-Befehl fragt Codewhale zuerst: `y` führt ihn einmal aus, `a` erlaubt ähnliche Befehle für den Rest des Gesprächs, und `n` lehnt ab. `/undo` nimmt deine letzte Anfrage samt ihrer Dateiänderungen zurück, und `/help` listet Befehle und Tasten auf.
|
||||
Befehle, die nur lesen, laufen sofort. Bei jedem anderen Shell-Befehl fragt
|
||||
Codewhale vorher: `y` führt ihn einmal aus, `a` erlaubt ähnliche Befehle für den
|
||||
Rest der Unterhaltung, `n` lehnt ab. `/undo` macht deine letzte Anfrage samt
|
||||
ihren Dateiänderungen rückgängig.
|
||||
`/help` listet Befehle und Tasten auf.
|
||||
|
||||
Für Skripte und CI führt `codewhale exec` eine Aufgabe ohne Oberfläche aus. Lies [Läufe ohne Oberfläche](../INSTALL.md#headless-scripts-ci), bevor du ihm mit `--auto` Werkzeuge gibst, denn `--auto` nutzt Full Access, solange du keine Sandbox angibst.
|
||||
Für Skripte und CI führt `codewhale exec` eine Aufgabe ohne Oberfläche aus.
|
||||
Standardmäßig nutzt `--auto` Full Access: Tools laufen ohne Nachfrage,
|
||||
Shell-Befehle haben keine Codewhale-Sandbox, und der Vertrauensmodus erlaubt
|
||||
Datei-Tools, außerhalb des Arbeitsbereichs zu schreiben. Füge
|
||||
`--sandbox workspace-write` hinzu, um die Shell-Sandbox und die Beschränkung
|
||||
der Datei-Tools auf den Arbeitsbereich beizubehalten, bei ausgeschaltetem
|
||||
Vertrauensmodus:
|
||||
|
||||
## Was es tut
|
||||
```bash
|
||||
codewhale exec --auto --sandbox workspace-write "fix the failing tests and explain what changed"
|
||||
```
|
||||
|
||||
Gib Codewhale eine Aufgabe, und es liest das Repository, bearbeitet Dateien und führt Befehle aus, bis die Aufgabe erledigt ist oder es deine Eingabe braucht. Die [Hinweise zur neuesten Version](https://github.com/codewhale-hq/Codewhale/releases/latest) listen die neuesten Änderungen, und das [Änderungsprotokoll](../../CHANGELOG.md) enthält jede Version.
|
||||
## Modi und Berechtigungen
|
||||
|
||||
### Jedes Modell
|
||||
Eine neue Installation startet im Work-Modus mit der Berechtigung Accept Edits. Ein einziger Regler schaltet beides um: `Shift+Tab` schaltet schrittweise durch Ask → Accept Edits → Plan → Auto-Review → Full Access.
|
||||
|
||||
Mehr als 40 Anbieter sind eingebaut, darunter Anthropic, OpenAI, DeepSeek, Google, Mistral, Moonshot, OpenRouter und xAI. Jeder OpenAI-kompatible Endpunkt funktioniert, ebenso lokale Modelle über Ollama, vLLM oder SGLang. Das Standardmodell ist DeepSeeks `deepseek-flash`, hinter dem jetzt DeepSeek V4.1 Flash steht ([Details](../PROVIDERS.md#deepseek-v41-flash)).
|
||||
| | Wechseln mit | Optionen |
|
||||
| --- | --- | --- |
|
||||
| Modus | die Plan-Stufe auf `Shift+Tab` oder `/mode` | **Plan** liest und durchsucht, ändert aber nichts. **Work** (jede andere Stufe) bearbeitet Dateien und führt Befehle aus; starte ein Ziel mit `/goal`, wenn eine Anfrage mehr als einen Turn braucht. |
|
||||
| Berechtigung | `Shift+Tab` | **Ask** fragt vor jeder Dateiänderung und vor jedem Shell-Befehl, der nicht nur liest. **Accept Edits** wendet Dateiänderungen in deinem Git-Repository an, ohne zu fragen, und fragt weiterhin vor Befehlen. **Auto-Review** lässt Regeln und ein Prüfmodell Routineaufrufe freigeben und lehnt Aufrufe mit hohem Risiko ab; wechsle zu Ask, um diese selbst freizugeben. **Full Access** fragt nie, und Befehle können auf alles auf diesem Computer zugreifen; Repository-Regeln und verwaltete Richtlinien gelten weiterhin. |
|
||||
|
||||
`/model` wechselt das Modell zwischen Runden, und `/model auto` wählt für jede Runde eines (`/router` legt fest, wie). `/turn inspect` zeigt, welches Modell eine Runde genutzt hat, was sie gekostet hat und wie lange sie dauerte. Siehe [Anbieter](../PROVIDERS.md).
|
||||
Details stehen in der [Anleitung zu Modi und Berechtigungen](../MODES.md).
|
||||
|
||||
### Belege und Rückgängig
|
||||
## Was es kann
|
||||
|
||||
`/receipts`, oder `codewhale receipts --last` nach dem Beenden, listet die Dateien, die eine Sitzung geändert hat, die ausgeführten Befehle, ihre Web- und MCP-Aufrufe, die gestarteten Agenten, jede Freigabe und wer sie erteilt hat, und was fehlgeschlagen ist. `/undo` nimmt deine letzte Anfrage und die von ihr geänderten Dateien in einem Schritt zurück. `/restore` setzt den ganzen Arbeitsbereich auf einen früheren Snapshot zurück; weil das spätere Änderungen überschreibt, braucht es `/trust on` oder Full Access. Siehe [Belege](../RECEIPTS.md).
|
||||
### Neu in 0.10.2
|
||||
|
||||
### Mehrere Agenten an einer Aufgabe
|
||||
- `/pet on` verwandelt dein Terminal in einen animierten Wal, der zeigt, was der
|
||||
Agent gerade tut, während das Nachrichtenfeld weiter benutzbar bleibt. Seine
|
||||
980 Partikel schreiben den aktuellen Schritt auf Englisch, Chinesisch oder
|
||||
Japanisch.
|
||||
- `/workbar terminal` zeigt die Shells, die der Agent ausführt, während sie
|
||||
laufen. Tippe in eine davon, oder drücke `Ctrl+N` für eine eigene Shell
|
||||
(macOS und Linux).
|
||||
- `/undo` macht deine gesamte letzte Anfrage in einem Schritt rückgängig,
|
||||
einschließlich jeder Datei, die sie geändert hat, und `/diff` funktioniert in
|
||||
Ordnern ohne Git.
|
||||
- Wenn ein Plan bereit ist, fragt Codewhale, ob es Work mit Ask oder
|
||||
Auto-Review starten oder weiterplanen soll.
|
||||
- `/color` gibt jedem Chat eine eigene Farbe für das Nachrichtenfeld, die mit
|
||||
der Sitzung gespeichert wird.
|
||||
- `web_search` kann die neue Search API von DeepSeek verwenden: setze
|
||||
`[search] provider = "deepseek"` mit einem DeepSeek-Schlüssel.
|
||||
- Außerhalb von Full Access läuft ein `curl` oder `wget`, das Zugangsdaten
|
||||
hochladen würde, nie ohne deine Freigabe, und ein Befehl, der unsichtbares
|
||||
Unicode versteckt, wird abgelehnt. `write` ersetzt keine Datei mehr, die der
|
||||
Agent nicht gelesen hat oder die sich geändert hat, nachdem er sie gelesen
|
||||
hat.
|
||||
|
||||
`/goal` setzt ein Ziel, das über mehrere Runden bestehen bleibt. Codewhale kann Arbeit auf [Sub-Agenten](../SUBAGENTS.md) verteilen, eine [Fleet](../FLEET.md) von Agenten mit eigenen Rollen und Modellen betreiben oder einem im Repository eingecheckten [Workflow](../WORKFLOW_AUTHORING.md) folgen.
|
||||
### Modelle
|
||||
|
||||
Mehr als 40 Anbieter sind eingebaut, darunter Anthropic, OpenAI, DeepSeek,
|
||||
Google, Mistral, Moonshot, OpenRouter und xAI. Jeder OpenAI-kompatible Endpunkt
|
||||
funktioniert, ebenso lokale Modelle über Ollama, vLLM oder SGLang. Standard ist
|
||||
`deepseek-flash` von DeepSeek, das DeepSeek am 2026-10-17 auf
|
||||
DeepSeek-V4.1-Flash-1017 umgestellt hat; 0.10.1 nutzt es ohne Update
|
||||
([Details](../PROVIDERS.md#deepseek-v41-flash)).
|
||||
|
||||
`/model` wechselt das Modell zwischen den Turns, und `/model auto` wählt für
|
||||
jeden Turn eines aus (`/router` legt fest, wie). `/turn inspect` zeigt, welches
|
||||
Modell ein Turn verwendet hat, was er gekostet hat und wie lange er gedauert
|
||||
hat. Mit `codewhale auth chatgpt` kannst du statt eines API-Schlüssels ein
|
||||
ChatGPT-Abo verwenden (experimentell). Siehe [Anbieter](../PROVIDERS.md).
|
||||
|
||||
### Belege und Rückgängigmachen
|
||||
|
||||
`/receipts`, oder nach dem Beenden `codewhale receipts --last`, listet geänderte
|
||||
Dateien, ausgeführte Befehle, Web- und MCP-Aufrufe, gestartete Agenten,
|
||||
Freigaben und wer sie erteilt hat sowie Fehlschläge auf. `/undo` macht deine letzte Anfrage und ihre Dateiänderungen in einem Schritt rückgängig, ohne Vertrauensmodus. `/restore` setzt den gesamten
|
||||
Arbeitsbereich auf einen früheren Snapshot zurück; weil dabei spätere
|
||||
Änderungen überschrieben werden, braucht es `/trust on` oder Full Access. Siehe
|
||||
[Belege](../RECEIPTS.md).
|
||||
|
||||
Exec-Läufe, die Tools anbieten, werden in jedem Ausgabeformat gespeichert,
|
||||
sodass `codewhale receipts --last` nach `exec --auto` funktioniert, auch mit
|
||||
`--json`. Läufe, die nur antworten, werden im Textformat und mit `--json` nur
|
||||
beim Fortsetzen oder beim Beginn der Gesprächskompaktierung gespeichert;
|
||||
verwende `--output-format stream-json`, um sie immer für Belege oder
|
||||
`exec --continue` zu speichern.
|
||||
|
||||
### Längere Arbeit
|
||||
|
||||
`/goal` setzt ein Ziel, das über mehrere Turns bestehen bleibt. Codewhale kann
|
||||
Arbeit auf [Sub-Agenten](../SUBAGENTS.md) verteilen, eine
|
||||
[Flotte](../FLEET.md) von Agenten betreiben oder einem im Repository
|
||||
eingecheckten [Workflow](../WORKFLOW_AUTHORING.md) folgen.
|
||||
|
||||
### Umstieg von einem anderen Agenten
|
||||
|
||||
Codewhale liest `AGENTS.md` von selbst. Anweisungsdateien für andere Tools wie
|
||||
`CLAUDE.md`, `.cursorrules` oder `.github/copilot-instructions.md` werden nur
|
||||
geladen, wenn du sie mit `project_instruction_imports = ["claude"]` (oder
|
||||
`"cursor"`, `"cline"`, `"windsurf"`, `"copilot"`, `"all"`) in deiner
|
||||
Konfiguration aktivierst; siehe [config.example.toml](../../config.example.toml).
|
||||
Codewhale warnt, wenn es eine findet, die es nicht geladen hat.
|
||||
|
||||
`/import-claude` bereitet einen Import deiner Claude-Code-Einstellungen und
|
||||
MCP-Server vor, den du prüfen kannst. `/plugin install` nimmt Claude-Code-Plugins
|
||||
mit Skills, Befehlen, Agentenprofilen und MCP-Servern an und lehnt Plugins ab,
|
||||
die Hooks oder LSP-Server brauchen
|
||||
([Details](../CLAUDE_PLUGIN_COMPAT.md)).
|
||||
|
||||
### Erweiterungen
|
||||
|
||||
Verbinde [MCP-Server](../MCP.md), füge [Skills](../SKILLS.md) und [Plugins](../PLUGINS.md) hinzu und führe [Hooks](../HOOKS.md) bei Sitzungs- und Werkzeugereignissen aus. Der Code-Modus (experimentell, standardmäßig an) lässt das Modell ein `execute_tools`-Programm schreiben, das mehrere MCP- und Plugin-Werkzeuge aufruft; jeder Aufruf folgt weiter deinen Freigaberegeln und behält seinen Beleg. Das in v0.10.2 enthaltene Computer-Use-Plugin (experimentell, nur macOS) kann andere Apps beobachten und bedienen; es bleibt aus, bis du seinen Zugriff geprüft und es aktiviert hast ([Anleitung](https://github.com/codewhale-hq/Codewhale/blob/v0.10.2/crates/tui/plugins/computer-use/README.md)). Kommt in 0.10.3: Das Plugin erscheint [separat](https://github.com/codewhale-hq/codewhale-cu-plugin).
|
||||
Verbinde [MCP-Server](../MCP.md), füge [Skills](../SKILLS.md) und
|
||||
[Plugins](../PLUGINS.md) hinzu und führe [Hooks](../HOOKS.md) bei Sitzungs-
|
||||
und Tool-Ereignissen aus. Der Code-Modus (experimentell, standardmäßig
|
||||
eingeschaltet) lässt das Modell ein einziges `execute_tools`-Programm schreiben,
|
||||
das mehrere MCP- und Plugin-Tools aufruft; jeder Aufruf unterliegt weiterhin
|
||||
deinen Freigaberegeln und behält seinen Beleg. Das unabhängig installierte Plugin
|
||||
Computer Use (experimentell, nur macOS) kann andere Apps beobachten und
|
||||
bedienen. Es bleibt ausgeschaltet,
|
||||
bis du seine Zugriffsrechte geprüft und es aktiviert hast
|
||||
([Anleitung](https://github.com/codewhale-hq/codewhale-cu-plugin)).
|
||||
|
||||
### Wege, es zu starten
|
||||
## Wege, es auszuführen
|
||||
|
||||
| Befehl | Was er tut |
|
||||
| --- | --- |
|
||||
| `codewhale` | Die interaktive Terminal-App |
|
||||
| `codewhale exec "…"` | Eine Aufgabe ohne Oberfläche, für Skripte und CI ([Läufe ohne Oberfläche](../INSTALL.md#headless-scripts-ci)) |
|
||||
| `codewhale web` | Ein lokaler Web-Client auf `127.0.0.1` ([Anleitung](../WEB.md)) |
|
||||
| `codewhale review --pr N` | Ein beratendes Review eines GitHub-Pull-Requests; die [Einrichtung des GitHub-Bots](../GITHUB_ACTION.md#set-up-your-own-bot) führt es mit deinem eigenen Modellschlüssel aus |
|
||||
| `codewhale app-server --http` | Die [Runtime API](../RUNTIME_API.md), eine lokale HTTP-API für Threads, Ereignisse, Freigaben und die Dateiänderungen jeder Runde |
|
||||
| `codewhale exec "…"` | Eine Aufgabe ohne Oberfläche. Ohne weitere Optionen antwortet es, ohne Tools zu verwenden; `--auto` aktiviert Tools standardmäßig mit Full Access, `--sandbox workspace-write` schränkt sie ein, und `--output-format stream-json` streamt Ereignisse als JSON |
|
||||
| `codewhale web` | Eine lokale Web-Oberfläche auf `127.0.0.1` ([Anleitung](../WEB.md)) |
|
||||
| `codewhale review --pr N` | Ein beratendes Review eines GitHub-Pull-Requests. Die [GitHub-Bot-Einrichtung](../GITHUB_ACTION.md#set-up-your-own-bot) erzeugt einen gepinnten Workflow für automatische Reviews und `@codewhale review`, mit deinem eigenen Modellschlüssel und optionaler App-Identität |
|
||||
| `codewhale app-server --http` | Die [Runtime API](../RUNTIME_API.md), eine lokale HTTP-API für Threads, Ereignisse, Freigaben und die Dateiänderungen jedes Turns |
|
||||
| `codewhale serve --acp` | Das Agent Client Protocol über stdio, für Editoren wie Zed |
|
||||
|
||||
Die Terminal-App, `exec`, `web` und die Runtime API teilen sich eine Engine ([Architektur](../ARCHITECTURE.md)). Eine von der Community gepflegte [VS-Code-Erweiterung](https://marketplace.visualstudio.com/items?itemName=HengQuWorld.brotherwhale-vscode) verbindet sich aus der Seitenleiste damit ([Quellcode](https://github.com/HengQuWorld/CodeWhale-VSCode)).
|
||||
Die Terminal-App, `exec`, `web` und die Runtime API teilen sich eine Engine
|
||||
([Architektur](../ARCHITECTURE.md)). Eine von der Community gepflegte
|
||||
[VS-Code-Erweiterung](https://marketplace.visualstudio.com/items?itemName=HengQuWorld.brotherwhale-vscode)
|
||||
verbindet sich aus der Seitenleiste damit ([Quellcode](https://github.com/HengQuWorld/CodeWhale-VSCode)).
|
||||
Eine native Desktop-App auf Basis der Runtime API ist in Entwicklung; einen
|
||||
öffentlichen Download gibt es noch nicht.
|
||||
|
||||
## Wie es sicher bleibt
|
||||
## Kosten und Konten
|
||||
|
||||
Zwei Schalter legen fest, wie viel Codewhale ohne Rückfrage tun darf. Bei leerer Eingabe wechselt `Tab` den Modus: **Plan** liest und sucht, ändert aber nichts, **Work** bearbeitet Dateien und führt Befehle aus, und **Operate** hat dieselben Rechte wie Work und arbeitet weiter, bis das Ziel geprüft ist oder es dich braucht. `Shift+Tab` wechselt die Berechtigung. **Ask**, die Voreinstellung, übernimmt Dateiänderungen in deinem Repository ohne Rückfrage und fragt vor Shell-Befehlen, die nicht nur lesen. **Auto-Review** lässt Regeln und ein Review-Modell Routineaufrufe freigeben und lehnt riskante ab; wechsle zu Ask, um sie selbst freizugeben. **Full Access** fragt nie und schaltet die Sandbox von Codewhale ab, wobei Repository-Regeln und verwaltete Richtlinien weiter gelten. Die [Anleitung zu v0.10.2](https://github.com/codewhale-hq/Codewhale/blob/v0.10.2/docs/MODES.md) hat die Details.
|
||||
Codewhale ist kostenlos und unter MIT lizenziert. Mit deinem eigenen
|
||||
API-Schlüssel bezahlst du den Anbieter direkt; lokale Modelle brauchen keinen
|
||||
Schlüssel. Die Fußzeile und `/cost` zeigen, was eine Sitzung gekostet hat, wenn
|
||||
der Preis bekannt ist; ein unbekannter Preis wird als unbekannt angezeigt, nie
|
||||
als kostenlos.
|
||||
|
||||
Kommt in 0.10.3: `Shift+Tab` wird ein einziger Regler durch Ask, Accept Edits, Plan, Auto-Review und Full Access, `Tab` wechselt den Modus nicht mehr, und Operate entfällt. Ask fragt dann auch vor jeder Dateiänderung, und die neue Voreinstellung, Accept Edits, übernimmt Änderungen im Git-Repository und fragt weiterhin vor Befehlen. Die [aktuelle Anleitung](../MODES.md) beschreibt das.
|
||||
Du brauchst kein Konto. Schlüssel, die du mit `/provider` hinzufügst, bleiben
|
||||
auf deinem Rechner.
|
||||
|
||||
In Ask und Auto-Review laufen Befehle in einer Betriebssystem-Sandbox, wo das System eine hat: Seatbelt unter macOS und bubblewrap unter Linux, wenn es installiert ist. Windows hat noch keine Betriebssystem-Sandbox, und Full Access schaltet die Sandbox ab. In der Sandbox können Shell-Befehle und Terminal-Sitzungen keine Zugangsdatenspeicher wie `~/.ssh`, `~/.aws` und `.env`-Dateien lesen. `/preview-request` zeigt eine geschwärzte Vorschau der nächsten Anfrage, ohne sie zu senden.
|
||||
## Sicherheit und Datenschutz
|
||||
|
||||
Du brauchst kein Konto, und Schlüssel, die du mit `/provider` hinzufügst, bleiben auf deinem Rechner. Mit deinem eigenen API-Schlüssel bezahlst du den Anbieter direkt, und lokale Modelle brauchen keinen Schlüssel. Die Fußzeile und `/cost` zeigen, was eine Sitzung gekostet hat, wenn der Preis bekannt ist; ein unbekannter Preis wird als unbekannt angezeigt, nie als kostenlos.
|
||||
In Ask und Auto-Review laufen Befehle in einer Betriebssystem-Sandbox, sofern
|
||||
das System eine hat: Seatbelt unter macOS und bubblewrap unter Linux, wenn es
|
||||
installiert ist. Windows hat noch keine Betriebssystem-Sandbox, und Full Access
|
||||
schaltet die Sandbox ab. In der Sandbox können Shell-Befehle keine Speicherorte
|
||||
für Zugangsdaten wie `~/.ssh`, `~/.aws` und `.env`-Dateien lesen; ab 0.10.2
|
||||
gilt dieselbe Regel auch für Terminalsitzungen und weitere Datei-Tools.
|
||||
`/preview-request` zeigt eine geschwärzte Vorschau der nächsten Anfrage, ohne
|
||||
sie zu senden.
|
||||
|
||||
Telemetrie ist standardmäßig an. Sie sendet zusammengefasste Zahlen (Version und Plattform, Sitzungsdauer und Ergebnis, welche eingebauten Anbieter du genutzt hast, Funktions- und Fehlerzähler) mit einer zufälligen Installations-ID, die alle 90 Tage wechselt, und niemals Gespräche, Code, Prompts, Modellnamen, Datei- oder Repository-Namen oder Zugangsdaten. Dieser Befehl schaltet sie ab:
|
||||
|
||||
```bash
|
||||
codewhale config set telemetry false
|
||||
```
|
||||
|
||||
Beim Start prüft Codewhale außerdem auf GitHub, ob es eine neue Version gibt; setze `CODEWHALE_NO_UPDATE_CHECK=1`, um das zu überspringen. Mehr unter [Reihenfolge der Autorisierung](../AUTHORIZATION_ORDER.md), [Sandbox](../SANDBOX.md) und [Telemetrie](../TELEMETRY.md).
|
||||
|
||||
## Wohin es geht
|
||||
|
||||
Codewhale wird zu einer einzigen Umgebung über dein Terminal, eine Desktop-App, den Browser und Cloud-Computer hinweg, alles von einer Engine betrieben. Jeder Teil unten sagt, wo er heute steht.
|
||||
|
||||
| Teil | Stand | Heute |
|
||||
| --- | --- | --- |
|
||||
| Terminal-App | Veröffentlicht | Installiere sie unter macOS, Linux oder Windows und führe `codewhale` in einem beliebigen Repository aus. |
|
||||
| Lokaler Browser-Client | Veröffentlicht | Führe `codewhale web` aus, um das Gespräch zu lesen, eine Aufgabe zu senden und Freigaben in deinem Browser zu beantworten; er gehört zum Terminal und braucht kein Konto. |
|
||||
| Viele Agenten an einer Aufgabe | Veröffentlicht | Subagenten, Fleets und Workflows lassen mehrere Agenten eine Aufgabe teilen, jeder mit eigener Rolle. |
|
||||
| Desktop-App | In Entwicklung, kein Download | Wir bauen eine Desktop-App auf derselben Engine, und sie hat noch keinen öffentlichen Download. |
|
||||
| Cloud-Computer | Nur auf Einladung | Cloud-Computer gibt es nur auf Einladung, und wir bauen sie so, dass eine Sitzung auf einer Maschine in der Cloud weiterarbeiten kann. |
|
||||
| Eine Sitzung über alle hinweg | Im Aufbau | Wir bauen eine Sitzung, die dich vom Terminal zum Desktop, zum Browser und in die Cloud begleitet und auf dem ganzen Weg deine bleibt. |
|
||||
|
||||
Die [Roadmap](../ROADMAP.md) sagt, was jede Version abdeckt und wo du helfen kannst.
|
||||
|
||||
## Von einem anderen Agenten kommend
|
||||
|
||||
Codewhale liest `AGENTS.md` von selbst. Anweisungsdateien für andere Werkzeuge wie `CLAUDE.md`, `.cursorrules` oder `.github/copilot-instructions.md` werden nur geladen, wenn du sie mit `project_instruction_imports = ["claude"]` (oder `"cursor"`, `"cline"`, `"windsurf"`, `"copilot"`, `"all"`) in deiner Konfiguration einschaltest; siehe [config.example.toml](../../config.example.toml). Codewhale warnt, wenn es eine solche Datei findet und nicht geladen hat.
|
||||
|
||||
`/import-claude` bereitet einen Import deiner Claude-Code-Einstellungen und MCP-Server vor, den du prüfen kannst. `/plugin install` nimmt Claude-Code-Plugins mit Skills, Befehlen, Agentenprofilen und MCP-Servern an und lehnt Plugins ab, die Hooks oder LSP-Server brauchen ([Details](../CLAUDE_PLUGIN_COMPAT.md)).
|
||||
Telemetrie ist standardmäßig eingeschaltet. Sie sendet aggregierte Zählwerte
|
||||
(Version und Plattform, Sitzungsdauer und -ergebnis, welche eingebauten
|
||||
Anbieter du verwendet hast, Funktions- und Fehlerzähler) mit einer zufälligen
|
||||
Installations-ID, die alle 90 Tage wechselt, und niemals Unterhaltungen, Code,
|
||||
Prompts, Modellnamen, Datei- oder Repository-Namen oder Zugangsdaten. `codewhale config set telemetry false` schaltet sie ab. Beim Start
|
||||
prüft Codewhale außerdem auf GitHub, ob es ein neues Release gibt; setze
|
||||
`CODEWHALE_NO_UPDATE_CHECK=1`, um das zu überspringen. Mehr dazu unter
|
||||
[Autorisierungsreihenfolge](../AUTHORIZATION_ORDER.md),
|
||||
[Sandboxing](../SANDBOX.md) und [Telemetrie](../TELEMETRY.md).
|
||||
|
||||
## Dokumentation
|
||||
|
||||
Diese Anleitungen folgen dem Hauptzweig und können Änderungen beschreiben, die noch nicht im neuesten Release sind.
|
||||
|
||||
| Hier anfangen | Referenz |
|
||||
| Zum Einstieg | Referenz |
|
||||
| --- | --- |
|
||||
| [Benutzerhandbuch](../GUIDE.md) | [Konfiguration](../CONFIGURATION.md) |
|
||||
| [Installation](../INSTALL.md) | [Architektur](../ARCHITECTURE.md) |
|
||||
| [Anbieter und lokale Modelle](../PROVIDERS.md) | [Runtime API](../RUNTIME_API.md) |
|
||||
| [Modi und Berechtigungen](../MODES.md) | [Plugins schreiben](../PLUGIN_AUTHORING.md) |
|
||||
| [Tastenbelegung](../KEYBINDINGS.md) | [Belege](../RECEIPTS.md) |
|
||||
| [Review von GitHub-Pull-Requests](../GITHUB_ACTION.md) | [Gesamte Dokumentation](../README.md) |
|
||||
| [Installation](../INSTALL.md) | [Konfiguration](../CONFIGURATION.md) |
|
||||
| [Anbieter und lokale Modelle](../PROVIDERS.md) | [Architektur](../ARCHITECTURE.md) |
|
||||
| [Modi und Berechtigungen](../MODES.md) | [Runtime API](../RUNTIME_API.md) |
|
||||
| [Tastenbelegung](../KEYBINDINGS.md) | [Plugins entwickeln](../PLUGIN_AUTHORING.md) |
|
||||
| [GitHub-PR-Review](../GITHUB_ACTION.md) | [Gesamte Dokumentation](../README.md) |
|
||||
|
||||
## Mitwirken
|
||||
## Community
|
||||
|
||||
Codewhale wird offen von den Menschen gebaut, die es nutzen, in vielen Sprachen.
|
||||
Codewhale ist durch seine Community zu dem geworden, was es ist, und jedes Feedback ist willkommen: ein Fehler, eine Idee oder etwas, das falsch aussah oder sich falsch anfühlte.
|
||||
[Eröffne ein Issue](https://github.com/codewhale-hq/CodeWhale/issues/new/choose) für jedes davon; ein Issue ist auch der beste Ort, um ein neues Feature zu beginnen.
|
||||
Pull Requests sind willkommen für Korrekturen, Übersetzungen, Anbieter und geplante Arbeit.
|
||||
Der [Leitfaden für Beiträge](../../CONTRIBUTING.md) zeigt, wohin welche Art von Hilfe gehört, und die [Roadmap](../ROADMAP.md) zeigt, wohin das Projekt geht.
|
||||
Beitragende behalten die Anerkennung für übernommene Arbeit, und der [Aufbau des Repositorys](../../CONTRIBUTING.md#project-structure) zeigt, wo sich was befindet.
|
||||
|
||||
<a href="../CONTRIBUTORS.md#everyone"><img src="../assets/contributors.png" width="760" alt="Die Codewhale-Wand der Mitwirkenden: ein Avatar für jede Person, deren Code, mitverfasste Commits, weitergeführte Pull-Requests oder gewürdigte Meldungen in Codewhale stecken, oben die Gesamtzahl."></a>
|
||||
|
||||
Die [Wand der Mitwirkenden](../CONTRIBUTORS.md) zählt alle, deren Code, mitverfasste Commits, weitergeführte Pull-Requests oder gewürdigte Meldungen in Codewhale stecken, auch Menschen, die GitHubs eigene Liste übersieht. `scripts/contributor-wall.py` erzeugt Bild und Liste neu aus der Git-Historie und GitHub.
|
||||
|
||||
Jedes Feedback ist willkommen: ein Fehler, eine Idee oder etwas, das falsch aussah oder sich falsch anfühlte. [Eröffne ein Issue](https://github.com/codewhale-hq/Codewhale/issues/new/choose) für all das; ein Issue ist auch der beste Ort, um eine neue Funktion zu beginnen. Pull-Requests sind willkommen für Fehlerbehebungen, Übersetzungen, Anbieter und geplante Arbeit. Fragen gehören in die [Discussions](https://github.com/codewhale-hq/Codewhale/discussions). Die [Anleitung zum Mitwirken](../../CONTRIBUTING.md) zeigt, wohin jede Art von Hilfe gehört, wie deine Arbeit mit deinem Namen landet und [wo was liegt](../../CONTRIBUTING.md#project-structure).
|
||||
|
||||
Chatte auf [Discord](https://discord.gg/37gfS3ksug), oder füge den Maintainer Hunter auf WeChat (`hunterbown`) hinzu und frag nach der Gruppe Whale Brothers. Alle sind willkommen.
|
||||
Chatte auf [Discord](https://discord.gg/37gfS3ksug), oder füge den Maintainer
|
||||
Hunter auf WeChat (`hunterbown`) hinzu und bitte darum, in die Gruppe
|
||||
Whale Brothers aufgenommen zu werden. Alle sind willkommen.
|
||||
|
||||
## Geschichte und Lizenz
|
||||
|
||||
Codewhale begann als `deepseek-tui` und liest weiterhin dessen Konfiguration und Sitzungen. Es wird unabhängig gepflegt und ist weder mit DeepSeek noch mit einem anderen Modellanbieter verbunden.
|
||||
Codewhale begann als `deepseek-tui` und liest weiterhin die Konfiguration und
|
||||
die Sitzungen dieses Projekts. Es wird unabhängig gepflegt und ist weder mit
|
||||
DeepSeek noch mit einem anderen Modellanbieter verbunden. Danke an
|
||||
[alle, die beigetragen haben](../CONTRIBUTORS.md).
|
||||
|
||||
[MIT](../../LICENSE). Aus anderen Open-Source-Projekten übernommener Code steht in den Hinweisen zu Drittanbietern für [eingebundene Quellen](../../THIRD_PARTY_NOTICES.md) und [übernommene Entwürfe](../THIRD_PARTY_NOTICES.md).
|
||||
[](../CONTRIBUTORS.md#everyone)
|
||||
|
||||
[MIT](../../LICENSE). Code, der aus anderen Open-Source-Projekten übernommen wurde,
|
||||
ist in den [Hinweisen zu Drittanbietern](../THIRD_PARTY_NOTICES.md)
|
||||
aufgeführt.
|
||||
+215
-91
@@ -1,23 +1,23 @@
|
||||
<!-- source: README.md sha256:fd76d718e3a2 -->
|
||||
<!-- source: README.md sha256:a9ec5fb3f3dd -->
|
||||
<div align="center">
|
||||
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="../../brand/mark-reversed.svg">
|
||||
<img src="../../brand/mark-mono.svg" alt="Codewhale" width="72" height="72">
|
||||
<source media="(prefers-color-scheme: dark)" srcset="../../brand/wordmark-inverted.svg">
|
||||
<img src="../../brand/wordmark.svg" alt="Codewhale" width="320">
|
||||
</picture>
|
||||
|
||||
<h1>El modelo puede ser de cualquiera. El agente debería ser tuyo.</h1>
|
||||
**Un agente de programación de código abierto para tu terminal que funciona con cualquier modelo.**
|
||||
|
||||
Codewhale es un agente de programación de código abierto para tu terminal, escrito en Rust. Funciona con cualquier modelo, puede repartir una tarea entre varios agentes y puedes leer cada línea de su código.
|
||||
Dale una tarea a Codewhale y lee el repositorio, edita archivos y ejecuta
|
||||
comandos hasta terminar la tarea o hasta que necesite una respuesta tuya.
|
||||
Funciona con más de 40 proveedores integrados, con DeepSeek V4.1 Flash de forma
|
||||
predeterminada, y con modelos que se ejecutan en tu propia máquina. `/receipts`
|
||||
enumera los archivos que cambió, los comandos que ejecutó y las llamadas web y
|
||||
MCP que hizo.
|
||||
|
||||
```bash
|
||||
curl -fsSL https://codewhale.net/install.sh | sh
|
||||
```
|
||||
|
||||
Esto instala la versión más reciente en macOS o Linux, y no necesita cuenta. [Windows, npm y Cargo](#instalación) están más abajo.
|
||||
|
||||
[](https://github.com/codewhale-hq/Codewhale/releases/latest)
|
||||
[](https://github.com/codewhale-hq/Codewhale/actions/workflows/ci.yml)
|
||||
[](https://github.com/codewhale-hq/CodeWhale/actions/workflows/ci.yml)
|
||||
[](https://crates.io/crates/codewhale-cli)
|
||||
[](https://www.npmjs.com/package/codewhale)
|
||||
[](../../LICENSE)
|
||||
[](https://discord.gg/37gfS3ksug)
|
||||
|
||||
@@ -25,141 +25,265 @@ Esto instala la versión más reciente en macOS o Linux, y no necesita cuenta. [
|
||||
|
||||
[English](../../README.md) · [简体中文](README.zh-CN.md) · [日本語](README.ja-JP.md) · [Tiếng Việt](README.vi.md) · [Bahasa Indonesia](README.id.md) · [한국어](README.ko-KR.md) · [Português](README.pt-BR.md) · [Русский](README.ru.md) · [Українська](README.uk.md) · [Français](README.fr.md) · [Deutsch](README.de.md) · [繁體中文](README.zh-TW.md) · [हिन्दी](README.hi.md) · [Türkçe](README.tr.md) · [Italiano](README.it.md) · [Polski](README.pl.md) · [العربية](README.ar.md) · [Català](README.ca.md)
|
||||
|
||||
<sub>Esta traducción es un borrador automático que aún no ha revisado un hablante nativo.</sub>
|
||||
<img src="../../web/public/codewhale-tui-0102-demo.png" alt="Una sesión de terminal de Codewhale" width="760">
|
||||
|
||||
<img src="../../web/public/codewhale-tui-0102-demo.png" width="760" alt="Codewhale en una terminal después de una tarea de demostración. Informa que agregó una opción --json a export.py, que pasaron las 3 pruebas locales y que una revisión delegada no encontró problemas. La lista de tareas de abajo muestra tres pasos completados, y la línea de estado muestra Full Access y el proveedor local website-demo.">
|
||||
|
||||
<sub>Una compilación de desarrollo de 0.10.3, que aún no se ha publicado, con un modelo de prueba guionizado, en Full Access dentro de un espacio de trabajo desechable. La edición, tres pruebas y una revisión de un subagente se ejecutaron de verdad.</sub>
|
||||
<sub>Una compilación de desarrollo de 0.10.2 con un modelo de prueba con guion, en Full Access dentro de un espacio de trabajo desechable. La edición, tres pruebas y una revisión de un sub-agente se ejecutaron de verdad.</sub>
|
||||
|
||||
</div>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://www.orcarouter.ai/redeem/1a6250545fdc4e0aaccb0a4470159e67" rel="sponsored">
|
||||
<img src="../../web/public/codewhale-x-orcarouter.webp" alt="Codewhale × OrcaRouter — 30% más de uso de DeepSeek v4.1 Flash" width="760">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
[Ver la oferta de Codewhale × OrcaRouter](https://www.orcarouter.ai/redeem/1a6250545fdc4e0aaccb0a4470159e67). Codewhale recibe una comisión por referidos de OrcaRouter.
|
||||
|
||||
## Instalación
|
||||
|
||||
El comando de arriba coloca `codewhale`, verificado con suma de comprobación, y su alias corto `codew` en `~/.local/bin`. Si después tu shell dice `command not found`, sigue la nota sobre el PATH que imprime el instalador ([detalles](../INSTALL.md#put-it-on-your-path)). `codewhale update` lo actualiza.
|
||||
macOS y Linux:
|
||||
|
||||
```bash
|
||||
winget install --id HunterBown.CodeWhale --exact --source winget # Windows x64
|
||||
npm install -g codewhale # wraps the same release binaries
|
||||
cargo install codewhale-cli --locked # builds from source; can trail GitHub Releases
|
||||
curl -fsSL https://codewhale.net/install.sh | sh
|
||||
```
|
||||
|
||||
Hay binarios precompilados para Linux, macOS y Windows en x64 y arm64. Scoop, Nix, Homebrew en Linux, Android y Termux (vista previa) y las descargas manuales están en la [guía de instalación](../INSTALL.md), y los contenedores en [Docker](../DOCKER.md). Usa un solo método de instalación por máquina; si no, quedan dos copias en tu `PATH` y gana la que aparezca primero.
|
||||
El instalador coloca `codewhale` y su alias corto `codew`, con la suma de
|
||||
verificación comprobada, en `~/.local/bin`. Si después tu shell responde
|
||||
`command not found`, ejecuta la línea de PATH que muestra el instalador
|
||||
([detalles](../INSTALL.md#put-it-on-your-path)). `codewhale update` lo
|
||||
actualiza. Estos comandos instalan la versión más reciente. Esta página sigue a
|
||||
`main`, así que una función marcada con una versión llega en esa versión; el
|
||||
[registro de cambios](../../CHANGELOG.md) enumera lo que agrega cada versión.
|
||||
|
||||
<details>
|
||||
<summary><b>Windows, npm, Cargo y otras formas de instalarlo</b></summary>
|
||||
|
||||
```bash
|
||||
winget install HunterBown.CodeWhale # Windows x64 (or Scoop, or the installer from GitHub Releases)
|
||||
npm install -g codewhale # wraps the same release binaries
|
||||
cargo install codewhale-cli --locked # crates.io; can trail GitHub Releases
|
||||
```
|
||||
|
||||
Hay binarios precompilados para Linux, macOS y Windows en x64 y arm64. Nix,
|
||||
Homebrew en Linux, Android/Termux (versión preliminar) y las descargas manuales
|
||||
están en la [guía de instalación](../INSTALL.md), y los contenedores, en
|
||||
[Docker](../DOCKER.md). Usa un solo método de instalación por máquina; de lo
|
||||
contrario, terminas con dos copias en el `PATH` y gana la que aparezca primero.
|
||||
|
||||
</details>
|
||||
|
||||
## Inicio rápido
|
||||
|
||||
1. Haz commit o stash de tu trabajo y ejecuta `codewhale` desde la raíz de un repositorio git. De forma predeterminada aplica los cambios de archivos del repositorio sin preguntar y pregunta antes de cada comando de shell que no sea de solo lectura.
|
||||
2. Ejecuta `/provider` (o presiona `F3`), elige un proveedor, pega su clave de API y elige un modelo. DeepSeek es el proveedor predeterminado. En el primer inicio sin clave, Codewhale te pide un proveedor cuando envías tu primer mensaje, y ofrece un modelo local cuando responde un servidor Ollama local.
|
||||
1. Haz commit de tu trabajo o guárdalo con stash, y luego ejecuta `codewhale`
|
||||
desde la raíz de un repositorio git. De forma predeterminada (**Accept Edits**), edita archivos del repositorio sin preguntar
|
||||
y pregunta antes de cada comando; `Shift+Tab` recorre el control de permisos cuando quieras que pregunte más o menos.
|
||||
2. Ejecuta `/provider` (o presiona `F3`), elige un proveedor, pega su clave API
|
||||
y selecciona un modelo. DeepSeek es el proveedor predeterminado. Si no tienes
|
||||
clave, elige un entorno local. Si no hay ningún proveedor configurado y
|
||||
Ollama ya está sirviendo un modelo de chat, Codewhale lo usa automáticamente;
|
||||
a partir de 0.10.2 te lo ofrece cuando envías tu primer mensaje.
|
||||
3. Escribe una tarea:
|
||||
|
||||
```text
|
||||
Fix the failing tests and explain what changed.
|
||||
```
|
||||
|
||||
Los comandos de solo lectura se ejecutan de inmediato. Para cualquier otro comando de shell, Codewhale pregunta primero: `y` lo ejecuta una vez, `a` permite comandos parecidos durante el resto de la conversación y `n` lo rechaza. `/undo` deshace tu última petición y sus cambios de archivos, y `/help` muestra los comandos y las teclas.
|
||||
Los comandos de solo lectura se ejecutan de inmediato. Para cualquier otro
|
||||
comando de shell, Codewhale pregunta primero: `y` lo ejecuta una vez, `a`
|
||||
permite comandos similares durante el resto de la conversación y `n` lo
|
||||
rechaza. `/undo` deshace tu última solicitud y sus cambios en archivos. `/help` muestra los comandos y las teclas.
|
||||
|
||||
Para scripts y CI, `codewhale exec` ejecuta una tarea sin la interfaz. Lee [ejecuciones sin interfaz](../INSTALL.md#headless-scripts-ci) antes de darle herramientas con `--auto`, porque `--auto` usa Full Access a menos que agregues un sandbox.
|
||||
Para scripts y CI, `codewhale exec` ejecuta una tarea sin la interfaz. Por
|
||||
defecto, `--auto` usa Full Access: las herramientas se ejecutan sin preguntar,
|
||||
los comandos de shell no tienen sandbox de Codewhale y el modo de confianza
|
||||
permite que las herramientas de archivos escriban fuera del espacio de trabajo.
|
||||
Agrega `--sandbox workspace-write` para mantener el sandbox de shell y las
|
||||
restricciones de las herramientas de archivos al espacio de trabajo, con el
|
||||
modo de confianza desactivado:
|
||||
|
||||
```bash
|
||||
codewhale exec --auto --sandbox workspace-write "fix the failing tests and explain what changed"
|
||||
```
|
||||
|
||||
## Modos y permisos
|
||||
|
||||
Una instalación nueva arranca en el modo Work con el permiso Accept Edits. Un solo control cambia ambos: `Shift+Tab` avanza por Ask → Accept Edits → Plan → Auto-Review → Full Access.
|
||||
|
||||
| | Se cambia con | Opciones |
|
||||
| --- | --- | --- |
|
||||
| Modo | la parada Plan de `Shift+Tab`, o `/mode` | **Plan** lee y busca, pero no cambia nada. **Work** (todas las demás paradas) edita archivos y ejecuta comandos; inicia una meta con `/goal` cuando una solicitud necesite más de un turno. |
|
||||
| Permiso | `Shift+Tab` | **Ask** pregunta antes de cada edición de archivos y de cada comando de shell que no sea de solo lectura. **Accept Edits** aplica las ediciones de archivos en tu repositorio git sin preguntar y sigue preguntando antes de los comandos. **Auto-Review** deja que unas reglas y un modelo revisor aprueben las llamadas rutinarias y rechaza las de alto riesgo; cambia a Ask para aprobar esas tú mismo. **Full Access** nunca pregunta, y los comandos pueden acceder a todo lo que hay en esta computadora; las reglas del repositorio y la política administrada siguen aplicándose. |
|
||||
|
||||
Consulta la [guía de modos y permisos](../MODES.md) para ver los detalles.
|
||||
|
||||
## Qué hace
|
||||
|
||||
Dale una tarea a Codewhale y leerá el repositorio, editará archivos y ejecutará comandos hasta terminar la tarea o necesitar tu respuesta. Las [notas de la última versión](https://github.com/codewhale-hq/Codewhale/releases/latest) listan los cambios más recientes, y el [registro de cambios](../../CHANGELOG.md) tiene todas las versiones.
|
||||
### Novedades en 0.10.2
|
||||
|
||||
### Cualquier modelo
|
||||
- `/pet on` convierte tu terminal en una ballena animada que muestra lo que está
|
||||
haciendo el agente, mientras el cuadro de mensajes sigue activo. Sus 980
|
||||
partículas escriben el paso actual en inglés, chino o japonés.
|
||||
- `/workbar terminal` muestra las shells que ejecuta el agente mientras se
|
||||
ejecutan. Escribe en una, o presiona `Ctrl+N` para abrir una shell propia
|
||||
(macOS y Linux).
|
||||
- `/undo` deshace toda tu última solicitud en un solo paso, incluido cada
|
||||
archivo que cambió, y `/diff` funciona en carpetas sin Git.
|
||||
- Cuando un plan está listo, Codewhale pregunta si quieres empezar Work con
|
||||
Ask o Auto-Review, o seguir planificando.
|
||||
- `/color` le da a cada chat su propio color en el cuadro de mensajes, guardado
|
||||
con la sesión.
|
||||
- `web_search` puede usar la nueva Search API de DeepSeek: define
|
||||
`[search] provider = "deepseek"` con una clave de DeepSeek.
|
||||
- Fuera de Full Access, un `curl` o `wget` que subiría una credencial nunca se
|
||||
ejecuta sin tu aprobación, y se rechaza cualquier comando que oculte Unicode
|
||||
invisible. `write` ya no reemplaza un archivo que el agente no ha leído, ni
|
||||
uno que cambió después de que lo leyó.
|
||||
|
||||
Trae más de 40 proveedores integrados, entre ellos Anthropic, OpenAI, DeepSeek, Google, Mistral, Moonshot, OpenRouter y xAI. Funciona con cualquier endpoint compatible con OpenAI, y también con modelos locales mediante Ollama, vLLM o SGLang. El modelo predeterminado es `deepseek-flash` de DeepSeek, que ahora sirve DeepSeek V4.1 Flash ([detalles](../PROVIDERS.md#deepseek-v41-flash)).
|
||||
### Modelos
|
||||
|
||||
`/model` cambia de modelo entre turnos, y `/model auto` elige uno para cada turno (`/router` define cómo). `/turn inspect` muestra qué modelo usó un turno, cuánto costó y cuánto tardó. Consulta [Proveedores](../PROVIDERS.md).
|
||||
Hay más de 40 proveedores integrados, entre ellos Anthropic, OpenAI, DeepSeek,
|
||||
Google, Mistral, Moonshot, OpenRouter y xAI. Funciona cualquier endpoint
|
||||
compatible con OpenAI, y también los modelos locales mediante Ollama, vLLM o
|
||||
SGLang. El modelo predeterminado es `deepseek-flash` de DeepSeek, que DeepSeek
|
||||
cambió a DeepSeek-V4.1-Flash-1017 el 2026-10-17; 0.10.1 lo ejecuta sin
|
||||
necesidad de actualizar ([detalles](../PROVIDERS.md#deepseek-v41-flash)).
|
||||
|
||||
`/model` cambia de modelo entre turnos, y `/model auto` elige uno para cada
|
||||
turno (`/router` configura cómo). `/turn inspect` muestra qué modelo usó un
|
||||
turno, cuánto costó y cuánto tardó. `codewhale auth chatgpt` te permite usar un
|
||||
plan de ChatGPT en lugar de una clave API (experimental). Consulta
|
||||
[Proveedores](../PROVIDERS.md).
|
||||
|
||||
### Recibos y deshacer
|
||||
|
||||
`/receipts`, o `codewhale receipts --last` después de salir, lista los archivos que cambió una sesión, los comandos que ejecutó, sus llamadas web y MCP, los agentes que inició, cada aprobación y quién la dio, y lo que falló. `/undo` deshace tu última petición y los archivos que cambió en un solo paso. `/restore` regresa todo el espacio de trabajo a una instantánea anterior, y como eso sobrescribe ediciones posteriores, necesita `/trust on` o Full Access. Consulta [Recibos](../RECEIPTS.md).
|
||||
`/receipts`, o `codewhale receipts --last` después de salir, enumera los
|
||||
archivos modificados, los comandos ejecutados, las llamadas web y MCP, los
|
||||
agentes iniciados, las aprobaciones y quién las dio, y los fallos. `/undo` deshace tu última solicitud y los archivos que modificó, en un solo paso y sin el modo de confianza. `/restore` regresa todo el
|
||||
espacio de trabajo a una instantánea anterior; como eso sobrescribe las
|
||||
ediciones posteriores, requiere `/trust on` o Full Access. Consulta
|
||||
[Recibos](../RECEIPTS.md).
|
||||
|
||||
### Varios agentes en una tarea
|
||||
Las ejecuciones de exec que ofrecen herramientas se guardan en todos los
|
||||
formatos de salida, así que `codewhale receipts --last` funciona después de
|
||||
`exec --auto`, incluso con `--json`. Las ejecuciones que solo dan una respuesta
|
||||
en texto o con `--json` se guardan solo al reanudar una conversación o cuando
|
||||
empieza la compactación de la conversación; usa `--output-format stream-json`
|
||||
para guardarlas siempre para los recibos o para `exec --continue`.
|
||||
|
||||
`/goal` fija un objetivo que se mantiene entre turnos. Codewhale puede repartir el trabajo entre [subagentes](../SUBAGENTS.md), ejecutar una [Fleet](../FLEET.md) de agentes con sus propios roles y modelos, o seguir un [flujo de trabajo](../WORKFLOW_AUTHORING.md) guardado en el repositorio.
|
||||
### Trabajos más largos
|
||||
|
||||
`/goal` define un objetivo que se mantiene entre turnos. Codewhale puede
|
||||
repartir el trabajo entre [sub-agentes](../SUBAGENTS.md), ejecutar una
|
||||
[flota](../FLEET.md) de agentes o seguir un
|
||||
[flujo de trabajo](../WORKFLOW_AUTHORING.md) versionado en el repositorio.
|
||||
|
||||
### Si vienes de otro agente
|
||||
|
||||
Codewhale lee `AGENTS.md` por su cuenta. Los archivos de instrucciones escritos
|
||||
para otras herramientas, como `CLAUDE.md`, `.cursorrules` o
|
||||
`.github/copilot-instructions.md`, solo se cargan si los habilitas con
|
||||
`project_instruction_imports = ["claude"]` (o `"cursor"`, `"cline"`,
|
||||
`"windsurf"`, `"copilot"`, `"all"`) en tu configuración; consulta
|
||||
[config.example.toml](../../config.example.toml). Codewhale te avisa cuando encuentra
|
||||
uno que no cargó.
|
||||
|
||||
`/import-claude` prepara una importación de tu configuración y tus servidores
|
||||
MCP de Claude Code para que la revises. `/plugin install` acepta plugins de
|
||||
Claude Code que contengan skills, comandos, perfiles de agente y servidores MCP,
|
||||
y rechaza los plugins que necesitan hooks o servidores LSP
|
||||
([detalles](../CLAUDE_PLUGIN_COMPAT.md)).
|
||||
|
||||
### Extensiones
|
||||
|
||||
Conecta [servidores MCP](../MCP.md), agrega [skills](../SKILLS.md) y [plugins](../PLUGINS.md), y ejecuta [hooks](../HOOKS.md) en eventos de sesión y de herramientas. El modo código (experimental, activado por defecto) permite que el modelo escriba un programa `execute_tools` que llama a varias herramientas MCP y de plugins; cada llamada sigue respetando tus reglas de aprobación y conserva su recibo. El plugin Computer Use incluido en v0.10.2 (experimental, solo macOS) puede observar y operar otras apps, y permanece apagado hasta que revises su acceso y lo actives ([guía](https://github.com/codewhale-hq/Codewhale/blob/v0.10.2/crates/tui/plugins/computer-use/README.md)). Próximamente en 0.10.3: el plugin se publica [por separado](https://github.com/codewhale-hq/codewhale-cu-plugin).
|
||||
Conecta [servidores MCP](../MCP.md), agrega [skills](../SKILLS.md) y
|
||||
[plugins](../PLUGINS.md), y ejecuta [hooks](../HOOKS.md) en eventos de
|
||||
sesión y de herramientas. Code mode (experimental, activado de forma
|
||||
predeterminada) permite que el modelo escriba un solo programa `execute_tools`
|
||||
que llama a varias herramientas de MCP y de plugins; cada llamada sigue sujeta a
|
||||
tus reglas de aprobación y conserva su recibo. El plugin Computer Use instalado por separado
|
||||
(experimental, solo macOS) puede observar y operar otras aplicaciones. Permanece desactivado hasta
|
||||
que revises su acceso y lo habilites
|
||||
([guía](https://github.com/codewhale-hq/codewhale-cu-plugin)).
|
||||
|
||||
### Formas de ejecutarlo
|
||||
## Formas de ejecutarlo
|
||||
|
||||
| Comando | Qué hace |
|
||||
| --- | --- |
|
||||
| `codewhale` | La aplicación interactiva de terminal |
|
||||
| `codewhale exec "…"` | Una tarea sin la interfaz, para scripts y CI ([ejecuciones sin interfaz](../INSTALL.md#headless-scripts-ci)) |
|
||||
| `codewhale web` | Un cliente web local en `127.0.0.1` ([guía](../WEB.md)) |
|
||||
| `codewhale review --pr N` | Una revisión orientativa de un pull request de GitHub; la [configuración del bot de GitHub](../GITHUB_ACTION.md#set-up-your-own-bot) la ejecuta con tu propia clave de modelo |
|
||||
| `codewhale exec "…"` | Una tarea sin la interfaz. Por sí solo responde sin usar herramientas; `--auto` habilita las herramientas con Full Access de forma predeterminada, `--sandbox workspace-write` las restringe, y `--output-format stream-json` transmite los eventos como JSON |
|
||||
| `codewhale web` | Una interfaz web local en `127.0.0.1` ([guía](../WEB.md)) |
|
||||
| `codewhale review --pr N` | Una revisión orientativa de un pull request de GitHub. La [configuración del bot de GitHub](../GITHUB_ACTION.md#set-up-your-own-bot) genera un workflow con versión fijada para revisiones automáticas y `@codewhale review`, usando tu propia clave de modelo y una identidad de App opcional |
|
||||
| `codewhale app-server --http` | La [Runtime API](../RUNTIME_API.md), una API HTTP local para hilos, eventos, aprobaciones y los cambios de archivos de cada turno |
|
||||
| `codewhale serve --acp` | El Agent Client Protocol sobre stdio, para editores como Zed |
|
||||
|
||||
La aplicación de terminal, `exec`, `web` y la Runtime API comparten un solo motor ([arquitectura](../ARCHITECTURE.md)). Una [extensión de VS Code](https://marketplace.visualstudio.com/items?itemName=HengQuWorld.brotherwhale-vscode) mantenida por la comunidad se conecta desde la barra lateral ([código fuente](https://github.com/HengQuWorld/CodeWhale-VSCode)).
|
||||
La aplicación de terminal, `exec`, `web` y la Runtime API comparten un mismo
|
||||
motor ([arquitectura](../ARCHITECTURE.md)). Una
|
||||
[extensión de VS Code](https://marketplace.visualstudio.com/items?itemName=HengQuWorld.brotherwhale-vscode)
|
||||
mantenida por la comunidad se conecta a él desde la barra lateral
|
||||
([código fuente](https://github.com/HengQuWorld/CodeWhale-VSCode)).
|
||||
Está en desarrollo una aplicación de escritorio nativa basada en la Runtime API;
|
||||
todavía no tiene una descarga pública.
|
||||
|
||||
## Cómo se mantiene seguro
|
||||
## Costo y cuentas
|
||||
|
||||
Dos interruptores dicen cuánto puede hacer Codewhale por su cuenta. Con la entrada vacía, `Tab` cambia el modo: **Plan** lee y busca pero no cambia nada, **Work** edita archivos y ejecuta comandos, y **Operate** tiene el mismo acceso que Work y sigue hasta verificar el objetivo o necesitarte. `Shift+Tab` cambia el permiso. **Ask**, el predeterminado, aplica cambios de archivos en tu repositorio sin preguntar y pregunta antes de los comandos de shell que no son de solo lectura. **Auto-Review** deja que reglas y un modelo revisor aprueben las llamadas rutinarias y rechaza las de alto riesgo; cambia a Ask para aprobarlas tú. **Full Access** nunca pregunta y desactiva el sandbox de Codewhale, aunque las reglas del repositorio y la política administrada siguen aplicando. La [guía de v0.10.2](https://github.com/codewhale-hq/Codewhale/blob/v0.10.2/docs/MODES.md) tiene los detalles.
|
||||
Codewhale es gratuito y tiene licencia MIT. Con tu propia clave API le pagas
|
||||
directamente al proveedor; los modelos locales no necesitan clave. La barra
|
||||
inferior y `/cost` muestran cuánto ha costado una sesión cuando se conoce el
|
||||
precio; un precio desconocido se muestra como desconocido, nunca como gratuito.
|
||||
|
||||
Próximamente en 0.10.3: `Shift+Tab` se convierte en un solo selector que pasa por Ask, Accept Edits, Plan, Auto-Review y Full Access, `Tab` deja de cambiar el modo y Operate se retira. Ask preguntará también antes de cada cambio de archivo, y el nuevo predeterminado, Accept Edits, aplicará los cambios dentro del repositorio git y seguirá preguntando antes de los comandos. La [guía actual](../MODES.md) lo describe.
|
||||
No necesitas una cuenta. Las claves que agregas con `/provider` se quedan en tu
|
||||
máquina.
|
||||
|
||||
En Ask y Auto-Review, los comandos se ejecutan en un sandbox del sistema operativo cuando el sistema tiene uno: Seatbelt en macOS, y bubblewrap en Linux cuando está instalado. Windows aún no tiene sandbox del sistema operativo, y Full Access apaga el sandbox. Dentro del sandbox, los comandos de shell y las sesiones de terminal no pueden leer almacenes de credenciales como `~/.ssh`, `~/.aws` y los archivos `.env`. `/preview-request` muestra una vista previa censurada de la próxima petición sin enviarla.
|
||||
## Seguridad y privacidad
|
||||
|
||||
No necesitas cuenta, y las claves que agregas con `/provider` se quedan en tu máquina. Con tu propia clave de API le pagas directamente al proveedor, y los modelos locales no necesitan clave. El pie de página y `/cost` muestran lo que ha costado una sesión cuando se conoce el precio, y un precio desconocido se muestra como desconocido, nunca como gratis.
|
||||
En Ask y Auto-Review, los comandos se ejecutan en un sandbox del sistema
|
||||
operativo cuando el sistema tiene uno: Seatbelt en macOS, y bubblewrap en Linux
|
||||
cuando está instalado. Windows todavía no tiene sandbox del sistema operativo, y
|
||||
Full Access desactiva el sandbox. Dentro del sandbox, los comandos de shell no
|
||||
pueden leer almacenes de credenciales como `~/.ssh`, `~/.aws` y los archivos
|
||||
`.env`; a partir de 0.10.2, la misma regla cubre las sesiones de terminal y más
|
||||
herramientas de archivos. `/preview-request` muestra una vista previa de la
|
||||
siguiente solicitud, con los datos sensibles ocultos, sin enviarla.
|
||||
|
||||
La telemetría está activada por defecto. Envía conteos agregados (versión y plataforma, duración y resultado de la sesión, qué proveedores integrados usaste, conteos de funciones y de errores) con un ID de instalación aleatorio que cambia cada 90 días, y nunca conversaciones, código, prompts, nombres de modelos, nombres de archivos o repositorios, ni credenciales. Este comando la desactiva:
|
||||
|
||||
```bash
|
||||
codewhale config set telemetry false
|
||||
```
|
||||
|
||||
Al iniciar, Codewhale también busca en GitHub una versión nueva; define `CODEWHALE_NO_UPDATE_CHECK=1` para omitirlo. Más en [orden de autorización](../AUTHORIZATION_ORDER.md), [sandbox](../SANDBOX.md) y [telemetría](../TELEMETRY.md).
|
||||
|
||||
## Hacia dónde va
|
||||
|
||||
Codewhale se está convirtiendo en un único entorno que abarca tu terminal, una aplicación de escritorio, el navegador y computadoras en la nube, todo gobernado por un solo motor. Cada parte de abajo indica en qué punto está hoy.
|
||||
|
||||
| Parte | Estado | Hoy |
|
||||
| --- | --- | --- |
|
||||
| Aplicación de terminal | Publicado | Instálala en macOS, Linux o Windows y ejecuta `codewhale` en cualquier repositorio. |
|
||||
| Cliente de navegador local | Publicado | Ejecuta `codewhale web` para leer la conversación, enviar una tarea y responder a las aprobaciones en tu navegador; viene con la terminal y no necesita cuenta. |
|
||||
| Varios agentes en una tarea | Publicado | Los subagentes, las Fleets y los flujos de trabajo permiten que varios agentes compartan una tarea, cada uno con su propio rol. |
|
||||
| Aplicación de escritorio | En desarrollo, sin descarga | Estamos construyendo una aplicación de escritorio sobre el mismo motor, y todavía no tiene descarga pública. |
|
||||
| Computadoras en la nube | Solo por invitación | Las computadoras en la nube son solo por invitación, y las estamos construyendo para que una sesión pueda seguir trabajando en una máquina en la nube. |
|
||||
| Una sesión a través de todos | En construcción | Estamos construyendo una sesión que te acompaña desde la terminal hasta el escritorio, el navegador y la nube, y sigue siendo tuya en todo el camino. |
|
||||
|
||||
La [hoja de ruta](../ROADMAP.md) dice qué cubre cada versión y dónde puedes ayudar.
|
||||
|
||||
## Si vienes de otro agente
|
||||
|
||||
Codewhale lee `AGENTS.md` por su cuenta. Los archivos de instrucciones escritos para otras herramientas, como `CLAUDE.md`, `.cursorrules` o `.github/copilot-instructions.md`, solo se cargan si los activas con `project_instruction_imports = ["claude"]` (o `"cursor"`, `"cline"`, `"windsurf"`, `"copilot"`, `"all"`) en tu configuración; consulta [config.example.toml](../../config.example.toml). Codewhale te avisa cuando encuentra uno que no cargó.
|
||||
|
||||
`/import-claude` prepara una importación de tu configuración de Claude Code y tus servidores MCP para que la revises. `/plugin install` acepta plugins de Claude Code que contienen skills, comandos, perfiles de agente y servidores MCP, y rechaza los que necesitan hooks o servidores LSP ([detalles](../CLAUDE_PLUGIN_COMPAT.md)).
|
||||
La telemetría está activada de forma predeterminada. Envía conteos agregados
|
||||
(versión y plataforma, duración y resultado de la sesión, qué proveedores
|
||||
integrados usaste, conteos de funciones y de errores) con un ID de instalación
|
||||
aleatorio que cambia cada 90 días, y nunca conversaciones, código, prompts,
|
||||
nombres de modelos, nombres de archivos o de repositorios, ni credenciales. `codewhale config set telemetry false` la desactiva. Al
|
||||
iniciar, Codewhale también consulta GitHub para ver si hay una versión nueva;
|
||||
define `CODEWHALE_NO_UPDATE_CHECK=1` para omitir esa consulta. Más información
|
||||
en [orden de autorización](../AUTHORIZATION_ORDER.md),
|
||||
[sandboxing](../SANDBOX.md) y [telemetría](../TELEMETRY.md).
|
||||
|
||||
## Documentación
|
||||
|
||||
Estas guías siguen la rama principal y pueden describir cambios que todavía no están en la última versión.
|
||||
|
||||
| Empieza aquí | Referencia |
|
||||
| --- | --- |
|
||||
| [Guía del usuario](../GUIDE.md) | [Configuración](../CONFIGURATION.md) |
|
||||
| [Instalación](../INSTALL.md) | [Arquitectura](../ARCHITECTURE.md) |
|
||||
| [Proveedores y modelos locales](../PROVIDERS.md) | [Runtime API](../RUNTIME_API.md) |
|
||||
| [Modos y permisos](../MODES.md) | [Crear plugins](../PLUGIN_AUTHORING.md) |
|
||||
| [Atajos de teclado](../KEYBINDINGS.md) | [Recibos](../RECEIPTS.md) |
|
||||
| [Revisión de pull requests en GitHub](../GITHUB_ACTION.md) | [Toda la documentación](../README.md) |
|
||||
| [Instalación](../INSTALL.md) | [Configuración](../CONFIGURATION.md) |
|
||||
| [Proveedores y modelos locales](../PROVIDERS.md) | [Arquitectura](../ARCHITECTURE.md) |
|
||||
| [Modos y permisos](../MODES.md) | [Runtime API](../RUNTIME_API.md) |
|
||||
| [Atajos de teclado](../KEYBINDINGS.md) | [Creación de plugins](../PLUGIN_AUTHORING.md) |
|
||||
| [Revisión de PR en GitHub](../GITHUB_ACTION.md) | [Toda la documentación](../README.md) |
|
||||
|
||||
## Contribuir
|
||||
## Comunidad
|
||||
|
||||
Codewhale se construye en abierto por las personas que lo usan, en muchos idiomas.
|
||||
Codewhale es lo que es gracias a su comunidad, y todos los comentarios son bienvenidos: un error, una idea o algo que se vio o se sintió raro.
|
||||
[Abre un issue](https://github.com/codewhale-hq/CodeWhale/issues/new/choose) para cualquiera de ellos; un issue también es el mejor lugar para empezar una función nueva.
|
||||
Los pull requests son bienvenidos para correcciones, traducciones, proveedores y trabajo ya planificado.
|
||||
La [guía de contribución](../../CONTRIBUTING.md) indica adónde va cada tipo de ayuda, y la [hoja de ruta](../ROADMAP.md) indica hacia dónde va el proyecto.
|
||||
Quienes contribuyen conservan el crédito por el trabajo que se integra, y la [estructura del repositorio](../../CONTRIBUTING.md#project-structure) muestra dónde está cada cosa.
|
||||
|
||||
<a href="../CONTRIBUTORS.md#everyone"><img src="../assets/contributors.png" width="760" alt="El muro de contribuidores de Codewhale: un avatar por cada persona cuyo código, commits en coautoría, pull requests retomados o reportes acreditados están en Codewhale, con el total arriba."></a>
|
||||
|
||||
El [muro de contribuidores](../CONTRIBUTORS.md) cuenta a todas las personas cuyo código, commits en coautoría, pull requests retomados o reportes acreditados están en Codewhale, incluidas las que la propia lista de GitHub no muestra. `scripts/contributor-wall.py` regenera la imagen y la lista a partir del historial de git y de GitHub.
|
||||
|
||||
Toda opinión es bienvenida: un error, una idea o algo que se veía o se sentía mal. [Abre un issue](https://github.com/codewhale-hq/Codewhale/issues/new/choose) para cualquiera de ellas; un issue también es el mejor lugar para empezar una función nueva. Los pull requests son bienvenidos para correcciones, traducciones, proveedores y trabajo planificado. Las preguntas van a [Discussions](https://github.com/codewhale-hq/Codewhale/discussions). La [guía de contribución](../../CONTRIBUTING.md) muestra a dónde va cada tipo de ayuda, cómo entra tu trabajo con tu nombre y [dónde está cada cosa](../../CONTRIBUTING.md#project-structure).
|
||||
|
||||
Conversa en [Discord](https://discord.gg/37gfS3ksug), o agrega al mantenedor, Hunter, en WeChat (`hunterbown`) y pide unirte al grupo Whale Brothers. Todos son bienvenidos.
|
||||
Conversa en [Discord](https://discord.gg/37gfS3ksug), o agrega al mantenedor,
|
||||
Hunter, en WeChat (`hunterbown`) y pide unirte al grupo Whale Brothers. Todas las personas son bienvenidas.
|
||||
|
||||
## Historia y licencia
|
||||
|
||||
Codewhale empezó como `deepseek-tui` y todavía lee la configuración y las sesiones de ese proyecto. Se mantiene de forma independiente y no está afiliado a DeepSeek ni a ningún otro proveedor de modelos.
|
||||
Codewhale comenzó como `deepseek-tui` y todavía lee la configuración y las
|
||||
sesiones de ese proyecto. Se mantiene de forma independiente y no está afiliado
|
||||
a DeepSeek ni a ningún otro proveedor de modelos. Gracias a
|
||||
[todas las personas que han contribuido](../CONTRIBUTORS.md).
|
||||
|
||||
[MIT](../../LICENSE). El código adaptado de otros proyectos de código abierto aparece en los avisos de terceros para [fuentes incluidas](../../THIRD_PARTY_NOTICES.md) y [diseños adaptados](../THIRD_PARTY_NOTICES.md).
|
||||
[](../CONTRIBUTORS.md#everyone)
|
||||
|
||||
[MIT](../../LICENSE). El código adaptado de otros proyectos de código abierto se
|
||||
enumera en los [avisos de terceros](../THIRD_PARTY_NOTICES.md).
|
||||
+229
-93
@@ -1,23 +1,23 @@
|
||||
<!-- source: README.md sha256:fd76d718e3a2 -->
|
||||
<!-- source: README.md sha256:a9ec5fb3f3dd -->
|
||||
<div align="center">
|
||||
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="../../brand/mark-reversed.svg">
|
||||
<img src="../../brand/mark-mono.svg" alt="Codewhale" width="72" height="72">
|
||||
<source media="(prefers-color-scheme: dark)" srcset="../../brand/wordmark-inverted.svg">
|
||||
<img src="../../brand/wordmark.svg" alt="Codewhale" width="320">
|
||||
</picture>
|
||||
|
||||
<h1>Le modèle peut être à n’importe qui. L’agent devrait être à vous.</h1>
|
||||
**Un agent de code open source pour votre terminal qui fonctionne avec n’importe quel modèle.**
|
||||
|
||||
Codewhale est un agent de code open source pour votre terminal, écrit en Rust. Il fonctionne avec n’importe quel modèle, peut répartir une tâche entre plusieurs agents, et chaque ligne de son code est à lire par vous.
|
||||
Donnez une tâche à Codewhale : il lit le dépôt, modifie des fichiers et exécute
|
||||
des commandes jusqu’à ce que la tâche soit terminée ou qu’il ait besoin d’une
|
||||
réponse de votre part. Il fonctionne avec plus de 40 fournisseurs intégrés,
|
||||
DeepSeek V4.1 Flash par défaut, ainsi qu’avec des modèles qui tournent sur votre
|
||||
propre machine. `/receipts` liste les fichiers qu’il a modifiés, les commandes
|
||||
qu’il a exécutées et les appels web et MCP qu’il a effectués.
|
||||
|
||||
```bash
|
||||
curl -fsSL https://codewhale.net/install.sh | sh
|
||||
```
|
||||
|
||||
Cette commande installe la dernière version sur macOS ou Linux, et elle ne demande aucun compte. [Windows, npm et Cargo](#installation) sont plus bas.
|
||||
|
||||
[](https://github.com/codewhale-hq/Codewhale/releases/latest)
|
||||
[](https://github.com/codewhale-hq/Codewhale/actions/workflows/ci.yml)
|
||||
[](https://github.com/codewhale-hq/CodeWhale/actions/workflows/ci.yml)
|
||||
[](https://crates.io/crates/codewhale-cli)
|
||||
[](https://www.npmjs.com/package/codewhale)
|
||||
[](../../LICENSE)
|
||||
[](https://discord.gg/37gfS3ksug)
|
||||
|
||||
@@ -25,141 +25,277 @@ Cette commande installe la dernière version sur macOS ou Linux, et elle ne dema
|
||||
|
||||
[English](../../README.md) · [简体中文](README.zh-CN.md) · [日本語](README.ja-JP.md) · [Tiếng Việt](README.vi.md) · [Bahasa Indonesia](README.id.md) · [한국어](README.ko-KR.md) · [Español](README.es-419.md) · [Português](README.pt-BR.md) · [Русский](README.ru.md) · [Українська](README.uk.md) · [Deutsch](README.de.md) · [繁體中文](README.zh-TW.md) · [हिन्दी](README.hi.md) · [Türkçe](README.tr.md) · [Italiano](README.it.md) · [Polski](README.pl.md) · [العربية](README.ar.md) · [Català](README.ca.md)
|
||||
|
||||
<sub>Cette traduction est un brouillon automatique qui n'a pas encore été relu par un locuteur natif.</sub>
|
||||
<img src="../../web/public/codewhale-tui-0102-demo.png" alt="Une session de terminal Codewhale" width="760">
|
||||
|
||||
<img src="../../web/public/codewhale-tui-0102-demo.png" width="760" alt="Codewhale dans un terminal après une tâche de démonstration. Il indique avoir ajouté une option --json à export.py, que les 3 tests locaux passent et qu’une revue déléguée n’a trouvé aucun problème. La liste de tâches en dessous montre trois étapes terminées, et la ligne d’état affiche Full Access et le fournisseur local website-demo.">
|
||||
|
||||
<sub>Une version de développement de la 0.10.3, qui n’est pas encore publiée, avec un modèle de test scripté, en Full Access dans un espace de travail jetable. La modification, trois tests et une revue par un sous-agent ont réellement tourné.</sub>
|
||||
<sub>Une version de développement de la 0.10.2 avec un modèle de test scripté, en Full Access dans un espace de travail jetable. La modification, trois tests et une revue par un sous-agent ont réellement été exécutés.</sub>
|
||||
|
||||
</div>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://www.orcarouter.ai/redeem/1a6250545fdc4e0aaccb0a4470159e67" rel="sponsored">
|
||||
<img src="../../web/public/codewhale-x-orcarouter.webp" alt="Codewhale × OrcaRouter — 30 % d’utilisation de DeepSeek v4.1 Flash en plus" width="760">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
[Voir l’offre Codewhale × OrcaRouter](https://www.orcarouter.ai/redeem/1a6250545fdc4e0aaccb0a4470159e67). Codewhale reçoit une commission de recommandation d’OrcaRouter.
|
||||
|
||||
## Installation
|
||||
|
||||
La commande ci-dessus place `codewhale`, vérifié par somme de contrôle, et son alias court `codew` dans `~/.local/bin`. Si votre shell affiche ensuite `command not found`, suivez la note sur le PATH qu’affiche l’installateur ([détails](../INSTALL.md#put-it-on-your-path)). `codewhale update` le met à jour.
|
||||
macOS et Linux :
|
||||
|
||||
```bash
|
||||
winget install --id HunterBown.CodeWhale --exact --source winget # Windows x64
|
||||
npm install -g codewhale # wraps the same release binaries
|
||||
cargo install codewhale-cli --locked # builds from source; can trail GitHub Releases
|
||||
curl -fsSL https://codewhale.net/install.sh | sh
|
||||
```
|
||||
|
||||
Des binaires précompilés existent pour Linux, macOS et Windows sur x64 et arm64. Scoop, Nix, Homebrew sous Linux, Android et Termux (aperçu) et les téléchargements manuels sont dans le [guide d’installation](../INSTALL.md), et les conteneurs dans [Docker](../DOCKER.md). Utilisez une seule méthode d’installation par machine ; sinon deux copies se retrouvent dans votre `PATH` et la première trouvée l’emporte.
|
||||
L’installeur place `codewhale` et son alias court `codew` dans `~/.local/bin`,
|
||||
après vérification de leur somme de contrôle. Si votre shell répond ensuite
|
||||
`command not found`, exécutez la ligne PATH qu’affiche l’installeur
|
||||
([détails](../INSTALL.md#put-it-on-your-path)). `codewhale update` le met à
|
||||
jour. Ces commandes installent la dernière version publiée. Cette page suit
|
||||
`main` : une fonctionnalité marquée d’un numéro de version arrive avec cette
|
||||
version, et le [journal des modifications](../../CHANGELOG.md) liste ce qu’apporte
|
||||
chaque version.
|
||||
|
||||
<details>
|
||||
<summary><b>Windows, npm, Cargo et autres méthodes d’installation</b></summary>
|
||||
|
||||
```bash
|
||||
winget install HunterBown.CodeWhale # Windows x64 (or Scoop, or the installer from GitHub Releases)
|
||||
npm install -g codewhale # wraps the same release binaries
|
||||
cargo install codewhale-cli --locked # crates.io; can trail GitHub Releases
|
||||
```
|
||||
|
||||
Des binaires précompilés sont fournis pour Linux, macOS et Windows, en x64 et
|
||||
arm64. Nix, Homebrew sous Linux, Android/Termux (préversion) et les
|
||||
téléchargements manuels sont décrits dans le
|
||||
[guide d’installation](../INSTALL.md), et les conteneurs dans
|
||||
[Docker](../DOCKER.md). Tenez-vous-en à une seule méthode d’installation par
|
||||
machine ; sinon, deux copies se retrouvent dans le `PATH` et c’est la première
|
||||
trouvée qui l’emporte.
|
||||
|
||||
</details>
|
||||
|
||||
## Démarrage rapide
|
||||
|
||||
1. Validez ou mettez de côté votre travail, puis lancez `codewhale` à la racine d’un dépôt git. Par défaut, il applique les modifications de fichiers du dépôt sans demander et demande avant chaque commande shell qui ne fait pas que lire.
|
||||
2. Lancez `/provider` (ou appuyez sur `F3`), choisissez un fournisseur, collez sa clé d’API et choisissez un modèle. DeepSeek est le fournisseur par défaut. Au premier lancement sans clé, Codewhale demande un fournisseur quand vous envoyez votre premier message, et propose un modèle local quand un serveur Ollama local répond.
|
||||
3. Tapez une tâche :
|
||||
1. Commitez ou mettez de côté (stash) votre travail, puis lancez `codewhale` à
|
||||
la racine d’un dépôt git. Par défaut (**Accept Edits**), il modifie les fichiers du dépôt sans demander
|
||||
et demande avant chaque commande ; `Shift+Tab` parcourt le cadran des permissions quand vous voulez qu’il demande plus ou moins.
|
||||
2. Lancez `/provider` (ou appuyez sur `F3`), choisissez un fournisseur, collez
|
||||
sa clé API et choisissez un modèle. DeepSeek est le fournisseur par défaut.
|
||||
Sans clé, choisissez plutôt un runtime local. Si aucun fournisseur n’est
|
||||
configuré et qu’Ollama sert déjà un modèle de chat, Codewhale l’utilise
|
||||
automatiquement ; à partir de la 0.10.2, il vous le propose lorsque vous
|
||||
envoyez votre premier message.
|
||||
3. Saisissez une tâche :
|
||||
|
||||
```text
|
||||
Fix the failing tests and explain what changed.
|
||||
```
|
||||
|
||||
Les commandes en lecture seule s’exécutent tout de suite. Pour toute autre commande shell, Codewhale demande d’abord : `y` l’exécute une fois, `a` autorise les commandes semblables pour le reste de la conversation, et `n` refuse. `/undo` annule votre dernière demande et ses modifications de fichiers, et `/help` liste les commandes et les touches.
|
||||
Les commandes en lecture seule s’exécutent immédiatement. Pour toute autre
|
||||
commande shell, Codewhale demande d’abord : `y` l’exécute une fois, `a` autorise
|
||||
les commandes similaires pour le reste de la conversation, `n` refuse. `/undo`
|
||||
annule votre dernière demande et ses modifications de fichiers. `/help` liste les commandes et les touches.
|
||||
|
||||
Pour les scripts et la CI, `codewhale exec` exécute une tâche sans interface. Lisez [exécutions sans interface](../INSTALL.md#headless-scripts-ci) avant de lui donner des outils avec `--auto`, car `--auto` utilise Full Access tant que vous n’ajoutez pas de bac à sable.
|
||||
Pour les scripts et la CI, `codewhale exec` exécute une tâche sans l’interface.
|
||||
Par défaut, `--auto` utilise Full Access : les outils s’exécutent sans demander,
|
||||
les commandes shell n’ont pas de bac à sable Codewhale et le mode de confiance
|
||||
permet aux outils de fichiers d’écrire en dehors de l’espace de travail.
|
||||
Ajoutez `--sandbox workspace-write` pour conserver le bac à sable du shell et
|
||||
les restrictions des outils de fichiers à l’espace de travail, avec le mode de
|
||||
confiance désactivé :
|
||||
|
||||
```bash
|
||||
codewhale exec --auto --sandbox workspace-write "fix the failing tests and explain what changed"
|
||||
```
|
||||
|
||||
## Modes et permissions
|
||||
|
||||
Une nouvelle installation démarre en mode Work avec la permission Accept Edits. Un seul cadran change les deux : `Shift+Tab` parcourt Ask → Accept Edits → Plan → Auto-Review → Full Access.
|
||||
|
||||
| | Changer avec | Options |
|
||||
| --- | --- | --- |
|
||||
| Mode | l’étape Plan de `Shift+Tab`, ou `/mode` | **Plan** lit et effectue des recherches, mais ne modifie rien. **Work** (toutes les autres étapes) modifie des fichiers et exécute des commandes ; lancez un objectif avec `/goal` quand une requête nécessite plus d’un tour. |
|
||||
| Permission | `Shift+Tab` | **Ask** demande avant chaque modification de fichier et chaque commande shell qui n’est pas en lecture seule. **Accept Edits** applique sans demander les modifications de fichiers dans votre dépôt git et demande toujours avant les commandes. **Auto-Review** laisse des règles et un modèle de revue approuver les appels courants et refuse ceux à haut risque ; passez en Ask pour les approuver vous-même. **Full Access** ne demande jamais, et les commandes peuvent accéder à tout ce qui se trouve sur cet ordinateur ; les règles du dépôt et la politique gérée s’appliquent toujours. |
|
||||
|
||||
Le [guide des modes et des permissions](../MODES.md) donne les détails.
|
||||
|
||||
## Ce qu’il fait
|
||||
|
||||
Donnez une tâche à Codewhale : il lit le dépôt, modifie des fichiers et exécute des commandes jusqu’à ce que la tâche soit terminée ou qu’il ait besoin de vous. Les [notes de la dernière version](https://github.com/codewhale-hq/Codewhale/releases/latest) listent les changements les plus récents, et le [journal des modifications](../../CHANGELOG.md) couvre chaque version.
|
||||
### Nouveautés de la 0.10.2
|
||||
|
||||
### N’importe quel modèle
|
||||
- `/pet on` transforme votre terminal en une baleine animée qui montre ce que
|
||||
fait l’agent, tandis que la zone de message reste utilisable. Ses
|
||||
980 particules écrivent l’étape en cours en anglais, en chinois ou en
|
||||
japonais.
|
||||
- `/workbar terminal` affiche les shells que l’agent exécute, pendant leur
|
||||
exécution. Tapez dans l’un d’eux, ou appuyez sur `Ctrl+N` pour ouvrir votre
|
||||
propre shell (macOS et Linux).
|
||||
- `/undo` annule toute votre dernière demande en une seule étape, y compris
|
||||
chaque fichier qu’elle a modifié, et `/diff` fonctionne dans les dossiers sans
|
||||
Git.
|
||||
- Quand un plan est prêt, Codewhale demande s’il faut lancer Work avec Ask ou
|
||||
Auto-Review, ou continuer à planifier.
|
||||
- `/color` donne à chaque conversation sa propre couleur de zone de saisie,
|
||||
enregistrée avec la session.
|
||||
- `web_search` peut utiliser la nouvelle Search API de DeepSeek : définissez
|
||||
`[search] provider = "deepseek"` avec une clé DeepSeek.
|
||||
- En dehors de Full Access, un `curl` ou un `wget` qui enverrait des
|
||||
informations d’identification ne s’exécute jamais sans votre approbation, et
|
||||
une commande qui dissimule de l’Unicode invisible est refusée. `write` ne
|
||||
remplace plus un fichier que l’agent n’a pas lu, ni un fichier modifié après
|
||||
qu’il l’a lu.
|
||||
|
||||
Plus de 40 fournisseurs sont intégrés, dont Anthropic, OpenAI, DeepSeek, Google, Mistral, Moonshot, OpenRouter et xAI. Tout point d’accès compatible OpenAI fonctionne, tout comme les modèles locaux via Ollama, vLLM ou SGLang. Le modèle par défaut est `deepseek-flash` de DeepSeek, qui sert désormais DeepSeek V4.1 Flash ([détails](../PROVIDERS.md#deepseek-v41-flash)).
|
||||
### Modèles
|
||||
|
||||
`/model` change de modèle entre deux tours, et `/model auto` en choisit un à chaque tour (`/router` règle comment). `/turn inspect` montre quel modèle un tour a utilisé, ce qu’il a coûté et combien de temps il a pris. Voir [Fournisseurs](../PROVIDERS.md).
|
||||
Plus de 40 fournisseurs sont intégrés, dont Anthropic, OpenAI, DeepSeek,
|
||||
Google, Mistral, Moonshot, OpenRouter et xAI. Tout point d’accès compatible
|
||||
OpenAI fonctionne, tout comme les modèles locaux via Ollama, vLLM ou SGLang.
|
||||
Le modèle par défaut est `deepseek-flash` de DeepSeek, que DeepSeek a fait
|
||||
passer à DeepSeek-V4.1-Flash-1017 le 2026-10-17 ; la 0.10.1 l’exécute sans
|
||||
mise à jour ([détails](../PROVIDERS.md#deepseek-v41-flash)).
|
||||
|
||||
`/model` change de modèle entre deux tours, et `/model auto` en choisit un pour
|
||||
chaque tour (`/router` permet de régler comment). `/turn inspect` montre quel
|
||||
modèle un tour a utilisé, ce qu’il a coûté et combien de temps il a pris.
|
||||
`codewhale auth chatgpt` vous permet d’utiliser un abonnement ChatGPT au lieu
|
||||
d’une clé API (expérimental). Voir [Fournisseurs](../PROVIDERS.md).
|
||||
|
||||
### Reçus et annulation
|
||||
|
||||
`/receipts`, ou `codewhale receipts --last` après avoir quitté, liste les fichiers modifiés par une session, les commandes lancées, ses appels web et MCP, les agents démarrés, chaque approbation et qui l’a donnée, et ce qui a échoué. `/undo` annule votre dernière demande et les fichiers qu’elle a modifiés en une seule étape. `/restore` ramène tout l’espace de travail à un instantané antérieur ; comme cela écrase les modifications suivantes, il faut `/trust on` ou Full Access. Voir [Reçus](../RECEIPTS.md).
|
||||
`/receipts`, ou `codewhale receipts --last` après avoir quitté, liste les
|
||||
fichiers modifiés, les commandes exécutées, les appels web et MCP, les agents
|
||||
lancés, les approbations et qui les a données, ainsi que les échecs. `/undo` annule votre dernière demande et les fichiers qu’elle a modifiés, en une seule étape et sans mode de confiance. `/restore`
|
||||
ramène tout l’espace de travail à un instantané antérieur ; comme cela écrase
|
||||
les modifications ultérieures, il faut `/trust on` ou Full Access. Voir
|
||||
[Reçus](../RECEIPTS.md).
|
||||
|
||||
### Plusieurs agents sur une tâche
|
||||
Les exécutions d’exec qui proposent des outils sont enregistrées dans tous les
|
||||
formats de sortie ; `codewhale receipts --last` fonctionne donc après
|
||||
`exec --auto`, y compris avec `--json`. Les exécutions qui se limitent à une
|
||||
réponse en texte ou avec `--json` ne sont enregistrées que lors d’une reprise
|
||||
ou lorsque la compaction de la conversation commence ; utilisez
|
||||
`--output-format stream-json` pour toujours les enregistrer pour les reçus ou
|
||||
pour `exec --continue`.
|
||||
|
||||
`/goal` fixe un objectif qui se poursuit d’un tour à l’autre. Codewhale peut répartir le travail entre des [sous-agents](../SUBAGENTS.md), faire tourner une [Fleet](../FLEET.md) d’agents avec leurs propres rôles et modèles, ou suivre un [workflow](../WORKFLOW_AUTHORING.md) versionné dans le dépôt.
|
||||
### Travail de longue durée
|
||||
|
||||
`/goal` fixe un objectif qui se maintient d’un tour à l’autre. Codewhale peut
|
||||
répartir le travail entre des [sous-agents](../SUBAGENTS.md), faire tourner
|
||||
une [flotte](../FLEET.md) d’agents ou suivre un
|
||||
[workflow](../WORKFLOW_AUTHORING.md) versionné dans le dépôt.
|
||||
|
||||
### Venir d’un autre agent
|
||||
|
||||
Codewhale lit `AGENTS.md` de lui-même. Les fichiers d’instructions écrits pour
|
||||
d’autres outils, comme `CLAUDE.md`, `.cursorrules` ou
|
||||
`.github/copilot-instructions.md`, ne sont chargés que si vous l’activez
|
||||
explicitement avec `project_instruction_imports = ["claude"]` (ou `"cursor"`,
|
||||
`"cline"`, `"windsurf"`, `"copilot"`, `"all"`) dans votre configuration ; voir
|
||||
[config.example.toml](../../config.example.toml). Codewhale vous avertit lorsqu’il en
|
||||
trouve un qu’il n’a pas chargé.
|
||||
|
||||
`/import-claude` prépare une importation de vos paramètres Claude Code et de vos
|
||||
serveurs MCP, pour que vous la vérifiiez. `/plugin install` accepte les plugins
|
||||
Claude Code contenant des skills, des commandes, des profils d’agent et des
|
||||
serveurs MCP, et refuse ceux qui ont besoin de hooks ou de serveurs LSP
|
||||
([détails](../CLAUDE_PLUGIN_COMPAT.md)).
|
||||
|
||||
### Extensions
|
||||
|
||||
Connectez des [serveurs MCP](../MCP.md), ajoutez des [skills](../SKILLS.md) et des [plugins](../PLUGINS.md), et lancez des [hooks](../HOOKS.md) sur les événements de session et d’outil. Le mode code (expérimental, activé par défaut) permet au modèle d’écrire un programme `execute_tools` qui appelle plusieurs outils MCP et de plugins ; chaque appel respecte toujours vos règles d’approbation et garde son reçu. Le plugin Computer Use fourni avec v0.10.2 (expérimental, macOS uniquement) peut observer et piloter d’autres applications ; il reste désactivé tant que vous n’avez pas vérifié ses accès et ne l’avez pas activé ([guide](https://github.com/codewhale-hq/Codewhale/blob/v0.10.2/crates/tui/plugins/computer-use/README.md)). À venir dans 0.10.3 : le plugin est publié [séparément](https://github.com/codewhale-hq/codewhale-cu-plugin).
|
||||
Connectez des [serveurs MCP](../MCP.md), ajoutez des [skills](../SKILLS.md)
|
||||
et des [plugins](../PLUGINS.md), et exécutez des [hooks](../HOOKS.md) sur
|
||||
les événements de session et d’outil. Le mode code (expérimental, activé par
|
||||
défaut) permet au modèle d’écrire un seul programme `execute_tools` qui appelle
|
||||
plusieurs outils MCP et de plugins ; chaque appel reste soumis à vos règles
|
||||
d’approbation et conserve son reçu. Le plugin Computer Use installé séparément
|
||||
(expérimental, macOS uniquement) peut observer et utiliser d’autres
|
||||
applications. Il reste désactivé tant que vous n’avez pas examiné ses accès et
|
||||
ne l’avez pas activé ([guide](https://github.com/codewhale-hq/codewhale-cu-plugin)).
|
||||
|
||||
### Façons de le lancer
|
||||
## Façons de l’exécuter
|
||||
|
||||
| Commande | Ce qu’elle fait |
|
||||
| --- | --- |
|
||||
| `codewhale` | L’application de terminal interactive |
|
||||
| `codewhale exec "…"` | Une tâche sans interface, pour les scripts et la CI ([exécutions sans interface](../INSTALL.md#headless-scripts-ci)) |
|
||||
| `codewhale web` | Un client web local sur `127.0.0.1` ([guide](../WEB.md)) |
|
||||
| `codewhale review --pr N` | Une revue consultative d’une pull request GitHub ; la [configuration du bot GitHub](../GITHUB_ACTION.md#set-up-your-own-bot) la lance avec votre propre clé de modèle |
|
||||
| `codewhale` | L’application interactive dans le terminal |
|
||||
| `codewhale exec "…"` | Une tâche sans l’interface. Sans option, il répond sans utiliser d’outils ; `--auto` active les outils avec Full Access par défaut, `--sandbox workspace-write` les restreint, et `--output-format stream-json` diffuse les événements en JSON |
|
||||
| `codewhale web` | Une interface web locale sur `127.0.0.1` ([guide](../WEB.md)) |
|
||||
| `codewhale review --pr N` | Une revue consultative d’une pull request GitHub. La [configuration du bot GitHub](../GITHUB_ACTION.md#set-up-your-own-bot) génère un workflow épinglé pour les revues automatiques et `@codewhale review`, avec votre propre clé de modèle et une identité App facultative |
|
||||
| `codewhale app-server --http` | La [Runtime API](../RUNTIME_API.md), une API HTTP locale pour les fils, les événements, les approbations et les modifications de fichiers de chaque tour |
|
||||
| `codewhale serve --acp` | L’Agent Client Protocol sur stdio, pour des éditeurs comme Zed |
|
||||
|
||||
L’application de terminal, `exec`, `web` et la Runtime API partagent un même moteur ([architecture](../ARCHITECTURE.md)). Une [extension VS Code](https://marketplace.visualstudio.com/items?itemName=HengQuWorld.brotherwhale-vscode) maintenue par la communauté s’y connecte depuis la barre latérale ([source](https://github.com/HengQuWorld/CodeWhale-VSCode)).
|
||||
L’application de terminal, `exec`, `web` et la Runtime API partagent un seul
|
||||
moteur ([architecture](../ARCHITECTURE.md)). Une
|
||||
[extension VS Code](https://marketplace.visualstudio.com/items?itemName=HengQuWorld.brotherwhale-vscode)
|
||||
maintenue par la communauté s’y connecte depuis la barre latérale
|
||||
([source](https://github.com/HengQuWorld/CodeWhale-VSCode)). Une application de
|
||||
bureau native reposant sur la Runtime API est en cours de développement ; elle
|
||||
n’est pas encore disponible en téléchargement public.
|
||||
|
||||
## Comment il reste sûr
|
||||
## Coût et comptes
|
||||
|
||||
Deux réglages disent ce que Codewhale peut faire sans demander. Quand la saisie est vide, `Tab` change de mode : **Plan** lit et cherche mais ne change rien, **Work** modifie des fichiers et exécute des commandes, et **Operate** a les mêmes accès que Work et continue jusqu’à ce que l’objectif soit vérifié ou qu’il ait besoin de vous. `Shift+Tab` change la permission. **Ask**, par défaut, applique les modifications de fichiers de votre dépôt sans demander et demande avant les commandes shell qui ne font pas que lire. **Auto-Review** laisse des règles et un modèle relecteur approuver les appels courants et refuse ceux à haut risque ; passez en Ask pour les approuver vous-même. **Full Access** ne demande jamais et désactive le bac à sable de Codewhale, mais les règles du dépôt et la politique gérée s’appliquent toujours. Le [guide de v0.10.2](https://github.com/codewhale-hq/Codewhale/blob/v0.10.2/docs/MODES.md) donne les détails.
|
||||
Codewhale est gratuit et sous licence MIT. Avec votre propre clé API, vous payez
|
||||
directement le fournisseur ; les modèles locaux ne nécessitent aucune clé. Le
|
||||
pied de page et `/cost` indiquent ce qu’une session a coûté lorsque le prix est
|
||||
connu ; un prix inconnu est affiché comme inconnu, jamais comme gratuit.
|
||||
|
||||
À venir dans 0.10.3 : `Shift+Tab` devient un seul sélecteur qui passe par Ask, Accept Edits, Plan, Auto-Review et Full Access, `Tab` ne change plus de mode et Operate est retiré. Ask demandera aussi avant chaque modification de fichier, et le nouveau réglage par défaut, Accept Edits, appliquera les modifications dans le dépôt git tout en demandant avant les commandes. Le [guide actuel](../MODES.md) le décrit.
|
||||
Aucun compte n’est nécessaire. Les clés que vous ajoutez avec `/provider`
|
||||
restent sur votre machine.
|
||||
|
||||
En Ask et en Auto-Review, les commandes tournent dans un bac à sable du système quand il en existe un : Seatbelt sur macOS, et bubblewrap sur Linux quand il est installé. Windows n’a pas encore de bac à sable système, et Full Access désactive le bac à sable. Dans le bac à sable, les commandes shell et les sessions de terminal ne peuvent pas lire les magasins d’identifiants comme `~/.ssh`, `~/.aws` et les fichiers `.env`. `/preview-request` affiche un aperçu expurgé de la prochaine requête sans l’envoyer.
|
||||
## Sécurité et confidentialité
|
||||
|
||||
Vous n’avez besoin d’aucun compte, et les clés ajoutées avec `/provider` restent sur votre machine. Avec votre propre clé d’API, vous payez directement le fournisseur, et les modèles locaux n’ont pas besoin de clé. Le pied de page et `/cost` indiquent ce qu’une session a coûté quand le prix est connu ; un prix inconnu est affiché comme inconnu, jamais comme gratuit.
|
||||
En Ask et en Auto-Review, les commandes s’exécutent dans un bac à sable du
|
||||
système d’exploitation lorsque le système en propose un : Seatbelt sur macOS, et
|
||||
bubblewrap sous Linux lorsqu’il est installé. Windows n’a pas encore de bac à
|
||||
sable du système, et Full Access désactive le bac à sable. Dans le bac à sable,
|
||||
les commandes shell ne peuvent pas lire les emplacements qui stockent des
|
||||
informations d’identification, comme `~/.ssh`, `~/.aws` et les fichiers `.env` ;
|
||||
à partir de la 0.10.2, la même règle couvre les sessions de terminal et
|
||||
davantage d’outils de fichiers. `/preview-request` affiche un aperçu expurgé de
|
||||
la prochaine requête sans l’envoyer.
|
||||
|
||||
La télémétrie est activée par défaut. Elle envoie des compteurs agrégés (version et plateforme, durée et issue des sessions, fournisseurs intégrés utilisés, compteurs de fonctionnalités et d’erreurs) avec un identifiant d’installation aléatoire renouvelé tous les 90 jours, et jamais de conversations, de code, de prompts, de noms de modèles, de noms de fichiers ou de dépôts, ni d’identifiants. Cette commande la désactive :
|
||||
|
||||
```bash
|
||||
codewhale config set telemetry false
|
||||
```
|
||||
|
||||
Au démarrage, Codewhale vérifie aussi sur GitHub s’il existe une nouvelle version ; définissez `CODEWHALE_NO_UPDATE_CHECK=1` pour l’éviter. Plus d’informations dans [l’ordre d’autorisation](../AUTHORIZATION_ORDER.md), [le bac à sable](../SANDBOX.md) et [la télémétrie](../TELEMETRY.md).
|
||||
|
||||
## Où il va
|
||||
|
||||
Codewhale devient un seul environnement qui couvre votre terminal, une application de bureau, le navigateur et des ordinateurs dans le cloud, le tout animé par un seul moteur. Chaque partie ci-dessous indique où elle en est aujourd’hui.
|
||||
|
||||
| Partie | État | Aujourd’hui |
|
||||
| --- | --- | --- |
|
||||
| Application de terminal | Publié | Installez-la sur macOS, Linux ou Windows et lancez `codewhale` dans n’importe quel dépôt. |
|
||||
| Client de navigateur local | Publié | Lancez `codewhale web` pour lire la conversation, envoyer une tâche et répondre aux demandes d’approbation dans votre navigateur ; il est fourni avec le terminal et ne demande aucun compte. |
|
||||
| Plusieurs agents sur une tâche | Publié | Les sous-agents, les Fleets et les workflows permettent à plusieurs agents de partager une tâche, chacun avec son propre rôle. |
|
||||
| Application de bureau | En développement, sans téléchargement | Nous construisons une application de bureau sur le même moteur, et elle n’a pas encore de téléchargement public. |
|
||||
| Ordinateurs dans le cloud | Sur invitation | Les ordinateurs dans le cloud sont sur invitation, et nous les construisons pour qu’une session puisse continuer à travailler sur une machine dans le cloud. |
|
||||
| Une session à travers tout | En construction | Nous construisons une session qui vous suit du terminal au bureau, au navigateur et au cloud, et reste à vous tout au long du chemin. |
|
||||
|
||||
La [feuille de route](../ROADMAP.md) indique ce que couvre chaque version et où vous pouvez aider.
|
||||
|
||||
## Venir d’un autre agent
|
||||
|
||||
Codewhale lit `AGENTS.md` de lui-même. Les fichiers d’instructions écrits pour d’autres outils, comme `CLAUDE.md`, `.cursorrules` ou `.github/copilot-instructions.md`, ne sont chargés que si vous les activez avec `project_instruction_imports = ["claude"]` (ou `"cursor"`, `"cline"`, `"windsurf"`, `"copilot"`, `"all"`) dans votre configuration ; voir [config.example.toml](../../config.example.toml). Codewhale vous avertit quand il en trouve un qu’il n’a pas chargé.
|
||||
|
||||
`/import-claude` prépare l’import de vos réglages Claude Code et de vos serveurs MCP pour que vous les examiniez. `/plugin install` accepte les plugins Claude Code qui contiennent des skills, des commandes, des profils d’agent et des serveurs MCP, et refuse ceux qui ont besoin de hooks ou de serveurs LSP ([détails](../CLAUDE_PLUGIN_COMPAT.md)).
|
||||
La télémétrie est activée par défaut. Elle envoie des compteurs agrégés (version
|
||||
et plateforme, durée et résultat des sessions, fournisseurs intégrés que vous
|
||||
avez utilisés, compteurs de fonctionnalités et d’erreurs) avec un identifiant
|
||||
d’installation aléatoire renouvelé tous les 90 jours, et jamais de
|
||||
conversations, de code, de prompts, de noms de modèles, de noms de fichiers ou
|
||||
de dépôts, ni d’informations d’identification.
|
||||
`codewhale config set telemetry false` la désactive. Au démarrage, Codewhale
|
||||
vérifie aussi sur GitHub si une nouvelle version est disponible ; définissez
|
||||
`CODEWHALE_NO_UPDATE_CHECK=1` pour l’éviter. Pour en savoir plus :
|
||||
[ordre d’autorisation](../AUTHORIZATION_ORDER.md),
|
||||
[bac à sable](../SANDBOX.md) et [télémétrie](../TELEMETRY.md).
|
||||
|
||||
## Documentation
|
||||
|
||||
Ces guides suivent la branche principale et peuvent décrire des changements qui ne sont pas encore dans la dernière version.
|
||||
|
||||
| Pour commencer | Référence |
|
||||
| --- | --- |
|
||||
| [Guide de l’utilisateur](../GUIDE.md) | [Configuration](../CONFIGURATION.md) |
|
||||
| [Installation](../INSTALL.md) | [Architecture](../ARCHITECTURE.md) |
|
||||
| [Fournisseurs et modèles locaux](../PROVIDERS.md) | [Runtime API](../RUNTIME_API.md) |
|
||||
| [Modes et permissions](../MODES.md) | [Écrire un plugin](../PLUGIN_AUTHORING.md) |
|
||||
| [Raccourcis clavier](../KEYBINDINGS.md) | [Reçus](../RECEIPTS.md) |
|
||||
| [Revue de pull requests GitHub](../GITHUB_ACTION.md) | [Toute la documentation](../README.md) |
|
||||
| [Installation](../INSTALL.md) | [Configuration](../CONFIGURATION.md) |
|
||||
| [Fournisseurs et modèles locaux](../PROVIDERS.md) | [Architecture](../ARCHITECTURE.md) |
|
||||
| [Modes et permissions](../MODES.md) | [Runtime API](../RUNTIME_API.md) |
|
||||
| [Raccourcis clavier](../KEYBINDINGS.md) | [Création de plugins](../PLUGIN_AUTHORING.md) |
|
||||
| [Revue de PR GitHub](../GITHUB_ACTION.md) | [Toute la documentation](../README.md) |
|
||||
|
||||
## Contribuer
|
||||
## Communauté
|
||||
|
||||
Codewhale est construit au grand jour par les personnes qui l’utilisent, dans de nombreuses langues.
|
||||
Codewhale est ce qu’il est grâce à sa communauté, et tous les retours sont les bienvenus : un bug, une idée, ou quelque chose qui vous a paru étrange à l’écran ou à l’usage.
|
||||
[Ouvrez une issue](https://github.com/codewhale-hq/CodeWhale/issues/new/choose) dans tous ces cas ; une issue est aussi le meilleur point de départ pour une nouvelle fonctionnalité.
|
||||
Les pull requests sont les bienvenues pour les correctifs, les traductions, les fournisseurs et le travail déjà planifié.
|
||||
Le [guide de contribution](../../CONTRIBUTING.md) indique où va chaque type d’aide, et la [feuille de route](../ROADMAP.md) indique où va le projet.
|
||||
Les contributeurs conservent le crédit du travail intégré, et l’[organisation du dépôt](../../CONTRIBUTING.md#project-structure) montre où se trouve chaque élément.
|
||||
|
||||
<a href="../CONTRIBUTORS.md#everyone"><img src="../assets/contributors.png" width="760" alt="Le mur des contributeurs de Codewhale : un avatar pour chaque personne dont le code, les commits coécrits, les pull requests reprises ou les signalements crédités font partie de Codewhale, avec le total en haut."></a>
|
||||
|
||||
Le [mur des contributeurs](../CONTRIBUTORS.md) compte toutes les personnes dont le code, les commits coécrits, les pull requests reprises ou les signalements crédités font partie de Codewhale, y compris celles que la liste de contributeurs de GitHub oublie. `scripts/contributor-wall.py` régénère l’image et la liste à partir de l’historique git et de GitHub.
|
||||
|
||||
Tous les retours sont bienvenus : un bug, une idée, ou quelque chose qui avait l’air ou semblait faux. [Ouvrez une issue](https://github.com/codewhale-hq/Codewhale/issues/new/choose) pour chacun d’eux ; une issue est aussi le meilleur endroit pour lancer une nouvelle fonctionnalité. Les pull requests sont bienvenues pour les corrections, les traductions, les fournisseurs et le travail prévu. Les questions vont dans les [Discussions](https://github.com/codewhale-hq/Codewhale/discussions). Le [guide de contribution](../../CONTRIBUTING.md) montre où va chaque type d’aide, comment votre travail est intégré à votre nom, et [où se trouve chaque chose](../../CONTRIBUTING.md#project-structure).
|
||||
|
||||
Discutez sur [Discord](https://discord.gg/37gfS3ksug), ou ajoutez le mainteneur, Hunter, sur WeChat (`hunterbown`) et demandez à rejoindre le groupe Whale Brothers. Tout le monde est bienvenu.
|
||||
Discutez sur [Discord](https://discord.gg/37gfS3ksug), ou ajoutez le mainteneur,
|
||||
Hunter, sur WeChat (`hunterbown`) et demandez à rejoindre le groupe Whale
|
||||
Brothers. Tout le monde est le bienvenu.
|
||||
|
||||
## Historique et licence
|
||||
|
||||
Codewhale a commencé sous le nom `deepseek-tui` et lit toujours la configuration et les sessions de ce projet. Il est maintenu de façon indépendante et n’est affilié ni à DeepSeek ni à aucun autre fournisseur de modèles.
|
||||
Codewhale a commencé sous le nom de `deepseek-tui` et lit toujours la
|
||||
configuration et les sessions de ce projet. Il est maintenu de manière
|
||||
indépendante et n’est affilié ni à DeepSeek ni à aucun autre fournisseur de
|
||||
modèles. Merci à [toutes les personnes qui ont contribué](../CONTRIBUTORS.md).
|
||||
|
||||
[MIT](../../LICENSE). Le code adapté d’autres projets open source est listé dans les mentions tierces pour les [sources intégrées](../../THIRD_PARTY_NOTICES.md) et les [conceptions adaptées](../THIRD_PARTY_NOTICES.md).
|
||||
[](../CONTRIBUTORS.md#everyone)
|
||||
|
||||
[MIT](../../LICENSE). Le code adapté d’autres projets open source est répertorié dans
|
||||
les [mentions tierces](../THIRD_PARTY_NOTICES.md).
|
||||
+212
-104
@@ -1,165 +1,273 @@
|
||||
<!-- source: README.md sha256:fd76d718e3a2 -->
|
||||
<!-- source: README.md sha256:a9ec5fb3f3dd -->
|
||||
<div align="center">
|
||||
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="../../brand/mark-reversed.svg">
|
||||
<img src="../../brand/mark-mono.svg" alt="Codewhale" width="72" height="72">
|
||||
<source media="(prefers-color-scheme: dark)" srcset="../../brand/wordmark-inverted.svg">
|
||||
<img src="../../brand/wordmark.svg" alt="Codewhale" width="320">
|
||||
</picture>
|
||||
|
||||
<h1>मॉडल किसी का भी हो सकता है। एजेंट आपका होना चाहिए।</h1>
|
||||
**आपके टर्मिनल के लिए एक ओपन सोर्स कोडिंग एजेंट, जो किसी भी मॉडल के साथ काम करता है।**
|
||||
|
||||
Codewhale आपके टर्मिनल के लिए एक ओपन सोर्स कोडिंग एजेंट है, जो Rust में लिखा गया है। यह किसी भी मॉडल के साथ काम करता है, एक काम को कई एजेंटों में बाँट सकता है, और इसकी हर पंक्ति आप पढ़ सकते हैं।
|
||||
Codewhale को कोई काम दें, और वह रिपॉज़िटरी पढ़ता है, फ़ाइलें संपादित करता है और कमांड
|
||||
चलाता है, जब तक काम पूरा न हो जाए या उसे आपसे इनपुट की ज़रूरत न पड़े। यह 40 से अधिक
|
||||
बिल्ट-इन प्रोवाइडर के साथ काम करता है (डिफ़ॉल्ट रूप से DeepSeek V4.1 Flash), और आपकी
|
||||
अपनी मशीन पर चल रहे मॉडलों के साथ भी। `/receipts` उन फ़ाइलों की सूची देता है जो इसने बदलीं, उन
|
||||
कमांड की जो इसने चलाईं, और उन वेब और MCP कॉल की जो इसने कीं।
|
||||
|
||||
[](https://github.com/codewhale-hq/CodeWhale/actions/workflows/ci.yml)
|
||||
[](https://crates.io/crates/codewhale-cli)
|
||||
[](https://www.npmjs.com/package/codewhale)
|
||||
[](../../LICENSE)
|
||||
[](https://discord.gg/37gfS3ksug)
|
||||
|
||||
[वेबसाइट](https://codewhale.net) · [दस्तावेज़](../README.md) · [बदलावों की सूची](../../CHANGELOG.md) · [योगदान](../../CONTRIBUTING.md)
|
||||
|
||||
[English](../../README.md) · [简体中文](README.zh-CN.md) · [日本語](README.ja-JP.md) · [Tiếng Việt](README.vi.md) · [Bahasa Indonesia](README.id.md) · [한국어](README.ko-KR.md) · [Español](README.es-419.md) · [Português](README.pt-BR.md) · [Русский](README.ru.md) · [Українська](README.uk.md) · [Français](README.fr.md) · [Deutsch](README.de.md) · [繁體中文](README.zh-TW.md) · [Türkçe](README.tr.md) · [Italiano](README.it.md) · [Polski](README.pl.md) · [العربية](README.ar.md) · [Català](README.ca.md)
|
||||
|
||||
<img src="../../web/public/codewhale-tui-0102-demo.png" alt="Codewhale का एक टर्मिनल सेशन" width="760">
|
||||
|
||||
<sub>0.10.2 का एक डेवलपमेंट बिल्ड, स्क्रिप्टेड टेस्ट मॉडल के साथ, एक अस्थायी वर्कस्पेस के भीतर Full Access में। संपादन, तीन टेस्ट और एक सब-एजेंट समीक्षा सचमुच चलीं।</sub>
|
||||
|
||||
</div>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://www.orcarouter.ai/redeem/1a6250545fdc4e0aaccb0a4470159e67" rel="sponsored">
|
||||
<img src="../../web/public/codewhale-x-orcarouter.webp" alt="Codewhale × OrcaRouter — DeepSeek v4.1 Flash का 30% अतिरिक्त उपयोग" width="760">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
[Codewhale × OrcaRouter का ऑफ़र देखें](https://www.orcarouter.ai/redeem/1a6250545fdc4e0aaccb0a4470159e67). Codewhale को OrcaRouter से रेफ़रल शुल्क मिलता है।
|
||||
|
||||
## इंस्टॉल करें
|
||||
|
||||
macOS और Linux:
|
||||
|
||||
```bash
|
||||
curl -fsSL https://codewhale.net/install.sh | sh
|
||||
```
|
||||
|
||||
यह macOS या Linux पर नवीनतम रिलीज़ इंस्टॉल करता है, और इसके लिए खाते की ज़रूरत नहीं है। [Windows, npm और Cargo](#इंस्टॉल-करें) नीचे हैं।
|
||||
इंस्टॉलर चेकसम से सत्यापित `codewhale` और उसका छोटा उपनाम `codew` `~/.local/bin` में
|
||||
रखता है। अगर इसके बाद आपका शेल `command not found` कहे, तो इंस्टॉलर जो PATH लाइन
|
||||
प्रिंट करता है, उसे चलाएँ ([विवरण](../INSTALL.md#put-it-on-your-path))।
|
||||
`codewhale update` इसे अपग्रेड करता है। ये कमांड नवीनतम रिलीज़ इंस्टॉल करती हैं।
|
||||
यह पेज `main` के अनुसार चलता है, इसलिए जिस फ़ीचर पर कोई संस्करण लिखा है, वह उसी रिलीज़
|
||||
में आता है; [बदलावों की सूची](../../CHANGELOG.md) बताती है कि हर रिलीज़ में क्या जुड़ा।
|
||||
|
||||
[](https://github.com/codewhale-hq/Codewhale/releases/latest)
|
||||
[](https://github.com/codewhale-hq/Codewhale/actions/workflows/ci.yml)
|
||||
[](../../LICENSE)
|
||||
[](https://discord.gg/37gfS3ksug)
|
||||
|
||||
[वेबसाइट](https://codewhale.net) · [दस्तावेज़](../README.md) · [बदलाव सूची](../../CHANGELOG.md) · [योगदान](../../CONTRIBUTING.md)
|
||||
|
||||
[English](../../README.md) · [简体中文](README.zh-CN.md) · [日本語](README.ja-JP.md) · [Tiếng Việt](README.vi.md) · [Bahasa Indonesia](README.id.md) · [한국어](README.ko-KR.md) · [Español](README.es-419.md) · [Português](README.pt-BR.md) · [Русский](README.ru.md) · [Українська](README.uk.md) · [Français](README.fr.md) · [Deutsch](README.de.md) · [繁體中文](README.zh-TW.md) · [Türkçe](README.tr.md) · [Italiano](README.it.md) · [Polski](README.pl.md) · [العربية](README.ar.md) · [Català](README.ca.md)
|
||||
|
||||
<sub>यह अनुवाद मशीन द्वारा बनाया गया मसौदा है और इसकी अभी किसी मूल भाषी द्वारा समीक्षा नहीं हुई है।</sub>
|
||||
|
||||
<img src="../../web/public/codewhale-tui-0102-demo.png" width="760" alt="एक डेमो काम के बाद टर्मिनल में Codewhale। यह बताता है कि उसने export.py में --json फ़्लैग जोड़ा, तीनों स्थानीय टेस्ट पास हुए और सौंपी गई समीक्षा में कोई समस्या नहीं मिली। नीचे की कार्य सूची में तीन पूरे चरण हैं, और स्टेटस लाइन Full Access और स्थानीय website-demo प्रदाता दिखाती है।">
|
||||
|
||||
<sub>स्क्रिप्ट किए गए टेस्ट मॉडल वाला 0.10.3 का डेवलपमेंट बिल्ड (0.10.3 अभी रिलीज़ नहीं हुआ है), एक बार इस्तेमाल होने वाले वर्कस्पेस में Full Access में। बदलाव, तीन टेस्ट और एक सब-एजेंट समीक्षा वाकई चले।</sub>
|
||||
|
||||
</div>
|
||||
|
||||
## इंस्टॉल करें
|
||||
|
||||
ऊपर का कमांड चेकसम से जाँचा गया `codewhale` और उसका छोटा नाम `codew` `~/.local/bin` में रखता है। अगर इसके बाद आपका शेल `command not found` कहे, तो इंस्टॉलर जो PATH नोट दिखाता है उसका पालन करें ([विवरण](../INSTALL.md#put-it-on-your-path))। `codewhale update` इसे अपग्रेड करता है।
|
||||
<details>
|
||||
<summary><b>Windows, npm, Cargo और इंस्टॉल करने के अन्य तरीके</b></summary>
|
||||
|
||||
```bash
|
||||
winget install --id HunterBown.CodeWhale --exact --source winget # Windows x64
|
||||
npm install -g codewhale # wraps the same release binaries
|
||||
cargo install codewhale-cli --locked # builds from source; can trail GitHub Releases
|
||||
winget install HunterBown.CodeWhale # Windows x64 (or Scoop, or the installer from GitHub Releases)
|
||||
npm install -g codewhale # wraps the same release binaries
|
||||
cargo install codewhale-cli --locked # crates.io; can trail GitHub Releases
|
||||
```
|
||||
|
||||
पहले से बनी बाइनरी x64 और arm64 पर Linux, macOS और Windows के लिए हैं। Scoop, Nix, Linux पर Homebrew, Android और Termux (प्रीव्यू) और मैन्युअल डाउनलोड [इंस्टॉलेशन गाइड](../INSTALL.md) में हैं, और कंटेनर [Docker](../DOCKER.md) में। हर मशीन पर एक ही इंस्टॉल तरीका अपनाएँ; वरना आपके `PATH` पर दो प्रतियाँ होंगी और जो पहले मिलेगी वही चलेगी।
|
||||
पहले से बनी बाइनरी x64 और arm64 पर Linux, macOS और Windows के लिए उपलब्ध हैं। Nix,
|
||||
Linux पर Homebrew, Android/Termux (प्रीव्यू) और मैन्युअल डाउनलोड
|
||||
[इंस्टॉलेशन गाइड](../INSTALL.md) में हैं, और कंटेनर [Docker](../DOCKER.md) में।
|
||||
हर मशीन पर इंस्टॉल का एक ही तरीका अपनाएँ; वरना `PATH` पर दो कॉपी हो जाती हैं और जो
|
||||
पहले आती है, वही चलती है।
|
||||
|
||||
## जल्दी शुरुआत
|
||||
</details>
|
||||
|
||||
1. अपना काम commit या stash करें, फिर किसी git रिपॉज़िटरी की रूट से `codewhale` चलाएँ। डिफ़ॉल्ट रूप से यह रिपॉज़िटरी में फ़ाइल बदलाव बिना पूछे लागू करता है और हर उस shell कमांड से पहले पूछता है जो सिर्फ़ पढ़ती नहीं।
|
||||
2. `/provider` चलाएँ (या `F3` दबाएँ), एक प्रदाता चुनें, उसकी API कुंजी चिपकाएँ और मॉडल चुनें। डिफ़ॉल्ट प्रदाता DeepSeek है। बिना कुंजी के पहली बार चलाने पर, जब आप पहला संदेश भेजते हैं तब Codewhale प्रदाता पूछता है, और कोई स्थानीय Ollama सर्वर जवाब दे तो स्थानीय मॉडल सुझाता है।
|
||||
3. एक काम लिखें:
|
||||
## क्विकस्टार्ट
|
||||
|
||||
1. अपना काम commit या stash करें, फिर किसी git रिपॉज़िटरी के रूट से `codewhale` चलाएँ।
|
||||
डिफ़ॉल्ट रूप से (**Accept Edits**) यह रिपॉज़िटरी में फ़ाइलें बिना पूछे संपादित करता है और हर कमांड से पहले पूछता है; ज़्यादा या कम पूछवाने के लिए `Shift+Tab` से अनुमति एक-एक पड़ाव आगे बढ़ाएँ।
|
||||
2. `/provider` चलाएँ (या `F3` दबाएँ), कोई प्रोवाइडर चुनें, उसकी API कुंजी पेस्ट करें और
|
||||
एक मॉडल चुनें। DeepSeek डिफ़ॉल्ट प्रोवाइडर है। अगर कुंजी नहीं है, तो इसके बजाय कोई
|
||||
लोकल रनटाइम चुनें। अगर कोई प्रोवाइडर कॉन्फ़िगर नहीं है और Ollama पहले से कोई चैट
|
||||
मॉडल चला रहा है, तो Codewhale अपने-आप उसका इस्तेमाल करता है; 0.10.2 से, जब आप
|
||||
अपना पहला संदेश भेजते हैं, तब यह उसे इस्तेमाल करने का विकल्प देता है।
|
||||
3. कोई काम लिखें:
|
||||
|
||||
```text
|
||||
Fix the failing tests and explain what changed.
|
||||
```
|
||||
|
||||
केवल पढ़ने वाले कमांड तुरंत चलते हैं। किसी भी दूसरे शेल कमांड के लिए Codewhale पहले पूछता है: `y` उसे एक बार चलाता है, `a` बाकी बातचीत के लिए वैसे कमांड की अनुमति देता है, और `n` मना करता है। `/undo` आपका पिछला अनुरोध और उसके फ़ाइल बदलाव वापस लेता है, और `/help` कमांड और कुंजियाँ दिखाता है।
|
||||
केवल पढ़ने वाली (read-only) कमांड तुरंत चलती हैं। किसी भी दूसरी शेल कमांड के लिए
|
||||
Codewhale पहले पूछता है: `y` उसे एक बार चलाता है, `a` बाकी बातचीत के लिए उस जैसी
|
||||
कमांड की अनुमति देता है, `n` मना कर देता है। `/undo` आपका पिछला अनुरोध और उससे हुए
|
||||
फ़ाइल बदलाव वापस करता है। `/help` कमांड और
|
||||
कीबोर्ड कुंजियों की सूची देता है।
|
||||
|
||||
स्क्रिप्ट और CI के लिए `codewhale exec` बिना इंटरफ़ेस के एक काम चलाता है। `--auto` से इसे टूल देने से पहले [बिना इंटरफ़ेस के चलाना](../INSTALL.md#headless-scripts-ci) पढ़ें, क्योंकि जब तक आप सैंडबॉक्स न जोड़ें, `--auto` Full Access इस्तेमाल करता है।
|
||||
स्क्रिप्ट और CI के लिए `codewhale exec` बिना इंटरफ़ेस के एक काम चलाता है। डिफ़ॉल्ट
|
||||
रूप से `--auto` Full Access का इस्तेमाल करता है: टूल बिना पूछे चलते हैं, शेल कमांड
|
||||
पर Codewhale का सैंडबॉक्स लागू नहीं होता, और ट्रस्ट मोड फ़ाइल टूल को वर्कस्पेस के
|
||||
बाहर लिखने देता है। शेल सैंडबॉक्स और फ़ाइल टूल की वर्कस्पेस सीमाएँ बनाए रखने और
|
||||
ट्रस्ट मोड बंद रखने के लिए `--sandbox workspace-write` जोड़ें:
|
||||
|
||||
```bash
|
||||
codewhale exec --auto --sandbox workspace-write "fix the failing tests and explain what changed"
|
||||
```
|
||||
|
||||
## मोड और अनुमतियाँ
|
||||
|
||||
नया इंस्टॉलेशन Work मोड और Accept Edits अनुमति के साथ शुरू होता है। एक ही डायल दोनों को बदलता है: `Shift+Tab` क्रम से Ask → Accept Edits → Plan → Auto-Review → Full Access पर ले जाता है।
|
||||
|
||||
| | कैसे बदलें | विकल्प |
|
||||
| --- | --- | --- |
|
||||
| मोड | `Shift+Tab` पर Plan वाला पड़ाव, या `/mode` | **Plan** पढ़ता और खोजता है, पर कुछ नहीं बदलता। **Work** (बाकी सभी पड़ाव) फ़ाइलें संपादित करता है और कमांड चलाता है। जब किसी अनुरोध में एक से अधिक टर्न लगें, तो `/goal` से लक्ष्य शुरू करें। |
|
||||
| अनुमति | `Shift+Tab` | **Ask** हर फ़ाइल संपादन से पहले और हर उस शेल कमांड से पहले पूछता है जो read-only नहीं है। **Accept Edits** आपकी git रिपॉज़िटरी में फ़ाइल संपादन बिना पूछे लागू करता है और कमांड से पहले अब भी पूछता है। **Auto-Review** नियमों और एक समीक्षक मॉडल को सामान्य कॉल मंज़ूर करने देता है और उच्च जोखिम वाली कॉल मना कर देता है; उन्हें खुद मंज़ूर करने के लिए Ask पर जाएँ। **Full Access** कभी नहीं पूछता, और कमांड इस कंप्यूटर पर किसी भी चीज़ तक पहुँच सकती हैं; रिपॉज़िटरी के नियम और प्रबंधित नीति (managed policy) फिर भी लागू रहते हैं। |
|
||||
|
||||
विवरण के लिए [मोड और अनुमतियों की गाइड](../MODES.md) देखें।
|
||||
|
||||
## यह क्या करता है
|
||||
|
||||
Codewhale को कोई काम दें, और यह काम पूरा होने या आपके इनपुट की ज़रूरत पड़ने तक रिपॉज़िटरी पढ़ता है, फ़ाइलें बदलता है और कमांड चलाता है। [नवीनतम रिलीज़ नोट्स](https://github.com/codewhale-hq/Codewhale/releases/latest) में सबसे नए बदलाव हैं, और [चेंजलॉग](../../CHANGELOG.md) में हर संस्करण है।
|
||||
### 0.10.2 में नया
|
||||
|
||||
### कोई भी मॉडल
|
||||
- `/pet on` आपके टर्मिनल को एक एनिमेटेड व्हेल में बदल देता है, जो दिखाती है कि एजेंट
|
||||
क्या कर रहा है, और इस दौरान संदेश बॉक्स चालू रहता है। इसके 980 कण मौजूदा कदम को
|
||||
अंग्रेज़ी, चीनी या जापानी में लिखते हैं।
|
||||
- `/workbar terminal` एजेंट द्वारा चलाए जा रहे शेल को चलते-चलते दिखाता है। उनमें से
|
||||
किसी में टाइप करें, या अपना खुद का शेल खोलने के लिए `Ctrl+N` दबाएँ (macOS और Linux)।
|
||||
- `/undo` आपका पूरा पिछला अनुरोध एक ही कदम में वापस करता है, उससे बदली गई हर फ़ाइल
|
||||
समेत, और `/diff` बिना Git वाले फ़ोल्डरों में भी काम करता है।
|
||||
- जब कोई प्लान तैयार होता है, तो Codewhale पूछता है कि Work को Ask या Auto-Review
|
||||
के साथ शुरू करें, या प्लानिंग जारी रखें।
|
||||
- `/color` हर चैट के कंपोज़र को उसका अपना रंग देता है, जो सेशन के साथ सहेजा जाता है।
|
||||
- `web_search` DeepSeek के नए Search API का इस्तेमाल कर सकता है: DeepSeek कुंजी के
|
||||
साथ `[search] provider = "deepseek"` सेट करें।
|
||||
- Full Access के बाहर, कोई `curl` या `wget` जो कोई क्रेडेंशियल अपलोड करता, आपकी
|
||||
मंज़ूरी के बिना कभी नहीं चलता, और अदृश्य Unicode छिपाने वाली कमांड मना कर दी जाती
|
||||
है। `write` अब ऐसी फ़ाइल को नहीं बदलता जिसे एजेंट ने पढ़ा नहीं है, या जो उसके पढ़ने
|
||||
के बाद बदल गई है।
|
||||
|
||||
40 से ज़्यादा प्रदाता पहले से शामिल हैं, जिनमें Anthropic, OpenAI, DeepSeek, Google, Mistral, Moonshot, OpenRouter और xAI हैं। कोई भी OpenAI-संगत एंडपॉइंट काम करता है, और Ollama, vLLM या SGLang के ज़रिए स्थानीय मॉडल भी। डिफ़ॉल्ट मॉडल DeepSeek का `deepseek-flash` है, जो अब DeepSeek V4.1 Flash से चलता है ([विवरण](../PROVIDERS.md#deepseek-v41-flash))।
|
||||
### मॉडल
|
||||
|
||||
`/model` बारियों के बीच मॉडल बदलता है, और `/model auto` हर बारी के लिए एक मॉडल चुनता है (कैसे, यह `/router` तय करता है)। `/turn inspect` दिखाता है कि किसी बारी ने कौन-सा मॉडल इस्तेमाल किया, उसकी लागत क्या थी और उसमें कितना समय लगा। [प्रदाता](../PROVIDERS.md) देखें।
|
||||
40 से अधिक प्रोवाइडर बिल्ट-इन हैं, जिनमें Anthropic, OpenAI, DeepSeek, Google,
|
||||
Mistral, Moonshot, OpenRouter और xAI शामिल हैं। कोई भी OpenAI-संगत एंडपॉइंट काम करता
|
||||
है, और Ollama, vLLM या SGLang के ज़रिए लोकल मॉडल भी। डिफ़ॉल्ट मॉडल DeepSeek का
|
||||
`deepseek-flash` है, जिसे DeepSeek ने 2026-10-17 को DeepSeek-V4.1-Flash-1017 पर
|
||||
कर दिया; 0.10.1 इसे बिना अपडेट के चलाता है
|
||||
([विवरण](../PROVIDERS.md#deepseek-v41-flash))।
|
||||
|
||||
### रसीदें और वापसी
|
||||
`/model` टर्न के बीच मॉडल बदलता है, और `/model auto` हर टर्न के लिए एक मॉडल चुनता है
|
||||
(`/router` तय करता है कि कैसे)। `/turn inspect` दिखाता है कि किसी टर्न ने कौन-सा मॉडल
|
||||
इस्तेमाल किया, उस पर कितना खर्च हुआ और उसमें कितना समय लगा। `codewhale auth chatgpt`
|
||||
से आप API कुंजी के बजाय ChatGPT प्लान इस्तेमाल कर सकते हैं (प्रायोगिक)।
|
||||
[प्रोवाइडर](../PROVIDERS.md) देखें।
|
||||
|
||||
`/receipts`, या बाहर निकलने के बाद `codewhale receipts --last`, किसी सत्र में बदली गई फ़ाइलें, चलाए गए कमांड, उसके वेब और MCP कॉल, शुरू किए गए एजेंट, हर मंज़ूरी और उसे किसने दिया, और क्या विफल हुआ, यह सब दिखाता है। `/undo` आपका पिछला अनुरोध और उससे बदली गई फ़ाइलें एक ही कदम में वापस लेता है। `/restore` पूरे वर्कस्पेस को पहले के स्नैपशॉट पर लौटाता है, और क्योंकि यह बाद के बदलावों को मिटा देता है, इसके लिए `/trust on` या Full Access चाहिए। [रसीदें](../RECEIPTS.md) देखें।
|
||||
### रसीदें और पूर्ववत करना
|
||||
|
||||
### एक काम पर कई एजेंट
|
||||
`/receipts`, या बाहर निकलने के बाद `codewhale receipts --last`, बदली गई फ़ाइलों,
|
||||
चलाई गई कमांड, वेब और MCP कॉल, शुरू किए गए एजेंट, अनुमोदन और उन्हें किसने दिया, और
|
||||
विफलताओं की सूची देता है। `/undo` आपका पिछला अनुरोध और उससे बदली गई फ़ाइलें एक ही कदम में वापस करता है, बिना ट्रस्ट मोड के। `/restore` पूरे
|
||||
वर्कस्पेस को किसी पिछले स्नैपशॉट पर वापस ले जाता है; क्योंकि इससे बाद के संपादन
|
||||
ओवरराइट हो जाते हैं, इसके लिए `/trust on` या Full Access चाहिए।
|
||||
[रसीदें](../RECEIPTS.md) देखें।
|
||||
|
||||
`/goal` एक लक्ष्य तय करता है जो कई बारियों तक चलता है। Codewhale काम को [सब-एजेंटों](../SUBAGENTS.md) में बाँट सकता है, अपनी भूमिकाओं और मॉडलों वाले एजेंटों की [Fleet](../FLEET.md) चला सकता है, या रिपॉज़िटरी में रखे [वर्कफ़्लो](../WORKFLOW_AUTHORING.md) का पालन कर सकता है।
|
||||
जिन exec रन में टूल उपलब्ध होते हैं, वे हर आउटपुट फ़ॉर्मैट में सहेजे जाते हैं, इसलिए
|
||||
`codewhale receipts --last`, `exec --auto` के बाद काम करता है, `--json` के साथ भी।
|
||||
केवल जवाब देने वाले टेक्स्ट और `--json` रन तभी सहेजे जाते हैं जब उन्हें फिर से शुरू
|
||||
किया जाता है या बातचीत का कॉम्पैक्शन शुरू होता है; रसीदों या `exec --continue`
|
||||
के लिए उन्हें हमेशा सहेजने के लिए `--output-format stream-json` इस्तेमाल करें।
|
||||
|
||||
### लंबे काम
|
||||
|
||||
`/goal` एक ऐसा उद्देश्य तय करता है जो कई टर्न तक बना रहता है। Codewhale काम को
|
||||
[सब-एजेंटों](../SUBAGENTS.md) में बाँट सकता है, एजेंटों का एक [fleet](../FLEET.md)
|
||||
चला सकता है, या रिपॉज़िटरी में रखे किसी [वर्कफ़्लो](../WORKFLOW_AUTHORING.md) का
|
||||
पालन कर सकता है।
|
||||
|
||||
### किसी दूसरे एजेंट से आ रहे हैं
|
||||
|
||||
Codewhale `AGENTS.md` अपने-आप पढ़ता है। दूसरे टूल के लिए लिखी गई निर्देश फ़ाइलें, जैसे
|
||||
`CLAUDE.md`, `.cursorrules` या `.github/copilot-instructions.md`, तभी लोड होती हैं जब
|
||||
आप अपने कॉन्फ़िग में `project_instruction_imports = ["claude"]` (या `"cursor"`,
|
||||
`"cline"`, `"windsurf"`, `"copilot"`, `"all"`) से इन्हें चालू करते हैं;
|
||||
[config.example.toml](../../config.example.toml) देखें। जब Codewhale को ऐसी कोई फ़ाइल
|
||||
मिलती है जिसे उसने लोड नहीं किया, तो वह चेतावनी देता है।
|
||||
|
||||
`/import-claude` आपकी Claude Code सेटिंग और MCP सर्वर का इंपोर्ट तैयार करता है, ताकि
|
||||
आप उसकी समीक्षा कर सकें। `/plugin install` ऐसे Claude Code प्लगइन स्वीकार करता है
|
||||
जिनमें स्किल, कमांड, एजेंट प्रोफ़ाइल और MCP सर्वर हों, और उन प्लगइन को मना कर देता है
|
||||
जिन्हें हुक या LSP सर्वर चाहिए ([विवरण](../CLAUDE_PLUGIN_COMPAT.md))।
|
||||
|
||||
### एक्सटेंशन
|
||||
|
||||
[MCP सर्वर](../MCP.md) जोड़ें, [स्किल](../SKILLS.md) और [प्लगइन](../PLUGINS.md) जोड़ें, और सत्र और टूल इवेंट पर [हुक](../HOOKS.md) चलाएँ। कोड मोड (प्रायोगिक, डिफ़ॉल्ट रूप से चालू) मॉडल को एक `execute_tools` प्रोग्राम लिखने देता है जो कई MCP और प्लगइन टूल बुलाता है; हर कॉल फिर भी आपके मंज़ूरी नियमों का पालन करता है और अपनी रसीद रखता है। v0.10.2 के साथ आने वाला Computer Use प्लगइन (प्रायोगिक, केवल macOS) दूसरे ऐप देख और चला सकता है, और जब तक आप इसकी पहुँच की समीक्षा करके इसे चालू नहीं करते, यह बंद रहता है ([गाइड](https://github.com/codewhale-hq/Codewhale/blob/v0.10.2/crates/tui/plugins/computer-use/README.md))। 0.10.3 में आ रहा है: यह प्लगइन [अलग से रिलीज़](https://github.com/codewhale-hq/codewhale-cu-plugin) होगा।
|
||||
[MCP सर्वर](../MCP.md) कनेक्ट करें, [स्किल](../SKILLS.md) और
|
||||
[प्लगइन](../PLUGINS.md) जोड़ें, और सेशन व टूल इवेंट पर [हुक](../HOOKS.md) चलाएँ।
|
||||
Code mode (प्रायोगिक, डिफ़ॉल्ट रूप से चालू) मॉडल को एक ऐसा `execute_tools` प्रोग्राम
|
||||
लिखने देता है जो कई MCP और प्लगइन टूल कॉल करता है; हर कॉल फिर भी आपके अनुमोदन नियमों
|
||||
से गुज़रती है और उसकी रसीद बनी रहती है। अलग से इंस्टॉल किया गया Computer Use प्लगइन (प्रायोगिक, केवल macOS) दूसरे ऐप देख और चला सकता
|
||||
है। जब तक आप उसके एक्सेस की समीक्षा करके उसे सक्षम नहीं करते, वह बंद रहता है
|
||||
([गाइड](https://github.com/codewhale-hq/codewhale-cu-plugin))।
|
||||
|
||||
### चलाने के तरीके
|
||||
## इसे चलाने के तरीके
|
||||
|
||||
| कमांड | यह क्या करता है |
|
||||
| कमांड | क्या करता है |
|
||||
| --- | --- |
|
||||
| `codewhale` | इंटरैक्टिव टर्मिनल ऐप |
|
||||
| `codewhale exec "…"` | बिना इंटरफ़ेस के एक काम, स्क्रिप्ट और CI के लिए ([बिना इंटरफ़ेस के चलाना](../INSTALL.md#headless-scripts-ci)) |
|
||||
| `codewhale web` | `127.0.0.1` पर एक स्थानीय वेब क्लाइंट ([गाइड](../WEB.md)) |
|
||||
| `codewhale review --pr N` | GitHub पुल रिक्वेस्ट की सलाह देने वाली समीक्षा; [GitHub बॉट सेटअप](../GITHUB_ACTION.md#set-up-your-own-bot) इसे आपकी अपनी मॉडल कुंजी से चलाता है |
|
||||
| `codewhale app-server --http` | [Runtime API](../RUNTIME_API.md), थ्रेड, इवेंट, मंज़ूरियों और हर बारी के फ़ाइल बदलावों के लिए एक स्थानीय HTTP API |
|
||||
| `codewhale exec "…"` | बिना इंटरफ़ेस के एक काम। अकेले चलाने पर यह टूल का इस्तेमाल किए बिना जवाब देता है; `--auto` डिफ़ॉल्ट रूप से Full Access के साथ टूल चालू करता है, `--sandbox workspace-write` उन्हें सीमित करता है, और `--output-format stream-json` इवेंट को JSON के रूप में स्ट्रीम करता है |
|
||||
| `codewhale web` | `127.0.0.1` पर एक लोकल वेब UI ([गाइड](../WEB.md)) |
|
||||
| `codewhale review --pr N` | किसी GitHub pull request की सलाह के तौर पर समीक्षा। [GitHub बॉट सेटअप](../GITHUB_ACTION.md#set-up-your-own-bot) स्वचालित समीक्षाओं और `@codewhale review` के लिए एक पिन किया हुआ workflow बनाता है, जो आपकी अपनी मॉडल कुंजी और वैकल्पिक App पहचान का उपयोग करता है |
|
||||
| `codewhale app-server --http` | [Runtime API](../RUNTIME_API.md), थ्रेड, इवेंट, अनुमोदन और हर टर्न के फ़ाइल बदलावों के लिए एक लोकल HTTP API |
|
||||
| `codewhale serve --acp` | stdio पर Agent Client Protocol, Zed जैसे एडिटर के लिए |
|
||||
|
||||
टर्मिनल ऐप, `exec`, `web` और Runtime API एक ही इंजन साझा करते हैं ([आर्किटेक्चर](../ARCHITECTURE.md))। समुदाय द्वारा बनाए रखा गया एक [VS Code एक्सटेंशन](https://marketplace.visualstudio.com/items?itemName=HengQuWorld.brotherwhale-vscode) साइडबार से इससे जुड़ता है ([स्रोत](https://github.com/HengQuWorld/CodeWhale-VSCode))।
|
||||
टर्मिनल ऐप, `exec`, `web` और Runtime API एक ही इंजन साझा करते हैं
|
||||
([आर्किटेक्चर](../ARCHITECTURE.md))। समुदाय द्वारा अनुरक्षित एक
|
||||
[VS Code एक्सटेंशन](https://marketplace.visualstudio.com/items?itemName=HengQuWorld.brotherwhale-vscode)
|
||||
साइडबार से इससे जुड़ता है ([सोर्स](https://github.com/HengQuWorld/CodeWhale-VSCode))।
|
||||
Runtime API पर बने एक नेटिव डेस्कटॉप ऐप पर काम चल रहा है; उसका अभी कोई सार्वजनिक
|
||||
डाउनलोड नहीं है।
|
||||
|
||||
## यह सुरक्षित कैसे रहता है
|
||||
## लागत और खाते
|
||||
|
||||
दो स्विच तय करते हैं कि Codewhale बिना पूछे कितना कर सकता है। इनपुट खाली होने पर `Tab` मोड बदलता है: **Plan** पढ़ता और खोजता है पर कुछ नहीं बदलता, **Work** फ़ाइलें बदलता और कमांड चलाता है, और **Operate** के पास Work जितनी ही पहुँच है और वह तब तक चलता रहता है जब तक लक्ष्य सत्यापित न हो जाए या उसे आपकी ज़रूरत न हो। `Shift+Tab` अनुमति बदलता है। डिफ़ॉल्ट **Ask** आपकी रिपॉज़िटरी में फ़ाइल बदलाव बिना पूछे लागू करता है और उन shell कमांड से पहले पूछता है जो सिर्फ़ पढ़ती नहीं। **Auto-Review** नियमों और एक समीक्षक मॉडल को सामान्य कॉल मंज़ूर करने देता है और उच्च जोखिम वाली कॉल अस्वीकार करता है; उन्हें खुद मंज़ूर करने के लिए Ask पर जाएँ। **Full Access** कभी नहीं पूछता और Codewhale का sandbox बंद कर देता है, हालाँकि रिपॉज़िटरी नियम और प्रबंधित नीति लागू रहती हैं। विवरण [v0.10.2 गाइड](https://github.com/codewhale-hq/Codewhale/blob/v0.10.2/docs/MODES.md) में है।
|
||||
Codewhale मुफ़्त है और MIT लाइसेंस के तहत है। अपनी API कुंजी के साथ आप सीधे प्रोवाइडर
|
||||
को भुगतान करते हैं; लोकल मॉडलों के लिए किसी कुंजी की ज़रूरत नहीं होती। जब कीमत ज्ञात
|
||||
हो, तो फ़ुटर और `/cost` दिखाते हैं कि किसी सेशन पर कितना खर्च हुआ है, और अज्ञात कीमत
|
||||
को अज्ञात ही दिखाया जाता है, कभी मुफ़्त नहीं।
|
||||
|
||||
0.10.3 में आ रहा है: `Shift+Tab` एक ही डायल बन जाएगा जो Ask, Accept Edits, Plan, Auto-Review और Full Access से होकर गुज़रता है, `Tab` अब मोड नहीं बदलेगा, और Operate हटा दिया जाएगा। Ask हर फ़ाइल बदलाव से पहले भी पूछेगा, और नया डिफ़ॉल्ट Accept Edits git रिपॉज़िटरी के अंदर बदलाव लागू करेगा और कमांड से पहले पूछता रहेगा। [मौजूदा गाइड](../MODES.md) इसका वर्णन करती है।
|
||||
आपको खाते की ज़रूरत नहीं है। `/provider` से जोड़ी गई कुंजियाँ आपकी मशीन पर ही रहती हैं।
|
||||
|
||||
Ask और Auto-Review में कमांड ऑपरेटिंग सिस्टम के सैंडबॉक्स में चलते हैं, जहाँ सिस्टम में वह हो: macOS पर Seatbelt, और Linux पर bubblewrap अगर वह इंस्टॉल हो। Windows में अभी ऑपरेटिंग सिस्टम सैंडबॉक्स नहीं है, और Full Access सैंडबॉक्स बंद कर देता है। सैंडबॉक्स के अंदर शेल कमांड और टर्मिनल सत्र `~/.ssh`, `~/.aws` और `.env` फ़ाइलों जैसे क्रेडेंशियल भंडार नहीं पढ़ सकते। `/preview-request` अगले अनुरोध का संवेदनशील हिस्सा छिपाकर पूर्वावलोकन दिखाता है, उसे भेजे बिना।
|
||||
## सुरक्षा और गोपनीयता
|
||||
|
||||
आपको खाते की ज़रूरत नहीं है, और `/provider` से जोड़ी गई कुंजियाँ आपकी मशीन पर रहती हैं। अपनी API कुंजी से आप प्रदाता को सीधे भुगतान करते हैं, और स्थानीय मॉडलों को कुंजी नहीं चाहिए। कीमत पता हो तो फ़ुटर और `/cost` सत्र की लागत दिखाते हैं, और अज्ञात कीमत अज्ञात के रूप में दिखती है, कभी मुफ़्त के रूप में नहीं।
|
||||
Ask और Auto-Review में, जहाँ सिस्टम में OS सैंडबॉक्स है, वहाँ कमांड उसी के भीतर चलती
|
||||
हैं: macOS पर Seatbelt, और Linux पर bubblewrap, जब वह इंस्टॉल हो। Windows पर अभी कोई
|
||||
OS सैंडबॉक्स नहीं है, और Full Access सैंडबॉक्स बंद कर देता है। सैंडबॉक्स के भीतर
|
||||
शेल कमांड `~/.ssh`, `~/.aws` और `.env` फ़ाइलों जैसे क्रेडेंशियल स्टोर नहीं पढ़ सकतीं;
|
||||
0.10.2 से यही नियम टर्मिनल सेशन और ज़्यादा फ़ाइल टूल पर भी लागू होता है।
|
||||
`/preview-request` अगले अनुरोध को भेजे बिना उसका संपादित (redacted) प्रीव्यू दिखाता है।
|
||||
|
||||
टेलीमेट्री डिफ़ॉल्ट रूप से चालू है। यह कुल गिनतियाँ (संस्करण और प्लेटफ़ॉर्म, सत्र की अवधि और परिणाम, आपने कौन-से अंतर्निहित प्रदाता इस्तेमाल किए, सुविधा और त्रुटि गिनती) एक यादृच्छिक इंस्टॉल ID के साथ भेजती है जो हर 90 दिन में बदलती है, और कभी बातचीत, कोड, प्रॉम्प्ट, मॉडल के नाम, फ़ाइल या रिपॉज़िटरी के नाम, या क्रेडेंशियल नहीं भेजती। यह कमांड इसे बंद करता है:
|
||||
|
||||
```bash
|
||||
codewhale config set telemetry false
|
||||
```
|
||||
|
||||
शुरू होते समय Codewhale GitHub पर नई रिलीज़ भी जाँचता है; इसे छोड़ने के लिए `CODEWHALE_NO_UPDATE_CHECK=1` सेट करें। और जानकारी [प्राधिकरण क्रम](../AUTHORIZATION_ORDER.md), [सैंडबॉक्स](../SANDBOX.md) और [टेलीमेट्री](../TELEMETRY.md) में है।
|
||||
|
||||
## यह कहाँ जा रहा है
|
||||
|
||||
Codewhale एक ऐसा वातावरण बन रहा है जो आपके टर्मिनल, डेस्कटॉप ऐप, ब्राउज़र और क्लाउड कंप्यूटरों में फैला है, सब एक इंजन से चलता है। नीचे का हर हिस्सा बताता है कि वह आज किस स्थिति में है।
|
||||
|
||||
| हिस्सा | स्थिति | आज |
|
||||
| --- | --- | --- |
|
||||
| टर्मिनल ऐप | जारी | इसे macOS, Linux या Windows पर इंस्टॉल करें और किसी भी रिपॉज़िटरी में `codewhale` चलाएँ। |
|
||||
| लोकल ब्राउज़र क्लाइंट | जारी | `codewhale web` चलाकर अपने ब्राउज़र में बातचीत पढ़ें, काम भेजें और मंज़ूरियों का जवाब दें; यह टर्मिनल के साथ आता है और इसके लिए खाते की ज़रूरत नहीं है। |
|
||||
| एक काम पर कई एजेंट | जारी | सब-एजेंट, Fleet और वर्कफ़्लो कई एजेंटों को एक काम बाँटने देते हैं, हर एक की अपनी भूमिका के साथ। |
|
||||
| डेस्कटॉप ऐप | विकास में, डाउनलोड नहीं | हम उसी इंजन पर एक डेस्कटॉप ऐप बना रहे हैं, और उसका अभी कोई सार्वजनिक डाउनलोड नहीं है। |
|
||||
| क्लाउड कंप्यूटर | केवल आमंत्रण | क्लाउड कंप्यूटर केवल आमंत्रण पर हैं, और हम इन्हें इसलिए बना रहे हैं कि एक सत्र क्लाउड की किसी मशीन पर काम करता रह सके। |
|
||||
| इन सबके पार एक सत्र | निर्माणाधीन | हम एक ऐसा सत्र बना रहे हैं जो टर्मिनल से डेस्कटॉप, ब्राउज़र और क्लाउड तक आपके साथ जाता है, और पूरे रास्ते आपका ही रहता है। |
|
||||
|
||||
[रोडमैप](../ROADMAP.md) बताता है कि हर रिलीज़ में क्या है और आप कहाँ मदद कर सकते हैं।
|
||||
|
||||
## किसी दूसरे एजेंट से आ रहे हैं
|
||||
|
||||
Codewhale `AGENTS.md` अपने आप पढ़ता है। दूसरे टूल के लिए लिखी निर्देश फ़ाइलें, जैसे `CLAUDE.md`, `.cursorrules` या `.github/copilot-instructions.md`, तभी लोड होती हैं जब आप अपने कॉन्फ़िग में `project_instruction_imports = ["claude"]` (या `"cursor"`, `"cline"`, `"windsurf"`, `"copilot"`, `"all"`) से इन्हें चालू करें; [config.example.toml](../../config.example.toml) देखें। ऐसी कोई फ़ाइल मिलने पर जिसे उसने लोड नहीं किया, Codewhale चेतावनी देता है।
|
||||
|
||||
`/import-claude` आपकी Claude Code सेटिंग और MCP सर्वरों का आयात आपकी समीक्षा के लिए तैयार करता है। `/plugin install` स्किल, कमांड, एजेंट प्रोफ़ाइल और MCP सर्वर वाले Claude Code प्लगइन स्वीकार करता है, और जिन प्लगइनों को हुक या LSP सर्वर चाहिए उन्हें मना करता है ([विवरण](../CLAUDE_PLUGIN_COMPAT.md))।
|
||||
टेलीमेट्री डिफ़ॉल्ट रूप से चालू है। यह समेकित गिनतियाँ (संस्करण और प्लेटफ़ॉर्म, सेशन
|
||||
की अवधि और परिणाम, आपने कौन-से बिल्ट-इन प्रोवाइडर इस्तेमाल किए, फ़ीचर और त्रुटियों की
|
||||
गिनती) एक रैंडम इंस्टॉल ID के साथ भेजती है, जो हर 90 दिन में बदल जाती है। यह कभी बातचीत,
|
||||
कोड, प्रॉम्प्ट, मॉडल के नाम, फ़ाइल या रिपॉज़िटरी के नाम, या क्रेडेंशियल नहीं भेजती। `codewhale config set telemetry false` इसे बंद कर
|
||||
देता है। शुरू होते समय Codewhale नई रिलीज़ के लिए GitHub भी जाँचता है; इसे छोड़ने के
|
||||
लिए `CODEWHALE_NO_UPDATE_CHECK=1` सेट करें। ज़्यादा जानकारी
|
||||
[अधिकार क्रम](../AUTHORIZATION_ORDER.md), [सैंडबॉक्सिंग](../SANDBOX.md) और
|
||||
[टेलीमेट्री](../TELEMETRY.md) में है।
|
||||
|
||||
## दस्तावेज़
|
||||
|
||||
ये गाइड मुख्य ब्रांच के साथ चलती हैं, इसलिए इनमें ऐसे बदलाव हो सकते हैं जो अभी नवीनतम रिलीज़ में नहीं हैं।
|
||||
|
||||
| यहाँ से शुरू करें | संदर्भ |
|
||||
| --- | --- |
|
||||
| [उपयोगकर्ता गाइड](../GUIDE.md) | [कॉन्फ़िगरेशन](../CONFIGURATION.md) |
|
||||
| [इंस्टॉलेशन](../INSTALL.md) | [आर्किटेक्चर](../ARCHITECTURE.md) |
|
||||
| [प्रदाता और स्थानीय मॉडल](../PROVIDERS.md) | [Runtime API](../RUNTIME_API.md) |
|
||||
| [मोड और अनुमतियाँ](../MODES.md) | [प्लगइन लिखना](../PLUGIN_AUTHORING.md) |
|
||||
| [कीबाइंडिंग](../KEYBINDINGS.md) | [रसीदें](../RECEIPTS.md) |
|
||||
| [GitHub पुल रिक्वेस्ट समीक्षा](../GITHUB_ACTION.md) | [सभी दस्तावेज़](../README.md) |
|
||||
| [इंस्टॉलेशन](../INSTALL.md) | [कॉन्फ़िगरेशन](../CONFIGURATION.md) |
|
||||
| [प्रोवाइडर और लोकल मॉडल](../PROVIDERS.md) | [आर्किटेक्चर](../ARCHITECTURE.md) |
|
||||
| [मोड और अनुमतियाँ](../MODES.md) | [Runtime API](../RUNTIME_API.md) |
|
||||
| [कीबाइंडिंग](../KEYBINDINGS.md) | [प्लगइन लेखन](../PLUGIN_AUTHORING.md) |
|
||||
| [GitHub PR समीक्षा](../GITHUB_ACTION.md) | [सभी दस्तावेज़](../README.md) |
|
||||
|
||||
## योगदान
|
||||
## समुदाय
|
||||
|
||||
Codewhale को इसे इस्तेमाल करने वाले लोग कई भाषाओं में खुले में बनाते हैं।
|
||||
Codewhale आज जो है, अपने समुदाय की वजह से है, और हर फ़ीडबैक का स्वागत है: कोई बग, कोई आइडिया, या कुछ ऐसा जो देखने या इस्तेमाल करने में ठीक न लगा हो।
|
||||
इनमें से किसी के लिए भी [issue खोलें](https://github.com/codewhale-hq/CodeWhale/issues/new/choose); नई सुविधा शुरू करने की सबसे अच्छी जगह भी issue ही है।
|
||||
फ़िक्स, अनुवाद, प्रोवाइडर और पहले से तय काम के लिए pull request का स्वागत है।
|
||||
[योगदान गाइड](../../CONTRIBUTING.md) बताती है कि किस तरह की मदद कहाँ जाती है, और [रोडमैप](../ROADMAP.md) बताता है कि प्रोजेक्ट किस ओर जा रहा है।
|
||||
जो काम शामिल होता है उसका श्रेय योगदानकर्ताओं के पास रहता है, और [रिपॉज़िटरी लेआउट](../../CONTRIBUTING.md#project-structure) बताता है कि क्या कहाँ है।
|
||||
|
||||
<a href="../CONTRIBUTORS.md#everyone"><img src="../assets/contributors.png" width="760" alt="Codewhale योगदानकर्ता दीवार: हर उस व्यक्ति का एक अवतार जिसका कोड, सह-लेखक commit, आगे बढ़ाए गए पुल रिक्वेस्ट या श्रेय पाई रिपोर्ट Codewhale में है, और सबसे ऊपर कुल संख्या।"></a>
|
||||
|
||||
[योगदानकर्ता दीवार](../CONTRIBUTORS.md) हर उस व्यक्ति को गिनती है जिसका कोड, सह-लेखक commit, आगे बढ़ाए गए पुल रिक्वेस्ट या श्रेय पाई रिपोर्ट Codewhale में है, उन लोगों समेत जिन्हें GitHub की अपनी योगदानकर्ता सूची छोड़ देती है। `scripts/contributor-wall.py` git इतिहास और GitHub से तस्वीर और सूची दोबारा बनाता है।
|
||||
|
||||
हर प्रतिक्रिया का स्वागत है: कोई बग, कोई विचार, या कुछ ऐसा जो दिखने या इस्तेमाल में गलत लगा। इनमें से किसी के लिए भी [issue खोलें](https://github.com/codewhale-hq/Codewhale/issues/new/choose); नई सुविधा शुरू करने के लिए भी issue सबसे अच्छी जगह है। सुधार, अनुवाद, प्रदाता और योजनाबद्ध काम के लिए पुल रिक्वेस्ट का स्वागत है। सवाल [Discussions](https://github.com/codewhale-hq/Codewhale/discussions) में पूछें। [योगदान गाइड](../../CONTRIBUTING.md) बताती है कि हर तरह की मदद कहाँ जाती है, आपका काम आपके नाम के साथ कैसे जुड़ता है, और [कौन-सी चीज़ कहाँ है](../../CONTRIBUTING.md#project-structure)।
|
||||
|
||||
[Discord](https://discord.gg/37gfS3ksug) पर बात करें, या मेंटेनर Hunter को WeChat (`hunterbown`) पर जोड़ें और Whale Brothers समूह में शामिल होने को कहें। सबका स्वागत है।
|
||||
[Discord](https://discord.gg/37gfS3ksug) पर बात करें, या WeChat पर मेंटेनर Hunter
|
||||
(`hunterbown`) को जोड़ें और Whale Brothers समूह में शामिल होने के लिए कहें। सबका स्वागत है।
|
||||
|
||||
## इतिहास और लाइसेंस
|
||||
|
||||
Codewhale `deepseek-tui` के रूप में शुरू हुआ था और अब भी उस प्रोजेक्ट का कॉन्फ़िगरेशन और सत्र पढ़ता है। इसे स्वतंत्र रूप से बनाए रखा जाता है और यह DeepSeek या किसी अन्य मॉडल प्रदाता से संबद्ध नहीं है।
|
||||
Codewhale की शुरुआत `deepseek-tui` के रूप में हुई थी और यह आज भी उस प्रोजेक्ट का
|
||||
कॉन्फ़िगरेशन और सेशन पढ़ता है। यह स्वतंत्र रूप से अनुरक्षित है और DeepSeek या किसी भी
|
||||
अन्य मॉडल प्रोवाइडर से संबद्ध नहीं है।
|
||||
[योगदान देने वाले हर व्यक्ति](../CONTRIBUTORS.md) का धन्यवाद।
|
||||
|
||||
[MIT](../../LICENSE)। दूसरे ओपन सोर्स प्रोजेक्ट से लिया गया कोड तृतीय-पक्ष सूचनाओं में [शामिल स्रोतों](../../THIRD_PARTY_NOTICES.md) और [अपनाए गए डिज़ाइनों](../THIRD_PARTY_NOTICES.md) के लिए सूचीबद्ध है।
|
||||
[](../CONTRIBUTORS.md#everyone)
|
||||
|
||||
[MIT](../../LICENSE)। अन्य ओपन सोर्स प्रोजेक्ट से अनुकूलित कोड
|
||||
[थर्ड-पार्टी नोटिस](../THIRD_PARTY_NOTICES.md) में सूचीबद्ध है।
|
||||
Loaded 100 of 137 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user