Skip to content

v9.0.0 — Firestorm

Choose a tag to compare

@github-actions github-actions released this 26 Jul 17:02
· 62 commits to main since this release

[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 official github.com/modelcontextprotocol/go-sdk (v1.6.1, which bumps the go directive to 1.25.0). The server auto-starts watchfired if needed and connects over the same path as the CLI, then serves a data-driven tool registry (toolSpec rows carrying name/title/description/annotations/schema/handler). Tools take an optional project argument (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 skips mcp commands so nothing ever writes to stdout — which is the transport.

  • Project tools: list_projects (project list enriched with live agent status) and get_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 through TaskService gRPC — the validated daemon path; no YAML is ever authored directly, so MCP-created tasks can't reproduce the malformed-file class of bug. create_task takes a schema-level status enum (draft|ready, default draft), optional acceptance criteria/position, and an agent-backend override validated against SettingsService.ListAgents with the valid backend list surfaced on mismatch. update_task is a partial update restricted to draft↔ready (done is agent-written); an empty agent string clears the override. delete_task is 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 to AgentService.StartAgent; a GetAgentStatus pre-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_agent is idempotent (stopped: false on an idle project). wait_for_task is the factory loop's synchronization point: it polls GetTask every ~2s, honours MCP request cancellation, and reports a timeout as a normal timed_out: true result 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 or scope: global), and list_logs / get_log (past session transcripts, capped at 64 KiB keeping the tail with an explicit truncation note).

  • --read-only mode. watchfire mcp serve --read-only filters 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 from tools/list and unknown when called by name. Suitable for dashboards or less-trusted callers.

  • Client onboarding: watchfire mcp install [client]. New internal/mcpserver/install/ provides pure, dependency-light (stdlib + BurntSushi/toml) Detect/Status/Install/Snippet writers for claude-code, codex, gemini, opencode and copilot, 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 --print for the generic {"command": "watchfire", "args": ["mcp", "serve"]} block.

  • Daemon onboarding RPCs — the cycle's only proto/daemon change. SettingsService.GetMcpClientStatus returns one McpClientStatus per known harness (stable key, display name, detected, configured, config path, message) plus custom_snippet, so every surface renders the Custom option from one source of truth; SettingsService.InstallMcpClient performs the install and returns the post-install state. Both are thin calls into the shared install package, 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 returns configured=false with 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) is InvalidArgument. The daemon pulls in install/ only — no MCP SDK or server runtime (verified via go 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 shows not detected / detected / ✓ configured plus 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 reveals custom_snippet verbatim. 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_snippet in a monospace block with a copy button. Settings search gains an mcp category between Inbound and Updates.

  • Test layers for the MCP surface. make test-mcp-e2e (behind the mcpe2e build tag, so make test never compiles it) drives the real watchfire mcp serve binary over stdio as an MCP client against a real watchfired under an isolated HOME: 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 to ready starts nothing. catalog_test.go audits the real tools/list payload (uniform argument naming, published schema constraints, required fields, and the specific sentences an outer agent needs); local_only_test.go parses the serve path's own source and fails on any listener or non-stdio transport.

  • Validation-on-write for task files. config.ValidateTask round-trips a task through yaml.Marshal → yaml.Unmarshal and rejects anything that doesn't survive byte-for-byte; config.SaveTask calls it before every write, so any daemon-side task write (TUI/GUI/CLI/RPC/MCP) is guaranteed to be loadable. yaml.Marshal already quotes scalars correctly, so this formalizes + tests that guarantee.

  • Malformed-task visibility. config.LoadAllTasksWithErrors / config.LoadMalformedTasks, the TaskService.ListMalformedTasks gRPC RPC (MalformedTask / MalformedTaskList messages), a CLI warning in watchfire task list, and a TUI status-bar indicator.

  • Safer agent task authoring. The agent context prompt (watchfire-prompt.txt) now steers task creation through watchfire task add (the safe, auto-quoting path) and, for direct YAML writes, mandates single-quoting any title: 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 MCP annotations (readOnlyHint, destructiveHint, idempotentHint, openWorldHint: false — the tools' world is this machine). The audit caught two real problems: (1) create_task / update_task promised that status ready may auto-start an agent when auto_start_tasks is enabled, but no daemon codepath reads Project.AutoStartTasks — an agent that believed it would file a ready task and wait forever; the descriptions now say ready only queues. (2) list_tasks and get_task are pure reads but carried the task registry group, so --read-only served get_task_diff (strictly more revealing) while hiding the task itself; both moved to the inspect group, and a test now forbids the readOnly annotation 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: rpcErr strips the gRPC envelope while keeping the daemon's specific message and reports Unavailable/DeadlineExceeded as an unreachable daemon with the command that fixes it; startupErr explains that the MCP server is a thin client needing a local watchfired; 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 serve exited 1 and dumped cobra usage on every normal shutdown, which clients log as a crash. Serve now distinguishes a clean EOF or cancelled context from a real fault and exits 0, and mcp serve sets SilenceUsage so a genuine startup failure isn't buried under a flag dump.
  • Docs: ARCHITECTURE.md and README.md cover 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 Server section: 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
    unquoted title: containing a second : (e.g. title: v8 Inferno — Main: window registry) is parsed by gopkg.in/yaml.v3 as a nested mapping and
    rejected. The v7.2.0 resilience fix in config.LoadAllTasks caught 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 list prints a ⚠ N task file(s) failed to load warning with each file's
    name + parse error, and the TUI status bar shows a persistent
    ⚠ N task file(s) failed to load indicator (fed by the new
    TaskService.ListMalformedTasks RPC).