---
id: PRD-0011
title: "M10 — CLI Ergonomics & Operator Visibility"
status: done
owner: "Aaron Roney"
created: 2026-07-03
updated: 2026-07-03

depends_on:
- PRD-0009

principles:
- "Every durable moderation state must be auditable from the CLI — no write-only state (the ban/invite corollary of the acl-list fix)."
- "The CLI is also a human client: a person should be able to watch and post to channels without a Claude session."
- "Wire changes stay append-only; existing response frames are reused where they fit (UserList) and new ones are added only when the shape demands it (InviteList)."
- "Every verb ships a test (constitution §8); e2e drives the real binary."

references:
- name: "Conclave design (CLI surface §13, presence/heartbeat §10, aggregation-log future §19)"
  url: "docs/DESIGN.md"
- name: "Operator-visibility precedent: admin channel enumeration + acl list (e72aeb8)"
  url: ".prds/PRD-0007.md"

acceptance_tests:
- id: uat-001
  name: "A channel admin can list a channel's bans; unban lifts a durable ban (without granting ACL membership); both are refused for non-admins"
  command: cargo nextest run -E 'test(/ban_visibility/)'
  uat_status: verified
- id: uat-002
  name: "A channel admin can list a channel's outstanding invites (token, uses, expiry); refused for non-admins"
  command: cargo nextest run -E 'test(/invite_list/)'
  uat_status: verified
- id: uat-003
  name: "conclave status reports registrations, per-server reachability, and the resolved permission table"
  command: cargo nextest run -E 'test(/cli_status/)'
  uat_status: verified
- id: uat-004
  name: "conclave send posts a message that a subscribed session receives; conclave tail streams an inbound message to stdout"
  command: cargo nextest run -E 'test(/send_and_tail/)'
  uat_status: verified
- id: uat-005
  name: "The bridge leave_channel tool unsubscribes a session (it stops receiving fan-out) without disconnecting"
  command: cargo nextest run -E 'test(/leave_channel/)'
  uat_status: verified
- id: uat-006
  name: "Full gate is green (cargo make uat)"
  command: cargo make uat
  uat_status: verified

tasks:
- id: T-001
  title: "Moderation visibility: unban + bans (channel ban list)"
  priority: 1
  status: done
  notes: "Bans are durable (093bc84 successor) but write-only: no list verb, and unban is a non-obvious acl-add side effect. Wire (append-only): AdminOp::Unban { channel, user } and AdminOp::BanList { channel } answered with the existing UserList frame. Hub: both gated by authorize_channel_admin; unban calls remove_ban (durable + in-memory) WITHOUT touching ACL membership. Store: list_channel_bans(channel). CLI: top-level `conclave unban` (mirrors `ban`) and `conclave bans` (noun verb like `who`)."
- id: T-002
  title: "invite list"
  priority: 1
  status: done
  notes: "Invites are auditable only if you kept the token. Store: list_invites(channel) -> Vec<InviteRecord>. Wire: AdminOp::InviteList { channel } + a new InviteList response frame carrying token/uses_remaining/expires_at (appended). Channel-admin gated. CLI: `conclave invite list --server S --channel C` printing token/uses/expiry columns."
- id: T-003
  title: "conclave status (who am I)"
  priority: 2
  status: done
  notes: "No single view of: local registrations ([[servers]] url/username/machine), per-server reachability (one-shot auth + Who with the T-004 control timeouts), and the resolved perm table (reuse perm-show rendering). Purely local + existing frames; no wire change. Unreachable servers report the error and a non-zero summary exit."
- id: T-004
  title: "send + tail: the CLI as a human client"
  priority: 2
  status: done
  notes: "send: one-shot control connection that Joins the channel (await Joined), sends ChannelMsg, awaits the Ack (sends are server-acked since PRD-0008). tail: persistent connection that auths, Joins, then streams inbound ChannelMsg/Whisper to stdout as `[channel] from: text` lines until Ctrl-C; must send app-level Ping keepalives (< the 60s idle reaper) on a ticker. Minimal §19 aggregation-log."
