<objective> Build the CLI: up, down, status, doctor, and mcp commands with exit codes 0/1/2, detached daemon spawn, and the bounded PODMAN_MISSING path.
Purpose: Deliver the user-facing side of CLI-01 and CLI-02 plus the P6-D9 stdio entry, on top of the 06-01 channel and reusing the 06-03 handle builder. Output: CLI module plus its test file. </objective>
<execution_context> @~/.claude/gsd-core/workflows/execute-plan.md @~/.claude/gsd-core/templates/summary.md </execution_context>
<context> @.planning/PROJECT.md @.planning/ROADMAP.md @.planning/phases/06-cli-daemon-and-host-packaging/06-CONTEXT.md @.planning/phases/06-cli-daemon-and-host-packaging/06-01-PLAN.md @.planning/phases/06-cli-daemon-and-host-packaging/06-03-PLAN.md </context>
<tasks>
<task type="tdd"> <name>Task 1: Argv parsing, exit codes, and daemon spawn (P6-D1, P6-D7, P6-D13)</name> <files>src/cli/main.ts, test/cli/cli.test.ts</files> <read_first> - src/daemon/channel.ts (from plan 06-01; the socket client surface this task dials) - src/session/paths.ts lines 12-24 (resolveStateDir: XDG_STATE_HOME or HOME; no process.env read inside modules) - src/mcp/server.ts lines 224-253 (createMcpServer stdio shape the mcp command will reuse in Task 3) </read_first> <behavior> - Test (CLI-01): up with no daemon spawns the detached child (argv form, recorded by an injected spawn fake) then reports the daemon status over the socket pair - Test (CLI-01): down sends down, and status with no daemon reports disconnected while saying the daemon is absent - Test (CLI-01): unknown argv exits 2 with usage on stderr - Test titles cite CLI-01 </behavior> <action>Write the failing tests first per behavior, then implement src/cli/main.ts per P6-D1, P6-D7, and P6-D13: parse argv into up, down, status, doctor, and mcp; resolve the state directory from the passed environment (the module reads no process.env itself); dial the socket with SOCKET_TIMEOUT_MS; when absent for up, spawn the detached daemon with node:child_process argv form (process.execPath plus the compiled daemon entry, detached, stdio ignore) then connect within the bound; exit 0 on success, 1 on catalogue-coded failure, 2 on usage error, with the machine-readable code on stdout and the reason on stderr. Inject spawn, connect, and clock seams so no process starts in tests. Test titles cite CLI-01.</action> <acceptance_criteria> - All five commands parse; exits are 0/1/2 per P6-D13; spawn uses argv form detached with stdio ignore - No test starts a process; the spawn fake records the exact argv </acceptance_criteria> <verify> <automated>npx tsc -p tsconfig.json && node --test dist/test/cli/cli.test.js</automated> </verify> <done>CLI parses, spawns, dials, and exits with the pinned codes.</done> </task>
<task type="auto"> <name>Task 2: PODMAN_MISSING fast path with bounded wait (P6-D5)</name> <files>src/cli/main.ts, test/cli/cli.test.ts</files> <read_first> - src/cli/main.ts (from Task 1 of this plan; the up path the probe precedes) - src/runtime/podman-probe.ts lines 39-96 (probePodman options and the PODMAN_MISSING/PODMAN_MACHINE_DOWN/PODMAN_UNSUPPORTED_VERSION outcomes) - src/doctor/checks.ts lines 489-492 (probeTimeoutOf default pattern to copy) </read_first> <action>Extend the Task 1 files: run probePodman with injected runCommand, connectSocket, and env plus a bounded timeoutMs before up proceeds; on PODMAN_MISSING report disconnected with PODMAN_MISSING and exit 1 promptly without spawning any daemon. Add tests proving the report carries the catalogue code and reason, the spawn fake is never called, and the call returns within an asserted wall-clock bound (short injected timeout). Test titles cite CLI-02.</action> <acceptance_criteria> - PODMAN_MISSING reports disconnected with the code and exits 1 with no spawn and no hang (wall-clock bound asserted) </acceptance_criteria> <verify> <automated>npx tsc -p tsconfig.json && node --test dist/test/cli/cli.test.js</automated> </verify> <done>Missing-Podman path pinned: correct code, no spawn, bounded return.</done> </task>
<task type="auto"> <name>Task 3: realm mcp stdio entry from the composition root (P6-D9, P6-D12)</name> <files>src/cli/main.ts, test/cli/cli.test.ts</files> <read_first> - src/cli/main.ts (from Task 2 of this plan; the command table the mcp command joins) - src/daemon/daemon.ts (from plan 06-03; buildDaemonHandle the mcp command reuses) - src/mcp/server.ts lines 25-32 (McpDispatcher is Pick of dispatcher call only) and 224-253 (createMcpServer input, output, diagnostics) - test/mcp/server.test.ts (the stdio driver pattern to copy for the mcp test) </read_first> <action>Extend the Task 2 files: implement the mcp command per P6-D9 and P6-D12 by building the handle through buildDaemonHandle and serving it with createMcpServer over the piped stdio streams (diagnostics to stderr; stdout carries only MCP messages), closing on stdin close. Add a test driving tools/list plus one tool call through the mcp command against a fake-backed handle, asserting the dispatcher call path and clean shutdown. Test titles cite CLI-01 (command surface) and note the CLI-04 entry.</action> <acceptance_criteria> - mcp serves the Phase 5 server with no second wiring; stdout carries only MCP messages; stdin close shuts down cleanly </acceptance_criteria> <verify> <automated>npx tsc -p tsconfig.json && node --test dist/test/cli/cli.test.js</automated> </verify> <done>realm mcp serves the shared dispatcher over stdio; snippets point at a working entry.</done> </task>
</tasks>
<threat_model>
Trust Boundaries
| Boundary | Description |
|---|---|
| argv -> command table | Untrusted argv crosses here; unknown commands exit 2 without touching the socket or spawning |
| probe result -> up path | Untrusted host state crosses here; PODMAN_MISSING stops before any spawn |
STRIDE Threat Register
| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
|---|---|---|---|---|---|
| T-06-09 | E (Elevation of Privilege) | src/cli/main.ts | medium | mitigate | Daemon spawn uses argv form with no shell, so argv cannot inject shell commands (P6-D7) |
| T-06-10 | D (Denial of Service) | src/cli/main.ts | medium | mitigate | The up path probes first with a bounded timeout and never spawns on PODMAN_MISSING; every socket wait is bounded by SOCKET_TIMEOUT_MS (P6-D5) |
| T-06-11 | I (Information Disclosure) | src/cli/main.ts | low | mitigate | Machine-readable codes go to stdout, human reasons to stderr; no env value or secret is printed (AUD-02 carry) |
| </threat_model> |
<verification> npx tsc -p tsconfig.json && node --test dist/test/cli/cli.test.js; grep test/cli/cli.test.ts for CLI-01 and CLI-02 in test titles. </verification>
<success_criteria> Five commands parse with pinned exit codes; up spawns or reuses the daemon; PODMAN_MISSING exits fast with the code; mcp serves the shared dispatcher over stdio. </success_criteria>
Artifacts this phase produces (contribution of this plan)
src/cli/main.ts(new): argv parsing, exit codes, spawn, socket client, probe path, mcp entry (P6-D1, P6-D5, P6-D7, P6-D9, P6-D12, P6-D13).test/cli/cli.test.ts(new): CLI-01 spawn/dial/exit cases, CLI-02 bounded PODMAN_MISSING case, mcp stdio case.
<output> Create .planning/phases/06-cli-daemon-and-host-packaging/06-04-SUMMARY.md when done </output>