Skip to main content
Watchfire
Main content

Changelog

Watchfire release notes — every shipped version with codename, date, and itemised change log for the daemon, CLI/TUI, and GUI clients.

Subscribe to releases

Get every Watchfire release in your feed reader: RSS · Atom · JSON Feed. RSS works in most readers (NetNewsWire, Feedly, Inoreader, Reeder); Atom is the more machine-correct cousin; JSON Feed is the lightweight modern format.

[10.0.4] Torch

Patch release: a mode switch always takes effect — the GUI's chat auto-start can no longer steal the slot mid-switch.

Fixed

  • /wildfire (and any mode switch over a running chat) no longer fails with "timed out waiting for previous agent to stop". StartAgent's replace path kills the running agent, then waits for the slot to empty before spawning the requested one. The GUI and TUI poll agent status every ~2 s and auto-start chat the moment they see isRunning=false — and that poll can land inside the kill→spawn gap. The opportunistic chat start passed the gate (nothing was running, no chain in flight), registered itself, and the user's switch then found "an agent" still present and bailed with the timeout message, leaving a fresh chat where wildfire had been asked for (the project log shows exited (mode: chat) immediately followed by started (chat mode)). The daemon now marks the replace window (Manager.replacing, the sibling of the v10 chain-transition mark) and refuseChatStart refuses chat while it is set, so the slot belongs to the switch; the GUI/TUI already treat that refusal as expected on their auto-start paths. The bail-out also says which case it hit — old process never left, or another client's non-chat start took the slot — instead of calling both a timeout.

[10.0.3] Torch

Patch release: Claude's periodic OAuth revocation ("Please run /login") can now be fixed from Telegram.

Added

  • /login — re-authenticate Claude from your phone. Claude Code's OAuth token gets revoked periodically (observed roughly every 12 hours on some accounts); the agent prints "Please run /login · API Error: 401 OAuth access token has been revoked" and stalls until someone at the machine re-auths. Watchfire can't stop the expiry — it's Claude's token lifetime — but it can make the fix remote. The sign-in URL the CLI shows carries a per-process PKCE challenge, so it can't be pre-generated; /login instead drives the live session's own dialog using only the bridge's existing read (screen snapshots) and write (injectSay) primitives: type /login, confirm the method picker, scrape the oauth/authorize URL off the screen (joining wrapped lines), send it to the chat, and arm the chat so your next plain-text message is pasted into the session as the code — you tap the link, approve, copy the code, send it back, done. "cancel" disarms. When a watched session raises auth_required, the relay posts a one-time hint pointing at /login, so the stall announces its own remedy. The dialog's screen markers were pinned from a live capture of Claude Code v2.1.238 and the flow is covered by a scripted-dialog test.

[10.0.2] Torch

Patch release: mode switches from Telegram now behave like every other client.

Fixed

  • Starting a mode from Telegram replaces the running agent instead of refusing. /run, /run all, /wildfire, /generate, and /plan had inherited the MCP run_task "never queue, never replace" contract — sensible when the caller is another agent, absurd for a human: with the GUI's always-on chat agent live, /wildfire answered "⛔ An agent is already running (mode chat)", /cancel wanted a task number, and /stop then /wildfire lost the race to the GUI auto-restarting chat. The starters now go straight through the daemon's StartAgent — the same atomic kill+restart the GUI mode buttons and the TUI use — and the confirmation names what was displaced ("▶ Started task #0009 … Replaced the running chat session."). One refusal remains, deliberately: /new (a chat start) never displaces a working non-chat agent, and says to /stop first. An already-running wildfire is reported rather than restarted. The MCP tools keep their never-replace contract untouched.

[10.0.1] Torch

Patch release: one transcript-lookup bug that silently disabled watch mode and session-transcript capture for any project whose path contains a dot or underscore (found live when n9o.xyz never streamed to Telegram while watchfire-website did).

Fixed

  • Projects with dots or underscores in their path get transcripts again. Claude Code names its per-project transcript directory by replacing every non-alphanumeric character in the working directory with - (/Users/x/source/n9o.xyz-Users-x-source-n9o-xyz, blowfish_examplesblowfish-examples); LocateTranscript only replaced /, so for such projects no transcript was ever found — watch mode relayed nothing (the tailer retried forever, silently) and the end-of-session log copier never captured a transcript (the project log shows the "transcript dir not found" line on every session end). The encoding now matches Claude's rule, pinned by a table test and a dotted-work-dir locate test.

[10.0.0] Torch

Torch puts the fleet in your pocket: a Telegram bridge lets you supervise the daemon from any phone — and not just with commands: just type in a paired chat to talk to a chat agent (one is auto-started if nothing is running), watch mode streams the replies back by default, /wildfire runs the autonomous loop with a phone-glanceable milestone feed, and task/run/digest events are pushed as they happen. Everything rides the bot's outgoing long-poll connection, so nothing ever listens on the machine. Pairing is the security boundary: anyone on Telegram can DM a bot, so the paired-chats list is the allowlist and a crypto-random one-time code is the only way onto it. Two invariants are enforced by a source-parsing guard test: the bridge never calls Resize, and injectSay — the /say verb and plain-text chat forwarding — is the only PTY write in the package.

Around the tentpole, Torch closes a cluster of long-standing friction: the v5.x Slack/Discord inbound handlers are finally registered in production, a new retrofit mode folds shipped tasks back into the project definition, quick-add turns a pasted list into a batch of tasks on every surface, sandbox-denied folders explain themselves, Claude's folder-trust dialog no longer stalls fresh-path runs, the integrated terminal gets a real login shell, and a race that silently ended run-all/wildfire chains is fixed at the daemon.

Added

  • Telegram bridge — client and plumbing. A thin, stdlib-only Bot API client mirroring slackbot/discordbot (getMe, long-poll getUpdates, sendMessage, editMessageText, setMyCommands, answerCallbackQuery), with a consistent error type classifying network/API/auth failures, retry_after surfaced on 429s, and the token scrubbed from error paths. The bot token routes through the secret store (watchfire.integration.telegram.bot_token) and never lands in integrations.yaml; an unconfigured install starts no goroutine and dials nothing.
  • Pairing — the allowlist. BeginTelegramPairing issues an 8-char code from crypto/rand over an unambiguous alphabet (no 0/O/1/I/l), 10-minute TTL, at most one active code, constant-time compare, single-use. The bridge long-polls with offset acking, exponential backoff, and graceful shutdown; /start <code> or /pair <code> persists the chat, everything else from an unpaired chat draws silence. RevokeTelegramChat removes a chat from disk and the live bridge immediately. CLI: watchfire telegram pair (code + t.me deep link, polls until paired) and watchfire telegram status.
  • Read-only commands. /projects (numbered list with agent-status glyphs and an inline keyboard), /use <name|number> (fuzzy match, persisted default project per chat), /status (reusing the shared command router verbatim), /tasks, /help. A renderer maps command responses to Telegram HTML with entity escaping and 4096-char chunking at line boundaries; setMyCommands publishes the verb set.
  • Outbound event relay. The Telegram relay adapter formats TASK_FAILED / RUN_COMPLETE / WEEKLY_DIGEST as Telegram HTML and fans out to every non-muted paired chat, aggregating per-chat failures so the dispatcher's retry + circuit breaker governs delivery. Registered only when enabled + token resolving + ≥1 paired chat, and hot-activated on integrations changes with no daemon restart. TestIntegration upgraded from getMe to real synthetic delivery with per-chat results.
  • GUI Settings → Telegram panel. Enable toggle, write-only bot-token field (empty submit keeps the stored token), event toggles, per-chat Muted/Watch toggles, confirm-gated Revoke, and a pairing flow rendering the code, t.me deep link, and a locally generated QR with live countdown and auto-refresh on paired. The QR encoder is hand-rolled — no external encoder, no network.
  • TUI + CLI parity for Telegram. The integrations overlay gains a Telegram summary row with paired chats as child rows, a masked write-only token step, pairing on p with code + deep link + live countdown, m/w per-chat mute/watch toggles, and confirm-gated revoke/delete. CLI: watchfire integrations add telegram with no-echo token prompt; re-adds rotate the token and preserve configured events.
  • Watch mode — live conversation relay. /watch on streams an agent's session to the chat. Claude Code sessions are first-class: a polling transcript tailer emits assistant text verbatim and tool uses as one-liners ("⚒ Edit internal/tui/model.go"); other backends fall back to debounced, change-only screen snapshots. Per-chat sender discipline: 4096-char chunking, coalescing (≤1 send per 2.5s), edit-in-place growth of the current assistant message, and a flood cap with hysteresis recovery. Sessions open with "▶ task NNNN — title" and close with the task outcome ("✔ merged" / "✖ failed: reason" / "⚠ merge failed" / "■ ended"). Read-only by construction — a guard test bans Resize/SendInput references from the watch path.
  • Run-control verbs. /run <n>, /run all, /retry <n>, /cancel, /screen (one-shot tail of the live screen), /say <text> (verbatim message + exactly one Enter — the single sanctioned PTY write), /mute on|off. Starters go through the same StartAgent path MCP uses, with the MCP refusal contract: never queue, never replace, rich refusal naming the in-flight task.
  • Talk to your agents in Telegram. Plain text in a paired chat now talks to a chat agent — no /say prefix. Three cases: a live chat session gets the message injected verbatim; a live working session (task, run-all, wildfire, generate) is never typed into implicitly — the reply names what's running and offers options (/watch on, /screen, explicit /say, /cancel); and with nothing running the bridge auto-starts a chat agent on the chat's project and delivers the message once the session paints its first screen. To make it work out of the box, watch defaults on for every paired chat, and a watching chat that never sent /use auto-attaches to the most recently started live session; /use pins it.
  • /wildfire and the wildfire milestone feed. Bare /wildfire starts the autonomous loop from Telegram; /wildfire off or /stop ends it with user-stop semantics so the chain doesn't continue. While watch is on, wildfire sessions relay milestones instead of a raw stream: "🔥 wildfire — generating new tasks…", "✚ generated task NNNN — title", "🔥 wildfire — implementing task NNNN — title", and the usual outcome markers — a phone-glanceable narrative of what the loop is deciding and shipping.
  • /generate and /plan from Telegram. The GUI's Generate (project definition from the codebase) and Plan (tasks from the definition) buttons get Telegram verbs, refusal-gated like /run.
  • /stop, /status all, /agent, /new. Stop whatever is running with the reply naming what was stopped; fleet status one line per project with live session state; show or switch the project's agent backend (uninstalled backends refused, applies to new sessions); start a fresh chat session, clearing the conversation context.
  • "typing…" while the agent works. The relay re-sends Telegram's typing chat-action while the session is active, so the gap between coalesced sends no longer reads as "nothing is happening".
  • Quick-add batch tasks. One shared parser turns pasted text into tasks: each top-level bullet becomes a task, nested lines fold into the prompt, AC:/Acceptance: lines become acceptance criteria; titles derive from the first sentence. A new TaskService.CreateTasksBatch validates every task before writing any. Surfaces: GUI Quick Add modal with live "will create N tasks" preview, TUI overlay on A, and watchfire task quick ($EDITOR template, --ready/--draft/--stdin).
  • Definition retrofit mode. A new retrofit-definition agent mode folds tasks completed since the last retrofit into an updated project definition, advancing a last_retrofit_task_number watermark. A confirm-gated archive then soft-deletes exactly the folded window; archived tasks carry retrofit_archived and keep counting in insights. Surfaces: GUI Definition tab action + modal, TUI r on the Definition tab, CLI watchfire definition retrofit [--archive] [--yes].
  • Fleet insights chart controls. The dashboard rollup card gains a day/week/month aggregation toggle and a "vs lines" compare mode that pairs the tasks chart with an aligned lines-of-code chart — same buckets, same column order, shared hover. Both persist alongside the existing 7d/30d/90d/All window preset.
  • Live run state gets its own header line. The current task and Stop control render as a dedicated line under the project title and git rows; the chat toolbar now carries only the mode starters.
  • Telegram is a first-class card in Settings → Integrations. The card renders whether or not a config exists — unconfigured installs get a fire-orange "Set up Telegram" call-to-action, a configured bridge shows a green "Configured ✓" pill with its events and paired-chat counts.

Changed (Telegram command surface)

  • The command set was consolidated. /runall folded into /run <n>|all and /unmute into /mute on|off (both old verbs remain as hidden aliases, so nothing breaks), and bare /wildfire now starts the loop directly. Telegram's setMyCommands menu registers the full canonical set, and /help groups everything into Project / Run / Session sections.

