Changelog
Watchfire release notes — every shipped version with codename, date, and itemised change log for the daemon, CLI/TUI, and GUI clients.
[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 seeisRunning=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 showsexited (mode: chat)immediately followed bystarted (chat mode)). The daemon now marks the replace window (Manager.replacing, the sibling of the v10 chain-transition mark) andrefuseChatStartrefuses 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;/logininstead 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 theoauth/authorizeURL 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 raisesauth_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/planhad inherited the MCPrun_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,/wildfireanswered "⛔ An agent is already running (mode chat)",/cancelwanted a task number, and/stopthen/wildfirelost the race to the GUI auto-restarting chat. The starters now go straight through the daemon'sStartAgent— 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/stopfirst. 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_examples→blowfish-examples);LocateTranscriptonly 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_aftersurfaced 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 inintegrations.yaml; an unconfigured install starts no goroutine and dials nothing. - Pairing — the allowlist.
BeginTelegramPairingissues an 8-char code fromcrypto/randover 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.RevokeTelegramChatremoves a chat from disk and the live bridge immediately. CLI:watchfire telegram pair(code + t.me deep link, polls until paired) andwatchfire 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;setMyCommandspublishes 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.
TestIntegrationupgraded 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
pwith code + deep link + live countdown,m/wper-chat mute/watch toggles, and confirm-gated revoke/delete. CLI:watchfire integrations add telegramwith no-echo token prompt; re-adds rotate the token and preserve configured events. - Watch mode — live conversation relay.
/watch onstreams 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 bansResize/SendInputreferences 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
/sayprefix. 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/useauto-attaches to the most recently started live session;/usepins it. /wildfireand the wildfire milestone feed. Bare/wildfirestarts the autonomous loop from Telegram;/wildfire offor/stopends 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./generateand/planfrom 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 newTaskService.CreateTasksBatchvalidates every task before writing any. Surfaces: GUI Quick Add modal with live "will create N tasks" preview, TUI overlay onA, andwatchfire task quick($EDITOR template,--ready/--draft/--stdin). - Definition retrofit mode. A new
retrofit-definitionagent mode folds tasks completed since the last retrofit into an updated project definition, advancing alast_retrofit_task_numberwatermark. A confirm-gated archive then soft-deletes exactly the folded window; archived tasks carryretrofit_archivedand keep counting in insights. Surfaces: GUI Definition tab action + modal, TUIron the Definition tab, CLIwatchfire 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.
/runallfolded into/run <n>|alland/unmuteinto/mute on|off(both old verbs remain as hidden aliases, so nothing breaks), and bare/wildfirenow starts the loop directly. Telegram'ssetMyCommandsmenu registers the full canonical set, and/helpgroups 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
CommandContexthad no production implementation —/watchfire status|retry|cancelwas reachable only from tests. A deps-injected productionCommandContextnow scopes projects by Discord guild / Slack team, routes Retry throughBulkUpdateStatusand 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.
StartAgentnow 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_dialogissue instead of looping input. - Sandbox-denied folders explain themselves (#17). A project under
~/Desktop,~/Documents, or~/Downloadsfailed with an opaque "unexpected error" because the sandbox denies those roots by design. A preflight check now blocks registration with an actionable message, andStartAgentrefuses before any PTY spawn with asandbox_deniedissue surfaced through the agent-issue plumbing.sandbox=noneruns 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~/.zprofilenever ran and user PATH entries were missing. The resolved shell now spawns with-lon 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 runningerror 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 buildbut nevernpm 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 fromindex.htmlso 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 composedAGENTS.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 doneline was theelsebranch 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 projline, MCPget_insights), each of which now shows a+N moreaffordance instead of silently implying it listed everything. - The fleet insights cache is versioned.
~/.watchfire/insights-cache/_global.jsoncarried 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 aschemafield, 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 itselflast 30dwhen 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
titletooltips. 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, arepointer-events-noneso 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_aton 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 wrotestatus: 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, soduration_msin<n>.metrics.yamlis 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 emitTASK_FAILEDtwice. - Insights recovers the ~580 pre-9.1 tasks. A done task's completion time now resolves as
completed_at→ the metrics file'scaptured_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_task → run_task → wait_for_task → get_task → get_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-startswatchfiredif needed and connects over the same path as the CLI, then serves a data-driven tool registry. Tools take an optionalprojectargument (id or name); started inside a registered project directory, that project is the default and the argument may be omitted, mirroring the CLI's directory walk-up. A single server instance can address all registered projects.- Project tools:
list_projects(project list enriched with live agent status) andget_project(project + git info + task counts + agent status). - Task-factory tools:
create_task,list_tasks,get_task,update_task,delete_task. All five write exclusively through the validatedTaskServicegRPC path — no YAML is ever authored directly, so MCP-created tasks can't reproduce the malformed-file class of bug.create_tasktakes astatusenum (draft|ready, defaultdraft), optional acceptance criteria and position, and an agent-backend override validated against the known backends.update_taskis a partial update restricted todraft↔ready(doneis agent-written).delete_taskis a soft delete, reversible from the TUI/GUI Trash — permanent deletion is deliberately not exposed over MCP. - Run tools:
run_task,run_all,start_wildfire,stop_agent,get_agent_status,wait_for_task. 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_agentis idempotent.wait_for_taskis the factory loop's synchronization point: it polls, honours client cancellation, and reports a timeout as a normaltimed_out: trueresult 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), andlist_logs/get_log(past session transcripts, capped with an explicit truncation note). --read-onlymode.watchfire mcp serve --read-onlyserves 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 intools/listat all. Suitable for dashboards or less-trusted callers.- Client onboarding:
watchfire mcp install [client]. Idempotent installers forclaude-code,codex,gemini,opencodeandcopilot, 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 installCLI, a TUI Settings → MCP section, and a GUI Global Settings → MCP panel, each showing per-harnessnot detected/detected/✓ configuredbadges 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
ListMalformedTasksRPC, a⚠ N task file(s) failed to loadwarning inwatchfire 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_taskpromised thatreadymay auto-start an agent when no daemon codepath actually does that — an agent that believed it would file a ready task and wait forever; andlist_tasks/get_taskwere grouped such that--read-onlyservedget_task_diffwhile 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 serveexited 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 themainWindowsingleton with aMapkeyed by window id: a singleton home/mission-control window plus one independentBrowserWindowper 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.jsonmigrates from a single bounds rectangle to a keyed schema —homebounds, a per-projectIdprojectsmap, and anopenProjectslist — 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.yamlgains seven code-output fields —commits,files_changed,lines_added,lines_removed,net_lines,merged,merge_kind— snapshotted from the livewatchfire/<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 aMetricsMissingCodehonesty 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 CodeMirrorMarkdownEditor, 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 registrybroadcast()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.mdupdated 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 richMarkdownEditor, 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 inApp.tsx. - Integrated-terminal output no longer bleeds across windows. Each PTY session now records the
windowIdthat spawned it and routesonData/onExitonly 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(forprojects.yamland 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 newWATCHFIRE_DEBUGenv 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
readytask 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 asstartedwith no agent behind it — stuck inreadyforever. The stamp now runs after the process spawns successfully, and the chain's "failed to start next" path emits aTASK_FAILEDnotification 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 limitandtoken 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 globaldaemon.log. - Global
daemon.logcap 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_DEBUGenv 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/Minimizebutton 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 tolocalStorage. - GUI version under the sidebar logo. The running version now shows as a small
v{version}line beneath the wordmark, fed by the existingwindow.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, andgui/package-lock.jsonbump 7.2.1 → 7.3.0 (codenameForgepreserved). The daemon and CLI pick it up through the existingMakefileldflags 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.loggrew 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 thetitle: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 forprompt:andacceptance_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 launcheswatchfired, the daemon inherits Electron's/dev/nullstdio, so every chain decision, task-load skip, and phase-watch line was discarded. The daemon now opens~/.watchfire/daemon.login append mode and writes throughio.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
TaskYAML 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 ontime.Parse(""). (B)config.LoadAllTasksnow 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 thestarted_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 infetchStatuswas synthesising{isRunning: false}, and ChatTab's auto-restart effect immediately firedstartAgent('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 stoptoast. A race between the in-flightstartAgentRPC 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.ModesControlno longer pre-callsstopAgent(the daemon already does an atomic kill+restart), and a per-projectstartAgentInFlightflag short-circuits status polls during the switch. - Run All / Wildfire — initial prompt actually renders on start and on phase transitions. Each daemon
Processrestart 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 offagentStatus.startedAtand 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.onEndnow 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+↑andShift+↓on a focused active row swap the selected task with its in-bounds same-status neighbour and fireTaskService.ReorderTaskswith 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 surfacesReorder failed — revertedon failure. Cross-section moves (Draft↔Ready, Ready↔Done) and top/bottom boundaries are silent no-ops. A race guard onTasksLoadedMsgdrops 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 gainsShift+↑/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-kitpieces already proven out by the Sidebar —DndContext+SortableContext+useSortable+arrayMovefrom@dnd-kit/sortable— so there's no new dependency. Each active status group ("In Development" / "Todo") owns its ownDndContextwith an 8 pxPointerSensoractivation distance so a stray pixel of pointer drift on a click never turns into a reorder. AGripVerticalicon 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 theReorderTasksRPC, and either commits the server response or restores the snapshot + toastsReorder failed: …on rejection. TaskService.ReorderTasksserver handler + manager method. The proto declaredTaskService.ReorderTasks(ReorderTasksRequest) returns (TaskList)but no server handler existed, so any call hitUnimplementedTaskServiceServer.ReorderTasksand returnedcodes.Unimplemented— blocking the drag-to-reorder UI. Newtask.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 viaconfig.SaveTask. The handler maps validation errors tocodes.InvalidArgumentand any other error tocodes.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_helperonly runs from login shells, andspawn(..., {shell: true})uses non-login/bin/sh -c, so the user's profile PATH is never sourced.codeat/usr/local/bin/code(and Homebrew shims at/opt/homebrew/bin) failed to resolve and every IDE pick errored out. NewspawnEnv()helper ingui/src/main/ipc.tsprepends 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. Fixesvscode,cursor,windsurf,zed,subl,webstorm,idea, andfleet;xcode(uses macOSopen -a) andfinder(usesshell.openPath) were never affected. Bug only surfaced when the GUI was launched from Finder/Dock —npm run devin 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::ListTaskswas sorting strictly descending bytask_number, contradicting the spec's "Task Work Order" rule. Every consumer readstasks[0]— thestart-allchain and both wildfire pickers — sowatchfire start-allandwatchfire wildfirewalked a 4-task queue backwards (4 → 3 → 2 → 1) instead of forward (1 → 2 → 3 → 4). ThePositionfield was already onmodels.Taskand 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 ASCprimary,task_number ASCtiebreaker.CreateTaskalso changed: the default position is nowmax(active.position) + 1(or1if zero active tasks), appending to the bottom of the work queue so a new task after a manual reorder no longer jumps ahead. An explicitopts.Positionstill 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, andChatTab.tsx's 2 s status poll routinely flippedactivetrue → false → truewhenevergetAgentStatushit a transient error, spuriously replaying the full daemon raw buffer from byte 0 mid-scroll. The effect is now idempotent — whenactive=trueand the previousAbortControlleris 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 —ProcesstracksrawTotalBytes(monotonic count of broadcast bytes), and the newSubscribeRawFrom(id, bytesReceived)slices the late-join snapshot so only bytes past the client's offset are sent.proto/watchfire.protoSubscribeRawOutputRequestgains anint64 bytes_receivedfield. Negative / past-end cursors are clamped; cursors before the 1 MiB rolling buffer's floor return the full buffer. Also: xtermscrollbackraised from 1 000 to 10 000 lines, and theResizeObservernow 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_receivedfield is optional and defaults to0(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.ReorderTasksis new but no schema or YAML field changed. Existing.watchfire/tasks/<n>.yamlfiles load identically — thepositionfield 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.goimplementing the fullBackendinterface 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 asAGENTS.md, headlesscursor-agent --workspace <worktree> --printlaunch with the yolo / trust flag, JSONL transcript located viaLocateTranscriptand rendered throughFormatTranscriptin the same shape every other backend uses. TUI and GUI agent pickers includecursorwith 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+Tabwalks the sidebar;↑/↓(j/k) walks rows inside the active section;/opens a search overlay that matches across labels + section breadcrumbs andEnterjumps to the matched row. - Project Notifications — per-event overrides. New
Notificationssection in the project Settings sidebar. Master mute (existing) plus a newOverride per-event preferencestoggle that gates per-event Enabled toggles fortask_failed/run_complete/weekly_digest. NewQuiet hours overridetoggle gates two HH:MM text inputs (start/end); same disabled-while-off treatment. Pre-v6project.yamlfiles load identically. - Integrations — per-project scoping. New
Integrationssection in the project Settings sidebar. GitHub auto-PR toggle binds the project to membership in the global~/.watchfire/integrations.yaml→github.project_scopeslist. Slack channel + Discord guild ID text fields persist on the project YAML under a newintegrations:block; empty string clears the binding (= inherit global default). - Danger-zone actions — Archive / Regenerate ID / Reset numbering / Prune merged branches / Unregister. New
Danger zonesection 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 recomputesnext_task_number = highest_existing + 1. Unregister drops the entry from~/.watchfire/projects.yamlwhile 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 —xon 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:urestores,xarms a y/N permanent-delete confirm (refused if thewatchfire/<n>branch reports unmerged work),Enteropens the read-only edit form,Dflips back. - TUI Definition tab —
eshells out to$EDITOR. The Definition tab was read-only; editing required dropping out of the TUI to runwatchfire define. Newe(andEnter) binding opens the project definition in$VISUAL→$EDITOR→vim→viviatea.ExecProcess. On exit, the diff against the pre-edit content decides whether to dispatchProjectService.UpdateProject. - TUI Branches overlay —
Ctrl+Blists, merges, deletes, prunes orphans. The TUI had zero visibility into git branches and their worktrees. NewCtrl+Boverlay lists everywatchfire/<n>branch with columns Branch, Task, Age, Status (merged/unmerged), and Worktree (present/absent, with merged-orphans rendered asabsent*). Action keys:mmerge,xdelete (refuses unmerged),Xforce-delete,Pprune all merged-orphans,rrefresh. - TUI text-select mode —
Ctrl+Ttoggles 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 mousebanner; the header appends atext-selectchip 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 abubbles/viewport—vt10x's grid had no scrollback, so PgUp / Shift+arrows / wheel-up either no-op'd or corrupted the pane. The TUI now subscribes toSubscribeRawOutput(the same raw-byte stream the GUI feeds to xterm.js) and runs the bytes through a TUI-side emulator built ongithub.com/charmbracelet/x/vt. Up to 5000 scrollback lines drop in below the visible grid;wheel-up/Shift+↑/PgUpwalk 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 tocharmbracelet/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 calledGetRawBufferin two separate critical sections;broadcastRawinterleaving between them would append bytes torawBufAND send them to the new channel, producing duplicates the new TUI vt emulator double-applied. NewProcess.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;35Mlands as aKeyRunesthat gets forwarded into the chat agent's PTY (visible as[<64;105;35M[<64;105;35M…in chat). New guard dropsKeyRuneswithAlt == trueand 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.Xvslayout.dividerColand the wheel branch is split out of the click-press case so it never mutatesfocusedPanel.Shift+↑/↓(line scroll) andShift+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::SaveYAMLnow writes to a sibling tmp file,fsyncs, thenos.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 globalprojects.yaml, task files, settings, agents, daemon state, integrations. LoadProjectrejects zero-valued reads.internal/config/projects.go::LoadProjectnow returnscorrupt project.yaml at <path>: …when the unmarshalled struct hasVersion == 0or emptyProjectID. 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
watchfiredprocesses 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 inrunDaemonbetween the legacydaemon.yamlPID check and the dynamic port bind. Newinternal/config/lock.goexposesAcquireDaemonLock()returningErrDaemonLockHeldon contention; the daemon acquires~/.watchfire/daemon.lockvia realsyscall.Flock(LOCK_EX|LOCK_NB)before any tray/server init and holds it for the full process lifetime.
Changed
models.ShouldNotifyconsults per-project overrides before global. The signature changed toShouldNotify(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 (whenOverrideEvents == trueAND a row exists inEvents) → globalcfg.Events. Quiet hours: projectQuietHoursOverride(when non-nil) replaces — does not union — the global window. TheMutedmaster kill-switch retains v4.0 Beacon semantics: any project setting it totrueshort-circuits before any other gate.
Migration
- All Phoenix changes are internal; no schema or API changes. Existing
project.yamlfiles load unchanged. - Daemon singleton:
~/.watchfire/daemon.lockis 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 everywatchfiredprocess is stopped. - Cursor backend:
cursor-agentmust be onPATH(or its absolute path set in~/.watchfire/agents.yaml::cursor.Path). Existing projects keep their currentdefault_agent; opt in viawatchfire configureor by editingdefault_agent: cursorin.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.goregistered atPOST /echo/github?project=<id>parsesX-GitHub-Event/X-Hub-Signature-256/X-GitHub-Delivery, resolves the per-project HMAC secret from the keyring, runsverify.VerifyGitHub, deduplicates against the LRU+TTL idempotency cache, narrows onevent == "pull_request" && action == "closed" && pull_request.merged == true, then matches the Watchfire task bypull_request.head.ref == watchfire/<n>and callstask.MarkDoneIfNotAlready+ emits a PulseRUN_COMPLETEnotification 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.gotranslates the URL-encoded slash-command form body (command,text,team_id,channel_id,user_id,trigger_id) into a call against the shared transport-agnosticcommands.Route(...)router, then rendersCommandResponseas Slack response JSON ({response_type: "in_channel" | "ephemeral", text, blocks})./watchfire status / retry / cancelnow 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 forchat.postMessageso 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 aConnected 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_hostfield onmodels.InboundConfiglets the existing GitHub HMAC-SHA256 verifier target arbitrary GitHub Enterprise hostnames. Newinternal/daemon/echo/handler_gitlab.goverifiesX-Gitlab-Token(per-project shared secret), narrows onMerge Request Hookevents withaction: merge. Newinternal/daemon/echo/handler_bitbucket.goverifiesX-Hub-Signature(HMAC-SHA256), narrows onpullrequest:fulfilledevents. 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 throughmodels.InboundConfig.RateLimitPerMin(0disables). 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_FAILEDBlock Kit template gains three action buttons:Retry,Cancel,View in Watchfire. New inbound endpointPOST /echo/slack/interactivityhandles theblock_actionsandview_submissionpayloads with the same v0 HMAC verification + 5-minute drift window as the slash-commands endpoint. Button presses route throughcommands.Routeso aRetryclick is the exact equivalent of/watchfire retry.Cancelopens a Slack modal that asks "Why are you cancelling?"; the supplied reason lands intask.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::registerForGuildhelper; it also subscribes toGUILD_CREATEGateway 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 manualwatchfire 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+Ffocuses search,Escclears,Up/Down/Enternavigate. TUI:/opens a search overlay with the same field-jumping behaviour. Deep-link routes (#integrationsetc.) still work.
Fixed
- Run-all silently halted on auto-merge failure. When
internal/daemon/agent/taskdone.go::HandleTaskDone's silent merge failed (dirtymain, merge conflict, post-merge hook failure), the chain stopped — but silently: the task YAML still showedstatus: done+success: true, no notification fired, and the user was left wondering why their queue stalled.onTaskDoneFnnow returns a structuredTaskDoneResult{Outcome, Reason}(withTaskDoneOK/TaskDoneMergeFailed/TaskDoneCancelled) instead of a bare bool;monitorProcessbranches onresult.Outcome == TaskDoneMergeFailedand emits aTASK_FAILED-shaped notification before the chain decision;runSilentMergepopulates the task's newmerge_failure_reasonfield (yaml: merge_failure_reason,omitempty, exposed via proto + GUI/TUI). The chain-stop semantics are unchanged — the user still has to clean upmainmanually — 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-startuseEffectdeps tightened to[!!agentStatus, isRunning, projectId]so a staleagentStatusreference from the previous project no longer fireshandleStarton a transient render edge; theautoStarted.current = falsereset onprojectIdchange runs before the auto-start check. Regression test simulates rapid project switching and assertshandleStartfires exactly once per navigation. - Failed-task UI hid the reason behind two clicks.
TaskStatusBadgenow carries atitle=tooltip for agent-reported failures (it already had one for merge failures only), populated by a new exported pure helpercomputeBadgeTooltipthat prefersMerge failed: …overFailed: …when both reasons are set and truncates to 500 runes.TaskItempassesfailureReason={task.failureReason}into the badge alongsidemergeFailureReason.TaskModal's tab decision is now lazy inuseState(() => …)AND kept in sync via the existing effect, sodonetasks 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.Routeround-tripsstatus/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
RateLimitPerMinfield defaults to 30; set to 0 to disable. - Multi-host inbound: leave
github_hostempty 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 fromfunc(...) booltofunc(...) 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 todaybetween 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 inlocalStorage[wf-dashboard-filter], with predicates shared viagui/src/renderer/src/lib/dashboard-filters.ts - Dashboard grid/list layout toggle —
LayoutGrid/Rows3toggle in the header; list mode renders one ~46 px row per project viagui/src/renderer/src/views/Dashboard/ProjectRow.tsx, with the selection persisted inlocalStorage[wf-dashboard-layout] - Elapsed-time badge on running ProjectCards — ticking
Ns / Nm / Nh Mmnext to the agent badge, sourced from a newAgentStatus.started_atproto field stamped inRunningAgent.StartedAt; flips tovar(--wf-warning)past 30 minutes - Last-activity timestamp on dashboard cards —
Active now / 5m ago / 4h ago / 2mo agosegment derived from the most recent taskupdated_at, formatted by a hand-rolled relative-time helper ingui/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.tsref-counts the underlyingAgentService.SubscribeScreenstream - Current-task surfacing on running ProjectCards — replaces the misleading
Next:line withWorking: <current task title>(withFlameicon) when the agent is actively running; reuses the existingAgentStatus.task_titlewith 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
AlertTrianglechip +N failedsegment in the counts row + red progress segment when any task hasstatus === 'done' && success === false - Notification bus — new
internal/daemon/notifypackage with a typedBus, channel fan-out (slow-consumer drop), stableMakeID(sha256(kind|project_id|task_number|emitted_at_unix)[:8]), and JSONL append to~/.watchfire/logs/<project_id>/notifications.logfor headless fallback - TASK_FAILED OS notification — fires from
internal/daemon/server/task_failed.go::emitTaskFailedondone && !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; bodyN tasks done · M failedover the run window - Bundled notification sounds —
assets/sounds/task-{done,failed}.wav(mono 22050 Hz, ~25 KB each); a pureshouldPlaySound(kind, prefs)decision ingui/src/renderer/src/stores/notifications-sound.tskeeps the OS toast silent precisely when the renderer plays its own audio - Dynamic system tray menu —
internal/daemon/tray/tray.gorebuilds on every project / task / agent / settings change; sections forNeeds attention/Working/Idleplus aNotifications (N today) ▸submenu reading the JSONL fallback, with click-through routed via the newDaemonService.SubscribeFocusEventsstream - 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 underdefaults.notificationsin~/.watchfire/settings.yamland gated bymodels.ShouldNotify - Weekly digest notification —
digestRunnerschedules with a re-armabletime.Timerfrommodels.DigestSchedule.NextFire(DST-stable, with 24-hour catch-up on daemon start); Markdown is always rendered to~/.watchfire/digests/<YYYY-MM-DD>.mdregardless of toast suppression. NewWEEKLY_DIGESTnotification kind +FOCUS_TARGET_DIGEST - Per-task metrics capture —
<n>.metrics.yamlsiblings carrying duration, exit reason, agent, tokens, and cost; the newinternal/daemon/metricspackage parses Claude Code, Codex, opencode, Gemini, and Copilot (stub), capturing from a non-blocking goroutine onhandleTaskChanged. Newwatchfire metrics backfillCLI for retroactive capture - Per-project Insights view —
internal/daemon/insights/project.goaggregates one project's tasks per window; new GUI Insights tab + TUI overlay (bound toi) 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 rollup —
internal/daemon/insights/global.goaggregates the whole fleet per window, cached at~/.watchfire/insights-cache/_global.json. Dashboard rollup card under the Beacon status bar; TUI fleet overlay bound toCtrl+f - Report export (CSV + Markdown) — shared
InsightsService.ExportReportRPC withoneofscope (project_id/global/single_task); Markdown templates ininternal/daemon/insights/templates/, CSV uses# section: <name>headers. Single<ExportPill>component on the dashboard + ProjectView headers; TUI bindsCtrl+e - Inline diff viewer — new
internal/daemon/diffpackage resolves diffs pre-merge (<merge-base>...HEADonwatchfire/<n>) and post-merge (locates the merge commit viagit log --grep); structuredFileDiffSetcapped at 10 000 lines, cache at~/.watchfire/diff-cache/<project_id>/<task_number>.json. GUI Inspect tab + TUI overlay (bound tod) - Outbound delivery framework + webhook adapter — new
internal/daemon/relaypackage with anAdapterinterface and aDispatchersubscribing tonotify.Bus; per-adapter retry ([500ms, 2s, 8s]) + circuit breaker (3 failures / 5-minute window). GenericWebhookAdapterPOSTs the canonical payload withX-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.gorenders threetext/templateBlock Kit envelopes (TASK_FAILED / RUN_COMPLETE / WEEKLY_DIGEST) with header / section / context / actions blocks; project-color →:large_<color>_square:shortcode map inslack_color.go - Discord adapter (rich embeds) —
internal/daemon/relay/discord.gorenders three embed envelopes with project-color tinting; sharedhexToInt/rfc3339template helpers and a defensive 4000-rune description trim with a single WARN on overflow. Newwatchfire integrationsCLI parent withlistandtestsubcommands - GitHub auto-PR creation — opt-in per project via
github.auto_pr.enabled: true. End-of-task lifecycle ininternal/daemon/git/pr.go::OpenPR:gh auth status→ parse<owner>/<repo>→git push --force-with-lease→ render PR body viapr_body.md.tmpl→gh 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
IntegrationsServicegRPC service withList/Save/Delete/TestRPCs;Savecarries aoneofpayload, secrets are write-only on the wire. GUIIntegrationsSection.tsxexposes per-type detail panels; TUI overlay is reachable viaCtrl+I - Inbound HTTP server framework —
internal/daemon/echo/server.gobindsListenAddr(default127.0.0.1:8765), with 5 s graceful shutdown drain, 1 MiB body cap + panic recovery middleware, an unauthenticated/echo/health, andRegisterProvider(method, path, handler)for plug-in handlers; bind failure logs ERROR but doesn't crash the daemon - Signature verification —
internal/daemon/echo/verify.goshipsVerifyGitHub(HMAC-SHA256 againstsha256=<hex>),VerifySlack(HMAC-SHA256 overv0:<timestamp>:<body>with 5-minute drift), andVerifyDiscord(Ed25519 overtimestamp || body, same drift) — all constant-time - Idempotency cache —
internal/daemon/echo/idempotency.gois an LRU+TTL cache (1000 entries / 24 h,container/list-backed,sync.Mutex-protected);Seen(key)refreshes TTL on hit - Slash-command router —
internal/daemon/echo/commands.go::Route(ctx, cmd, subcmd, rest, CommandContext) CommandResponsepowers slash-command transports with three commands (status/retry <task>/cancel <task>); theCommandResponse{text, blocks, ephemeral, in_channel}envelope is transport-agnostic - Discord interactions endpoint —
internal/daemon/echo/handler_discord.goexposesPOST /echo/discord/interactionswith end-to-end Ed25519 verification + replay window + idempotency; PING → PONG, APPLICATION_COMMAND → dispatch tocommands.Routeand render viadiscord_render.go::RenderInteraction. Slash-command registration viawatchfire integrations register-discord <guild_id>(idempotent) - Inbound settings UI (GUI + TUI) —
gui/src/renderer/src/views/Settings/InboundSection.tsxshows a Listening pill polled at 5 s, editableListenAddr+PublicURLwith 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
positionorder with bucketing into needs-attention → working → has-ready-tasks → idle (input-array index as the final tiebreaker for stability), with predicate helpers ingui/src/renderer/src/lib/dashboard-filters.ts. A mutedSorted by activitylabel 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
projectIdand survive navigation; Cmd+` toggles a non-destructivepanelCollapsedflag, anddestroyProjectSessions(projectId)is called only fromremoveProject.BottomPanel.tsxalways-mounts everyTerminalTabwith avisibleflag 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.tsruns$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. Newdefaults.terminal_shellglobal 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_digestdefaults 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
ghon PATH andgh auth statusreturning 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.goforpull_request.closedevents 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.Routedid not ship with Beacon —/watchfire status / retry / cancelworked in Discord but not in Slack until v5.0.0 Flare.
[3.0.0] Blaze
Added
- GitHub Copilot CLI backend — Copilot 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 viaAGENTS.mdin a per-sessionCOPILOT_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 updateacross filesystems on Linux (#25) — updating from/tmp(oftentmpfson Fedora/Ubuntu) into~/.local/binused to fail withEXDEV: 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…0047then wrapping to0001…0016). Sorting is now canonical everywhere: the task manager returns tasks strictly descending bytask_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/binahead 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.yamlwas 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'sagentfield) tocopilot. 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: donewithout runninggit 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 runsgit add -A && git commit --no-verifyinside 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.mdnow includes an explicitCRITICAL: Commit before marking a task doneaddendum 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
AgentBackendcontract (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 backends — Claude Code, OpenAI Codex, opencode, and Gemini CLI ship out of the box and are interchangeable across every agent mode
- Project default agent —
watchfire initnow asks which agent to use and seedsdefault_agentinproject.yaml - Per-task agent override — each task can pin itself to a specific backend via a new optional
agentfield 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 newSettingsService.ListAgentsRPC, 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 initto prompt every time - Agent badge on task lists — TUI and GUI render a compact agent badge next to a task's title whenever
task.agentis 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.agent→project.default_agent→settings.defaults.default_agent→claude-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-shellPATHbefore launchingwatchfired, and the macOS sandbox enrichment adds~/.local/binalongside the usual Homebrew prefixes - GUI blank window on macOS — the production renderer is now served over a custom
app://protocol instead offile://, restoring execution of thecrossoriginES-module entry bundle that Chromium was silently blocking. Globalerror/unhandledrejectionhandlers in the renderer entry now surface any future module-init failure in the window instead of rendering blank
Migration
- Existing projects without
default_agentcontinue to use Claude Code — no action required - Existing tasks without an
agentfield continue to use the project default — no action required - Custom
codex,opencode, andgeminibinary 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.logfile
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
.debpackages for x64 Linux, built in GitHub Actions onubuntu-latest. Bundled CLI + daemon binaries installed to~/.local/binon first launch withpkexecfallback for admin privileges. - Windows GUI — NSIS installer (
Watchfire-Setup-x.y.z.exe) for x64 Windows, built in GitHub Actions onwindows-latest. Bundled CLI + daemon binaries installed to%LOCALAPPDATA%\Watchfireon first launch with PowerShell elevation fallback. - Cross-platform auto-update for GUI —
electron-updaternow checkslatest-linux.yml(Linux) andlatest.yml(Windows) in addition tolatest-mac.yml(macOS). All three update manifests are generated and uploaded as release artifacts. - Linux GUI CI verification —
gui-build-linuxjob in CI workflow verifies Electron builds onubuntu-lateston every PR.
Changed
- CLI installer is cross-platform —
cli-installer.tsdetects OS and uses platform-appropriate install directories (/usr/local/binon macOS,~/.local/binon Linux,%LOCALAPPDATA%\Watchfireon Windows) with platform-specific privilege elevation (osascript,pkexec, PowerShell) - Window chrome adapts to platform — macOS uses
hiddenInsettitle bar with traffic lights; Linux and Windows use native window frames - electron-builder.yml — added
linux(AppImage + deb) andwin(NSIS) targets with platform-specificextraResourcesfor correct binary bundling (.exeon Windows) - Release workflow — added
build-gui-linuxandbuild-gui-windowsjobs; release job collects AppImage, deb, NSIS exe, and all update YAMLs as assets
[0.8.0] Ember
Fixed
watchfire updatenow works on Windows —stopDaemonForUpdateusesKill()instead ofSIGTERMfindDaemonBinary()handles Windows.exeextension correctly (was producingwatchfire.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 scripts —
scripts/install.sh(macOS/Linux) andscripts/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 chatCLI command — dedicated command to start an interactive chat session with full project context- Cross-platform sandbox abstraction — shared
SandboxPolicywith 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-sandboxCLI flags onrun,chat,plan,generate,wildfirecommands- 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
beeeplibrary - 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.gonow 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/onExitcallbacks now checkisDestroyed()before sending IPC messages
[0.4.0] Ember
Fixed
- Daemon crash (exit code 2) when macOS notification fires outside
.appbundle —hasAppBundle()pre-check and@try/@catchpreventNSInternalInconsistencyException - Agent subprocess inheriting
CLAUDECODEenv 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
onChangeFncallsListAgents()during state persist — moved callback to a goroutine
[0.3.0] Ember
Added
- Daemon health check (
PingRPC) for lightweight connection verification
Fixed
- Daemon startup race condition —
daemon.yamlis 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
-universalsuffix to match the actual DMG release asset name, fixingbrew 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--prepackagedelectron-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/binand/usr/local/binbefore prompting to install - Daemon discovery finds
watchfiredin 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-execwith 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.mdfor 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.yamlso 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/restorewatchfire definition— Edit project definition in$EDITORwatchfire settings— Configure project settings interactivelywatchfire agent start [task|all]— Start agent in chat, single-task, or run-all-ready modewatchfire agent wildfire— Autonomous three-phase loop: execute ready tasks → refine drafts → generate new tasks → repeatwatchfire agent generate definition|tasks— One-shot generation commandswatchfire daemon start|status|stop— Daemon lifecycle managementwatchfire 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 overlay —
Ctrl+hfor 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
| Mode | Description |
|---|---|
| Chat | Free-form conversation with the agent at project root |
| Task | Work on a specific task in an isolated worktree |
| Start All | Run all ready tasks in sequence, one at a time |
| Wildfire | Fully autonomous loop: execute → refine → generate → repeat until done |
| Generate Definition | One-shot: agent analyzes codebase and writes project definition |
| Generate Tasks | One-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: doneto 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
- Homebrew —
brew tap watchfire/tap && brew install watchfire - Auto-update — GUI via
electron-updater, CLI viawatchfire update, daemon checks on startup - CI/CD — GitHub Actions: lint, test, build matrix (arm64/amd64), sign, notarize, draft release
Keyboard Shortcuts
Printable cheat sheet for every Watchfire keybinding — TUI navigation, agent controls, task management, and the GUI shortcuts on macOS and Windows.
Roadmap
A snapshot of where Watchfire is heading after Inferno — what's shipped, what's likely next, and what we're still thinking about.