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