Fixed

  • Slack/Discord inbound commands actually work in production. The v5.x slash-command/interactivity handlers were never registered in production, and the command router's CommandContext had no production implementation — /watchfire status|retry|cancel was reachable only from tests. A deps-injected production CommandContext now scopes projects by Discord guild / Slack team, routes Retry through BulkUpdateStatus and Cancel through user-stop semantics, and the handlers register behind the same secret-ref gates as the Git hosts.
  • Chat auto-start no longer race-kills the run-all/wildfire chain. During a chain transition (finished agent removed, next worktree being created, ~1–2s), a GUI poll landing in that window auto-started chat, whose replace path marked the freshly chained task agent user-stopped — silently ending the run with ready tasks still queued. StartAgent now refuses a chat-mode start while a non-chat agent is running or while the chain transition is in flight, protecting all clients at the daemon.
  • Claude's folder-trust dialog no longer stalls fresh-path runs. Sessions in a directory Claude Code hadn't trusted sat silent on the interactive trust dialog until someone attached and hit Enter. The dialog frame is now detected (both current and classic CLI wordings, claude-code backend only, first 30s only) and auto-accepted once per session; a recurring frame raises a visible trust_dialog issue instead of looping input.
  • Sandbox-denied folders explain themselves (#17). A project under ~/Desktop, ~/Documents, or ~/Downloads failed with an opaque "unexpected error" because the sandbox denies those roots by design. A preflight check now blocks registration with an actionable message, and StartAgent refuses before any PTY spawn with a sandbox_denied issue surfaced through the agent-issue plumbing. sandbox=none runs are exempt; the policy itself is unchanged.
  • The integrated terminal spawns a login shell (#32). The in-app terminal spawned a non-login shell, so /etc/zprofile (path_helper) and ~/.zprofile never ran and user PATH entries were missing. The resolved shell now spawns with -l on macOS/Linux; the shell-setting precedence is unchanged and re-read per spawn.
  • Home-window issue streams: no more console spam, no more deaf panel. The needs-attention panel blind-subscribed to agent issues for every registered project, logging a no agent running error per idle project — and a dead stream was never retried, so the panel went permanently deaf to agents started later. Subscriptions are now gated on polled agent status and re-established whenever the set of running agents changes.
  • The GUI reports the right version. The About/Sidebar version comes from gui/package.json, which the v9.3/v9.4 release commits forgot to bump — packaged apps self-reported 9.2.0. Now synced as part of this release.
  • A running chat agent counts as working — everywhere. "Working" was defined as "a non-chat agent is running" across the tray, the dashboard dots, and the mini monitor, so a project with a live chat session read as Idle. The definition is now simply "an agent is running", chat included.
  • CI runs the GUI unit tests. Both GUI jobs ran npm ci + npm run build but never npm test, so the renderer unit tests were not exercised in CI. Both jobs now run them between install and build.
  • Watch mode no longer replays a stale session and then goes silent. Claude Code session names are reused across runs, and the transcript locator returned the first match in directory order — an arbitrary, usually old, dead transcript. The locator now filters matches to files touched at/after the session's start and picks the freshest, and the tailer re-locates after ~8 no-growth polls — locking onto a dead file can no longer silence the relay.
  • Replies no longer splice onto the previous message. The relay's edit-in-place growth happily appended the answer to your NEXT question onto the previous reply's bubble. A real user turn in the transcript now emits a turn break that ends the grown message, so every new answer arrives as a new message positioned after the user's own — whether the question came from Telegram, the GUI, or the TUI.
  • Dev-mode xterm crash on mount. A terminal opened and disposed within the same commit — React StrictMode's dev double-mount — crashed when xterm's internal open-timer fired. The terminal hook now defers open() by one frame behind a disposed guard.
  • Settings no longer wears a black bar. The home window stacked a full-width titlebar drag strip above the settings view, reading as a stray black band. Settings now provides its own drag region; the strip renders only for the other views.

Changed

  • Dashboard defaults to the list view. List is the default layout unless the user explicitly chose one; grid is the secondary presentation.
  • Project-window IA. Project Settings is always accessible: a persistent gear in the project-window header plus Cmd+,/Ctrl+, and an app-menu item. Reference tabs regrouped — Tasks/Definition/Insights primary with icon+label, Secrets/Trash/Settings as an icon-only utility cluster. Wildfire moved from the header into the chat toolbar's mode cluster, leading it in fire-orange.
  • Docs. The architecture document gains the Telegram Bridge chapter (components, long-polling local-only rationale, pairing security model, command set, watch-mode tiers and rate discipline, the two enforced invariants); the README gains "Supervise from Telegram" setup, Retrofit Definition, and quick-add.

[9.4.0] Firestorm

A bug-fix release closing the two open GUI issues: Nerd Font icons rendering as tofu boxes in the embedded terminals (#50), and the notification center being a read-only list (#49).

Fixed

  • Embedded terminals render Nerd Font glyphs. The GUI's xterm.js terminals use the DOM renderer, which resolves glyphs through CSS font fallback — and Chromium performs no system-wide fallback for Private Use Area codepoints, the range Nerd Fonts use for file-type/git/powerline icons. Unless the user happened to have one of the six fonts hard-coded in the terminal font stack installed under exactly that name, every icon rendered as a missing-glyph box (#50). The GUI now bundles Symbols Nerd Font Mono (the icons-only Nerd Font, MIT-licensed, converted to woff2) and appends it to the terminal font stack just before monospace, so icon codepoints always resolve regardless of which fonts the machine has — while text glyphs still come from the user's real monospace fonts earlier in the stack. The font is preloaded from index.html so the first paint can't race the fetch.
  • Notifications are clickable and can be marked as read. The bell dropdown's "Recent" entries were static list items, and the badge counted every notification plus every digest forever — there was no way to act on a notification or clear the count (#49). Each entry is now a button: clicking marks it read and routes by kind — a weekly digest opens the most recent saved digest, task/run events open (or focus) the project's own window on its Tasks tab — reusing the exact routing path OS-toast clicks already used, so it works from the home window and project windows alike. Records carry a read flag in the notifications store, unread entries show a dot and read ones dim, a "Mark all read" affordance sits in the section header, and the bell badge now counts only unread live notifications (digests are a browsable archive, not pending items).

[9.3.0] Firestorm

A cleanup release. The per-session agent home directories under ~/.watchfire/<agent>-home/ accumulated forever — one directory per session for every non-Claude backend — and deleting a project left all of its directories behind indefinitely (#47).

Fixed

  • Per-session agent home dirs are removed at session end. Every non-Claude backend (Codex, Copilot, Cursor, Gemini, opencode) materializes a per-session home under ~/.watchfire/<agent>-home/<session>/ holding the composed AGENTS.md/system prompt, config symlinks, and the agent CLI's own session state — and nothing ever deleted it, so the dirs accumulated one per project × mode × task even for live projects. The directory is pure scratch once the session's transcript has been exported to the session log, so the daemon now removes it right after writing the session log, guarded so a degenerate session name can never resolve the removal at the home root itself or anywhere outside it. The directory is recreated on the next session with the same name, so restarts are unaffected.
  • Deleting a project sweeps its leftover session homes. Deleting a project deliberately only unregisters it — its own .watchfire/ tree survives re-adding — but daemon-side scratch was never considered, which is the leak reported in #47. Session names begin with the project's name slug followed by :, so project deletion now sweeps every backend's home root for directories with that prefix. Directories belonging to currently running sessions are skipped — they are removed by their own end-of-session cleanup — which also defuses the edge case of two projects sharing a 30-character slug while one is mid-session. Logs, diff-cache, and insights entries are intentionally left alone: they re-attach cleanly if the project is re-added, and deleting them would lose history.

[9.2.0] Firestorm

A dashboard-legibility release. The list view showed different information for different projects for reasons that had nothing to do with the projects, and neither chart in the Fleet insights card rendered a single number.

Fixed

  • List-view task counts no longer vanish while an agent is working. In the list view's project row, the N todo · N in dev · N done line was the else branch of the running-agent check, so a project actively being worked on — the one whose numbers you most want — showed only a badge and the current task title. Counts now render whether or not an agent is running, alongside the badge. The grid view's project card already did this, so the two layouts agree again.
  • The per-row "shipped" chip is no longer restricted to five projects. The chip is derived from the fleet insights top-projects list, which the daemon truncated to five — a cap that exists to size the leaderboard pill row, inherited by accident when v8.0 started reading the same list for per-project churn lookup. A project ranked sixth by completed-task count showed no churn at all, however much code it had shipped. The daemon now returns every project with activity in the window, ordered as before; truncation moved to the renderers that actually want a leaderboard (GUI pill rows, the TUI Top proj line, MCP get_insights), each of which now shows a +N more affordance instead of silently implying it listed everything.
  • The fleet insights cache is versioned. ~/.watchfire/insights-cache/_global.json carried no schema stamp, so an upgraded daemon would keep serving pre-9.2 entries — capped at five projects — until a task happened to complete and cascade an invalidation. Entries now carry a schema field, a mismatch reads as a cache miss, and a write replaces a foreign-schema file wholesale rather than merging into it and relabelling stale data as fresh.

Added

  • Charts carry always-visible numbers. Both charts in the Fleet insights card previously rendered shape without magnitude — you could see when the fleet was busy but never how busy without hovering. Tasks-per-day now shows peak N · M total (plus window churn when code metrics exist), and the agent bar shows total tasks and agent count. The tasks chart also labels itself last 30d when a 90-day window is trimmed to the rendered cells, so the headline can't be read as covering the whole window.
  • Styled hover cards replace the native title tooltips. The old tooltips had the browser's ~1s delay, couldn't be styled, and carried one flat string. Hovering a day now shows the date, the succeeded/failed split, and lines added/removed for that day; hovering an agent segment shows its task count and fleet share, success rate and average duration, and its line churn and commits. Cards flip alignment near either edge so they can't clip, are pointer-events-none so they can't steal hover from the bar beneath them, and dim the unhovered bars. Day bars get a full-column hit area — a 2px bar on a quiet day was near-impossible to hover.

[9.1.0] Firestorm

A bug-fix release for Insights. Project Insights, fleet Insights, the CSV/Markdown exports and the weekly digest all showed zeros or near-zeros, because almost no completed task ever received a completed_at timestamp — and every rollup filters on it.

Fixed

  • The daemon now stamps completed_at on the done transition. The completion protocol tells agents never to write timestamps ("the daemon fills them in"), but the daemon never did on the watcher path: when an agent wrote status: done, the change handler stopped the agent, captured metrics and merged — without stamping. Only the interactive bulk set-status path stamped it, leaving 6 of 585 done tasks across the fleet with a timestamp. The stamp now happens on the first observed done event, before metrics capture, so duration_ms in <n>.metrics.yaml is computed from real endpoints instead of degrading to 0. The re-save fires one extra watcher event, which sees the stamp already present and does nothing — with a new guard so a failed task can't emit TASK_FAILED twice.
  • Insights recovers the ~580 pre-9.1 tasks. A done task's completion time now resolves as completed_at → the metrics file's captured_at (written seconds after completion) → updated_at (the agent's final YAML write), across the per-project aggregator, the fleet aggregator, the CSV/Markdown export stats and the weekly digest. Historical tasks — and their durations, day buckets, agent breakdowns and shipped-code numbers — appear in Insights without a single task file being rewritten.

[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_taskrun_taskwait_for_taskget_taskget_task_diff → iterate.

The MCP server is the fourth thin client, and it holds no orchestration logic of its own: every tool call translates 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. The entire release 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 listener or non-stdio transport in the serve path, and the end-to-end test inspects the live process from outside to confirm it.

Added

  • watchfire mcp serve — the stdio MCP server. Built on the official MCP Go SDK. The server auto-starts watchfired if needed and connects over the same path as the CLI, then serves a data-driven tool registry. 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 directory walk-up. A single server instance can address all registered projects.
  • 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 the validated TaskService gRPC path — no YAML is ever authored directly, so MCP-created tasks can't reproduce the malformed-file class of bug. create_task takes a status enum (draft | ready, default draft), optional acceptance criteria and position, and an agent-backend override validated against the known backends. update_task is a partial update restricted to draftready (done is agent-written). 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. A status 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. wait_for_task is the factory loop's synchronization point: it polls, honours client 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.
  • Inspect tools: get_task_diff (unified-diff text with per-file and total counts), get_agent_screen (tail of the live agent terminal, ANSI escapes stripped and spinner redraws resolved), get_insights (throughput + cost + shipped-code summary, project or global scope), and list_logs / get_log (past session transcripts, capped with an explicit truncation note).
  • --read-only mode. watchfire mcp serve --read-only serves exactly 8 of the 18 tools — the project and inspect groups. The write and run tools aren't merely refused: they're filtered at registration time, so they never appear in tools/list at all. Suitable for dashboards or less-trusted callers.
  • Client onboarding: watchfire mcp install [client]. Idempotent installers for claude-code, codex, gemini, opencode and copilot, plus a 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 degrades to printed manual instructions on a missing client or unparseable config — an existing config file is never clobbered.
  • Onboarding on every surface. The same installer backs the watchfire mcp install CLI, a TUI Settings → MCP section, and a GUI Global Settings → MCP panel, each showing per-harness not detected / detected / ✓ configured badges and the config path. No surface reads a harness config itself, so the wording cannot drift.
  • Validation-on-write for task files. Every task is round-tripped through marshal → unmarshal before being saved and rejected if it doesn't survive, so any daemon-side task write (TUI/GUI/CLI/RPC/MCP) is guaranteed to be loadable.
  • Malformed-task visibility. A new ListMalformedTasks RPC, a ⚠ N task file(s) failed to load warning in watchfire task list, and a persistent TUI status-bar indicator.

Changed

  • Tool 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, a title, and MCP annotations (readOnlyHint, destructiveHint, idempotentHint, openWorldHint: false). A pre-ship audit caught two real defects: create_task / update_task promised that ready may auto-start an agent when no daemon codepath actually does that — an agent that believed it would file a ready task and wait forever; and list_tasks / get_task were grouped such that --read-only served get_task_diff while hiding the task itself.
  • Actionable tool errors. Errors are read by a model, not a human tailing a log, so they name the problem and the way out: an unreachable daemon reports the command that fixes it, and an unknown project reads "not found — known projects: …".
  • Clean MCP shutdown exits 0. The MCP spec stops a stdio server by closing its stdin, which previously surfaced as a session error — so mcp serve exited 1 and dumped usage on every normal shutdown, which clients log as a crash.

Fixed

  • Malformed task files no longer vanish silently. A batch of v8 task files was invisible in the GUI/TUI and never scheduled because an unquoted title: containing a second : is parsed as a nested mapping and rejected. The v7.2.0 resilience fix 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 and surfaces them in both the CLI and the TUI status bar.

[8.0.0] Inferno

Inferno is the parallel-workspaces tentpole — the first feature-forward major since v4 "Beacon", built for supervising many projects at once. The Electron GUI goes multi-window: a main-process window registry replaces the single mainWindow, opening one independent window per project (single-instance lock, per-window state + session restore, Cmd+N and window cycling, IPC fan-out, and per-window PTY routing so a terminal's bytes and OS notifications never cross windows). Each project window flips to a chat-primary layout with the agent terminal as the wide left pane and Tasks/Definition/Insights/Secrets/Trash/Settings as a right reference region. The plain markdown <textarea>s become a real CodeMirror rich editor (formatting toolbar + source ⇄ split ⇄ preview, closing #22), wildfire mode lands in the GUI behind a confirm-gated start with a live Execute → Refine → Generate phase indicator, and the promoted Dashboard becomes mission control — a live home window with per-project wildfire phase, cross-project needs-attention with click-through, and a stretch always-on-top mini-monitor. Finally, code-output analytics measure what the agents actually shipped: per-task <n>.metrics.yaml captures commits/files/lines/merge-kind, rolled up into project and fleet Insights, the GUI's KPI cards and churn-by-day chart, mission-control "shipped" lines, and the CSV/Markdown exports plus weekly digest.

Added

  • Multi-window GUI foundation. A new main-process window registry (gui/src/main/windows.ts) replaces the mainWindow singleton with a Map keyed by window id: a singleton home/mission-control window plus one independent BrowserWindow per project (scoped via ?project=<id>). Re-opening an already-open project focuses its window instead of duplicating it. A single-instance lock stops a second Electron process from spawning a parallel daemon watcher.
  • Per-window state + session restore. window-state.json migrates from a single bounds rectangle to a keyed schema — home bounds, a per-projectId projects map, and an openProjects list — so every window's size and position persists independently. On relaunch, the project windows that were open at last quit re-open automatically (pruning any deleted project). The old flat shape is read transparently as the home bounds.
  • "Open in new window" affordances + window shortcuts. Dashboard cards/rows and sidebar rows open a project's own window via a hover button, ⌘/Ctrl-click, or a context-menu entry. A new app menu adds ⌘/Ctrl+N (open/focus the home window) and ⌘/Ctrl+Shift+] / [ to cycle focus across all open windows.
  • Wildfire is now controllable from the GUI. The autonomous Execute → Refine → Generate loop — previously TUI/daemon-only — is a first-class control in the ProjectView header, started through a confirm-before-start modal (it spends tokens unattended) and surfaced as a live phase stepper with the current task and a Stop control. Pure GUI wiring over the existing StartAgent(mode="wildfire") + AgentStatus.wildfire_phase; no proto or daemon changes.
  • Rich markdown editor (MarkdownEditor). A reusable, controlled CodeMirror 6 editor with line wrapping, a formatting toolbar (bold / italic / inline-code / link / heading / list), Cmd+B/Cmd+I, and a source ⇄ split ⇄ preview toggle — themed with the app's --wf-* tokens so it follows dark/light mode. Per spec it's source + live preview (not WYSIWYG) so values round-trip cleanly through YAML block scalars. First half of #22.
  • Mission control — the home window. The Dashboard becomes the persistent "what's running everywhere?" surface: a per-card wildfire phase badge, a cross-project Needs attention panel aggregating live agent issues (auth / rate-limit) and failed tasks with click-through to the offending project's own window, and open-window awareness (the per-card action flips to "Focus open window" when a window already exists). Tray clicks now open or focus the relevant project's window rather than a generic one.
  • Always-on-top mini-monitor. A small frameless floating window (Window → Mini Monitor, ⌘/Ctrl+Shift+M) with one compact, activity-sorted row per project — a pulsing status dot, the project name, and a one-line status (current task / chat / Wildfire · <phase> / needs-attention / ready-idle counts). Clicking a row opens or focuses that project's window. It's read-only and cheap, and floats above other apps and fullscreen spaces.
  • Code-output analytics. Per-task <n>.metrics.yaml gains seven code-output fields — commits, files_changed, lines_added, lines_removed, net_lines, merged, merge_kind — snapshotted from the live watchfire/<n> branch before worktree cleanup, for both silent-merge and GitHub auto-PR paths. They roll up into project and fleet Insights (totals, churn-by-day buckets, per-agent commits/lines) behind a MetricsMissingCode honesty counter, surface in the GUI's Insights KPI strips and churn chart and the mission-control per-card "shipped" line (+412 / −97 · 3 merges), and flow through the CSV/Markdown exports and the weekly digest. Metrics written before v8.0 read back as zeros.

Changed

  • ProjectView is chat-primary. The agent chat/terminal is now the wide left pane and Tasks/Definition/Insights/Secrets/Trash/Settings are a tabbed reference region on the right — inverting the old center-content + collapsible-chat layout to fit per-project windows. The v7.3 focus-chat toggle (and the divider double-click) now hide the right region for full-width chat; there's no side-swap toggle.
  • All three GUI markdown surfaces use the rich editor. The project definition, the Add Project wizard's definition step, and both long-text fields of the task modal (prompt, acceptance_criteria) now render the CodeMirror MarkdownEditor, each keeping its existing state, save, and validation semantics verbatim. Closes #22.
  • Per-window event routing. Daemon lifecycle (daemon-ready / daemon-shutdown) and the four auto-updater events now broadcast to every window via a registry broadcast() helper, while notification- and focus-clicks route to the right one — a TASK_FAILED / RUN_COMPLETE click opens (or focuses) the project's own window; a WEEKLY_DIGEST surfaces on the home window. To avoid duplicate toasts with N windows open, only the home window subscribes to the notification and focus streams (the single-notifier / single-router election).
  • Docs: ARCHITECTURE.md updated for the Inferno GUI — the multi-window model (window registry, per-window PTY routing, keyed window-state + session restore, single-instance lock, IPC fan-out, single-notifier election), the chat-primary Project View, GUI wildfire, the rich MarkdownEditor, and a new daemon "Task Metrics & Code-Output Analytics" subsection.

Fixed

  • OS notifications fire exactly once no matter how many windows are open. With one window per project, N subscribed renderers meant N toasts + N sounds per event. The home window is now the sole notifier — enforced both in the notifications store (start() early-returns unless it's the home window) and in App.tsx.
  • Integrated-terminal output no longer bleeds across windows. Each PTY session now records the windowId that spawned it and routes onData / onExit only to that window, so project A's terminal bytes can't surface in project B. Closing one project window tears down only its own terminals.

[7.4.0] Forge

Forge 7.4 closes a class of "wildfire stops even though there's a ready task sitting right there, and drops back to chat" reports. Traced live on a real project, the symptom turned out to be three independent daemon bugs plus a runaway log that buried the evidence: Generate produced a ready task, the chain stamped it started, but no agent ever ran it — and the one log line that would have explained why was drowned under hundreds of MB of self-referential watcher spam. This release fixes the runaway log at the source, moves the verbose per-project trail into per-project log files so the global daemon.log stays readable, makes a launch failure unable to strand a ready task, and stops the issue detector from false-positiving on ordinary agent output.

Fixed

  • The daemon log no longer feeds an infinite fsnotify loop. The watcher watches ~/.watchfire (for projects.yaml and friends), and since v7.2.1 the daemon writes its own log inside that dir — but it logged every raw fsnotify event unconditionally, so each log write produced an event that was itself logged, forever. One user's log family pinned at the full cap as pure watcher noise. The watcher now skips events on its own log path, and the per-event firehose lines are gated behind a new WATCHFIRE_DEBUG env var (off by default).
  • First run after upgrade reclaims an oversized legacy daemon.log. A pre-7.4.0 daemon could leave a multi-hundred-MB (or multi-GB) log plus an equally large backup; handing that to the rotating writer would just preserve the bloat. A new startup step deletes any over-cap numbered backup and truncates an oversized active log down to its last 256 KiB (with a migration marker), touching only files that exceed the cap.
  • A launch failure can no longer strand a ready task and silently drop to chat. The "mark task started" stamp used to run before the agent was confirmed running, so a spawn failure (e.g. PTY allocation under fd pressure) left the task persisted as started with no agent behind it — stuck in ready forever. The stamp now runs after the process spawns successfully, and the chain's "failed to start next" path emits a TASK_FAILED notification instead of going quietly idle.
  • The auth / rate-limit issue detector no longer false-positives on ordinary agent output. It scans the agent's entire PTY scrollback — including code and task prompts — but matched on bare substrings like rate limit and token expired, so a project that merely mentions rate limiting could trip a phantom issue that halts the autonomous chain. Both pattern sets now require the shape of a real provider error (HTTP 401/429, authentication_error, the Claude limit banners, or an OAuth / API-key qualifier).

Changed

  • The verbose per-project daemon trail moved to ~/.watchfire/logs/<project_id>/daemon.log. A new self-rotating per-project appender (16 MiB × 1 backup) now carries the "hardcore" operational lines — wildfire [chain] / [poll] decisions, agent start/exit/stop, the [merge] flow, the per-process [agent] PTY trail, and the server-side task-watch handlers. Genuinely global lines (daemon start/stop, update checks, OAuth, the weekly digest, integration errors) stay in the global daemon.log.
  • Global daemon.log cap dropped from ~1 GiB to ~64 MiB. With the fsnotify loop fixed and the per-project trail rerouted, the global log carries only coarse lifecycle lines, so the v7.3.0 1 GiB safety cap is far larger than needed (per-file 500 MiB → 32 MiB, one backup).

Added

  • WATCHFIRE_DEBUG env var. When set, the watcher re-enables the per-event [watcher] fsnotify: … and debounce lines for diagnostics. Off by default.

[7.3.0] Forge

Forge 7.3 is primarily a GUI release. The Electron app gains a focus-chat mode that collapses the center column so the right panel (Chat / Branches / Logs) takes the full row, and the running Watchfire version now sits under the sidebar logo so you no longer have to dig through Settings → About to know what you're on. The version bump to 7.3.0 lands across version.json, gui/package.json, and gui/package-lock.json. One daemon-side fix closes the size-cap deferral v7.2.1 left open: ~/.watchfire/daemon.log is now bounded after a user's log grew to 300 GB.

Added

  • GUI focus-chat mode. A new toggle collapses the center column (Tasks / Definition / Insights / …) so only the right panel remains, giving the agent terminal the full width. Switch it on from a Maximize/Minimize button in the project header or by double-clicking the right-panel divider; Branches and Logs stay reachable in focus mode. The state is per-project and persisted to localStorage.
  • GUI version under the sidebar logo. The running version now shows as a small v{version} line beneath the wordmark, fed by the existing window.watchfire.getVersion() IPC. The collapsed sidebar is left as-is, where the value remains in Settings → About.

Changed

  • All shipped components advertise 7.3.0. version.json, gui/package.json, and gui/package-lock.json bump 7.2.1 → 7.3.0 (codename Forge preserved). The daemon and CLI pick it up through the existing Makefile ldflags wiring, and the Electron app — including the new sidebar version line — reports 7.3.0.

Fixed

  • Daemon log is now size-capped at ~1 GB. Closes the cap deferral v7.2.1 called out explicitly — one user's ~/.watchfire/daemon.log grew to 300 GB before they noticed. A self-rotating writer caps the active file at 500 MB and keeps a single 500 MB backup (daemon.log.1), for ≈1 GB total. On startup an already-oversized log rotates immediately, and a rotation failure never crashes the daemon — it surfaces on stderr and continues.

[7.2.1] Forge

Forge 7.2.1 closes the second wildfire-chain leak that v7.2.0 only half-fixed. With v7.2.0 deployed and the daemon's logs going to /dev/null (Electron-launched stdio fan-out), the Generate phase kept emitting blog-post-style task titles like title: Write a blog post — "Headline: Subhead"; the unquoted : was parsed as a nested mapping, the strict decoder rejected the file, and it was silently skipped — losing 6 of 24 generated tasks in one ~5-hour run. Two prophylactic fixes.

Fixed

  • Generate prompts now require single-quoted title: scalars. Both generate system prompts gain an explicit "always single-quote the title: value" block, with correct forms (including the doubled-single-quote escape), the silently-dropped wrong form called out, and a nudge to use block scalars for prompt: and acceptance_criteria:. The model's output was valid English but invalid YAML — the prompt now pins it to the safe subset.
  • Daemon logs now persist to ~/.watchfire/daemon.log. When Watchfire.app launches watchfired, the daemon inherits Electron's /dev/null stdio, so every chain decision, task-load skip, and phase-watch line was discarded. The daemon now opens ~/.watchfire/daemon.log in append mode and writes through io.MultiWriter(os.Stderr, daemonLog) — foreground runs still print to stderr, and Electron-launched runs leave a forensic trail across restarts. Open is best-effort and never blocks startup.

[7.2.0] Forge

Forge 7.2 fixes the wildfire chain dying silently after the Generate phase whenever the agent wrote a task YAML with an empty-string timestamp like started_at: "". Caught live: Generate created tasks with started_at: "", LoadAllTasks aborted on the first parse error, and the daemon dropped into a NO-AGENT state with ready tasks no one would pick up — no chat session, no notification, no obvious failure signal. The fix has three layers.

Fixed

  • Wildfire chain no longer silently dies on an empty-string timestamp in a generated task. Three stacked fixes, one root cause. (A) A tolerant Task YAML decoder rewrites empty-string scalars on time-typed fields (created_at, started_at, completed_at, updated_at, deleted_at) to null, so they decode to a zero time instead of erroring on time.Parse(""). (B) config.LoadAllTasks now logs and skips an unparseable task file instead of aborting the whole list, so a single corrupt YAML can't poison the chain. (C) The generate prompts spell out exactly which fields belong in a new task YAML and forbid every timestamp field, calling out the started_at: "" failure mode so future agent versions don't emit it.

[7.1.0] Forge

Forge 7.1 is a GUI-only point release that cleans up the chat terminal regressions introduced alongside v7.0.0's bytes_received cursor work. Typing in chat mode no longer line-steps with [Agent stopped] floods between chunks, Run All / Wildfire starts (and phase transitions) now render the daemon-sent initial prompt instead of dropping it, same-process stream blips no longer replay the buffer onto a stale xterm and stack overlapping Claude Code banners, and the intermittent timed out waiting for previous agent to stop toast when switching modes is gone. The TUI streaming path and the daemon's SubscribeRawOutput protocol are untouched — the fix is GUI-only and lives in four renderer files.

Fixed

  • GUI chat — typing no longer line-steps with [Agent stopped] floods between chunks. A transient gRPC error in fetchStatus was synthesising {isRunning: false}, and ChatTab's auto-restart effect immediately fired startAgent('chat') against the still-live agent, killing it and writing an [Agent stopped] marker between streamed output chunks. The store now preserves the last-known status on transport errors, so chat sessions keep typing cleanly through gRPC churn.
  • GUI mode switcher — Generate / Plan / Run All / Wildfire actually start; no more timed out waiting for previous agent to stop toast. A race between the in-flight startAgent RPC and ChatTab's auto-restart effect was collapsing every special mode back to chat and occasionally tripping the daemon's 10 s cleanup-polling timeout. ModesControl no longer pre-calls stopAgent (the daemon already does an atomic kill+restart), and a per-project startAgentInFlight flag short-circuits status polls during the switch.
  • Run All / Wildfire — initial prompt actually renders on start and on phase transitions. Each daemon Process restart resets the raw-byte counter to zero, but the terminal hook only reset its cursor on project change — reconnecting with a stale cursor against the new Process landed past the new buffer's end and ate the daemon's initial prompt (Implement Task #…, Wildfire refine, Wildfire generate). The subscribe effect now keys off agentStatus.startedAt and resets emulator + cursor on a real generation change.
  • Same-process stream blip no longer replays the daemon buffer onto a stale xterm. An earlier fix attempt zeroed the cursor inside onEnd, forcing the daemon to re-send its full raw buffer on the next subscribe; the absolute cursor-positioning escapes from Claude Code's UI redraw landed at xterm's current cursor position and stacked overlapping banners. onEnd now leaves the cursor alone, schedules a 200 ms reconnect, and the [Agent stopped] marker write is removed entirely.

[7.0.0] Forge

Forge brings manual task reordering across the full stack — a new TaskService.ReorderTasks RPC backs Shift+↑/↓ in the TUI and @dnd-kit-powered drag-and-drop in the GUI, replacing the silent task-number-descending sort with the spec'd (position ASC, task_number ASC) order, and a new-task-defaults-to-bottom rule so manual orderings survive task creation. The GUI chat terminal stops snapping to byte 0 mid-scroll thanks to a daemon-side cursor on SubscribeRawOutput and an idempotent client-side subscription effect. The Open-in-IDE menu now finds CLIs installed outside the GUI's stripped-down PATH (code at /usr/local/bin/code, Homebrew shims at /opt/homebrew/bin) so VS Code, Cursor, Windsurf, Zed, Sublime, and the JetBrains shims launch from the GUI even when started from Finder or the Dock.

Added

  • TUI manual task reorder via Shift+↑/↓. Shift+↑ and Shift+↓ on a focused active row swap the selected task with its in-bounds same-status neighbour and fire TaskService.ReorderTasks with the project's full new active task-number ordering. The optimistic flow snapshots the pre-move state, replaces the active list immediately, and either accepts the server response on success or restores the snapshot and surfaces Reorder failed — reverted on failure. Cross-section moves (Draft↔Ready, Ready↔Done) and top/bottom boundaries are silent no-ops. A race guard on TasksLoadedMsg drops poll-driven refreshes that arrive between the optimistic swap and the RPC response when the task set is unchanged, so the moved row no longer snaps back. Help overlay gains Shift+↑ / Shift+↓ "Move selected task up/down" entries under the Task List section.
  • GUI drag-to-reorder for active tasks. The Tasks tab reuses the same @dnd-kit pieces already proven out by the Sidebar — DndContext + SortableContext + useSortable + arrayMove from @dnd-kit/sortable — so there's no new dependency. Each active status group ("In Development" / "Todo") owns its own DndContext with an 8 px PointerSensor activation distance so a stray pixel of pointer drift on a click never turns into a reorder. A GripVertical icon at the left of each row is the only target wired with drag listeners; the row body keeps its click-to-open-modal behaviour. The "Failed" and "Done" groups render non-sortable rows so historical groups stay click-to-open with zero drag affordance. Cross-group drags are a structural impossibility (each group is its own DndContext). The tasks store applies the new order optimistically, calls the ReorderTasks RPC, and either commits the server response or restores the snapshot + toasts Reorder failed: … on rejection.
  • TaskService.ReorderTasks server handler + manager method. The proto declared TaskService.ReorderTasks(ReorderTasksRequest) returns (TaskList) but no server handler existed, so any call hit UnimplementedTaskServiceServer.ReorderTasks and returned codes.Unimplemented — blocking the drag-to-reorder UI. New task.Manager.ReorderTasks(projectPath, taskNumbers) loads the active set, validates each number (unknown → task not found: <n>; duplicate → duplicate task in reorder request: <n>), appends any unmentioned active tasks in canonical Position-then-TaskNumber order so a partial-list request silently parks the leftovers at the end of the queue, then rewrites positions densely 1..N and persists each via config.SaveTask. The handler maps validation errors to codes.InvalidArgument and any other error to codes.Internal.

Fixed

  • GUI Open-in-IDE — finds CLIs installed outside the GUI's stripped-down PATH. Launching the GUI from Finder / Dock on macOS inherits a minimal PATH (/usr/bin:/bin:/usr/sbin:/sbin) — path_helper only runs from login shells, and spawn(..., {shell: true}) uses non-login /bin/sh -c, so the user's profile PATH is never sourced. code at /usr/local/bin/code (and Homebrew shims at /opt/homebrew/bin) failed to resolve and every IDE pick errored out. New spawnEnv() helper in gui/src/main/ipc.ts prepends the well-known macOS install locations (/usr/local/bin, /usr/local/sbin, /opt/homebrew/bin, /opt/homebrew/sbin, ~/.local/bin, ~/bin) to PATH for the spawned process; Linux gets ~/.local/bin + ~/bin. Fixes vscode, cursor, windsurf, zed, subl, webstorm, idea, and fleet; xcode (uses macOS open -a) and finder (uses shell.openPath) were never affected. Bug only surfaced when the GUI was launched from Finder/Dock — npm run dev in a terminal inherited the shell PATH and masked the issue.
  • Task work order — oldest-first by (position ASC, task_number ASC); new tasks default to bottom. internal/daemon/task/manager.go::ListTasks was sorting strictly descending by task_number, contradicting the spec's "Task Work Order" rule. Every consumer reads tasks[0] — the start-all chain and both wildfire pickers — so watchfire start-all and watchfire wildfire walked a 4-task queue backwards (4 → 3 → 2 → 1) instead of forward (1 → 2 → 3 → 4). The Position field was already on models.Task and surfaced through proto + converters but never read by the sort, so the dead-data path silently stripped the manual-override knob the spec relied on. Replaced the sort with the spec'd compound ascending order: position ASC primary, task_number ASC tiebreaker. CreateTask also changed: the default position is now max(active.position) + 1 (or 1 if zero active tasks), appending to the bottom of the work queue so a new task after a manual reorder no longer jumps ahead. An explicit opts.Position still wins.
  • GUI Chat terminal — viewport no longer snaps to byte 0 on scroll. Two stacked causes both fixed. Client side: the subscribe effect previously ran term.clear() + abort + re-subscribe on every dep change, and ChatTab.tsx's 2 s status poll routinely flipped active true → false → true whenever getAgentStatus hit a transient error, spuriously replaying the full daemon raw buffer from byte 0 mid-scroll. The effect is now idempotent — when active=true and the previous AbortController is still unaborted, the effect bails out (no clear, no resubscribe, no daemon round-trip). The unsubscribe path is debounced 3 s so a single poll flicker can't tear down a live subscription. term.clear() is no longer called anywhere. Server side: new catch-up cursor — Process tracks rawTotalBytes (monotonic count of broadcast bytes), and the new SubscribeRawFrom(id, bytesReceived) slices the late-join snapshot so only bytes past the client's offset are sent. proto/watchfire.proto SubscribeRawOutputRequest gains an int64 bytes_received field. Negative / past-end cursors are clamped; cursors before the 1 MiB rolling buffer's floor return the full buffer. Also: xterm scrollback raised from 1 000 to 10 000 lines, and the ResizeObserver now records the last (rows, cols) it sent and bails when the fitted dims are unchanged so scrollbar-appearance nudges no longer spam the daemon with no-op resize RPCs.

Migration

  • All Forge changes are additive on the wire — existing clients keep working. The new SubscribeRawOutputRequest.bytes_received field is optional and defaults to 0 (full snapshot), so v6 clients see no behaviour change.
  • Task ordering: any project that relied on the v3.0.0 Blaze descending-by-task_number sort will now see tasks listed oldest-first (position ASC, task_number ASC). New tasks now default to the bottom of the work queue so manual reorderings survive task creation.
  • task.Manager.ReorderTasks is new but no schema or YAML field changed. Existing .watchfire/tasks/<n>.yaml files load identically — the position field has been on the model since the start; Forge just finally honours it.

[6.0.0] Phoenix

Phoenix lands the project.yaml data-loss fix, the flock-based singleton-daemon hardening, and Cursor Agent CLI as a sixth first-class backend, plus a TUI rewrite — Project Settings sidebar refactor, Trash filter mode, Definition $EDITOR shellout, Branches overlay, text-select mode, and a full agent-pane terminal-emulator swap from hinshun/vt10x to charmbracelet/x/vt that fixes the long-standing "input lands at top" tear bug. The data-loss fix closes a non-atomic-write race in config.SaveYAML that let SyncNextTaskNumber overwrite project.yaml (and the global ~/.watchfire/projects.yaml) with a zero-valued struct. The singleton fix closes a TOCTOU race in runDaemon that let two watchfired processes bind separate dynamic ports and spawn two menu-bar tray icons.

Added

  • Cursor Agent CLI as a sixth first-class agent backend. New internal/daemon/agent/backend/cursor.go implementing the full Backend interface alongside Claude Code, Codex, opencode, Gemini, and Copilot. Mirrors the Copilot backend's structure: per-session ~/.watchfire/cursor-home/<project_id>/<session_id>/ directory with the user's real ~/.cursor/ auth/config files symlinked in, composed Watchfire system prompt installed as AGENTS.md, headless cursor-agent --workspace <worktree> --print launch with the yolo / trust flag, JSONL transcript located via LocateTranscript and rendered through FormatTranscript in the same shape every other backend uses. TUI and GUI agent pickers include cursor with no special-casing — see Supported Agents.
  • TUI Project Settings sidebar refactor. The per-project Settings tab drops the flat 7-row form for a macOS-style sidebar + content-pane layout matching the v5.0 Flare global-settings UX. Sidebar lists seven sections: General, Automation, Notifications, Integrations, Metadata, Secrets, Danger zone. Tab/Shift+Tab walks the sidebar; ↑/↓ (j/k) walks rows inside the active section; / opens a search overlay that matches across labels + section breadcrumbs and Enter jumps to the matched row.
  • Project Notifications — per-event overrides. New Notifications section in the project Settings sidebar. Master mute (existing) plus a new Override per-event preferences toggle that gates per-event Enabled toggles for task_failed / run_complete / weekly_digest. New Quiet hours override toggle gates two HH:MM text inputs (start / end); same disabled-while-off treatment. Pre-v6 project.yaml files load identically.
  • Integrations — per-project scoping. New Integrations section in the project Settings sidebar. GitHub auto-PR toggle binds the project to membership in the global ~/.watchfire/integrations.yamlgithub.project_scopes list. Slack channel + Discord guild ID text fields persist on the project YAML under a new integrations: block; empty string clears the binding (= inherit global default).
  • Danger-zone actions — Archive / Regenerate ID / Reset numbering / Prune merged branches / Unregister. New Danger zone section in the project Settings sidebar surfaces five destructive actions, each gated behind a y/N confirm in the status bar. Archived projects stop auto-starting tasks and drop from the dashboard active list. Reset task numbering recomputes next_task_number = highest_existing + 1. Unregister drops the entry from ~/.watchfire/projects.yaml while preserving local .watchfire/ so contact re-adds the project automatically.
  • TUI Trash filter mode — deleted tasks are visible + restorable. Tasks soft-delete (set deleted_at) but the TUI never surfaced them — x on a task hid it forever. The Tasks tab now carries a filter mode toggle: D (capital) flips the rendered list between the active subset and the soft-deleted subset. Trash row keys: u restores, x arms a y/N permanent-delete confirm (refused if the watchfire/<n> branch reports unmerged work), Enter opens the read-only edit form, D flips back.
  • TUI Definition tab — e shells out to $EDITOR. The Definition tab was read-only; editing required dropping out of the TUI to run watchfire define. New e (and Enter) binding opens the project definition in $VISUAL$EDITORvimvi via tea.ExecProcess. On exit, the diff against the pre-edit content decides whether to dispatch ProjectService.UpdateProject.
  • TUI Branches overlay — Ctrl+B lists, merges, deletes, prunes orphans. The TUI had zero visibility into git branches and their worktrees. New Ctrl+B overlay lists every watchfire/<n> branch with columns Branch, Task, Age, Status (merged / unmerged), and Worktree (present / absent, with merged-orphans rendered as absent*). Action keys: m merge, x delete (refuses unmerged), X force-delete, P prune all merged-orphans, r refresh.
  • TUI text-select mode — Ctrl+T toggles mouse capture. New global keybinding flips between Bubble Tea's mouse capture and host-native click-and-drag selection. Status bar swaps the normal hint row for a high-contrast ▎TEXT SELECT — drag to select · Ctrl+T to resume mouse banner; the header appends a text-select chip right after the project name so the mode is impossible to miss.
  • TUI agent pane — true scrollback via charmbracelet/x/vt. The agent pane previously consumed daemon-rendered ANSI snapshots dropped into a bubbles/viewportvt10x's grid had no scrollback, so PgUp / Shift+arrows / wheel-up either no-op'd or corrupted the pane. The TUI now subscribes to SubscribeRawOutput (the same raw-byte stream the GUI feeds to xterm.js) and runs the bytes through a TUI-side emulator built on github.com/charmbracelet/x/vt. Up to 5000 scrollback lines drop in below the visible grid; wheel-up / Shift+↑ / PgUp walk into real history.

Fixed

  • TUI agent terminal emulator — fixes "input lands at top" tear bug + claude terminal-query hang. vt10x's incomplete xterm coverage rendered claude code's chat with input visibly stuck on the top row. Swapped the TUI emulator to charmbracelet/x/vt, which renders claude's UI faithfully. A drain goroutine reads the emulator's terminal-query responses (DA1, DSR, focus, mouse) and forwards them back to the daemon's PTY so claude unblocks.
  • Daemon SubscribeRawOutput — atomic subscribe-and-snapshot closes the double-delivery race. The previous implementation registered the channel and called GetRawBuffer in two separate critical sections; broadcastRaw interleaving between them would append bytes to rawBuf AND send them to the new channel, producing duplicates the new TUI vt emulator double-applied. New Process.SubscribeRawWithSnapshot(id) acquires both locks together so new subscribers see every byte exactly once across snapshot + live.
  • TUI: drop CSI mouse-event byte-leak fragments before forwarding to the chat agent's stdin. Bubble Tea reads stdin in 256-byte chunks; rapid mouse-wheel scrolling regularly cuts an SGR mouse sequence (\x1b[<button;col;row M) mid-flight and the trailing <64;105;35M lands as a KeyRunes that gets forwarded into the chat agent's PTY (visible as [<64;105;35M[<64;105;35M… in chat). New guard drops KeyRunes with Alt == true and any rune string matching the CSI mouse-residue regex.
  • TUI right-panel scroll — wheel routes by cursor + Shift+arrow line scroll. Wheel events used to follow the focused panel rather than the cursor — opposite of every native macOS terminal + browser. Wheel routing now resolves the target panel from msg.X vs layout.dividerCol and the wheel branch is split out of the click-press case so it never mutates focusedPanel. Shift+↑/↓ (line scroll) and Shift+PgUp/PgDn (page scroll) are now intercepted before forward-to-agent.
  • Atomic YAML writes — closes the project.yaml data-loss race. internal/config/loader.go::SaveYAML now writes to a sibling tmp file, fsyncs, then os.Renames into place. POSIX rename is atomic on the same filesystem, so concurrent readers see either the old file or the new file, never a truncated one. The fix covers every YAML file the daemon writes — project.yaml, the global projects.yaml, task files, settings, agents, daemon state, integrations.
  • LoadProject rejects zero-valued reads. internal/config/projects.go::LoadProject now returns corrupt project.yaml at <path>: … when the unmarshalled struct has Version == 0 or empty ProjectID. Any future writer that introduces a similar race surfaces as a load error instead of silently rolling forward.
  • Double-daemon spawn — flock-based singleton hardening. Reproduced 2026-05-05: two watchfired processes running simultaneously, each owning a separate gRPC server on a different dynamic port and a separate macOS menu-bar tray icon. Root cause was a TOCTOU race in runDaemon between the legacy daemon.yaml PID check and the dynamic port bind. New internal/config/lock.go exposes AcquireDaemonLock() returning ErrDaemonLockHeld on contention; the daemon acquires ~/.watchfire/daemon.lock via real syscall.Flock(LOCK_EX|LOCK_NB) before any tray/server init and holds it for the full process lifetime.

Changed

  • models.ShouldNotify consults per-project overrides before global. The signature changed to ShouldNotify(kind, cfg, project ProjectNotifications, now) so the function has full access to the per-project block. The gate now resolves per-event toggles in this order: project override (when OverrideEvents == true AND a row exists in Events) → global cfg.Events. Quiet hours: project QuietHoursOverride (when non-nil) replaces — does not union — the global window. The Muted master kill-switch retains v4.0 Beacon semantics: any project setting it to true short-circuits before any other gate.

Migration

  • All Phoenix changes are internal; no schema or API changes. Existing project.yaml files load unchanged.
  • Daemon singleton: ~/.watchfire/daemon.lock is created on first daemon start in v6.0+ and never deleted — it is a flock target, not a stale-PID file. Do not remove it manually unless every watchfired process is stopped.
  • Cursor backend: cursor-agent must be on PATH (or its absolute path set in ~/.watchfire/agents.yaml::cursor.Path). Existing projects keep their current default_agent; opt in via watchfire configure or by editing default_agent: cursor in .watchfire/project.yaml.

[5.0.0] Flare

Flare closes the inbound loop Beacon left half-open and hardens the run-all path. Both "Known issues" filed against Beacon — the missing GitHub PR-merge handler and the missing Slack HTTP transport — ship; the inbound surface gains OAuth, multi-host parity (GitHub Enterprise / GitLab / Bitbucket), per-IP rate limiting, Slack interactive components, and Discord guild auto-registration; the run-all silent-halt bug, the chat-tab repaint loop, and the buried failure_reason are all fixed; and the global settings UI is reorganised into searchable category sub-pages.

Added

  • GitHub PR-merge handler — closes the v4.0 Beacon auto-PR loop. New internal/daemon/echo/handler_github.go registered at POST /echo/github?project=<id> parses X-GitHub-Event / X-Hub-Signature-256 / X-GitHub-Delivery, resolves the per-project HMAC secret from the keyring, runs verify.VerifyGitHub, deduplicates against the LRU+TTL idempotency cache, narrows on event == "pull_request" && action == "closed" && pull_request.merged == true, then matches the Watchfire task by pull_request.head.ref == watchfire/<n> and calls task.MarkDoneIfNotAlready + emits a Pulse RUN_COMPLETE notification titled <project> — PR #<number> merged. Closes the v4.0 Beacon "Known issue" #1.
  • Slack slash-command HTTP transport — closes the v4.0 Beacon Slack-parity gap. New internal/daemon/echo/handler_slack_commands.go translates the URL-encoded slash-command form body (command, text, team_id, channel_id, user_id, trigger_id) into a call against the shared transport-agnostic commands.Route(...) router, then renders CommandResponse as Slack response JSON ({response_type: "in_channel" | "ephemeral", text, blocks}). /watchfire status / retry / cancel now works in Slack at parity with the Discord interactions endpoint that shipped in Beacon. Closes the v4.0 Beacon "Known issue" #2.
  • OAuth bot tokens for Slack and Discord. Replaces the v4.0 paste-a-signing-secret model with a proper OAuth install flow. Slack: xoxb-... bot token from the workspace OAuth callback, used for chat.postMessage so slash responses can include rich attachments and DM the originator on private failures. Discord: Authorization: Bot <token> for inbound auth and command registration. New "Connect Slack" / "Connect Discord" buttons in the Integrations settings UI launch the flow in the user's default browser; success surfaces a Connected as <bot username> pill. The legacy signing-secret + public-key path stays additive for users mid-cutover.
  • GitHub Enterprise / GitLab / Bitbucket inbound parity. Per-project github_host field on models.InboundConfig lets the existing GitHub HMAC-SHA256 verifier target arbitrary GitHub Enterprise hostnames. New internal/daemon/echo/handler_gitlab.go verifies X-Gitlab-Token (per-project shared secret), narrows on Merge Request Hook events with action: merge. New internal/daemon/echo/handler_bitbucket.go verifies X-Hub-Signature (HMAC-SHA256), narrows on pullrequest:fulfilled events. Settings UI surfaces a "Git host" picker on inbound config.
  • Per-IP rate limiting on the inbound HTTP server. Per-IP token bucket via golang.org/x/time/rate, default 30 req/min/IP across every /echo/* route, configurable through models.InboundConfig.RateLimitPerMin (0 disables). Idempotent deliveries already in the LRU cache do NOT count against the bucket. On 429, the daemon logs a single WARN per IP per minute to avoid log flooding under sustained traffic.
  • Slack interactive components — buttons + cancel-reason modal. The Slack outbound TASK_FAILED Block Kit template gains three action buttons: Retry, Cancel, View in Watchfire. New inbound endpoint POST /echo/slack/interactivity handles the block_actions and view_submission payloads with the same v0 HMAC verification + 5-minute drift window as the slash-commands endpoint. Button presses route through commands.Route so a Retry click is the exact equivalent of /watchfire retry. Cancel opens a Slack modal that asks "Why are you cancelling?"; the supplied reason lands in task.failure_reason.
  • Discord slash-command auto-registration on guild join. The daemon now enumerates the guilds the bot is in at startup and POSTs the three slash-command schemas to each via the existing internal/cli/integrations_discord.go::registerForGuild helper; it also subscribes to GUILD_CREATE Gateway events so a freshly-added guild gets commands within 30 seconds (no CLI step). The Settings UI lists every guild with a ✓ / ✗ registration pill. The manual watchfire integrations register-discord <guild> CLI stays as a fallback. Discord's commands API is upsert-style, so re-running is safe.
  • Settings UI: macOS-style category sub-pages with search. Both GUI (gui/src/renderer/src/views/Settings/GlobalSettings.tsx) and TUI (internal/tui/settings.go) replace the single long scrolling page with a two-pane layout — left sidebar of eight categories (Appearance, Defaults, Agent Paths, Notifications, Integrations, Inbound, Updates, About), right pane shows only the selected category. New search input filters categories AND surfaces individual matching controls with category breadcrumbs; clicking a result navigates to the category and pulses the matching field for ~1.5s. GUI: Cmd/Ctrl+F focuses search, Esc clears, Up/Down/Enter navigate. TUI: / opens a search overlay with the same field-jumping behaviour. Deep-link routes (#integrations etc.) still work.

Fixed

  • Run-all silently halted on auto-merge failure. When internal/daemon/agent/taskdone.go::HandleTaskDone's silent merge failed (dirty main, merge conflict, post-merge hook failure), the chain stopped — but silently: the task YAML still showed status: done + success: true, no notification fired, and the user was left wondering why their queue stalled. onTaskDoneFn now returns a structured TaskDoneResult{Outcome, Reason} (with TaskDoneOK / TaskDoneMergeFailed / TaskDoneCancelled) instead of a bare bool; monitorProcess branches on result.Outcome == TaskDoneMergeFailed and emits a TASK_FAILED-shaped notification before the chain decision; runSilentMerge populates the task's new merge_failure_reason field (yaml: merge_failure_reason,omitempty, exposed via proto + GUI/TUI). The chain-stop semantics are unchanged — the user still has to clean up main manually — but the silence is gone.
  • GUI chat-tab repainted multiple times on project switch. Locked in single-mount + single-start guards in gui/src/renderer/src/views/ProjectView/RightPanel/ChatTab.tsx: the auto-start useEffect deps tightened to [!!agentStatus, isRunning, projectId] so a stale agentStatus reference from the previous project no longer fires handleStart on a transient render edge; the autoStarted.current = false reset on projectId change runs before the auto-start check. Regression test simulates rapid project switching and asserts handleStart fires exactly once per navigation.
  • Failed-task UI hid the reason behind two clicks. TaskStatusBadge now carries a title= tooltip for agent-reported failures (it already had one for merge failures only), populated by a new exported pure helper computeBadgeTooltip that prefers Merge failed: … over Failed: … when both reasons are set and truncates to 500 runes. TaskItem passes failureReason={task.failureReason} into the badge alongside mergeFailureReason. TaskModal's tab decision is now lazy in useState(() => …) AND kept in sync via the existing effect, so done tasks land on the Inspect tab on first paint without a flicker. The TUI task list (internal/tui/tasklist.go) renders an inline preview of both reasons (merge-failure precedence) under the [✗] glyph.

Tests

  • Inbound framework coverage gap closed. Filled out internal/daemon/echo/'s test surface — every signature verifier (GitHub HMAC-SHA256, Slack v0, Discord Ed25519) covers golden-path + every rejection mode (missing header, malformed signature, drift overshoot, replay window); idempotency.go's LRU+TTL behaves correctly under concurrent access, eviction, and TTL refresh; commands.Route round-trips status / retry <task> / cancel <task> against a mocked task manager.

Migration

  • All Flare features are additive — projects upgrade with no behaviour change.
  • Inbound: existing signing-secret + public-key configs continue to work; OAuth is opt-in via the new "Connect Slack" / "Connect Discord" buttons. The new RateLimitPerMin field defaults to 30; set to 0 to disable.
  • Multi-host inbound: leave github_host empty for github.com; set per-project for GitHub Enterprise. GitLab and Bitbucket handlers are inactive until their per-project secret is configured.
  • Discord auto-registration runs on next daemon start — existing guilds get re-upserted (idempotent). The CLI watchfire integrations register-discord <guild> stays available as a fallback.
  • Run-all halt fix: onTaskDoneFn's signature changed from func(...) bool to func(...) TaskDoneResult. Internal callback only — no external API impact, but third-party forks pinning to the old signature will need to update.

[4.0.0] Beacon

Beacon is Watchfire's consolidated dashboard, notifications, insights, and integrations release. It groups four feature tracks under one banner — a glanceable dashboard, proactive OS notifications, retrospective insights, and outbound + inbound integrations powered by a new notification bus.

Added

  • Dashboard aggregate status bar — single muted status line N working · N needs attention · N idle · N done today between the dashboard header and the project grid; counts derived from existing zustand stores so it updates live with no new gRPC
  • Dashboard filter chips — pill chips (All, Working, Needs attention, Idle, Has ready tasks) with live counts; selection persists in localStorage[wf-dashboard-filter], with predicates shared via gui/src/renderer/src/lib/dashboard-filters.ts
  • Dashboard grid/list layout toggleLayoutGrid / Rows3 toggle in the header; list mode renders one ~46 px row per project via gui/src/renderer/src/views/Dashboard/ProjectRow.tsx, with the selection persisted in localStorage[wf-dashboard-layout]
  • Elapsed-time badge on running ProjectCards — ticking Ns / Nm / Nh Mm next to the agent badge, sourced from a new AgentStatus.started_at proto field stamped in RunningAgent.StartedAt; flips to var(--wf-warning) past 30 minutes
  • Last-activity timestamp on dashboard cardsActive now / 5m ago / 4h ago / 2mo ago segment derived from the most recent task updated_at, formatted by a hand-rolled relative-time helper in gui/src/renderer/src/lib/relative-time.ts
  • Live PTY last-line preview on dashboard cards — latest non-blank terminal line in monospace muted text, throttled to 4 Hz; a singleton subscription manager in gui/src/renderer/src/stores/agent-preview-store.ts ref-counts the underlying AgentService.SubscribeScreen stream
  • Current-task surfacing on running ProjectCards — replaces the misleading Next: line with Working: <current task title> (with Flame icon) when the agent is actively running; reuses the existing AgentStatus.task_title with no proto change
  • Shell-count chip on running ProjectCards — terminal icon + alive-session count from useTerminalStore; pulses when any session emitted output in the last 2 s, click expands the bottom panel
  • Needs-attention treatment for failed tasks — red-tinted card border + header AlertTriangle chip + N failed segment in the counts row + red progress segment when any task has status === 'done' && success === false
  • Notification bus — new internal/daemon/notify package with a typed Bus, channel fan-out (slow-consumer drop), stable MakeID (sha256(kind|project_id|task_number|emitted_at_unix)[:8]), and JSONL append to ~/.watchfire/logs/<project_id>/notifications.log for headless fallback
  • TASK_FAILED OS notification — fires from internal/daemon/server/task_failed.go::emitTaskFailed on done && !success; title <project> — task #NNNN failed, body is the task title plus optional failure reason
  • RUN_COMPLETE OS notification — fires at the falling edge of every autonomous run (single-task, start-all, wildfire) bounded by a new RunningAgent.RunStartedAt; body N tasks done · M failed over the run window
  • Bundled notification soundsassets/sounds/task-{done,failed}.wav (mono 22050 Hz, ~25 KB each); a pure shouldPlaySound(kind, prefs) decision in gui/src/renderer/src/stores/notifications-sound.ts keeps the OS toast silent precisely when the renderer plays its own audio
  • Dynamic system tray menuinternal/daemon/tray/tray.go rebuilds on every project / task / agent / settings change; sections for Needs attention / Working / Idle plus a Notifications (N today) ▸ submenu reading the JSONL fallback, with click-through routed via the new DaemonService.SubscribeFocusEvents stream
  • Notification preferences UI — TUI (internal/tui/globalsettings.go) and GUI (gui/src/renderer/src/views/Settings/NotificationsSection.tsx) expose master / per-event / sounds / volume / quiet-hours / per-project mute, all under defaults.notifications in ~/.watchfire/settings.yaml and gated by models.ShouldNotify
  • Weekly digest notificationdigestRunner schedules with a re-armable time.Timer from models.DigestSchedule.NextFire (DST-stable, with 24-hour catch-up on daemon start); Markdown is always rendered to ~/.watchfire/digests/<YYYY-MM-DD>.md regardless of toast suppression. New WEEKLY_DIGEST notification kind + FOCUS_TARGET_DIGEST
  • Per-task metrics capture<n>.metrics.yaml siblings carrying duration, exit reason, agent, tokens, and cost; the new internal/daemon/metrics package parses Claude Code, Codex, opencode, Gemini, and Copilot (stub), capturing from a non-blocking goroutine on handleTaskChanged. New watchfire metrics backfill CLI for retroactive capture
  • Per-project Insights viewinternal/daemon/insights/project.go aggregates one project's tasks per window; new GUI Insights tab + TUI overlay (bound to i) with KPI strip, stacked-bar tasks-per-day, agent donut, and duration histogram. localStorage[wf-insights-window] persists the 7 d / 30 d / 90 d / All selector
  • Cross-project Insights rollupinternal/daemon/insights/global.go aggregates the whole fleet per window, cached at ~/.watchfire/insights-cache/_global.json. Dashboard rollup card under the Beacon status bar; TUI fleet overlay bound to Ctrl+f
  • Report export (CSV + Markdown) — shared InsightsService.ExportReport RPC with oneof scope (project_id / global / single_task); Markdown templates in internal/daemon/insights/templates/, CSV uses # section: <name> headers. Single <ExportPill> component on the dashboard + ProjectView headers; TUI binds Ctrl+e
  • Inline diff viewer — new internal/daemon/diff package resolves diffs pre-merge (<merge-base>...HEAD on watchfire/<n>) and post-merge (locates the merge commit via git log --grep); structured FileDiffSet capped at 10 000 lines, cache at ~/.watchfire/diff-cache/<project_id>/<task_number>.json. GUI Inspect tab + TUI overlay (bound to d)
  • Outbound delivery framework + webhook adapter — new internal/daemon/relay package with an Adapter interface and a Dispatcher subscribing to notify.Bus; per-adapter retry ([500ms, 2s, 8s]) + circuit breaker (3 failures / 5-minute window). Generic WebhookAdapter POSTs the canonical payload with X-Watchfire-Signature: sha256=<hex> HMAC; secrets via OS keyring (internal/config/keyring.go) with file-store fallback
  • Slack adapter (Block Kit messages)internal/daemon/relay/slack.go renders three text/template Block Kit envelopes (TASK_FAILED / RUN_COMPLETE / WEEKLY_DIGEST) with header / section / context / actions blocks; project-color → :large_<color>_square: shortcode map in slack_color.go
  • Discord adapter (rich embeds)internal/daemon/relay/discord.go renders three embed envelopes with project-color tinting; shared hexToInt / rfc3339 template helpers and a defensive 4000-rune description trim with a single WARN on overflow. New watchfire integrations CLI parent with list and test subcommands
  • GitHub auto-PR creation — opt-in per project via github.auto_pr.enabled: true. End-of-task lifecycle in internal/daemon/git/pr.go::OpenPR: gh auth status → parse <owner>/<repo>git push --force-with-lease → render PR body via pr_body.md.tmplgh api -X POST /repos/:owner/:repo/pulls. Sentinel errors distinguish silent fallback (one WARN per project lifetime) from per-attempt failures
  • Integrations settings UI (GUI + TUI) — new IntegrationsService gRPC service with List / Save / Delete / Test RPCs; Save carries a oneof payload, secrets are write-only on the wire. GUI IntegrationsSection.tsx exposes per-type detail panels; TUI overlay is reachable via Ctrl+I
  • Inbound HTTP server frameworkinternal/daemon/echo/server.go binds ListenAddr (default 127.0.0.1:8765), with 5 s graceful shutdown drain, 1 MiB body cap + panic recovery middleware, an unauthenticated /echo/health, and RegisterProvider(method, path, handler) for plug-in handlers; bind failure logs ERROR but doesn't crash the daemon
  • Signature verificationinternal/daemon/echo/verify.go ships VerifyGitHub (HMAC-SHA256 against sha256=<hex>), VerifySlack (HMAC-SHA256 over v0:<timestamp>:<body> with 5-minute drift), and VerifyDiscord (Ed25519 over timestamp || body, same drift) — all constant-time
  • Idempotency cacheinternal/daemon/echo/idempotency.go is an LRU+TTL cache (1000 entries / 24 h, container/list-backed, sync.Mutex-protected); Seen(key) refreshes TTL on hit
  • Slash-command routerinternal/daemon/echo/commands.go::Route(ctx, cmd, subcmd, rest, CommandContext) CommandResponse powers slash-command transports with three commands (status / retry <task> / cancel <task>); the CommandResponse{text, blocks, ephemeral, in_channel} envelope is transport-agnostic
  • Discord interactions endpointinternal/daemon/echo/handler_discord.go exposes POST /echo/discord/interactions with end-to-end Ed25519 verification + replay window + idempotency; PING → PONG, APPLICATION_COMMAND → dispatch to commands.Route and render via discord_render.go::RenderInteraction. Slash-command registration via watchfire integrations register-discord <guild_id> (idempotent)
  • Inbound settings UI (GUI + TUI)gui/src/renderer/src/views/Settings/InboundSection.tsx shows a Listening pill polled at 5 s, editable ListenAddr + PublicURL with restart button, Copy-as-<provider>-URL buttons, four write-only secret inputs, and per-provider last-delivery timestamps; the TUI mirrors this via a new "Inbound" tab inside the Integrations overlay

Changed

  • Dashboard auto-sorts projects by activity — replaces raw position order with bucketing into needs-attention → working → has-ready-tasks → idle (input-array index as the final tiebreaker for stability), with predicate helpers in gui/src/renderer/src/lib/dashboard-filters.ts. A muted Sorted by activity label appears whenever the activity order differs from the underlying position order

Fixed

  • GUI: switching projects silently killed every running shell in the bottom panel — PTY sessions now live in a global pool keyed by projectId and survive navigation; Cmd+` toggles a non-destructive panelCollapsed flag, and destroyProjectSessions(projectId) is called only from removeProject. BottomPanel.tsx always-mounts every TerminalTab with a visible flag so xterm.js scrollback survives React reconciliation
  • In-app terminal couldn't find pnpm / volta / fnm-managed binaries (#32) — new shared helper gui/src/main/login-shell.ts runs $SHELL -l -c env, parses PATH + dev-tool env vars, with a fallback PATH merge against the standard user-install locations; caches per Electron process. New defaults.terminal_shell global setting picks the shell binary (X_OK validated)

Migration

  • All Beacon features are additive — existing projects upgrade with no behaviour change
  • Notifications: master toggle defaults on, weekly_digest defaults off, quiet hours default off
  • Outbound integrations: nothing fans out until you configure an integration under Settings → Integrations
  • GitHub auto-PR: opt-in per project. Requires gh on PATH and gh auth status returning 0; missing prerequisites fall back to silent merge with one WARN per project lifetime
  • Inbound integrations: empty InboundConfig = no listener. Concrete handlers return 503 until the per-provider secret is configured

Known issues

  • The dedicated handler_github.go for pull_request.closed events did not ship with Beacon — auto-PR loop closed manually until v5.0.0 Flare.
  • The Slack HTTP transport on top of the shared commands.Route did not ship with Beacon — /watchfire status / retry / cancel worked in Discord but not in Slack until v5.0.0 Flare.

[3.0.0] Blaze

Added

  • GitHub Copilot CLI backendCopilot joins Claude Code, OpenAI Codex, opencode, and Gemini CLI as a fifth first-class backend, selectable per project or per task like any other agent. Sessions run in yolo mode (--allow-all); the Watchfire system prompt is delivered via AGENTS.md in a per-session COPILOT_HOME, while the user's real ~/.copilot/{config.json,mcp-config.json,session-store.db} are symlinked in so existing GitHub login, MCP config, and session history are reused. Transcripts render in the same User/Assistant format as the other backends

Fixed

  • watchfire update across filesystems on Linux (#25) — updating from /tmp (often tmpfs on Fedora/Ubuntu) into ~/.local/bin used to fail with EXDEV: invalid cross-device link. The updater now stages the download inside the install directory itself, so the final atomic rename is always same-filesystem. A belt-and-suspenders fallback copies, fsyncs, and renames if a caller ever stages elsewhere
  • Task list rotation with many tasks (#28) — projects with mixed-status tasks (e.g. 16 done + 31 ready) could render the task list rotated (0017…0047 then wrapping to 0001…0016). Sorting is now canonical everywhere: the task manager returns tasks strictly descending by task_number, and CLI, TUI, and GUI all rely on that order without re-sorting
  • GUI prompted to update the CLI on every launch (#30) — version comparison tripped on trailing whitespace, pre-release suffixes, and ANSI hyperlinks, and on Linux read the wrong binary because the search order put /usr/local/bin ahead of ~/.local/bin. Version parsing is now semver-aware, ANSI-stripping is broader (CSI + OSC + other ESC), and the search order matches the install target with a PATH fallback for rpm/deb/Linuxbrew installs
  • Newly-installed agents invisible in GUI/TUI pickers (#29) — installing Codex (or any agent) while Watchfire was running used to hide it from the agent picker until project.yaml was hand-edited. The backend registry is now the sole source of truth for pickers: every registered backend always appears, with a (not installed) suffix when unavailable, so users can select a backend they're mid-installing and get a clear error at spawn time rather than a silent absence. Linux fallback paths also broadened to cover /usr/bin/<name> and ~/.npm-global/bin/<name>

Migration

  • Existing projects and tasks are unaffected — Copilot is purely additive. To opt a project into Copilot, switch project.default_agent (or a specific task's agent field) to copilot. A custom Copilot binary path can be set in the global settings UI or by hand in ~/.watchfire/settings.yaml

[2.0.1] Spark

Fixed

  • Silently discarded work when an agent forgot to commit — if an agent edited files in its worktree and set status: done without running git commit, Watchfire saw no diff on the branch, skipped the merge, and deleted the branch and worktree — losing everything the agent did. The merge step now runs git add -A && git commit --no-verify inside the worktree as a safety net before the diff check, so uncommitted edits are always captured even when the agent skips the commit step
  • Codex commit reminder — Codex sessions' per-session AGENTS.md now includes an explicit CRITICAL: Commit before marking a task done addendum at the end, making the rule the last thing Codex reads before starting work

[2.0.0] Spark

Watchfire is no longer Claude Code only. Spark introduces a pluggable agent backend and ships first-class support for Claude Code, OpenAI Codex, opencode, and Gemini CLI — selectable per project or per task.

Added

  • Pluggable agent backend interface — any CLI coding agent can now be plugged into Watchfire through a single AgentBackend contract (executable resolution, command construction, sandbox extras, system-prompt delivery, transcript discovery and formatting). All existing surfaces — chat, task, start-all, wildfire — work against the backend registry unchanged
  • Four first-class backendsClaude Code, OpenAI Codex, opencode, and Gemini CLI ship out of the box and are interchangeable across every agent mode
  • Project default agentwatchfire init now asks which agent to use and seeds default_agent in project.yaml
  • Per-task agent override — each task can pin itself to a specific backend via a new optional agent field in its YAML, letting you mix and match agents within a single project (e.g. Claude Code for architecture work, Codex for trivial edits, or re-running a failed task under a different agent without touching project settings). An empty value defers to the project default, keeping existing tasks behaving exactly as before
  • Agent picker in watchfire init — the init wizard prompts for the coding agent to use when the global "Ask per project" setting is active
  • Agent selector in project settings (TUI + GUI) — switch an existing project's agent without re-running watchfire init. The GUI populates its selector from the daemon via a new SettingsService.ListAgents RPC, reaching parity with the TUI
  • Global settings UI for agent paths — new settings overlay registers custom binary paths per backend and picks the global default agent, including an "Ask per project" option that forces watchfire init to prompt every time
  • Agent badge on task lists — TUI and GUI render a compact agent badge next to a task's title whenever task.agent is set and differs from the project default. Tasks that defer to the project default render no badge, keeping the list visually quiet for the common case
  • Per-session homes for Codex, opencode, and Gemini — each backend runs inside its own per-session home so the Watchfire system prompt stays isolated from your personal configuration, while auth and global settings keep flowing from your real ~/.codex, ~/.config/opencode, and ~/.gemini
  • Transcripts for every backend — the log viewer now renders JSONL transcripts for Codex, opencode, and Gemini sessions in the same User/Assistant format as Claude Code. Transcript discovery is owned by each backend, so any future agent automatically gets the full log viewer experience

Changed

  • Agent resolution chain — the daemon resolves the backend for each session through a predictable four-step chain: task.agentproject.default_agentsettings.defaults.default_agentclaude-code. Empty strings defer to the next level, and chat / wildfire-refine / wildfire-generate sessions (which aren't scoped to a single task) skip the task step and start from the project default
  • Backend-owned transcript discovery — JSONL transcript location and formatting moved out of the agent manager and into each backend's implementation
  • Backend-contributed sandbox paths — writable paths, cache patterns, and stripped environment variables are now contributed by each backend instead of being hardcoded, keeping new agents self-contained

Fixed

  • Agent auth failure when launched from GUI — macOS GUI apps inherit a minimal environment (PATH=/usr/bin:/bin:/usr/sbin:/sbin) missing user-installed tool paths like ~/.local/bin. This caused Claude Code to misroute API calls through "extra usage" billing instead of the user's subscription, producing spurious "You're out of extra usage" errors in Task, Run All, and Wildfire modes while Chat worked fine. The Electron daemon spawner now resolves the user's full login-shell PATH before launching watchfired, and the macOS sandbox enrichment adds ~/.local/bin alongside the usual Homebrew prefixes
  • GUI blank window on macOS — the production renderer is now served over a custom app:// protocol instead of file://, restoring execution of the crossorigin ES-module entry bundle that Chromium was silently blocking. Global error / unhandledrejection handlers in the renderer entry now surface any future module-init failure in the window instead of rendering blank

Migration

  • Existing projects without default_agent continue to use Claude Code — no action required
  • Existing tasks without an agent field continue to use the project default — no action required
  • Custom codex, opencode, and gemini binary paths can be configured via the new global settings UI or by hand in ~/.watchfire/settings.yaml

[1.0.0] Ember

Added

  • JSONL transcript logs — session logs now capture Claude Code's structured JSONL transcripts (~/.claude/projects/) instead of raw PTY scrollback, producing clean readable User/Assistant conversation logs
  • Transcript auto-discovery — daemon locates Claude Code's transcript files by matching session names and copies them to ~/.watchfire/logs/ alongside the existing .log file

Changed

  • Log viewer — TUI and GUI now display formatted conversation transcripts (User/Assistant messages, tool call summaries) instead of garbled terminal output; falls back to PTY scrollback when no transcript is available

Fixed

  • Agent restart loop — wildfire/start-all now stops after 3 consecutive restarts of the same task and transitions to chat mode, preventing infinite loops on rate limits, crashes, or auth expiry
  • Sandbox blocks ~/Desktop projects (#17) — macOS Seatbelt sandbox no longer denies read access to protected directories (Desktop, Documents, Downloads, etc.) when the project is located inside one of them
  • TUI task list scroll with 100+ tasks (#18) — fixed height accounting for section header blank lines and scroll indicators that caused the last few tasks to be invisible
  • Install script "tmp_dir: unbound variable" (#20) — moved temp directory variable to global scope so the cleanup trap can access it after function returns
  • Desktop always thinks CLI tools are outdated (#21) — version check now strips ANSI escape codes before parsing and logs the actual error when the CLI binary can't be executed
  • Can't edit already created tasks in GUI (#23) — task editor no longer resets form contents when background polling refreshes the task list
  • Duplicate terminal headers in GUI — Chat panel no longer accumulates repeated Claude Code banners when switching projects or during wildfire phase transitions; terminal is properly cleared before each new subscription, and raw output subscriptions use their own abort map instead of colliding with screen subscriptions

[0.9.0] Ember

Added

  • Linux GUI — AppImage and .deb packages for x64 Linux, built in GitHub Actions on ubuntu-latest. Bundled CLI + daemon binaries installed to ~/.local/bin on first launch with pkexec fallback for admin privileges.
  • Windows GUI — NSIS installer (Watchfire-Setup-x.y.z.exe) for x64 Windows, built in GitHub Actions on windows-latest. Bundled CLI + daemon binaries installed to %LOCALAPPDATA%\Watchfire on first launch with PowerShell elevation fallback.
  • Cross-platform auto-update for GUIelectron-updater now checks latest-linux.yml (Linux) and latest.yml (Windows) in addition to latest-mac.yml (macOS). All three update manifests are generated and uploaded as release artifacts.
  • Linux GUI CI verificationgui-build-linux job in CI workflow verifies Electron builds on ubuntu-latest on every PR.

Changed

  • CLI installer is cross-platformcli-installer.ts detects OS and uses platform-appropriate install directories (/usr/local/bin on macOS, ~/.local/bin on Linux, %LOCALAPPDATA%\Watchfire on Windows) with platform-specific privilege elevation (osascript, pkexec, PowerShell)
  • Window chrome adapts to platform — macOS uses hiddenInset title bar with traffic lights; Linux and Windows use native window frames
  • electron-builder.yml — added linux (AppImage + deb) and win (NSIS) targets with platform-specific extraResources for correct binary bundling (.exe on Windows)
  • Release workflow — added build-gui-linux and build-gui-windows jobs; release job collects AppImage, deb, NSIS exe, and all update YAMLs as assets

[0.8.0] Ember

Fixed

  • watchfire update now works on Windows — stopDaemonForUpdate uses Kill() instead of SIGTERM
  • findDaemonBinary() handles Windows .exe extension correctly (was producing watchfire.exed)
  • Build directory fallback uses platform-appropriate binary name

[0.7.0] Ember

Added

  • Linux and Windows binaries in GitHub Releases — release workflow now builds amd64 + arm64 for darwin, linux, and windows (6 platform targets total)
  • Cross-platform CI — CI workflow verifies builds on macOS, Linux, and Windows
  • Install scriptsscripts/install.sh (macOS/Linux) and scripts/install.ps1 (Windows) for one-line installation from GitHub Releases
  • No-CGO tray fallback — daemon runs headless when built without CGO (enables Linux/Windows cross-compilation)

[0.6.0] Ember

Added

  • watchfire chat CLI command — dedicated command to start an interactive chat session with full project context
  • Cross-platform sandbox abstraction — shared SandboxPolicy with platform-specific backends: macOS Seatbelt, Linux Landlock (kernel 5.13+) / bubblewrap (fallback)
  • Landlock sandbox (Linux) — zero-dependency kernel-based sandboxing using go-landlock, daemon re-invokes itself as helper to apply restrictions before exec
  • Bubblewrap sandbox (Linux) — namespace-based isolation with read-only root, writable project dir, hidden credential dirs
  • --sandbox <backend> and --no-sandbox CLI flags on run, chat, plan, generate, wildfire commands
  • Sandbox backend configurable per-project (project.yaml) and globally (settings.yaml)
  • System tray icon abstraction for Linux — setTrayIcon() helper dispatches between macOS template icons and Linux standard icons
  • Windows build support — CLI and daemon compile and run on Windows (unsandboxed, no POSIX signal dependencies)
  • Windows notifications — toast notifications via beeep library
  • Platform-aware updater asset names — supports watchfire-<os>-<arch>[.exe] format

Fixed

  • Agent chaining not stopping on auth (401) or rate-limit (429) errors — start-all/wildfire mode now checks for active issues before spawning the next agent
  • Linux notification double-close bug — notify_linux.go now properly handles file close errors

Changed

  • Default sandbox changed from "sandbox-exec" to "auto" — platform auto-detects best backend
  • Sandbox setting priority: CLI flag > project setting > global default

[0.5.0] Ember

Added

  • Integrated terminal in the GUI — footer bar that expands into a resizable bottom panel with tabbed shell sessions via node-pty, Cmd+` toggle, Nerd Font support
  • Version display in system tray menu below "Watchfire Daemon" header for easy version identification

Fixed

  • Status indicator dots in sidebar/dashboard now only pulse for projects with an autonomous agent (task, wildfire, start-all) — chat mode no longer triggers pulsing
  • Dashboard project card X button overlapping chevron arrow on hover
  • GUI crash ("Object has been destroyed") when PTY emits data after BrowserWindow is closed — onData/onExit callbacks now check isDestroyed() before sending IPC messages

[0.4.0] Ember

Fixed

  • Daemon crash (exit code 2) when macOS notification fires outside .app bundle — hasAppBundle() pre-check and @try/@catch prevent NSInternalInconsistencyException
  • Agent subprocess inheriting CLAUDECODE env var — stripped from child process environment to prevent Claude Code nesting issues
  • Project color not updating in sidebar/dashboard after changing in settings — optimistic local store update now re-renders immediately
  • Tasks not updating in GUI when chat agent creates them on disk — removed flawed shallow comparison that suppressed store updates from protobuf-es objects
  • CLI wildfire/start-all crashing with "stream error: no agent running" during task transitions — stream errors are now handled gracefully in chaining mode
  • System tray concurrent update crashes — serialized Cocoa API calls through a single goroutine with debouncing
  • Agent manager deadlock when onChangeFn calls ListAgents() during state persist — moved callback to a goroutine

[0.3.0] Ember

Added

  • Daemon health check (Ping RPC) for lightweight connection verification

Fixed

  • Daemon startup race condition — daemon.yaml is now written only after the gRPC server is accepting connections, eliminating "connection refused" errors on startup
  • GUI no longer shows "Failed to fetch" when starting tasks immediately after daemon launch
  • TUI no longer shows "connection refused" on first connect attempt
  • GUI settings page (and all views) no longer vanish during brief daemon disconnects — disconnect message now shows as an overlay
  • CLI and GUI daemon startup now verify port readiness before proceeding

[0.2.0] Ember

Added

  • Agent memory file (.watchfire/memory.md) — agents can persist project-specific knowledge (conventions, preferences, patterns) across sessions

Changed

  • Removed configurable "default branch" setting — tasks now merge into whatever branch is currently checked out in the project root

Fixed

  • macOS notifications now display the Watchfire icon instead of a generic system icon
  • GUI terminal no longer duplicates output in an infinite loop when an agent stops

[0.1.3] Ember

Fixed

  • Homebrew Cask download URL now includes -universal suffix to match the actual DMG release asset name, fixing brew install --cask watchfire
  • GUI now polls tasks and agent status continuously so the interface updates when task files change
  • GUI project settings color changes now apply immediately without needing a restart

[0.1.2] Ember

Fixed

  • GUI auto-updater no longer fails with ENOENT: app-update.yml — the --prepackaged electron-builder flag skips generating this file; it is now created explicitly in the build workflow

[0.1.1] Ember

Fixed

  • GUI now detects Homebrew-installed binaries in /opt/homebrew/bin/ on Apple Silicon Macs
  • CLI installer checks both /opt/homebrew/bin and /usr/local/bin before prompting to install
  • Daemon discovery finds watchfired in Homebrew prefix when Electron's PATH is limited

[0.1.0] Ember — Initial Release

Watchfire orchestrates coding agent sessions (starting with Claude Code) based on project specs and tasks. Define what you want built, break it into tasks (or have agents do it), and let agents work through them autonomously — with full visibility into what's happening. Or just turn on wildfire mode and let your agents do it all for you.

Daemon (watchfired)

The always-on backend that manages everything:

  • Agent orchestration — Spawns coding agents in sandboxed PTYs with terminal emulation, one task per project, multiple projects in parallel
  • Git worktree isolation — Each task runs in its own worktree (watchfire/<task_number>), auto-merged back on completion with conflict detection
  • macOS sandbox — Agents run inside sandbox-exec with restricted filesystem/network access
  • File watching — Real-time detection of task completion and phase signals via fsnotify, with polling fallback for reliability
  • Session logs — Every agent session recorded to ~/.watchfire/logs/ with YAML metadata
  • System tray — Menu bar icon showing daemon status, active agents with colored project dots, and quick stop/quit actions
  • Secrets folder.watchfire/secrets/instructions.md for providing agents with external service credentials and setup instructions, injected into the system prompt
  • Issue detection — Monitors agent output for auth errors (401, expired tokens) and rate limits (429), with real-time notifications to clients
  • gRPC + gRPC-Web — Single port serves both native gRPC (CLI/TUI) and gRPC-Web (Electron GUI)
  • Auto-discovery — Writes connection info to ~/.watchfire/daemon.yaml so clients find it automatically

CLI (watchfire)

Project-scoped command-line interface:

  • watchfire init — Initialize a project (git setup, .watchfire/ structure, .gitignore, interactive config)
  • watchfire task add|list|edit|delete|restore — Full task CRUD with soft delete/restore
  • watchfire definition — Edit project definition in $EDITOR
  • watchfire settings — Configure project settings interactively
  • watchfire agent start [task|all] — Start agent in chat, single-task, or run-all-ready mode
  • watchfire agent wildfire — Autonomous three-phase loop: execute ready tasks → refine drafts → generate new tasks → repeat
  • watchfire agent generate definition|tasks — One-shot generation commands
  • watchfire daemon start|status|stop — Daemon lifecycle management
  • watchfire update — Self-update from GitHub Releases
  • Terminal attach — Raw PTY streaming with resize handling and Ctrl+C forwarding
  • Self-healing project index — Auto-registers projects, updates moved paths, reactivates archived projects

TUI (watchfire with no args)

Interactive split-view terminal interface:

  • Split layout — Task list (left) + agent terminal (right) with draggable divider
  • Left panel tabs — Tasks (grouped by status), Definition (read-only + $EDITOR), Settings (inline form)
  • Right panel tabs — Chat (live agent terminal), Logs (session history viewer)
  • Agent modes — Chat, task, start-all, and wildfire with phase display (Execute/Refine/Generate)
  • Issue banners — Auth required and rate limit detection with recovery guidance
  • Keyboard navigation — Vim-style (j/k), arrows, tab switching (1/2/3), panel focus (Tab)
  • Mouse support — Click to focus/select, scroll, drag divider to resize
  • Task management — Add, edit, status transitions (draft/ready/done), soft delete — all from the keyboard
  • Auto-reconnect — Reconnects to daemon on disconnect with status indicator
  • Help overlayCtrl+h for full keybinding reference

GUI (Electron)

Multi-project desktop application:

  • Dashboard — Project cards with task counts, status dots, active task display
  • Project view — Tasks, Definition, Secrets, Trash, Settings tabs with collapsible right panel (Chat, Branches, Logs)
  • Add Project wizard — Three-step flow: project info → git config → definition
  • Branch management — View, merge, delete, and bulk-manage worktree branches
  • Agent terminal — Live streaming via gRPC-Web with input support
  • Global settings — Defaults, appearance (system/light/dark theme), agent path config, update preferences
  • Daemon lifecycle — Auto-restarts daemon if it dies, handles binary updates gracefully

Agent Modes

ModeDescription
ChatFree-form conversation with the agent at project root
TaskWork on a specific task in an isolated worktree
Start AllRun all ready tasks in sequence, one at a time
WildfireFully autonomous loop: execute → refine → generate → repeat until done
Generate DefinitionOne-shot: agent analyzes codebase and writes project definition
Generate TasksOne-shot: agent reads definition and creates task files

Task Lifecycle

draft → ready → done (success ✓ or failure ✗)
  • Tasks are YAML files in .watchfire/tasks/
  • Agents detect completion by writing status: done to the task file
  • Daemon auto-merges the worktree branch, cleans up, and chains to the next task
  • Merge conflicts abort the chain to prevent cascading failures

Build & Distribution

  • macOS DMG — Universal binary (arm64 + amd64) with GUI, CLI, and daemon bundled
  • Code signing & notarization — Developer ID certificate with hardened runtime
  • Homebrewbrew tap watchfire/tap && brew install watchfire
  • Auto-update — GUI via electron-updater, CLI via watchfire update, daemon checks on startup
  • CI/CD — GitHub Actions: lint, test, build matrix (arm64/amd64), sign, notarize, draft release

On this page