Repository navigation
v9.0.0 — Firestorm
[9.0.0] Firestorm
Firestorm turns Watchfire inside out: instead of only driving coding agents, Watchfire is now driven by them. watchfire mcp serve exposes the whole orchestrator to any MCP-capable client — Claude Code, Codex, Gemini CLI, opencode, Copilot CLI, or a custom agent — as an 18-tool factory. The outer agent plans and reviews; Watchfire manufactures the code in sandboxed, git-worktree-isolated runs and merges the results. The canonical loop is create_task → run_task → wait_for_task → get_task → get_task_diff → iterate.
The MCP server is the fourth thin client, and it contains no orchestration logic of its own: every tool call is a translation to an existing daemon gRPC RPC, exactly like the TUI and GUI. The daemon stays the single brain — worktrees, sandboxing, merging, chaining and notifications all keep working unchanged, and a task created over MCP is indistinguishable from one typed into the TUI. Consequently the entire cycle needed one proto/daemon change: the GetMcpClientStatus / InstallMcpClient onboarding pair.
It is local-only by construction. The server's only transport is stdio, spawned as a subprocess by an MCP client on the same host as the daemon; it never opens a listening socket, and nothing in v9.0 makes Watchfire reachable from outside the machine. This is enforced rather than asserted — a source-parsing test fails the build on any net.Listen* / http.ListenAndServe / grpc.NewServer call in the serve path or any HTTP/SSE transport, and the end-to-end test lsofs the live process to confirm it from outside. --read-only serves only the 8 observation tools, filtered at registration time so the write and run tools never appear in tools/list at all.
Onboarding follows one UX rule on every surface: pick one of the five known harnesses and Watchfire does the whole setup for you, or pick Custom and get a snippet to paste into any MCP client. The shared, dependency-light installer writers back all four surfaces — the watchfire mcp install CLI, the daemon RPCs, a TUI Settings section, and a GUI Global Settings panel — so no surface reads a harness config itself and the wording cannot drift.
Added
-
watchfire mcp serve— the stdio MCP server (internal/mcpserver/). Built on the officialgithub.com/modelcontextprotocol/go-sdk(v1.6.1, which bumps the go directive to 1.25.0). The server auto-startswatchfiredif needed and connects over the same path as the CLI, then serves a data-driven tool registry (toolSpecrows carrying name/title/description/annotations/schema/handler). Tools take an optionalprojectargument (id or name); started inside a registered project directory, that project is the default and the argument may be omitted, mirroring the CLI's cwd walk-up + auto-register resolution. A single server instance can address all registered projects. The root command's update hint now skipsmcpcommands so nothing ever writes to stdout — which is the transport. -
Project tools:
list_projects(project list enriched with live agent status) andget_project(project + git info + task counts + agent status). -
Task-factory tools:
create_task,list_tasks,get_task,update_task,delete_task. All five write exclusively throughTaskServicegRPC — the validated daemon path; no YAML is ever authored directly, so MCP-created tasks can't reproduce the malformed-file class of bug.create_tasktakes a schema-levelstatusenum (draft|ready, defaultdraft), optional acceptance criteria/position, and an agent-backend override validated againstSettingsService.ListAgentswith the valid backend list surfaced on mismatch.update_taskis a partial update restricted todraft↔ready(doneis agent-written); an empty agent string clears the override.delete_taskis a soft delete, reversible from the TUI/GUI Trash — permanent deletion is deliberately not exposed over MCP. -
Run tools:
run_task,run_all,start_wildfire,stop_agent,get_agent_status,wait_for_task. The three starters map toAgentService.StartAgent; aGetAgentStatuspre-check refuses when an agent is already running (naming its mode and task) rather than silently replacing the in-flight run, since Watchfire runs at most one agent per project.stop_agentis idempotent (stopped: falseon an idle project).wait_for_taskis the factory loop's synchronization point: it pollsGetTaskevery ~2s, honours MCP request cancellation, and reports a timeout as a normaltimed_out: trueresult carrying live agent status, so clients simply call it again to keep waiting. All polling lives in the MCP layer — no proto or daemon changes. -
Inspect tools:
get_task_diff(the v6.0 diff rendered as unified-diff text with per-file and total counts plus the daemon's truncation note, alongside structured totals),get_agent_screen(tail of the live agent terminal, default 100 / max 1000 lines, ANSI escapes stripped and CR spinner-redraws resolved to plain text),get_insights(compact throughput + cost + shipped-code summary, project scope by default orscope: global), andlist_logs/get_log(past session transcripts, capped at 64 KiB keeping the tail with an explicit truncation note). -
--read-onlymode.watchfire mcp serve --read-onlyfilters the registry by group at registration time and serves exactly 8 of the 18 tools — the project and inspect groups. The write and run tools aren't merely refused: they're absent fromtools/listand unknown when called by name. Suitable for dashboards or less-trusted callers. -
Client onboarding:
watchfire mcp install [client]. Newinternal/mcpserver/install/provides pure, dependency-light (stdlib +BurntSushi/toml)Detect/Status/Install/Snippetwriters forclaude-code,codex,gemini,opencodeandcopilot, plus the generic Custom snippet. JSON configs are parse-merge-write key-by-key so unrelated user keys survive; the Codex TOML merge is line-based so comments and unrelated tables survive verbatim. Every installer is idempotent and degrades to printed manual instructions on a missing client or unparseable config — an existing config file is never clobbered. The CLI offers direct install, an interactive picker (five clients + Custom) with detection badges, and--printfor the generic{"command": "watchfire", "args": ["mcp", "serve"]}block. -
Daemon onboarding RPCs — the cycle's only proto/daemon change.
SettingsService.GetMcpClientStatusreturns oneMcpClientStatusper known harness (stable key, display name, detected, configured, config path, message) pluscustom_snippet, so every surface renders the Custom option from one source of truth;SettingsService.InstallMcpClientperforms the install and returns the post-install state. Both are thin calls into the sharedinstallpackage, so the TUI and GUI — pure gRPC clients — can offer setup without shelling out to the CLI. Install problems are deliberately not gRPC errors: a missing harness or unparseable config returnsconfigured=falsewith a message carrying the manual snippet, so UIs render the fallback path instead of an opaque failure; only an unknown client key (a caller bug) isInvalidArgument. The daemon pulls ininstall/only — no MCP SDK or server runtime (verified viago list -deps).install.Result.Message(displayName)is now the single source of truth for onboarding copy, printed by the CLI and returned by the RPC, so CLI/TUI/GUI wording cannot drift. -
TUI Settings → "MCP" section. Sits next to Integrations because it's the same kind of concern: wiring Watchfire to something outside it. A pure thin client over the two onboarding RPCs — every badge, config path and instruction block is verbatim daemon state. Focusing the section fires exactly one
GetMcpClientStatus(guarded so a burst of nav keys can't fan out); one row per harness showsnot detected/detected/✓ configuredplus the dimmed config path, elided from the left so long paths can't overflow narrow terminals. Enter installs a detected-but-unconfigured harness with an inline spinner; anything else reveals the daemon's message instead of erroring, and Enter retries a failed status fetch rather than acting on stale state. A trailing Custom row revealscustom_snippetverbatim. The Enter decision lives in the form (McpEnterAction) so it's driven purely by daemon-reported state and testable without a connection. -
GUI Global Settings → "MCP" panel. The GUI half of the same surface, equally thin: one card per harness with detected/configured badges, config path, and the daemon's explanation of what installing would do. Detected + unconfigured gets a primary Install button; configured shows a "Configured ✓" pill plus a ghost Reinstall (the path is idempotent); undetected gets a disabled button with a tooltip and explicit manual steps. Install runs optimistically (spinner, splice in the returned status) then refreshes, and an install that couldn't be done automatically renders its message inline rather than as a raw error toast — that message is where the manual instructions live. The Custom card renders
custom_snippetin a monospace block with a copy button. Settings search gains anmcpcategory between Inbound and Updates. -
Test layers for the MCP surface.
make test-mcp-e2e(behind themcpe2ebuild tag, somake testnever compiles it) drives the realwatchfire mcp servebinary over stdio as an MCP client against a realwatchfiredunder an isolatedHOME: initialize →tools/list→list_projects→create_task(draft)→update_task(ready)→get_task→delete_task, asserting JSON shapes and that the task lands on disk, plus the actionable-error,--read-only, no-listening-socket (lsof) and onboarding-consistency checks. It never starts a coding agent — the run tools are exercised only through refusal paths that fail before a process exists, and it asserts that flipping a task toreadystarts nothing.catalog_test.goaudits the realtools/listpayload (uniform argument naming, published schema constraints, required fields, and the specific sentences an outer agent needs);local_only_test.goparses the serve path's own source and fails on any listener or non-stdio transport. -
Validation-on-write for task files.
config.ValidateTaskround-trips a task throughyaml.Marshal→yaml.Unmarshaland rejects anything that doesn't survive byte-for-byte;config.SaveTaskcalls it before every write, so any daemon-side task write (TUI/GUI/CLI/RPC/MCP) is guaranteed to be loadable.yaml.Marshalalready quotes scalars correctly, so this formalizes + tests that guarantee. -
Malformed-task visibility.
config.LoadAllTasksWithErrors/config.LoadMalformedTasks, theTaskService.ListMalformedTasksgRPC RPC (MalformedTask/MalformedTaskListmessages), a CLI warning inwatchfire task list, and a TUI status-bar indicator. -
Safer agent task authoring. The agent context prompt (
watchfire-prompt.txt) now steers task creation throughwatchfire task add(the safe, auto-quoting path) and, for direct YAML writes, mandates single-quoting anytitle:containing:— matching the guidance already present in the generate/wildfire-generate prompts.Note on auto-repair: an optional best-effort auto-repair of malformed files on the watcher event was considered and rejected. Because yaml.v3 fails the whole parse for an unquoted-colon title (the struct is never produced), a reliable repair would need fragile line-level heuristics; visibility was chosen as the safer of the two documented alternatives.
Changed
- Two tool-description defects fixed before ship — descriptions are part of the contract. The catalog is the only thing an outer model reads before choosing a call, so every tool carries a paragraph stating consequences (not just capability), a
title, and MCPannotations(readOnlyHint,destructiveHint,idempotentHint,openWorldHint: false— the tools' world is this machine). The audit caught two real problems: (1)create_task/update_taskpromised that statusreadymay auto-start an agent whenauto_start_tasksis enabled, but no daemon codepath readsProject.AutoStartTasks— an agent that believed it would file a ready task and wait forever; the descriptions now sayreadyonly queues. (2)list_tasksandget_taskare pure reads but carried the task registry group, so--read-onlyservedget_task_diff(strictly more revealing) while hiding the task itself; both moved to the inspect group, and a test now forbids thereadOnlyannotation and the registry group from disagreeing. - Actionable tool errors (
internal/mcpserver/errors.go). Errors are read by a model, not by a human tailing a log, so they name the problem and the way out:rpcErrstrips the gRPC envelope while keeping the daemon's specific message and reportsUnavailable/DeadlineExceededas an unreachable daemon with the command that fixes it;startupErrexplains that the MCP server is a thin client needing a localwatchfired; an unknown project now reads "not found — known projects: …". - Clean MCP shutdown exits 0. The MCP spec stops a stdio server by closing its stdin, which the SDK surfaces as a session error — so
mcp serveexited 1 and dumped cobra usage on every normal shutdown, which clients log as a crash.Servenow distinguishes a clean EOF or cancelled context from a real fault and exits 0, andmcp servesetsSilenceUsageso a genuine startup failure isn't buried under a flag dump. - Docs:
ARCHITECTURE.mdandREADME.mdcover the MCP server. ARCHITECTURE gains the "MCP Server (watchfire mcp) — v9.0 Firestorm" chapter (thin-client model, transport/shutdown/scoping, the 18-tool catalog, description contract, error design, local-only guarantee and its enforcement,--read-only, the recursion caveat, onboarding across all four surfaces, package layout, test layers, and the excluded/deferred list) plus a component-table row, CLI command-table entries, and the SDK in the tech stack. README gains the## MCP Serversection: quickstart per harness, the generic snippet, the factory loop, read-only mode, and the recursion warning.
Fixed
- Malformed task files no longer vanish silently. A batch of v8 task files
(0101–0121) was invisible in the GUI/TUI and never scheduled because an
unquotedtitle:containing a second:(e.g.title: v8 Inferno — Main: window registry) is parsed bygopkg.in/yaml.v3as a nested mapping and
rejected. The v7.2.0 resilience fix inconfig.LoadAllTaskscaught the
per-file parse error and skipped it so the chain didn't halt — but the task
disappeared with only a daemon log line. The loader now collects skipped
files (config.LoadAllTasksWithErrors) and surfaces them:watchfire task listprints a⚠ N task file(s) failed to loadwarning with each file's
name + parse error, and the TUI status bar shows a persistent
⚠ N task file(s) failed to loadindicator (fed by the new
TaskService.ListMalformedTasksRPC).