返回 CodeWhale
TOOL_LIFECYCLE.md
根目录 / docs / TOOL_LIFECYCLE.md
1 # Historical Tool-Surface Lifecycle Policy (v0.8.53)
2
3 **Status:** Historical design record, not current runtime documentation. The
4 v0.9.1 canonical action surface and replay-only alias contract are documented in
5 [`RUNTIME_SIMPLIFICATION_DESIGN.md`](RUNTIME_SIMPLIFICATION_DESIGN.md) and
6 [`TOOL_SURFACE.md`](TOOL_SURFACE.md). No catalog code landed in this old cycle — the code
7 work is **deferred**. This document is the umbrella policy for GitHub **#2681**,
8 with **#2682** and **#2683** as concrete instances of the planned diet. It
9 describes *what will be done* and the invariants any future diet PR must hold.
10
11 **Scope of related open work (do not contradict):**
12 - PR **#2684** — subagent role vocabulary, lifecycle signals, eval ergonomics.
13 Legacy subagent-name cleanup + guardrail tests in this policy rebase on #2684.
14 - PR **#2685** — git-history active + RLM/field errors.
15
16 **What actually happened, so you can read the rest as history:** the "hidden
17 compatibility" plan below was *not* what shipped. The v0.9.x cutover **removed**
18 almost every alias this document promises to keep dispatchable. `exec_wait`,
19 `exec_interact`, all `checklist_*` and all `todo_*` are gone from the registry
20 and hard-error if called; only `tts`/`speech` survives as this document
21 describes, and `apply_patch` as the single replay-only alias.
22 [`TOOL_SURFACE.md`](TOOL_SURFACE.md) has the shipped contract and the tests that
23 pin it. Read §4 and §8 below as a rejected proposal, not as a guarantee.
24
25 **All file:line citations here were correct at v0.8.52/0.8.53 and are now
26 expired.** They have not been rewritten, because renumbering a historical
27 record makes it look current. Symbols this document cites that no longer exist
28 at all include `ARCEE_FIRST_TURN_NATIVE_TOOLS` and `apply_provider_tool_policy`
29 (both removed by `1bfcced43c`, "fix(engine): remove Arcee tool catalog
30 exception"), and the planned `HIDDEN_COMPATIBILITY_TOOLS` / `DEPRECATED_ALIASES`
31 sets, which were never written. Resolve any symbol here against the tree before
32 acting on it.
33
34 ---
35
36 ## 1. Purpose and the weaker-model problem
37
38 Codewhale ships a large native tool surface. The first-turn *active* partition
39 of that surface is what every model sees before it has run a single
40 `tool_search_*` call. Today that active set contains several **near-duplicate
41 tools** that map to the *same* implementation under different names:
42
43 - `exec_wait` and `exec_shell_wait` are both `ShellWaitTool`
44 (`crates/tui/src/tools/registry.rs:526,529`).
45 - `exec_interact` and `exec_shell_interact` are both `ShellInteractTool`
46 (`registry.rs:527,530`).
47 - `tts` and `speech` are both `SpeechTool`
48 (`registry.rs:787-792`, both deferred).
49 - `work_update`, `checklist_*`, and `todo_*` are the *same*
50 `TodoWriteTool` surface, with only `work_update` visible to models.
51
52 For a strong model, redundant names are harmless noise. For **weaker / smaller
53 models** (the Arcee Trinity lane, `deepseek-v4-flash` child executors, and any
54 non-thinking executor), every additional near-duplicate in the visible set is a
55 real cost:
56
57 - It widens the choice space with options that do *nothing distinct*, increasing
58 wrong-tool selection and oscillation between synonyms.
59 - It spends scarce first-turn catalog budget (Section 5) on zero-information
60 entries.
61 - It dilutes the "one name = one thing" contract that lets a small model reason
62 about the surface at all.
63
64 The lifecycle policy exists to **shrink and discipline the model-visible
65 surface** without ever breaking the ability to replay an old transcript that
66 referenced a now-retired name.
67
68 ### Canonical work-tracking surface for v0.9.1
69
70 The model-visible progress surface is a single tool: `work_update` (#4132).
71 Agents and Fleet workers use it for concrete To-do / Work progress under the
72 active runtime thread or durable task.
73
74 `task_*` and the Fleet/Workflow ledger remain the durable lifecycle owners.
75 Checklist metadata is the model-visible projection of progress:
76 `task_updates.checklist` carries the current items, completion percentage, and
77 in-progress item.
78
79 **The To-do is the only canonical Work ledger.** `update_plan` is conversational
80 reasoning — strategy, context, and route notes for complex initiatives. It is
81 not a progress surface, must not duplicate To-do items, and plan-only state is
82 never rendered as Work grounding.
83
84 Work grounding is one seam (#3983): `crates/tui/src/work_grounding.rs` renders
85 the To-do snapshot once, hard-bounded in both item count and characters, with
86 the in-progress item preserved preferentially and any elision marked. That body
87 is appended to each parent turn-loop and sub-agent step request as a transient
88 `<codewhale:work_state>` block — rebuilt per request, so a mid-turn
89 `work_update` is visible on the next step — and is never written to session
90 history or the stable system prefix.
91 Forked agents (`<codewhale:fork_state>`) and `/relay` reuse the same body.
92
93 Three properties of that seam are load-bearing:
94
95 - **Authority.** The snapshot is read from the `WorkRuntime` graph projection
96 when a runtime owns that list, because `work_update` stages there and only
97 publishes into the legacy `SharedTodoList` view later. Sessions with no
98 attached runtime read the list directly.
99 - **Per-agent isolation.** Every sub-agent gets the same tail rendered from
100 *its own* list (`#4810`), so a worker sees its own progress and never a
101 parent's or sibling's. The parent's ledger reaches a forked child only as the
102 immutable `<codewhale:fork_state>` Work section, resolved at the spawn seam so
103 a same-turn `work_update` is included.
104 - **Context accounting.** The parent turn-loop preflight token estimate runs
105 over the tail message that request actually carries, so it cannot approve a
106 request that goes over-limit once the block is appended. Offline counts stay
107 conservative estimates.
108
109 The renderer bounds and frames the ledger; it does not vet To-do content. It
110 guarantees that item text cannot close the wrapper early, cannot forge the line
111 format with control characters, and cannot exceed the item/character bounds —
112 not that arbitrary item text is safe to follow as instructions.
113
114 The legacy `checklist_*` and older `todo_*` names are hidden compatibility
115 aliases. They remain registered and dispatchable against the same To-do state
116 so old transcripts replay without data loss, but they are not advertised to the
117 model catalog.
118
119 ---
120
121 ## 2. The five lifecycle states
122
123 Every native tool name occupies exactly one lifecycle state.
124
125 | State | Meaning | Visible on first turn? | In `tool_search_*`? | Executes if called? | When used |
126 |---|---|---|---|---|---|
127 | **active** | Canonical, in the first-turn catalog head | **Yes** | n/a (already active) | Yes | The tool a model should reach for by default |
128 | **deferred** | Registered + discoverable, hydrated on demand | No | **Yes** | Yes | Real, useful tools that don't earn a first-turn slot |
129 | **hidden-compatibility** | Registered + dispatchable, but removed from active **and** from search | No | **No** | **Yes — identical behavior, silent** | Old synonym kept only so old transcripts replay; no model should newly discover it |
130 | **deprecated** | Like hidden-compat, but execution **appends a replacement notice to result metadata** | No | **No** | **Yes — works, plus a "use X instead" notice** | A retired name we actively steer callers off of, still safe to replay |
131 | **removed** | Not registered at all | No | No | **No — hard error** | Only after `planned_removal_version`, once replay support is formally dropped |
132
133 ### hidden-compatibility vs deprecated — be precise
134
135 Both states are **invisible** (not active, not in tool search) and both remain
136 **dispatchable** (calling them still works). The *only* difference is the
137 caller-facing signal:
138
139 - **hidden-compatibility:** completely silent. The tool behaves byte-for-byte
140 like its canonical twin. We use this when there is *no behavioral or naming
141 lesson to teach* — the name was a pure alias and we simply don't want models
142 re-learning it. (Example: `exec_wait` is literally `exec_shell_wait`.)
143 - **deprecated:** behaves identically *and succeeds*, but the tool result's
144 **metadata** carries an appended notice like
145 `"deprecated: use <replacement> instead"`. The notice goes **only in the
146 result metadata returned for that call** — never in the cached tool catalog
147 prefix (see Section 8). We use this when there is a canonical replacement we
148 want the caller (and any human reading the transcript) nudged toward.
149
150 Neither state ever changes the *behavior* of the call. Replay always works.
151
152 ---
153
154 ## 3. Representation in code
155
156 The lifecycle is represented as **const name-sets plus an alias/manifest table**
157 in `crates/tui/src/core/engine/tool_catalog.rs`, alongside the existing
158 `DEFAULT_ACTIVE_NATIVE_TOOLS` (`tool_catalog.rs:37-64`) and
159 `ARCEE_FIRST_TURN_NATIVE_TOOLS` (`tool_catalog.rs:106-115`).
160
161 ### 3a. Name-sets and the manifest (sketch)
162
163 ```rust
164 // crates/tui/src/core/engine/tool_catalog.rs (planned)
165
166 /// Tools removed from the active set AND from tool-search, but still
167 /// registered and dispatchable with byte-identical behavior. Silent.
168 pub(super) const HIDDEN_COMPATIBILITY_TOOLS: &[&str] = &[
169 "exec_wait", // == exec_shell_wait (ShellWaitTool)
170 "exec_interact", // == exec_shell_interact (ShellInteractTool)
171 "tts", // == speech (SpeechTool)
172 "checklist_write", // == work_update (TodoWriteTool)
173 "checklist_add", // == work_update single-item add
174 "checklist_update", // == work_update single-item update
175 "checklist_list", // == work_update list
176 "todo_write", // == work_update
177 "todo_add", // == work_update single-item add
178 "todo_update", // == work_update single-item update
179 "todo_list", // == work_update list
180 ];
181
182 /// Deprecated aliases: invisible + dispatchable, with a replacement notice
183 /// appended to RESULT METADATA only (never the cached prefix).
184 pub(super) struct DeprecatedAlias {
185 pub name: &'static str,
186 pub replacement: &'static str,
187 pub note: &'static str,
188 }
189
190 pub(super) const DEPRECATED_ALIASES: &[DeprecatedAlias] = &[
191 // Empty in the #4132 work-surface cutover: checklist_* and todo_* are
192 // silent hidden-compatibility aliases of work_update for transcript replay.
193 ];
194
195 #[inline]
196 pub(super) fn is_hidden_or_deprecated(name: &str) -> bool {
197 HIDDEN_COMPATIBILITY_TOOLS.contains(&name)
198 || DEPRECATED_ALIASES.iter().any(|d| d.name == name)
199 }
200 ```
201
202 ### 3b. The two filter points
203
204 1. **Catalog / tool-search exclusion (tool_catalog.rs).**
205 Deferral is decided by `should_default_defer_tool` (`tool_catalog.rs:66-82`),
206 and the active set is the head built by `build_model_tool_catalog`
207 (`tool_catalog.rs:178-196`). Hidden-compat and deprecated tools must be
208 forced *out of the active head* and *out of the tool-search-discoverable
209 pool*. Concretely, the deferral predicate gains a short-circuit so these
210 names are never active, and the tool-search index builder skips any name for
211 which `is_hidden_or_deprecated(name)` is true. Arcee's narrowed first-turn
212 path (`apply_provider_tool_policy`, `tool_catalog.rs:134-149`) already
213 excludes them by construction since they aren't in
214 `ARCEE_FIRST_TURN_NATIVE_TOOLS`.
215
216 2. **Result-notice append (tool_routing.rs).**
217 Dispatch already routes by tool name in
218 `crates/tui/src/tui/tool_routing.rs` (e.g. the wait/interact unification at
219 `tool_routing.rs:1139-1140`). After a successful dispatch, if the called name
220 is in `DEPRECATED_ALIASES`, the router appends the matching `note` to the
221 **result metadata only**. Hidden-compat names append nothing.
222
223 ### 3c. Why name-sets, not a per-`ToolSpec` enum field
224
225 A per-`ToolSpec` `lifecycle: Lifecycle` field was rejected for three reasons:
226
227 - **Prefix-cache safety.** The tool catalog array is part of DeepSeek's
228 immutable KV prefix (`tool_catalog.rs:169-177`). A per-spec field invites
229 serializing lifecycle state *into* each tool's schema, which is exactly the
230 kind of head mutation that forces a full re-prefill. Name-sets live entirely
231 in the catalog-build logic and never touch the emitted tool JSON.
232 - **Single source of truth + diffability.** The diet for a release is one small,
233 reviewable edit to two or three const arrays in one file, instead of scattered
234 field flips across many tool modules.
235 - **Registration stays orthogonal.** Tools remain registered exactly as today
236 (e.g. `with_shell_tools`, `registry.rs:523-531`). Lifecycle is a *catalog
237 policy* layered on top of registration, not a property baked into the tool.
238
239 ---
240
241 ## 4. Deprecation manifest (the #2681 acceptance-criteria table)
242
243 This was the proposed manifest. Columns are the #2681 AC columns. No entry was
244 "removed" in 0.8.53; replay was to be supported for everything listed.
245
246 > **Superseded.** Ten of the eleven rows below were removed rather than kept
247 > hidden-compatible. `exec_wait` and `exec_interact` are asserted absent at
248 > `registry.rs:2290-2331`; `checklist_*` and `todo_*` at `registry.rs:1476-1490`
249 > ("must no longer be callable"). Only `tts` is still dispatchable. The
250 > `replay_supported = Yes` column is false for everything except `tts`.
251
252 | Alias | Replacement (canonical) | Lifecycle state | first_deprecated_version | planned_removal_version | replay_supported |
253 |---|---|---|---|---|---|
254 | `exec_wait` | `exec_shell_wait` | hidden-compatibility | 0.8.53 | TBD (≥ 0.9.x) | Yes |
255 | `exec_interact` | `exec_shell_interact` | hidden-compatibility | 0.8.53 | TBD (≥ 0.9.x) | Yes |
256 | `tts` | `speech` | hidden-compatibility | 0.8.53 | TBD (≥ 0.9.x) | Yes |
257 | `checklist_write` | `work_update` | hidden-compatibility | 0.9.0 | TBD (≥ 0.9.x) | Yes |
258 | `checklist_add` | `work_update` | hidden-compatibility | 0.9.0 | TBD (≥ 0.9.x) | Yes |
259 | `checklist_update` | `work_update` | hidden-compatibility | 0.9.0 | TBD (≥ 0.9.x) | Yes |
260 | `checklist_list` | `work_update` | hidden-compatibility | 0.9.0 | TBD (≥ 0.9.x) | Yes |
261 | `todo_write` | `work_update` | hidden-compatibility | 0.8.53 | TBD (≥ 0.9.x) | Yes |
262 | `todo_add` | `work_update` | hidden-compatibility | 0.8.53 | TBD (≥ 0.9.x) | Yes |
263 | `todo_update` | `work_update` | hidden-compatibility | 0.8.53 | TBD (≥ 0.9.x) | Yes |
264 | `todo_list` | `work_update` | hidden-compatibility | 0.8.53 | TBD (≥ 0.9.x) | Yes |
265
266 The `todo_*` aliases first entered hidden compatibility in v0.8.53. v0.9.0
267 changes their canonical replacement to `work_update`; it does not reset their
268 first-deprecated version.
269
270 **Legacy subagent names — removed, no manifest entry needed.**
271 The model-visible subagent surface is only `agent`. The old lifecycle names and
272 the experimental tool-agent lane were removed rather than kept as hidden
273 compatibility tools.
274
275 `planned_removal_version` is intentionally `TBD`: a name only moves to **removed**
276 once we formally drop replay for transcripts old enough to contain it, which is a
277 separate, deliberate decision per name.
278
279 ---
280
281 ## 5. Active-catalog budget (per mode, per provider)
282
283 The active set is the first-turn cost. Do not duplicate the exact
284 `DEFAULT_ACTIVE_NATIVE_TOOLS` count here: adjacent PRs in the v0.8.53 batch may
285 add or remove active tools, and the source of truth is always
286 `tool_catalog.rs`. This document defines the diet policy and invariants, not a
287 second catalog snapshot.
288
289 ### Per provider
290
291 | Provider | First-turn active source | Budget policy |
292 |---|---|---|
293 | Default (DeepSeek et al.) | `DEFAULT_ACTIVE_NATIVE_TOOLS` | Remove duplicate aliases from the active head when their canonical twins stay active; any net growth needs an explicit budget decision. |
294 | Arcee (Trinity) | `ARCEE_FIRST_TURN_NATIVE_TOOLS` | Provider-specific read-only WAF workaround; unchanged by the default diet unless explicitly reviewed. |
295
296 The default diet removes `exec_wait` and `exec_interact` from the active head
297 (they become hidden-compat; their canonical twins `exec_shell_wait` /
298 `exec_shell_interact` stay). `tts` and `todo_*` are *already not* in the active
299 set, so they do not change the active budget in this diet. The net effect of
300 this specific diet is to remove two duplicate active aliases from whatever
301 default active head is current after the surrounding v0.8.53 PR batch.
302
303 ### Per mode (Plan / Agent / YOLO)
304
305 The native active head is the **same set across modes** by design — mode does not
306 add or remove native tools from `DEFAULT_ACTIVE_NATIVE_TOOLS`
307 (`should_default_defer_tool` ignores `_mode` for native tools,
308 `tool_catalog.rs:66-68`). Mode affects **MCP** deferral instead:
309 `apply_mcp_tool_deferral` keeps MCP tools deferred unless `mode == Yolo`
310 (`tool_catalog.rs:162-167`).
311
312 | Mode | Native active budget | MCP tools active? |
313 |---|---|---|
314 | Plan | same native head | No (deferred) |
315 | Agent | same native head | No (deferred) |
316 | YOLO | same native head | Yes (a known, intentional widening) |
317
318 **Budget rule:** the native active head must stay byte-identical across Plan ↔
319 Agent ↔ YOLO (Section 8). Any growth of the head requires retiring something
320 else or an explicit budget bump in this doc.
321
322 ---
323
324 ## 6. The canonical-surface rule
325
326 > **Every model-visible (active or deferred-discoverable) tool must have one
327 > clear niche. If a tool is superseded, it gets a named replacement and moves to
328 > hidden-compatibility or deprecated — it does not stay visible.**
329
330 ### Canonical vs compatibility summary for the confusing clusters
331
332 | Cluster | Canonical (keep visible) | Compatibility / retired | Notes |
333 |---|---|---|---|
334 | **Shell wait** | `exec_shell_wait` | `exec_wait` → hidden-compat | Same `ShellWaitTool` (`registry.rs:526,529`); router already unifies (`tool_routing.rs:1139`) |
335 | **Shell interact** | `exec_shell_interact` | `exec_interact` → hidden-compat | Same `ShellInteractTool` (`registry.rs:527,530`) |
336 | **Work progress / checklist / todo** | `work_update` | `checklist_write/add/update/list`, `todo_write/add/update/list` → hidden-compat | Same `TodoWriteTool`; compatibility names replay old transcripts only |
337 | **Speech / tts** | `speech` | `tts` → hidden-compat | Same `SpeechTool` (`registry.rs:787-792`) |
338 | **Subagent lifecycle** | `agent` | old lifecycle names and tool-agent lane removed | Single async launcher. (The "child agents are leaf workers" note here did not ship — see §7.) |
339 | **Edit family** | `apply_patch`, `edit_file`, `write_file`, `fim_edit` | none — **all distinct niches** | NOT touched (per #2681 non-goals); doc-only canonical guidance |
340 | **Search family** | `grep_files` (content), `file_search` (filename), `project_map` (structure) | none — **distinct niches** | NOT touched; no FTS5/BM25/semantic index exists today |
341
342 **Non-goals (explicitly NOT diet targets in this cycle, per #2681):**
343 `apply_patch` / `edit_file` / `write_file` / `fim_edit`;
344 `grep_files` / `file_search` / `project_map`;
345 `fetch_url` / `web.run` / `web_search`;
346 `task_shell_*`; `handle_read` / `retrieve_tool_result`. These have distinct
347 niches and receive **canonical guidance only** — no lifecycle change.
348
349 The RLM surface (`rlm_open` / `rlm_eval` / `rlm_configure` / `rlm_close` /
350 `rlm_session_objects`, `crates/tui/src/tools/rlm.rs`) is likewise out of scope;
351 `handle_read` retrieves var handles, and `finalize` / `FINAL` is an in-kernel
352 Python function, **not a tool** — so there is nothing to retire there.
353
354 ---
355
356 ## 7. Subagent cutover decision: one visible launcher
357
358 The old lifecycle trio and tool-agent lane are removed, not hidden compatibility
359 tools.
360
361 **Decision: expose only `agent`.**
362
363 - `agent` starts one focused background child and returns the agent id plus
364 transcript handle.
365 - Child results arrive as completion events. The parent should keep working
366 instead of polling a lifecycle tool.
367 - Child tool catalogs exclude the removed subagent *lifecycle* tools.
368 (**Not as shipped:** this bullet originally continued "so children are leaf
369 workers and cannot recursively summon more agents." That is not what landed.
370 Children receive `agent` and can recurse to the configured depth — see
371 `with_full_agent_surface_options` and `can_spawn_child` in
372 `tools/subagent/mod.rs`, and [`SUBAGENTS.md`](SUBAGENTS.md).)
373 - Detailed inspection goes through `handle_read` on the returned transcript
374 handle.
375
376 This is a lifecycle simplification, not a provider gate.
377
378 ---
379
380 ## 8. Prefix-cache safety + replay guarantee
381
382 ### Prefix-cache rules every diet PR MUST follow
383
384 The tools array is part of DeepSeek's immutable KV prefix. The catalog-head
385 byte-stability invariant (`tool_catalog.rs:169-196`) is binding:
386
387 1. **Never mutate the active head non-deterministically.** The first-turn active
388 block must be **byte-identical run-to-run** and across Plan ↔ Agent ↔ YOLO.
389 2. **A diet is a one-time deterministic edit.** Removing a name from
390 `DEFAULT_ACTIVE_NATIVE_TOOLS` shifts the head exactly once; after that it must
391 be stable. Land such edits as their own focused change.
392 3. **Notices live in result metadata, never the prefix.** Deprecated replacement
393 notes are appended at dispatch time in `tool_routing.rs` to the *call result*
394 only. **Nothing** about hidden/deprecated state may be serialized into a tool
395 schema, description, or the catalog array.
396 4. **Preserve ordering and partitioning.** `build_model_tool_catalog` sorts each
397 partition by name and keeps built-ins as a contiguous prefix ahead of MCP
398 tools (`tool_catalog.rs:186-194`). Diet edits must not break this.
399 5. **Hidden/deprecated tools are excluded *before* the head is built**, so their
400 removal is the only head change — they do not appear in the prefix at all.
401
402 ### Old-transcript replay guarantee (not adopted)
403
404 The guarantee below was proposed, not shipped. Of the names it calls out by
405 hand, only `tts` is still dispatchable; `exec_wait`, `exec_interact`, and every
406 `todo_*` were removed outright. Replaying an old transcript that calls one of
407 those does *not* produce the same result it always did — it fails as an unknown
408 tool. The shipped replay surface is a single alias, `apply_patch`; see
409 [`TOOL_SURFACE.md`](TOOL_SURFACE.md).
410
411 > For every name in the deprecation manifest with `replay_supported = Yes`, the
412 > tool stays **registered and dispatchable with identical behavior**. Replaying
413 > an old transcript that calls `exec_wait`, `exec_interact`, `tts`, or any
414 > `todo_*` produces the same result it always did. Deprecated names additionally
415 > attach a result-metadata notice; hidden-compat names are silent. A name is only
416 > ever made non-dispatchable (**removed**) after a deliberate, per-name decision
417 > to drop replay support at `planned_removal_version`.
418
419 ---
420
421 ## 9. Required tests
422
423 Any diet PR (and the umbrella #2681 work) must add/keep:
424
425 1. **Duplicate-active-alias guard.** A test asserting that no name in
426 `HIDDEN_COMPATIBILITY_TOOLS` or `DEPRECATED_ALIASES` appears in
427 `DEFAULT_ACTIVE_NATIVE_TOOLS` or `ARCEE_FIRST_TURN_NATIVE_TOOLS`, and that no
428 two active entries resolve to the same underlying tool implementation.
429
430 2. **Tool-search exclusion test.** Assert that hidden-compat and deprecated names
431 are absent from the tool-search-discoverable pool while remaining present in
432 the registry (dispatchable).
433
434 3. **Replay / dispatch tests.** For each manifest name, calling it still
435 executes and returns the same result as its canonical twin. Deprecated names
436 additionally assert the replacement note is present **in result metadata** and
437 absent from the catalog/prefix. Hidden-compat names assert **no** added
438 notice.
439
440 4. **Golden active-block byte test.** A snapshot test pinning the byte
441 serialization of the first-turn active tool block, asserting it is identical
442 across Plan / Agent / YOLO (native head) and stable run-to-run — enforcing the
443 `tool_catalog.rs:169-196` invariant. The golden updates **only** as a
444 reviewed, deliberate one-time edit when the diet lands.
445
446 5. **Subagent guardrail test.** Assert only `agent` is registered as a
447 model-visible subagent tool and that hidden/legacy names from
448 `subagent/mod.rs` are not advertised.
449
450 6. **Leaf-worker test.** Assert subagent tool catalogs exclude `agent` and
451 retired legacy lifecycle names.
452
452 lines MARKDOWN