docs/TOOL_SURFACE.md carried four claims the runtime's own tests contradict:
1. "The default-active policy contains exactly these ten names" listing
`update_plan`. `DEFAULT_ACTIVE_NATIVE_TOOLS`
(crates/tui/src/core/engine/tool_catalog.rs:44-58) has eight entries and
`update_plan` is not among them — it appears nowhere in tool_catalog.rs. The
policy is nine (those eight plus synthetic `tool_search`), eight with memory
disabled. `update_plan` is registered (crates/tui/src/tools/plan.rs:401) but
reachable only through `tool_search`; the tool table now says so.
2. "A memory-disabled or Moraine-fallback runtime". There is no Moraine
fallback — docs/MEMORY.md:11-13 records the removal, and
crates/tui/src/prompts.rs:2445-2449 is a test asserting MEMORY_GUIDANCE must
not contain the word.
3. A "Replay-only aliases" table promising "saved transcripts, sessions, and
recorded automation replay without migration" for 23 names, 16 of which are
asserted REMOVED at crates/tui/src/tools/registry.rs:2066-2088 ("{retired}
must stay removed") and 6 more at :2290-2304 ("{alias} must be removed").
Split into a "Removed spellings" section (with the registry.rs:313-316 note
that resolve has no fuzzy step, so those calls fail rather than dispatch) and
a "Replay-only aliases" section holding only what is still registered:
apply_patch, task_*, github_*, automation_*, rlm_*, checklist_*/todo_*.
4. A "Release verification" block whose three cargo filters name tests that do
not exist (`rg` finds those three strings only in that doc). `cargo test`
exits 0 with "0 passed; N filtered out" on a filter that matches nothing, so
a release engineer following it got three green checkmarks having verified
nothing. Replaced with the real names —
`shell_surface_contains_only_the_canonical_bash_tool` (registry.rs:2290) and
`runtime_task_families_expose_only_canonical_tools` (registry.rs:2333) — plus
the receipt test, and a warning about the silent-pass failure mode.
docs/RUNTIME_SIMPLIFICATION_DESIGN.md repeats errors 1 and 3 and is designated
authoritative by docs/TOOL_LIFECYCLE.md:3-7, but carries no status marker. Given
a status banner naming both divergences and pointing at TOOL_SURFACE.md; the
"Rejected alternatives" provenance is worth keeping, so not deleted.
docs/SUBAGENTS.md:
- "a bounded queue of up to 200 running plus queued sub-agents by default" —
`MAX_SUBAGENT_ADMISSION` is 1024 (crates/tui/src/config/subagent_limits.rs:21),
which is what docs/TOOL_SURFACE.md:182 already said. The 64/128 concurrency
figures on the same page were correct and are untouched.
- The memory section described a `memory.md` that does not exist and omitted the
`scope` parameter. crates/tui/src/tools/remember.rs:165 states the legacy
single-file path was removed in v0.9.4; writes go through
`NativeMemoryStore::remember(scope, workspace_id, note)` (remember.rs:77-108).
config.example.toml documented two key sets that do not exist. Neither struct has
`deny_unknown_fields`, so both were silently discarded rather than rejected:
- `[advisor] max_tool_pairs` / `system_prompt`. `AdvisorConfigToml`
(crates/config/src/lib.rs:2369-2394) has enabled, max_tool_calls (default 10,
clamped 1-50 — the doc said 8, max 32), rate_limit_secs, dedup_window_secs,
and model. `model` was undocumented; now it is.
- `[fleet.profiles.*.permissions] allow_tools` / `deny_tools`.
`FleetProfilePermissions` (lib.rs:1966-1977) has allow_shell, trust,
approval_required. `rg 'allow_tools|deny_tools' crates/` finds nothing. The
example value was `"exec_shell"`, itself a removed tool name.
docs/CONFIGURATION.md: deleted the "Parsed but currently unused" section. Its one
entry, `tools_file`, is not parsed by anything — the field was removed in
346bfe3b6 and the doc bullet was orphaned. Repo-wide `rg` finds the string only
in that section, and nothing links a #parsed-but-currently-unused anchor.
docs/TTC_DESIGN.md said implementation "is deferred beyond v0.9.0". The `verify`
tool shipped and is default-on (crates/tui/src/tools/verify.rs,
features.rs:262, registry.rs:1040-1041 with verify_tool_enabled defaulted true).
Retitled as landed-in-part; capability (B) is still genuinely deferred, so the
doc stays. Its interface line said `with_verify(critic)`; the real signature is
`with_verify_tool(client, model)` (registry.rs:886).
docs/skills/README.md advertised `gh-plan-issues`, deleted in 18de2ebc0, and
credited these skills to "the v0.8.61 release" at a 0.9.4 release.
docs/architecture/provider-model-settings-v091.md pinned
`provider_is_configured` to config.rs:8625-8669; it is at :10160 and that region
is now unrelated code. Replaced with the symbol name, since config.rs is under
active edit.
docs/architecture/command-dispatch.md:133 claimed EPIC-002 was "ready for PR".
The PR (#3706) merged and #2870 closed 2026-08-01. Line 145 was an empty
"Current Evidence (Draft)" heading with no content; removed.
.gitignore: `git check-ignore -v` attributes .claude/settings.json,
scheduled_tasks.lock, worktrees/, and *.local.* to the blanket `.claude/` at
line 126, not to the specific rules above them. Dropped the redundant ones and
annotated why the HANDOFF_/CODEMAP_ patterns are deliberately kept.
16 KiB
Tool surface
This document describes the current model-facing tool contract in the v0.9.1 source candidate. The registry remains larger than the first-turn catalog so saved transcripts can replay and uncommon capabilities can be loaded on demand. The model should learn one canonical name for each common operation.
Implementation sources:
crates/tui/src/core/engine/tool_catalog.rsowns the eager/deferred catalog.crates/tui/src/tools/registry.rsregisters canonical tools and hidden aliases.crates/tui/src/tools/{file_tool,git_tool,run_tool,web_tool,shell}.rsown the canonical action schemas.docs/RUNTIME_SIMPLIFICATION_DESIGN.mdrecords the v0.9.1 cutover and receipt.
Default-active contract
The default-active policy contains exactly these nine names:
BashFileGitRunagentremembertaskswork_updatetool_search
The first eight are DEFAULT_ACTIVE_NATIVE_TOOLS in
crates/tui/src/core/engine/tool_catalog.rs. tool_search is synthetic rather
than registry-backed and is always active.
remember is registered only when the user enables the built-in memory path;
once present, it stays eager so a model can capture a durable preference without
first discovering the tool. A memory-disabled runtime omits that registration and
therefore exposes eight of the nine policy names.
update_plan is not default-active. It is registered
(crates/tui/src/tools/plan.rs:401) but reachable only through tool_search, so
it does not appear in the first-turn catalog. Plan mode narrows the active set
further: Bash and Run drop out, leaving File, Git, agent, tasks,
work_update, and tool_search.
The surface is action-based. A model calls one stable tool name and selects the
operation through its action field instead of choosing among many synonymous
single-purpose tools.
Core action tools
| Tool | Actions | Purpose |
|---|---|---|
Bash |
run, wait, interact, cancel |
Run bounded commands, continue background work, send input, and cancel processes. |
File |
read, list, search_name, search_content, write, edit, patch |
Read, find, and modify workspace files with structured, workspace-aware results. |
Git |
status, diff, log, show, blame |
Inspect repository state and history without parsing shell output. |
Run |
tests, verifiers |
Run project tests or independent verifier gates with structured results. |
Bash appears only when the active session/profile permits shell use. Plan
keeps it unavailable. In Act and Operate, the active permission posture,
sandbox, command policy, trusted paths, repository law, and managed policy still
apply. Full Access removes ordinary approval prompts; it does not bypass hard
safety or repository-policy holds.
File is capability-filtered by mode. Plan advertises its read-only actions;
write/edit actions require Act or Operate, and patch also requires the
apply-patch feature. The same read-before-edit, workspace, and policy checks used
by the former spellings remain in force.
Coordination tools
| Tool | Purpose |
|---|---|
agent |
Dispatch one focused sub-agent run and return an id, compact receipt, and transcript handle. |
remember |
Append one terse durable preference or convention when the user has enabled built-in memory. |
tasks |
Create, list, read, cancel, gate, and inspect durable task work through one action family. |
update_plan |
Publish optional high-level strategy, phases, constraints, verification, and handoff context. Reachable through tool_search only — see the default-active contract above. |
work_update |
Replace the concrete To-do / Work progress projection for the active thread or durable task. |
tool_search |
Discover and load a deferred tool only when the current turn needs it. |
work_update writes the sole canonical Work ledger. update_plan is
conversational reasoning — strategy, constraints, and route notes that help a
reader understand the approach. It is not a second Work surface, and plan-only
state never becomes model-facing Work grounding.
That distinction is enforced at the request boundary (#3983): the current To-do
snapshot is rendered by one bounded renderer
(crates/tui/src/work_grounding.rs) and appended to each parent turn-loop and
sub-agent step request as a transient <codewhale:work_state> block. Forked
sub-agents and /relay handoffs embed the byte-identical body. An empty To-do
emits no block at all.
Deferred and dynamic tools
Web is a conditional, deferred action tool with search, fetch, and wait
actions. It is discoverable through tool_search only when the active network
policy and runtime backend permit it; it is not one of the ten default-active
names.
The durable github, automation, and rlm action families are also deferred
by default. rlm owns open, eval, configure, and close actions for a
persistent sandboxed Python session. Feature-gated native tools may be added to
the active or deferred catalog only when their implementation and host
dependencies are available.
MCP tools are dynamic. Successfully connected servers register names such as
mcp_<server>_<tool> from ~/.codewhale/mcp.json; a failed or disabled server
must not be presented as an available model tool.
Inspect the model-client request tool payload
Run /tools after a model turn to inspect a bounded projection of the exact
tool field in the latest prepared model-client request. /tools json emits the
same evidence as bounded machine-readable JSON. Both formats open in a pager;
they are not copied into transcript history. /tool-studio remains a human-
command compatibility alias; it is not a model tool.
The snapshot distinguishes an absent tool field from a present empty array. It
reports the exact model-client tool JSON byte count and SHA-256 digest only when
measurement fits the one-MiB inspection bound; larger payloads stay unavailable.
Provider adapters may transform, sanitize, or omit those fields while building
a provider-specific wire body, so /tools marks provider delivery and the wire
payload unavailable. Capture and rendering are bounded: retained schemas,
descriptions, caller lists, catalog rows, turn IDs, and payload measurement all
carry explicit truncation, omission, or unavailable receipts. The snapshot stays
in memory only for the current session and is replaced on each prepared request.
Provider, model, approval, registry provenance, and runtime capability metadata
are not fields in the request tool schema. /tools therefore reports them as
unavailable instead of joining against mutable state or inferring values. Use
the separate route and permission receipts for those facts.
Modes and permission postures
Modes and permission postures are separate controls:
- Plan is read-only. It exposes the read-only
Fileprojection and other safe inspection capabilities, but no shell or file mutation. - Act is ordinary interactive execution.
- Operate uses the same direct-tool authority as Act while preferring Fleet workers for independent, parallel, isolated, background, or long-running work.
- Ask, Auto-Review, and Full Access control approval behavior within an action-capable mode. They never widen a Plan turn into write access.
See docs/MODES.md for the full mode and posture contract.
Removed spellings
The per-action single-purpose names below are not registered. They were
deleted, not hidden: a call to any of them fails with tool '<name>' is not registered, because resolve has deliberately no fuzzy step
(crates/tui/src/tools/registry.rs:313-316 — "a hallucinated name must fail,
never dispatch"). There is no replay path for them; a transcript that calls one
will not re-execute.
| Removed spelling | Use instead |
|---|---|
exec_shell, exec_shell_wait, exec_wait, exec_shell_interact, exec_interact, exec_shell_cancel |
Bash: run, wait, interact, cancel |
read_file, list_dir, grep_files, file_search, write_file, edit_file |
File: read, list, search_content, search_name, write, edit |
git_status, git_diff, git_log, git_show, git_blame |
Git: matching action |
run_tests, run_verifiers |
Run: tests, verifiers |
web_search, fetch_url, wait_for_dev_server |
Web: search, fetch, wait |
Enforced by shell_surface_contains_only_the_canonical_bash_tool
(registry.rs:2290, "{alias} must be removed") and the retired-name loop at
registry.rs:2066-2088 ("{retired} must stay removed" /
"{retired} must not be advertised").
Replay-only aliases
These legacy names are still registered so saved transcripts, sessions, and
recorded automation replay without migration. They are hidden from the model
catalog and from tool_search; new prompts and docs must use the canonical
action tools.
| Replay-only spellings | Canonical action |
|---|---|
apply_patch |
File: patch (also DeepSeek Responses' one custom tool) |
task_create, task_list, task_read |
tasks: matching action |
github_issue_context, github_pr_context, github_comment |
github: matching action |
automation_* |
automation: matching action |
rlm_open, rlm_eval, rlm_configure, rlm_close |
rlm: open, eval, configure, close |
checklist_*, todo_* |
work_update |
Replay compatibility does not make an alias a supported spelling for new model calls. Alias execution must stay behaviorally equivalent to its canonical action and must not add the alias back to the advertised catalog.
Long-running work
Use Bash with action: "run" for bounded commands. Set its background option
for work that may outlive a normal foreground wait, then use wait, interact,
or cancel against the returned process id. Live shell jobs are also visible in
/jobs; process-local jobs must be marked stale after restart rather than shown
as reattached processes.
Use tasks when the work itself needs a durable lifecycle, structured gates,
artifacts, replayable timelines, or a stable task id. Large tool results should
remain behind bounded handles or artifacts instead of being copied wholesale
into the parent transcript.
Parallel fan-out
The sub-agent capacity source of truth is
crates/tui/src/config/subagent_limits.rs:
- default configured concurrency: 64;
- maximum configured concurrency: 128;
- maximum admitted running-plus-queued work: 1024.
These are capacity ceilings, not advice to dispatch every available slot. A manager should use the smallest useful fan-out, preserve a single owner for fan-in, and verify worker receipts before reporting combined completion.
RLM child-query batching is a different, cheaper cost class. Its
sub_query_batch helper accepts 1–16 one-shot children inside a live rlm
session; it is not a substitute for tool-carrying agent workers.
Human inspection: /tools (/tool-studio)
/tools renders a read-only, bounded human projection of the tool field of
the request that was prepared for one (turn, step). It is not a second
registry and not an execution surface.
The seam. The snapshot is built in crates/tui/src/core/engine/turn_loop.rs
immediately after MessageRequest is constructed, from request.tools — the
same value the model client is handed. The engine resolves the surrounding
per-turn data once in engine.rs (ToolSurfaceContext: flattened registry
facts, the MCP pool's own server attribution, the engine-injected catalog names,
and the resolved model client's receipt) and passes it as plain data, so the
per-step seam never re-locks the MCP pool or holds a tool object.
Turn and step identity. The tool set can differ between steps of a turn, so
each snapshot is stamped with turn id and step and each seam emits its own. The
TUI keeps only the latest (SessionState.last_tool_request_snapshot). Before
the first seam there is no snapshot and /tools says so rather than rebuilding
a registry in the UI.
Two kinds of fact are kept apart:
- Wire facts come from the prepared request: name, description, schema,
defer_loading/strict/allowed_callers/cache_control, byte accounting, and the catalog digest. - Surface facts come from the
ToolSurfaceContext: provenance (builtin/plugin/mcp/synthetic/unknown), MCP server identity, declared capabilities, declared approval requirement, and model visibility.
Contract:
- One digest.
active_tool_catalog_sha256(crates/tui/src/core/engine/preview.rs) is the single definition of the active-tool-catalog hash. The request manifest publishes it asToolSurfaceFacts::active_tool_catalog_sha256and/toolsreports the same value for the same prepared request; neither surface keeps a hash of its own. - Nothing is guessed. MCP server identity is shown only when the real pool
attributed that exact model tool name.
McpPool::mcp_model_tool_nameis the single definition shared by the model catalog and the human attribution, and an ambiguous name (two servers colliding on one model name) resolves to no server. Synthetic provenance comes fromdefault_synthetic_catalog_tool_names, which is asserted against the engine's ownis_synthetic_catalog_toolpredicate. A transmitted tool with no registry entry reportscapabilities: unknown, never "none". - Provider availability follows the resolved client. It comes from
Engine::tool_surface_provider_receipt, never from "a tool registry exists". With no client the receipt isunavailableeven when the registry is full. - Unknown shrinks, it does not vanish.
unavailable_for_this_requestalways containsprovider_wire_payload: nothing on this path observes what the provider adapter finally transmits. It additionally containsproviderandmodelwithout a resolved client, andprovenance/capabilities/approvalwhen no surface context was captured. - Absent stays distinct from empty. A request with no tools field is not a
request with an empty tools array; an unresolved field is
unknownwith a reason, not a default. - Bounded. Rendering is capped by tool count (32), name, description, schema bytes, allowed-caller count, and a payload measurement bound, each with an explicit truncation or omission receipt. Registered tools that this request does not carry are reported as a bounded name list plus an exact count rather than expanding the projection.
- Inert. The snapshot lives beside the transcript, never in
session.messages, so it cannot enter a model request or perturb the provider's prefix cache. It never executes a tool, never reads credentials, never reorders the catalog, and is never registered as a model-callable tool. - Delivery is never claimed. The capture happens before connection setup, so
delivery_statusstaysunknown.
Release verification
Do not infer the public surface from handler function names. Verify the model catalog and alias visibility at the exact candidate SHA:
python3 scripts/measure-runtime-contract.py
cargo test -p codewhale-tui --bin codewhale-tui --locked shell_surface_contains_only_the_canonical_bash_tool
cargo test -p codewhale-tui --bin codewhale-tui --locked runtime_task_families_expose_only_canonical_tools
cargo test --locked -p codewhale-tui --bin codewhale-tui print_mode_tool_catalog_metrics -- --ignored --nocapture
Check the test names against the source before trusting a green run: cargo test
exits 0 with "0 passed; N filtered out" when a filter matches nothing, so a
misspelled filter is indistinguishable from a pass. (Three filters printed here
before v0.9.4 named tests that did not exist.)
The provider-free full-policy receipt enables built-in memory and must report the
nine default-active names listed above. A memory-disabled receipt truthfully omits
remember and reports eight. A separate repository-wide tool count may include deferred, dynamic,
feature-gated, and replay-only registrations; it is not the number of tools
placed in the first-turn model catalog.