Skip to main content
Watchfire
Main content

Changelog

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

Subscribe to releases

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

[7.1.0] Forge

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

Fixed

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

[7.0.0] Forge

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

Added

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

Fixed

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

Migration

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

[6.0.0] Phoenix

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

Added

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

Fixed

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

Changed

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

Migration

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

[5.0.0] Flare

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

Added

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

Fixed

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

Tests

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

Migration

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

[4.0.0] Beacon

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

Added

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

Changed

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

Fixed

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

Migration

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

Known issues

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

[3.0.0] Blaze

Added

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

Fixed

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

Migration

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

[2.0.1] Spark

Fixed

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

[2.0.0] Spark

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

Added

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

Changed

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

Fixed

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

Migration

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

[1.0.0] Ember

Added

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

Changed

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

Fixed

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

[0.9.0] Ember

Added

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

Changed

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

[0.8.0] Ember

Fixed

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

[0.7.0] Ember

Added

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

[0.6.0] Ember

Added

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

Fixed

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

Changed

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

[0.5.0] Ember

Added

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

Fixed

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

[0.4.0] Ember

Fixed

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

[0.3.0] Ember

Added

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

Fixed

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

[0.2.0] Ember

Added

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

Changed

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

Fixed

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

[0.1.3] Ember

Fixed

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

[0.1.2] Ember

Fixed

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

[0.1.1] Ember

Fixed

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

[0.1.0] Ember — Initial Release

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

Daemon (watchfired)

The always-on backend that manages everything:

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

CLI (watchfire)

Project-scoped command-line interface:

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

TUI (watchfire with no args)

Interactive split-view terminal interface:

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

GUI (Electron)

Multi-project desktop application:

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

Agent Modes

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

Task Lifecycle

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

Build & Distribution

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

On this page