| 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 |