1ebb684be6
Add `docs/COMMAND_CONTROL_PLANE.md` describing the shared contract: stable `<domain>.<verb>` ids, read-vs-write authority, persistence scope, exact target selection, typed unavailability, and the bounded receipt. Update `docs/FLEET.md` for the new verbs and the `/fleet status` vs `/fleet workers` split, and record both entries in the changelog. Refs #1888, #4022.
148 lines
7.0 KiB
Markdown
148 lines
7.0 KiB
Markdown
# Shared command / control-plane contract
|
|
|
|
Issues #1888 and #4022.
|
|
|
|
Codewhale exposes the same lifecycle operations on three surfaces: a slash
|
|
command typed into the composer, a bound hotbar slot, and a CLI entrypoint.
|
|
Before this contract those three could — and did — drift: `/fleet status`
|
|
showed the current session's sub-agents while `codewhale fleet status` read the
|
|
durable ledger, and the CLI's Lane verbs had no slash equivalent at all.
|
|
|
|
The contract is one typed descriptor table plus one executor per domain, in
|
|
[`crates/lane/src/control.rs`](../crates/lane/src/control.rs) and
|
|
[`crates/tui/src/fleet/control.rs`](../crates/tui/src/fleet/control.rs).
|
|
`codewhale-lane` is the lowest crate the thin CLI facade and the TUI both
|
|
already depend on, so there is exactly one place the contract can live without
|
|
forking.
|
|
|
|
## Vocabulary
|
|
|
|
Unchanged and load-bearing: **Fleet = who**, **Workflow = order**, **Lane = one
|
|
running Workflow**, **Runtime = where/how**. Auto-Review is a permission
|
|
posture, never a reviewer role. There is no "Operation" product noun; the
|
|
internal `ControlOperation` type names control-plane *verbs* and never appears
|
|
in user-facing copy.
|
|
|
|
## What a descriptor pins down
|
|
|
|
Every `(domain, verb)` pair has exactly one `OperationDescriptor`, keyed by a
|
|
stable id of the form `<domain>.<verb>`:
|
|
|
|
| Field | Meaning |
|
|
| --- | --- |
|
|
| `id` | `lane.status`, `fleet.interrupt`, … — the same string on every surface and in every receipt |
|
|
| `authority` | `read` or `write`. Not a permission posture: it says whether the verb observes durable state or mutates it |
|
|
| `persistence` | Which durable store the effect lands in (`lane_registry`, `fleet_ledger`) |
|
|
| `target` | What exact identity it acts on (`none`, `lane_run`, `fleet_worker`, `fleet_run`) |
|
|
| `retry` | `idempotent` or `unsafe` |
|
|
| `surfaces` | Which surfaces offer it |
|
|
| `backend` | `Implemented`, `NotImplemented { hint }`, or `SurfaceLimited { available_on, hint }` |
|
|
| `slash_command` / `cli_invocation` | The exact bindings; the hotbar action id is always `slash.<slash_command>` |
|
|
|
|
The verb table today:
|
|
|
|
| Verb | Lane | Fleet |
|
|
| --- | --- | --- |
|
|
| `list` | read, whole registry | read, whole ledger |
|
|
| `status` | read, one Lane | read, whole ledger |
|
|
| `interrupt` | write, one Lane (idempotent) | write, one worker (idempotent) |
|
|
| `restart` | **no backend** — a Lane is re-created, not restarted | CLI-only (drives the manager loop) |
|
|
| `resume` | **no backend** — a stopped Lane's Runtime session is gone | write, one run (idempotent) |
|
|
|
|
## No surface advertises what it cannot do
|
|
|
|
`OperationDescriptor::availability(surface, ctx)` returns either `Available` or
|
|
a typed `UnavailableReason` with a sanitized hint:
|
|
|
|
- `backend_not_implemented` — nobody has built it. Every surface refuses.
|
|
- `surface_not_supported` — the backend exists but not here. The hint names the
|
|
surface that works (`codewhale fleet restart <worker-id>`).
|
|
- `no_lane_registry` / `no_fleet_ledger` — the durable store does not exist yet.
|
|
|
|
Availability is probed **read-only**. `LaneRegistry::open_default` and
|
|
`FleetManager::open` both create their store as a side effect, so a status verb
|
|
probes `lane_registry_root()` / `fleet_ledger_path()` first. Otherwise "this
|
|
workspace has no Fleet ledger" silently becomes "here is an empty Fleet ledger
|
|
I just made".
|
|
|
|
## Exact run identity
|
|
|
|
`parse_target` is the single target parser for all three surfaces: exactly one
|
|
token, exact ids only (no prefix or fuzzy matching), ASCII alphanumerics plus
|
|
`-`, `_`, `.`, no path separators, and a hard reject when a targetless verb is
|
|
handed an argument.
|
|
|
|
A write may be **fenced** by appending `@<lifecycle-seq>`:
|
|
|
|
```
|
|
codewhale lane interrupt lane-a1b2c3d4@3
|
|
/lane interrupt lane-a1b2c3d4@3
|
|
```
|
|
|
|
If the durable record has moved past sequence 3, the verb is rejected with a
|
|
`conflict` failure and the observed sequence, instead of stopping whatever
|
|
happens to be there now.
|
|
|
|
## Receipts
|
|
|
|
Every invocation returns a `ControlReceipt` carrying the operation id, surface,
|
|
authority, persistence scope, availability, target, `LifecycleOutcome`
|
|
(`inspected`, `transitioned`, `no_change`, `rejected`, `failed`), the observed
|
|
lifecycle sequence, retryability, an optional bounded sanitized failure, and an
|
|
optional bounded run page. `ControlReceipt::render()` is the only renderer; the
|
|
CLI prints it and the slash command returns it as a message. `--json` on the
|
|
Lane verbs emits the same struct.
|
|
|
|
## Typed unknowns
|
|
|
|
Run DTOs never imply absence. `Known<T>` is either `Known(value)` or
|
|
`Unknown(reason)` where the reason is `not_recorded`, `not_applicable`, or
|
|
`redacted`, and renders as `<not_recorded>` rather than a blank or a plausible
|
|
default.
|
|
|
|
Concretely: the Fleet receipt's `FleetResolvedRoute` records the **effective**
|
|
reasoning tier only, so `requested_reasoning` is `not_recorded` — it is not
|
|
back-filled from the effective value, and `reasoning_downgraded()` returns
|
|
`None` rather than guessing. The Lane registry records no route or usage at
|
|
all, so those fields are uniformly `not_recorded`. Fleet runs are fenced per
|
|
task rather than per run, so a Fleet run's `lifecycle_seq` is
|
|
`not_applicable`.
|
|
|
|
## Bounds and redaction
|
|
|
|
- Run lists are pages: `DEFAULT_RUN_LIST_LIMIT` (50) with a hard
|
|
`MAX_RUN_LIST_LIMIT` (200) ceiling, and the page reports `total` and
|
|
`truncated` so a bound is never mistaken for an empty result.
|
|
- Status worker rows and inspection artifact rows cap at 24 with an explicit
|
|
omission notice.
|
|
- Receipt detail caps at `MAX_DETAIL_LINES` (40) lines of `MAX_DETAIL_LINE_CHARS`
|
|
(240) characters.
|
|
- Every operator-visible string passes through `sanitize_line`: `$HOME`-rooted
|
|
paths collapse to `~/…`, credential-shaped `key=value` pairs and known token
|
|
prefixes (`sk-`, `ghp_`, `xoxb-`, `Bearer`, …) become `[redacted]`.
|
|
|
|
## Model-visible tool surface
|
|
|
|
Unchanged. This work adds no tool, no tool parameter, and no prompt text; the
|
|
model-facing sub-agent surface is still `agent` only. No tool-schema regression
|
|
measurement is required.
|
|
|
|
## Tests
|
|
|
|
- `crates/lane/src/control.rs` — descriptor-table integrity, the five-verb
|
|
symmetry across both domains, authority/persistence/target agreement across
|
|
surfaces, availability rules, target parsing and lifecycle fencing, receipt
|
|
round-trips, bounding, and redaction; plus executor tests proving all three
|
|
surfaces get byte-identical results for the same durable Lane and that
|
|
interrupt is idempotent and fenced.
|
|
- `crates/tui/src/fleet/control.rs` — route/usage DTO projection with typed
|
|
unknowns, bounded pages and rows, absent-ledger reporting without creation,
|
|
CLI-only `fleet.restart`, and cross-surface identity of `fleet.status`.
|
|
- `crates/tui/src/commands/groups/core/lane.rs` and `…/fleet.rs` — slash verbs
|
|
map onto the shared operations, `/fleet status` reads the durable ledger
|
|
rather than session sub-agents, and bare dispatch (what the hotbar fires) is
|
|
read-only.
|
|
- `crates/cli/src/lib.rs` — the CLI exposes exactly the declared Lane verbs
|
|
under the same ids, and `lane stop` is a compatibility spelling of
|
|
`lane interrupt`.
|