返回 CodeWhale
COMMAND_CONTROL_PLANE.md
根目录 / docs / COMMAND_CONTROL_PLANE.md
1 # Shared command / control-plane contract
2
3 Issues #1888 and #4022.
4
5 Codewhale exposes the same lifecycle operations on three surfaces: a slash
6 command typed into the composer, a bound hotbar slot, and a CLI entrypoint.
7 Before this contract those three could — and did — drift: `/fleet status`
8 showed the current session's sub-agents while `codewhale fleet status` read the
9 durable ledger, and the CLI's Lane verbs had no slash equivalent at all.
10
11 The contract is one typed descriptor table plus one executor per domain, in
12 [`crates/lane/src/control.rs`](../crates/lane/src/control.rs) and
13 [`crates/tui/src/fleet/control.rs`](../crates/tui/src/fleet/control.rs).
14 `codewhale-lane` is the lowest crate the thin CLI facade and the TUI both
15 already depend on, so there is exactly one place the contract can live without
16 forking.
17
18 ## Vocabulary
19
20 Unchanged and load-bearing: **Fleet = who**, **Workflow = order**, **Lane = one
21 running Workflow**, **Runtime = where/how**. Auto-Review is a permission
22 posture, never a reviewer role. There is no "Operation" product noun; the
23 internal `ControlOperation` type names control-plane *verbs* and never appears
24 in user-facing copy.
25
26 ## What a descriptor pins down
27
28 Every `(domain, verb)` pair has exactly one `OperationDescriptor`, keyed by a
29 stable id of the form `<domain>.<verb>`:
30
31 | Field | Meaning |
32 | --- | --- |
33 | `id` | `lane.status`, `fleet.interrupt`, … — the same string on every surface and in every receipt |
34 | `authority` | `read` or `write`. Not a permission posture: it says whether the verb observes durable state or mutates it |
35 | `persistence` | Which durable store the effect lands in (`lane_registry`, `fleet_ledger`) |
36 | `target` | What exact identity it acts on (`none`, `lane_run`, `fleet_worker`, `fleet_run`) |
37 | `retry` | `idempotent` or `unsafe` |
38 | `surfaces` | Which surfaces offer it |
39 | `backend` | `Implemented`, `NotImplemented { hint }`, or `SurfaceLimited { available_on, hint }` |
40 | `slash_command` / `cli_invocation` | The exact bindings; the hotbar action id is always `slash.<slash_command>` |
41
42 The verb table today:
43
44 | Verb | Lane | Fleet |
45 | --- | --- | --- |
46 | `list` | read, whole registry | read, whole ledger |
47 | `status` | read, one Lane | read, whole ledger |
48 | `interrupt` | write, one Lane (idempotent) | write, one worker (idempotent) |
49 | `restart` | **no backend** — a Lane is re-created, not restarted | CLI-only (drives the manager loop) |
50 | `resume` | **no backend** — a stopped Lane's Runtime session is gone | write, one run (idempotent) |
51
52 ## No surface advertises what it cannot do
53
54 `OperationDescriptor::availability(surface, ctx)` returns either `Available` or
55 a typed `UnavailableReason` with a sanitized hint:
56
57 - `backend_not_implemented` — nobody has built it. Every surface refuses.
58 - `surface_not_supported` — the backend exists but not here. The hint names the
59 surface that works (`codewhale fleet restart <worker-id>`).
60 - `no_lane_registry` / `no_fleet_ledger` — the durable store does not exist yet.
61
62 Availability is probed **read-only**. `LaneRegistry::open_default` and
63 `FleetManager::open` both create their store as a side effect, so a status verb
64 probes `lane_registry_root()` / `fleet_ledger_path()` first. Otherwise "this
65 workspace has no Fleet ledger" silently becomes "here is an empty Fleet ledger
66 I just made".
67
68 ## Exact run identity
69
70 `parse_target` is the single target parser for all three surfaces: exactly one
71 token, exact ids only (no prefix or fuzzy matching), ASCII alphanumerics plus
72 `-`, `_`, `.`, no path separators, and a hard reject when a targetless verb is
73 handed an argument.
74
75 A write may be **fenced** by appending `@<lifecycle-seq>`:
76
77 ```
78 codewhale lane interrupt lane-a1b2c3d4@3
79 /lane interrupt lane-a1b2c3d4@3
80 ```
81
82 If the durable record has moved past sequence 3, the verb is rejected with a
83 `conflict` failure and the observed sequence, instead of stopping whatever
84 happens to be there now.
85
86 ## Receipts
87
88 Every invocation returns a `ControlReceipt` carrying the operation id, surface,
89 authority, persistence scope, availability, target, `LifecycleOutcome`
90 (`inspected`, `transitioned`, `no_change`, `rejected`, `failed`), the observed
91 lifecycle sequence, retryability, an optional bounded sanitized failure, and an
92 optional bounded run page. `ControlReceipt::render()` is the only renderer; the
93 CLI prints it and the slash command returns it as a message. `--json` on the
94 Lane verbs emits the same struct.
95
96 ## Typed unknowns
97
98 Run DTOs never imply absence. `Known<T>` is either `Known(value)` or
99 `Unknown(reason)` where the reason is `not_recorded`, `not_applicable`, or
100 `redacted`, and renders as `<not_recorded>` rather than a blank or a plausible
101 default.
102
103 Concretely: the Fleet receipt's `FleetResolvedRoute` records the **effective**
104 reasoning tier only, so `requested_reasoning` is `not_recorded` — it is not
105 back-filled from the effective value, and `reasoning_downgraded()` returns
106 `None` rather than guessing. The Lane registry records no route or usage at
107 all, so those fields are uniformly `not_recorded`. Fleet runs are fenced per
108 task rather than per run, so a Fleet run's `lifecycle_seq` is
109 `not_applicable`.
110
111 ## Bounds and redaction
112
113 - Run lists are pages: `DEFAULT_RUN_LIST_LIMIT` (50) with a hard
114 `MAX_RUN_LIST_LIMIT` (200) ceiling, and the page reports `total` and
115 `truncated` so a bound is never mistaken for an empty result.
116 - Status worker rows and inspection artifact rows cap at 24 with an explicit
117 omission notice.
118 - Receipt detail caps at `MAX_DETAIL_LINES` (40) lines of `MAX_DETAIL_LINE_CHARS`
119 (240) characters.
120 - Every operator-visible string passes through `sanitize_line`: `$HOME`-rooted
121 paths collapse to `~/…`, credential-shaped `key=value` pairs and known token
122 prefixes (`sk-`, `ghp_`, `xoxb-`, `Bearer`, …) become `[redacted]`.
123
124 ## Model-visible tool surface
125
126 Unchanged. This work adds no tool, no tool parameter, and no prompt text; the
127 model-facing sub-agent surface is still `agent` only. No tool-schema regression
128 measurement is required.
129
130 ## Tests
131
132 - `crates/lane/src/control.rs` — descriptor-table integrity, the five-verb
133 symmetry across both domains, authority/persistence/target agreement across
134 surfaces, availability rules, target parsing and lifecycle fencing, receipt
135 round-trips, bounding, and redaction; plus executor tests proving all three
136 surfaces get byte-identical results for the same durable Lane and that
137 interrupt is idempotent and fenced.
138 - `crates/tui/src/fleet/control.rs` — route/usage DTO projection with typed
139 unknowns, bounded pages and rows, absent-ledger reporting without creation,
140 CLI-only `fleet.restart`, and cross-surface identity of `fleet.status`.
141 - `crates/tui/src/commands/groups/core/lane.rs` and `…/fleet.rs` — slash verbs
142 map onto the shared operations, `/fleet status` reads the durable ledger
143 rather than session sub-agents, and bare dispatch (what the hotbar fires) is
144 read-only.
145 - `crates/cli/src/lib.rs` — the CLI exposes exactly the declared Lane verbs
146 under the same ids, and `lane stop` is a compatibility spelling of
147 `lane interrupt`.
148
148 lines MARKDOWN