Changelog
Watchfire release notes — every shipped version with codename, date, and itemised change log for the daemon, CLI/TUI, and GUI clients.
[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