- id: T-005
  title: "Polish: leave_channel bridge tool, shell completions, pubkey docstring"
  priority: 3
  status: done
  notes: "leave_channel MCP tool: the server handles Leave (Ack{detail: channel}) but no tool exposes it — a session cannot unsubscribe without disconnecting; defer on the Ack and remove from the local joined set on resolution. completions: clap_complete generator verb `conclave completions <shell>`. Docstring bug: machine add --pubkey says '(PEM)' but the format is base64url from `conclave key`."
- id: T-006
  title: "Skill/docs refresh + ship v0.2.1"
  priority: 3
  status: done
  notes: "Skill curated body: add status to first-time flow verification, human send/tail examples, unban/bans/invite-list to the admin bullet, leave_channel to the tool list (the command reference regenerates from clap automatically). README touch if needed. Ship: bump 0.2.1, CHANGELOG section, tag v0.2.1 (release.yml publishes binaries), cargo publish, fly deploy --local-only, conclave skill install."
---

# Summary

Round out the CLI so operators can audit every durable moderation state (bans, invites — completing
the acl-list work), debug identity at a glance (`status`), and participate as humans (`send`,
`tail`) — plus the small polish items surfaced during live deployment testing (leave_channel tool,
completions, a docstring bug). Ships as v0.2.1.

# Problem

Live testing surfaced write-only state and missing ergonomics: durable bans with no list/unban
verb, invites auditable only via kept tokens, no "who am I" view, no way for a human to watch or
post to a channel without a Claude session, and sessions that cannot unsubscribe without
disconnecting.

# Goals

1. No write-only moderation state: bans and invites are listable by their admins.
2. `unban` is first-class, not an acl-add side effect.
3. One-command identity/connectivity debugging (`status`).
4. Human channel participation from the plain CLI (`send`, `tail`).
5. Ship it: skill/docs current, v0.2.1 released and deployed.

# Technical Approach

Wire additions are append-only `AdminOp` variants; `BanList` reuses the `UserList` response,
`InviteList` adds a purpose-built frame. Hub arms follow the `authorize_channel_admin` pattern from
acl-list. `tail` is a small persistent loop in the control client (select over inbound frames and a
keepalive ticker) — deliberately not the bridge, which stays MCP-only. `status` composes existing
pieces (config, perm rendering, one-shot Who probes with the PRD-0008 timeouts).

# Assumptions

- The deployed server is updated (fly deploy) before the new CLI verbs are exercised against it.
- Channel-scoped ban/invite listing covers the operator need; server-wide sweeps can come later.

# Constraints

- Append-only wire evolution (protocol §13); no breaking CLI-surface changes (`ban` keeps its
  current positional form; `unban`/`bans` are new verbs).
- Constitution §8: every verb/tool ships a test; e2e spawns the real binary.

# References to Code

- `src/protocol.rs` — AdminOp tail (append AclList precedent), response frames.
- `src/server/hub.rs` — admin() dispatcher, authorize_channel_admin, remove_ban.
- `src/store.rs` — list_bans/list_channel_members precedents; invite table.
- `src/control.rs` — one-shot client + timeouts (send); new tail loop.
- `src/bin.rs` — verb definitions; `src/bridge.rs` — tool set (leave_channel).
- `src/skill/SKILL.md` — curated body (reference auto-regenerates).

# Non-Goals (MVP)

- Server-wide (cross-channel) ban/invite sweeps and `--json` output — later if needed.
- The full §19 aggregation TUI — `tail` is the minimal version.
- Rate limiting / registration gating (deliberately kept open per the 2026-07-03 decision).

# History
(Entries appended during implementation go below this line.)

## 2026-07-03 — All tasks done; shipped as v0.2.1

T-001..T-005 landed with TDD (176 tests green): unban/bans/invite-list (no write-only moderation
state), status, send/tail (human CLI participation), leave_channel tool, completions, and the
pubkey docstring fix. T-006: skill curated body gained the human/audit verbs and `status`; the
README command block was regenerated from the real `--help`. Shipped as v0.2.1 (tag → release.yml
binaries, crates.io publish, Fly redeploy, skill reinstall).
