Date: 2026-04-06
Branch: hal/cli-completeness (from main post PR #39 merge)
Spec refs: §7.1–§7.8, §8.7–§8.8, §9, §19.1, §20.2
Issues: New — "Implement all deferred CLI commands"
Priority: P0 — 13 of 19 top-level commands return "not implemented yet"
Estimated: ~650 LoC net new across 5 files
Coverage lift: §11 Platform Architecture 85%→95%, CLI implemented 32%→95%
After PR #39, the server-side World API is feature-complete: agents can list peers, exchange messages, ping, and stream events. However 13 CLI commands still return "not implemented yet" via deferred_output(). This means users cannot access any of these features from the command line.
The gap breaks the spec contract: §7.6 defines awn agents, awn ping, awn send, awn messages as required commands. §20.2 defines awn reset. §8 defines trust dispute CLI operations. All server endpoints and storage methods exist — only the CLI wiring is missing.
| Command | File:Line | Server Endpoint | Storage |
|---|---|---|---|
awn agents |
commands.rs:235 (catch-all) |
GET /v0/agents exists |
AgentsResponse at endpoints.rs:383 |
awn ping |
commands.rs:235 (catch-all) |
POST /v0/ping exists |
PingResponse at endpoints.rs:445 |
awn send |
commands.rs:235 (catch-all) |
POST /v0/messages exists |
AgentMessage at messages.rs:11 |
awn messages |
commands.rs:235 (catch-all) |
GET /v0/messages exists |
MessagesResponse at messages.rs:37 |
awn reset |
commands.rs:235 (catch-all) |
N/A (local FS) | N/A |
awn trust dispute |
commands.rs:2005 |
N/A (local storage) | file_dispute() at storage.rs:1155 |
awn trust disputes |
commands.rs:2024 |
N/A (local storage) | list_disputes() at storage.rs:1290 |
awn trust dispute-status |
commands.rs:2042 |
N/A (local storage) | get_dispute() at storage.rs:1240 |
awn auth (no subcommand) |
commands.rs:668 |
N/A | N/A — just help text |
awn daemon (no subcommand) |
commands.rs:750 |
N/A | N/A — just help text |
awn world (no subcommand) |
commands.rs:778 |
N/A | N/A — just help text |
awn gateway (no subcommand) |
commands.rs:1428 |
N/A | N/A — just help text |
awn trust (no subcommand) |
commands.rs:1465 |
N/A | N/A — just help text |
awn identity (no subcommand) |
commands.rs:2763 |
N/A | N/A — just help text |
awn agent (no subcommand) |
commands.rs:1337 |
N/A | N/A — just help text |
| Component | File:Line | What It Provides |
|---|---|---|
WorldClient |
src/world_client.rs:60-190 |
HTTP client with AWN-Ed25519 auth; has join(), action(), leave(), state() — needs agents(), ping(), send_message(), messages() |
WorldClient::authed_request() |
src/world_client.rs:157-189 |
Generic signed HTTP request method — reuse for all new methods |
parse_api_response<T>() |
src/world_client.rs:193-224 |
Generic JSON envelope parser — reuse for all new return types |
JoinCommand pattern |
src/cli/commands.rs:3365-3454 |
Reference pattern: load identity → create WorldClient → block_on → format TOON |
ActionCommand pattern |
src/cli/commands.rs:3469-3594 |
Reference pattern: resolve session → load identity → WorldClient → action |
SessionManager |
src/session.rs:47-163 |
save(), load(), list(), resolve(world_id_or_url), next_seq() |
RuntimeDirectories |
src/config.rs |
resolve() → dirs for identity, sessions, state, trust |
Identity |
src/identity/mod.rs |
load_from(dirs) → agent_id(), signing_key() |
AgentSummary |
src/world_api/endpoints.rs:369 |
awn_id, subject_id, slot, joined_at, metadata |
AgentsResponse |
src/world_api/endpoints.rs:383 |
agents: Vec<AgentSummary>, count: usize |
PingRequest |
src/world_api/endpoints.rs:437 |
target: String |
PingResponse |
src/world_api/endpoints.rs:445 |
target, reachable, connected_via |
SendMessageRequest |
src/world_api/messages.rs:27 |
to, content, content_type |
AgentMessage |
src/world_api/messages.rs:11 |
message_id, from, to, content, content_type, sent_at |
MessagesResponse |
src/world_api/messages.rs:37 |
messages: Vec<AgentMessage>, count: usize |
MessagesQuery |
src/world_api/messages.rs:47 |
peek, limit |
Dispute struct |
src/trust/dispute.rs:59-80 |
dispute_id, receipt_id, filed_by, filed_against, reason, tier, status, outcome |
ReceiptStore::file_dispute() |
src/trust/storage.rs:1155 |
Files dispute against receipt in SQLite |
ReceiptStore::list_disputes() |
src/trust/storage.rs:1290 |
Lists disputes with optional status filter |
ReceiptStore::get_dispute() |
src/trust/storage.rs:1240 |
Gets dispute by ID |
deferred_output() |
src/cli/commands.rs:235 |
Catch-all returning "not implemented yet" |
world_client_error_result() |
src/cli/commands.rs |
Converts WorldClientError → CommandResult failure |
OutputNode, Section, KeyValueRow |
src/cli/output.rs |
TOON output formatting primitives |
Goal: Add agents(), ping(), send_message(), messages() methods to WorldClient.
Spec: §7.6 — all 4 commands need HTTP calls to World API.
Pattern: Follow existing join() / action() / leave() / state() pattern — all use authed_request() + parse_api_response<T>().
// src/world_client.rs — add 4 methods
/// List agents in the world.
/// GET /v0/agents
pub async fn agents(&self) -> Result<AgentsResponse, WorldClientError> {
let response = self.authed_request("GET", "/v0/agents", &[]).await?;
parse_api_response(response).await
}
/// Ping an agent to check reachability.
/// POST /v0/ping
pub async fn ping(&self, target: &str) -> Result<PingResponse, WorldClientError> {
let request = PingRequest { target: target.to_string() };
let body = serde_json::to_vec(&request)
.map_err(|e| WorldClientError::InvalidResponse(e.to_string()))?;
let response = self.authed_request("POST", "/v0/ping", &body).await?;
parse_api_response(response).await
}
/// Send a message to another agent.
/// POST /v0/messages
pub async fn send_message(
&self, to: &str, content: serde_json::Value,
) -> Result<AgentMessage, WorldClientError> {
let request = SendMessageRequest {
to: to.to_string(),
content,
content_type: Some("application/json".to_string()),
};
let body = serde_json::to_vec(&request)
.map_err(|e| WorldClientError::InvalidResponse(e.to_string()))?;
let response = self.authed_request("POST", "/v0/messages", &body).await?;
parse_api_response(response).await
}
/// Read messages from inbox.
/// GET /v0/messages[?peek=true&limit=N]
pub async fn messages(
&self, peek: bool, limit: Option<u32>,
) -> Result<MessagesResponse, WorldClientError> {
let mut path = "/v0/messages".to_string();
let mut params = vec![];
if peek { params.push("peek=true".to_string()); }
if let Some(l) = limit { params.push(format!("limit={l}")); }
if !params.is_empty() {
path = format!("{}?{}", path, params.join("&"));
}
let response = self.authed_request("GET", &path, &[]).await?;
parse_api_response(response).await
}Imports needed: Add AgentsResponse, PingRequest, PingResponse from endpoints, and SendMessageRequest, AgentMessage, MessagesResponse from messages to world_client.rs imports.
Files: src/world_client.rs
LoC: ~60
AC:
-
WorldClient::agents()→GET /v0/agents→AgentsResponse -
WorldClient::ping(target)→POST /v0/ping→PingResponse -
WorldClient::send_message(to, content)→POST /v0/messages→AgentMessage -
WorldClient::messages(peek, limit)→GET /v0/messages→MessagesResponse - All methods use
authed_request()+parse_api_response()pattern - Test:
agents()constructs correct request - Test:
ping()serializes PingRequest correctly - Test:
messages()builds query string with peek/limit
Goal: List joined agents in a world.
Spec: §7.6 — awn agents "inspect peers"
Spec output format (§19.1 TOON style):
Agents (3)
aw:sha256:abc123 slot=0 joined 2m ago
aw:sha256:def456 slot=1 joined 5m ago
aw:sha256:ghi789 slot=2 joined 1m ago
Step 1: Add clap args to Agents variant:
// Replace: Agents,
// With:
/// List agents in a joined world.
Agents(AgentsCommand),
#[derive(Debug, Args, Clone)]
pub struct AgentsCommand {
/// World URL or ID of the joined world.
pub world: String,
/// Output JSON instead of TOON.
#[arg(long)]
pub json: bool,
}Step 2: Implement AgentsCommand::result():
Follow JoinCommand pattern (commands.rs:3365-3454):
RuntimeDirectories::resolve()Identity::load_from(&directories)SessionManager::new(&directories).resolve(&self.world)→ get world_urlWorldClient::new(world_url, agent_id, signing_key)Runtime::new()→rt.block_on(client.agents())- Format as TOON with Section "Agents" + rows per agent
Files: src/cli/commands.rs
LoC: ~50
AC:
-
awn agents <world>lists all joined agents with awn_id, slot, joined_at -
--jsonflag returns raw JSON - Error: no identity → "run
awn identity initfirst" (exit 3) - Error: no session → "not joined — run
awn joinfirst" (exit 6) - Error: API error → formatted error (exit 4)
- Test: TOON output matches spec format
Goal: Check if a target agent is reachable via SSE.
Spec: §7.6 — awn ping <agent-id>
Spec output:
aw:sha256:abc123 is reachable (via SSE)
or:
aw:sha256:abc123 is unreachable
/// Ping an agent in a joined world.
Ping(PingCommand),
#[derive(Debug, Args, Clone)]
pub struct PingCommand {
/// World URL or ID of the joined world.
pub world: String,
/// Target agent AWN ID to ping.
pub target: String,
/// Output JSON instead of TOON.
#[arg(long)]
pub json: bool,
}Follow same pattern: identity → session → WorldClient → client.ping(&self.target) → format result.
Files: src/cli/commands.rs
LoC: ~45
AC:
-
awn ping <world> <target>shows reachable/unreachable -
--jsonreturns{ "target": "...", "reachable": true, "connectedVia": "sse" } - Error handling: identity/session/API errors
- Test: reachable agent → "reachable (via SSE)"
Goal: Send a direct message to another agent in the same world.
Spec: §7.6 — awn send <agent-id> <message>
Spec: §8.8 encoding rules — support --message as JSON string, or --message-file for file input.
/// Send a message to an agent in a joined world.
Send(SendCommand),
#[derive(Debug, Args, Clone)]
pub struct SendCommand {
/// World URL or ID of the joined world.
pub world: String,
/// Recipient agent AWN ID.
pub to: String,
/// Message content (JSON string).
#[arg(long)]
pub message: String,
/// Output JSON instead of TOON.
#[arg(long)]
pub json: bool,
}Parse --message as JSON (fallback to wrapping as {"text": "..."} if not valid JSON). Call client.send_message(&self.to, content).
Files: src/cli/commands.rs
LoC: ~55
AC:
-
awn send <world> <to> --message '{"text":"hello"}'sends message - Plain text fallback:
--message "hello"→{"text": "hello"} - Returns: message_id, sent_at confirmation
-
--jsonreturns fullAgentMessageJSON - Error: recipient not joined → formatted error
- Test: JSON message sent correctly
Goal: Read agent's inbox in a world.
Spec: §7.6 — awn messages "read inbound messages"
/// Read messages from inbox in a joined world.
Messages(MessagesCommand),
#[derive(Debug, Args, Clone)]
pub struct MessagesCommand {
/// World URL or ID of the joined world.
pub world: String,
/// Peek at messages without consuming them.
#[arg(long)]
pub peek: bool,
/// Maximum messages to return (default 50).
#[arg(long)]
pub limit: Option<u32>,
/// Output JSON instead of TOON.
#[arg(long)]
pub json: bool,
}Call client.messages(self.peek, self.limit). Format as TOON table:
Messages (2)
msg_019d3a0f from=aw:sha256:abc123 2m ago
{"text": "hello"}
msg_019d3a10 from=aw:sha256:def456 5m ago
{"action": "propose_trade", "amount": 100}
Files: src/cli/commands.rs
LoC: ~55
AC:
-
awn messages <world>drains inbox and displays messages -
--peekreads without consuming -
--limit Nlimits returned messages -
--jsonreturns rawMessagesResponseJSON - Empty inbox → "No messages."
- Test: messages displayed in TOON format
Goal: Reset local agent state per spec §20.2.
Spec (§20.2 exact text):
awn reset # Interactive reset (prompts for confirmation)
awn reset --auth # Clear auth binding only
awn reset --identity # Regenerate keypair (loses all bindings!)
awn reset --cache # Clear template and image cache
awn reset --all # Full reset (requires explicit confirmation)
/// Reset local agent state.
Reset(ResetCommand),
#[derive(Debug, Args, Clone)]
pub struct ResetCommand {
/// Clear auth binding only.
#[arg(long)]
pub auth: bool,
/// Regenerate keypair (DESTRUCTIVE — loses all bindings).
#[arg(long)]
pub identity: bool,
/// Clear template and image cache.
#[arg(long)]
pub cache: bool,
/// Full reset (requires --yes to skip confirmation).
#[arg(long)]
pub all: bool,
/// Skip confirmation prompts.
#[arg(long)]
pub yes: bool,
}Implementation per flag:
--auth: Removebinding.jsonfrom config dir--identity: Remove keypair files from identity dir + all sessions (WARN if not--yes)--cache: Remove template cache dir--all: All of the above (REQUIRE--yesor interactive confirmation)- No flags: Show help text listing available reset options
Files: src/cli/commands.rs
LoC: ~80
AC:
-
awn reset --authremoves auth binding file -
awn reset --identity --yesregenerates keypair, removes sessions -
awn reset --cacheclears template cache -
awn reset --all --yesperforms full reset -
awn reset --identitywithout--yesreturns error with warning per §20.2 -
awn resetwith no flags shows usage - Test:
--authremoves binding file - Test:
--allwithout--yes→ error
Goal: Wire the 3 deferred trust dispute commands to the existing ReceiptStore dispute methods.
Spec: §8 Governance — dispute filing, listing, status inspection.
Current state: TrustDisputeCommand, TrustDisputesCommand, TrustDisputeStatusCommand have full clap args (receipt_id, reason, status filter, dispute_id) but result() returns deferred_output(). Storage layer is fully implemented: file_dispute() at storage.rs:1155, list_disputes() at storage.rs:1290, get_dispute() at storage.rs:1240.
awn trust dispute <receipt-id> --reason "...":
- Load
RuntimeDirectories→ openReceiptStore - Load
Identityforagent_id - Verify receipt exists via
store.get_receipt(receipt_id) - Call
store.file_dispute(receipt_id, agent_id, reason, ...) - Return dispute_id, status, filed_at
awn trust disputes [--status filed|under-review|resolved|expired]:
- Open
ReceiptStore - Load
Identityforagent_id - Call
store.list_disputes(status_filter, ...) - Format as TOON table with dispute_id, receipt_id, status, filed_at
awn trust dispute-status <dispute-id>:
- Open
ReceiptStore - Call
store.get_dispute(dispute_id) - Format full dispute details: id, receipt_id, filed_by, filed_against, reason, status, outcome, evidence
Pattern: Follow existing trust commands (e.g., trust_pending_command_result in commands.rs) which load ReceiptStore from RuntimeDirectories.
Files: src/cli/commands.rs
LoC: ~120
AC:
-
awn trust dispute <receipt-id> --reason "bad outcome"files dispute in local store - Returns dispute_id and confirmation
-
awn trust disputeslists all disputes with status -
--status filedfilters by status -
awn trust dispute-status <dispute-id>shows full dispute details - All 3 support
--jsonflag - Error: receipt not found → "Receipt not found" (exit 6)
- Error: dispute already exists → "Dispute already filed" (exit 7)
- Test: file → list → status round-trip
Goal: Replace deferred_output() with proper help text for group commands called without subcommands.
Current: 7 command groups return "not implemented yet" when called with no subcommand. They should display usage instead.
| Command | Line | Fix |
|---|---|---|
awn auth |
668 | "Usage: awn auth <link|whoami|agents|revoke|unlink>" |
awn daemon |
750 | Already says "requires a subcommand" — OK as-is |
awn world |
778 | "Usage: awn world <new|check|dev|stop|ps|list|publish|..." |
awn gateway |
1428 | "Usage: awn gateway " |
awn trust |
1465 | "Usage: awn trust <pending|confirm|reject|history|...>" |
awn identity |
2763 | "Usage: awn identity <show|init|register|verify|...>" |
awn agent |
1337 | Already says "Usage: awn agent <status|online|offline>" — OK |
Implementation: Replace deferred_output(...) with Output::new(vec![OutputNode::Row(KeyValueRow::new("usage", "awn <group> <subcommand>..."))]) and return with CliExitCode::InvalidInput (exit 2).
Files: src/cli/commands.rs
LoC: ~30
AC:
-
awn auth(no subcommand) → shows usage with available subcommands (exit 2) -
awn world(no subcommand) → shows usage (exit 2) -
awn gateway(no subcommand) → shows usage (exit 2) -
awn trust(no subcommand) → shows usage (exit 2) -
awn identity(no subcommand) → shows usage (exit 2) - No more
deferred_output()calls remain in codebase
| File | Change Type | LoC | Description |
|---|---|---|---|
src/world_client.rs |
Modified | ~60 | Add agents(), ping(), send_message(), messages() methods |
src/cli/commands.rs |
Modified | ~470 | 5 command structs + impls (US-002–006), 3 trust dispute impls (US-007), 5 help text fixes (US-008) |
src/cli/mod.rs |
Modified | ~5 | Import updates |
src/world_api/messages.rs |
Modified | ~2 | Add Deserialize derive to response types if missing (needed for client-side parsing) |
src/world_api/endpoints.rs |
Modified | ~2 | Add Deserialize derive to AgentsResponse, PingResponse if missing |
Total: ~650 LoC net new across 5 files
US-001 (WorldClient methods) ─── FIRST — foundation for US-002–005
↓
US-002 (awn agents) ─── depends on US-001
US-003 (awn ping) ─── depends on US-001
US-004 (awn send) ─── depends on US-001
US-005 (awn messages) ─── depends on US-001
↓
US-006 (awn reset) ─── independent (local FS only)
US-007 (trust disputes) ─── independent (local storage only)
US-008 (help text) ─── independent (trivial fixes)
Recommended commit order:
1. US-001 (WorldClient methods) — enables all agent CLI commands
2. US-002 + US-003 (agents + ping) — small, foundational
3. US-004 + US-005 (send + messages) — messaging pair
4. US-006 (reset) — independent
5. US-007 (trust disputes) — independent
6. US-008 (help text fixes) — cleanup, last
| Command | Spec | Interaction |
|---|---|---|
awn agents <world> [--json] |
§7.6 | WorldClient → GET /v0/agents |
awn ping <world> <target> [--json] |
§7.6 | WorldClient → POST /v0/ping |
awn send <world> <to> --message <json> [--json] |
§7.6 | WorldClient → POST /v0/messages |
awn messages <world> [--peek] [--limit N] [--json] |
§7.6 | WorldClient → GET /v0/messages |
awn reset [--auth|--identity|--cache|--all] [--yes] |
§20.2 | Local FS operations |
awn trust dispute <receipt-id> --reason <text> [--json] |
§8 | ReceiptStore → file_dispute() |
awn trust disputes [--status <s>] [--json] |
§8 | ReceiptStore → list_disputes() |
awn trust dispute-status <dispute-id> [--json] |
§8 | ReceiptStore → get_dispute() |
| Test | Story | Validates |
|---|---|---|
world_client_agents_request |
US-001 | GET /v0/agents constructed correctly |
world_client_ping_request |
US-001 | POST /v0/ping with PingRequest body |
world_client_send_message_request |
US-001 | POST /v0/messages with SendMessageRequest body |
world_client_messages_query_string |
US-001 | GET /v0/messages?peek=true&limit=10 |
agents_command_toon_output |
US-002 | 3 agents → TOON formatted list |
agents_command_no_session |
US-002 | No session → "not joined" error |
ping_command_reachable |
US-003 | Reachable → "reachable (via SSE)" |
ping_command_unreachable |
US-003 | Unreachable → "unreachable" |
send_command_json_message |
US-004 | JSON message sent correctly |
send_command_text_fallback |
US-004 | Plain text → {"text": "..."} |
messages_command_empty_inbox |
US-005 | Empty → "No messages." |
messages_command_peek_flag |
US-005 | --peek passed to query |
reset_auth_removes_binding |
US-006 | --auth clears binding file |
reset_identity_requires_yes |
US-006 | --identity without --yes → error |
reset_all_requires_yes |
US-006 | --all without --yes → error |
trust_dispute_file_and_list |
US-007 | file → list → get round-trip |
| Test | Stories | Validates |
|---|---|---|
cli_agent_workflow_e2e |
US-001–005 | join → agents → ping → send → messages → leave |
cli_dispute_workflow_e2e |
US-007 | propose receipt → file dispute → list disputes → get status |
| Feature | Why | Where |
|---|---|---|
awn auth link (device code flow) |
Requires AgentWorlds Hub backend (external service) | Future PR |
awn world new (template scaffolding) |
Requires Hub template registry | Future PR |
awn world publish (package upload) |
Requires Hub package registry | Future PR |
awn deploy * (AWS deployment) |
Requires AWS infrastructure + Hub integration | Future PR |
awn identity export/import (backup) |
Encryption/passphrase UX design needed | Future PR |
Interactive confirmation for awn reset |
Requires stdin interaction; use --yes for now |
Future enhancement |
| ANSI color output | Spec says default is no color; --color flag deferred |
Future enhancement |
| Risk | Level | Mitigation |
|---|---|---|
Response types missing Deserialize |
Low | Check derives on AgentsResponse, PingResponse, MessagesResponse; add if missing |
| Session resolution for new commands | Low | Reuse SessionManager::resolve() — same pattern as ActionCommand |
awn reset --identity data loss |
Medium | Require --yes flag; print clear warning without it |
| Trust dispute store path resolution | Low | Follow same pattern as trust_pending_command_result |
| Query string in authed_request | Low | Auth token is signed against path only (no query); verify GET /v0/messages?peek=true works |
-
US-001: The
authed_requestmethod signs against path only. Formessages(), the query string?peek=true&limit=Nshould be appended to the path BEFORE signing, since the server's auth verification includes the full path. Verify this matches how the server verifies auth tokens — checkAuthToken::create()inauth_token.rs. -
US-002–005: Follow the EXACT pattern from
JoinCommand::result()atcommands.rs:3365-3454:RuntimeDirectories::resolve() → Identity::load_from() → SessionManager::resolve() → WorldClient::new() → Runtime::new() → rt.block_on() → format outputFor
awn agents,awn ping,awn send,awn messagesthe user passes<world>as first positional arg. UseSessionManager::resolve()which accepts either a world ID or URL. -
US-004 (send): Parse
--messagewithserde_json::from_str()first. If it fails (not valid JSON), wrap as{"text": "<raw string>"}. This matches the spec's intent that both JSON and plain text are accepted. -
US-006 (reset): Use
RuntimeDirectoriesto resolve all paths. Key directories:- Auth binding:
dirs.config_dir().join("binding.json") - Identity:
dirs.identity_dir()(contains keypair.enc, agent_id) - Sessions:
dirs.state_dir().join("sessions/") - Cache:
dirs.cache_dir()
- Auth binding:
-
US-007 (disputes): Open
ReceiptStoreusing the same pattern as other trust commands — seetrust_pending_command_result()in commands.rs for reference. The store path isdirs.trust_dir().join("receipts.db"). -
US-008: Replace
deferred_output("not implemented yet")with usage text andCliExitCode::InvalidInput. Don't useeprintln!— return asCommandResultfor proper formatting. -
The catch-all
_at line 235 should be REMOVED entirely after all commands are implemented. Replace with explicit matches so the compiler catches any future missing variants. -
Run
cargo fmtandcargo clippybefore every commit. CI is strict.
Review — 2026-04-06
❌ CRITICAL: Entire plan is already implemented — PR #40 has zero remaining work
The plan was written assuming 13 deferred CLI commands exist. Verification shows ALL of them are already implemented on the current `main` branch (post PR #39 merge). There is no `deferred_output()` anywhere in the codebase.
Evidence
`deferred_output` completely absent:
All Command enum variants dispatch to real implementations:
WorldClient methods already exist:
No "no subcommand" deferred handlers remain — the catch-all `_ =>` case at former line 235 no longer exists. All variants have explicit match arms.
What Happened
PR #39 ("Agent Communication Foundations") implemented all of these commands as part of its scope. The plan at `/tmp/pr39-agent-communication-v2.md` listed US-001 through US-006 as server-side only, but the actual PR #39 implementation went further and wired up:
Conclusion
This PR plan should be cancelled. There is no work to do — the codebase has 0 deferred CLI commands remaining.
What's Actually Next
With CLI completeness at ~95%, the real remaining gaps per spec are: