Skip to main content
Watchfire
Components
Main content

Daemon (watchfired)

The Watchfire daemon is the backend brain — managing projects, spawning agents, handling git workflows, and serving clients over gRPC.

The daemon is the backend brain of Watchfire. It manages multiple projects simultaneously, watches for file changes, spawns coding agents in sandboxed PTYs with terminal emulation, handles git worktree workflows, and serves state to thin clients over gRPC.

Lifecycle

AspectBehavior
DevelopmentRun watchfired --foreground for hot reload (tray still active)
ProductionRuns in background, started automatically by CLI/TUI/GUI if not running
PersistenceStays running when thin clients close
ShutdownCtrl+C (foreground), CLI command, or system tray quit

The daemon starts automatically when you run any watchfire command. It persists in the background even after the CLI/TUI exits, so agent sessions keep running.

Network

AspectDecision
ProtocolgRPC + gRPC-Web (multiplexed on same port)
PortDynamic allocation (OS assigns free port)
DiscoveryConnection info written to ~/.watchfire/daemon.yaml
ClientsCLI/TUI use native gRPC, GUI uses gRPC-Web

Multi-Project Management

AspectBehavior
Projects index~/.watchfire/projects.yaml lists all registered projects
RegistrationProjects added via CLI (watchfire init) or GUI
ConcurrencyOne active task per project, multiple projects in parallel
Client trackingTracks which clients are watching which projects
Task cancellationTask stops only when ALL clients for that project disconnect

File Watching

The daemon uses fsnotify to watch for changes:

AspectBehavior
Mechanismfsnotify with debouncing
RobustnessHandles create-then-rename pattern (common with AI tools)
Per-project.watchfire/project.yaml, .watchfire/tasks/*.yaml
Polling fallback5s polling as safety net for missed watcher events
Re-watch on chainRe-watches directories created during earlier phases

Agent Backends

The daemon dispatches all agent-specific behavior through a pluggable Backend interface. Each supported agent — Claude Code, OpenAI Codex, opencode, Gemini CLI, and GitHub Copilot CLI — is a Backend implementation registered with a process-wide registry at startup. The daemon looks backends up by name and asks them to:

  • Resolve the agent binary (user-configured path → PATH → common install locations)
  • Build the PTY command line (binary, args, env, initial prompt handling)
  • Install the composed Watchfire system prompt into whatever form the agent expects (CLI flag, AGENTS.md, system.md, …)
  • Locate and format the JSONL transcript the session produced
  • Contribute writable paths, cache patterns, and env vars to strip to the sandbox policy

Agent Resolution

When spawning a session, the daemon walks a four-step chain to pick the backend:

task.agent → project.default_agent → settings.defaults.default_agent → claude-code

Empty string at any level defers to the next. Chat and wildfire-refine/generate sessions aren't scoped to a single task, so they skip the task step.

Per-Session Homes

Codex, opencode, Gemini, and Copilot each run with an isolated per-session home so the Watchfire system prompt and per-session data don't leak into the user's personal config, while auth still flows from the real config directory:

BackendEnv var(s)Per-session path
CodexCODEX_HOME~/.watchfire/codex-home/<session>/
opencodeOPENCODE_CONFIG_DIR, OPENCODE_DATA_DIR~/.watchfire/opencode-home/<session>/
GeminiGEMINI_SYSTEM_MD~/.watchfire/gemini-home/<session>/system.md
CopilotCOPILOT_HOME, COPILOT_CUSTOM_INSTRUCTIONS_DIRS~/.watchfire/copilot-home/<session>/

Claude Code has no isolation — the daemon delivers the system prompt via the --append-system-prompt CLI flag and uses ~/.claude/ directly.

SettingsService.ListAgents

The daemon exposes a SettingsService.ListAgents RPC that returns the registered backends (name + display name). The GUI (and TUI settings tab) call this to populate agent-picker dropdowns, so any new backend registered in the daemon automatically appears in the UI.

Task Lifecycle

The daemon manages the reactive task lifecycle:

1. Client calls StartTask(task_id)
2. Daemon resolves the backend (task → project → global → claude-code)
3. Daemon creates git worktree for task
4. Daemon calls backend.InstallSystemPrompt (e.g. writes AGENTS.md into a per-session home)
5. Daemon spawns coding agent in sandboxed PTY (inside worktree)
6. Daemon streams screen buffer to subscribed clients
7. Agent updates task file when done (status: done)
8. Daemon detects via fsnotify OR polling fallback (5s)
9. Daemon kills agent (if still running)
10. Daemon calls backend.LocateTranscript and copies JSONL to logs
11. Daemon processes git rules (merge, delete worktree)
12. Daemon starts next task (if queued and merge succeeded)

Session Logging

The daemon captures two types of logs for each agent session, stored in ~/.watchfire/logs/<project_id>/:

FormatFilenameSource
PTY scrollback<task_number>-<session>-<timestamp>.logRaw terminal output captured during the session
JSONL transcript<task_number>-<session>-<timestamp>.jsonlStructured conversation transcript from the active backend (preferred)

Transcript Auto-Discovery

Transcript discovery is owned by each backend — the daemon calls backend.LocateTranscript on agent exit and copies (or synthesizes) the resulting JSONL into the logs directory:

BackendSource
Claude Code~/.claude/projects/<encoded-cwd>/<sessionId>.jsonl, matched by customTitle against the session name
Codex<CODEX_HOME>/sessions/**/rollout-*.jsonl in the per-session home
opencodePer-message JSON files under <OPENCODE_DATA_DIR>/storage/message/ collated into a synthesized transcript.jsonl
Gemini~/.gemini/tmp/<project_hash>/chats/session-*.jsonl (or legacy logs.json)
Copilot<COPILOT_HOME>/session-state/**/events.jsonl in the per-session home

Log Viewer

The ReadLog RPC serves session logs to clients (TUI and GUI). It prefers the .jsonl transcript when available and dispatches to the active backend's FormatTranscript to render a readable User/Assistant conversation with tool-call summaries. If no JSONL transcript exists, it falls back to the .log file (raw PTY scrollback). The same flow runs for every backend — no special-casing for Claude Code.

Phase Completion Signals

The daemon detects phase completion via signal files:

PhaseSignal FileDaemon Response
TaskTask YAML status: doneStop agent, merge worktree, start next
Refine.watchfire/refine_done.yamlStop agent, start next phase
Generate.watchfire/generate_done.yamlCheck for new tasks or end wildfire
Generate Definition.watchfire/definition_done.yamlStop agent (single-shot)
Generate Tasks.watchfire/tasks_done.yamlStop agent (single-shot)

System Tray

The tray menu (internal/daemon/tray/tray.go) is rebuilt dynamically as project state changes — instead of a flat list of running agents it groups projects into intent-driven sections:

SectionContent
Status header"Watchfire Daemon" + version + "Running on port: port"
Needs attentionProjects with at least one failed task or a TASK_FAILED event in the recent JSONL window — clicking jumps straight to that project's task list
WorkingProjects with an active agent — clicking opens the live Chat panel
IdleRegistered projects with nothing in flight — clicking opens the project view
Notifications (N today) ▸Submenu listing today's events from the JSONL fallback (~/.watchfire/logs/<project_id>/notifications.log), newest first. Each row routes back into the GUI for the originating task.
Open GUILaunches the Electron GUI
QuitShuts down the daemon

Click-through routing flows over the new DaemonService.SubscribeFocusEvents stream: when a tray entry is clicked the daemon publishes a focus event (project + task + tab), and any connected GUI/TUI subscribed to the stream brings the right view forward. The same mechanism is used by clicking an OS toast.

Desktop Notifications

The Beacon notification subsystem lives in the internal/daemon/notify package and exposes a typed notify.Bus that fans events out to every subscribed channel (slow consumers are dropped, never blocked). Every event carries a stable MakeID derived as sha256(kind|project_id|task_number|emitted_at_unix)[:8], so the same event can be deduplicated across the OS toast, the tray submenu, the focus router, and any future subscriber.

In addition to live in-process delivery, every event is appended as a JSONL line to ~/.watchfire/logs/<project_id>/notifications.log. This file is the headless fallback: clients that weren't connected when an event fired (or the dynamic tray submenu rebuilding from cold) read it back to reconstruct the recent timeline.

PlatformOS toast backend
macOSNative UNUserNotificationCenter via CGo (displays Watchfire icon)
Linuxgithub.com/gen2brain/beeep
OtherNo-op (logs to stderr)

On macOS, notifications are safely handled when running outside the .app bundle (e.g., during development or when launched directly). The daemon checks for app bundle availability before sending notifications to avoid crashes. This was fixed in v0.4.0 — previously, a notification outside the .app bundle could cause a daemon crash (exit code 2) due to an unhandled NSInternalInconsistencyException.

Event Kinds

KindWhen it firesOS toast title / body
TASK_FAILEDEmitted from internal/daemon/server/task_failed.go::emitTaskFailed whenever a task transitions to done && !success.Title <project> — task #NNNN failed; body is the task title plus the optional failure_reason.
RUN_COMPLETEFires on the falling edge of every autonomous run — single-task, run all, and wildfire — bounded by RunningAgent.RunStartedAt so a run is exactly the work between a start and the moment no agent is running for that project.Title names the project + run kind; body is N tasks done · M failed.
WEEKLY_DIGESTScheduled by digestRunner using a re-armable time.Timer driven by models.DigestSchedule.NextFire. The schedule is DST-stable and includes a 24-hour catch-up window so a digest missed during sleep/shutdown still fires once on next start. The Markdown report is rendered to ~/.watchfire/digests/<YYYY-MM-DD>.md and a toast points at it.Defaults off — opt in from settings.

Bundled Sounds

Beacon ships two short cues under assets/sounds/:

FilePlays for
task-done.wavRUN_COMPLETE
task-failed.wavTASK_FAILED

Both are mono 22050 Hz so the renderer can decode them with no extra dependencies. The shouldPlaySound(kind, prefs) helper decides whether a given event should play audio at all (master toggle, per-event toggle, sounds enabled, quiet hours, per-project mute). When the GUI renderer is foregrounded it plays the sound itself at the user-set volume — and in that case the OS toast is sent silent so users never hear the cue twice.

Settings & Gating

Notification preferences live under defaults.notifications in ~/.watchfire/settings.yaml (the same file used by the rest of the daemon defaults). Every send path funnels through the models.ShouldNotify helper, which combines the master toggle, per-event toggle, quiet-hours window, and per-project mute list before any toast, sound, or JSONL append happens. The schema is the single source of truth — both the GUI Settings panel and the TUI globalsettings tab read and write the same fields.

Health Check

The daemon exposes a Ping RPC — a lightweight health check that clients can use to verify the daemon is alive and accepting connections. Unlike GetStatus, which returns detailed daemon information, Ping is a simple empty-to-empty call designed for fast connection verification.

Startup Reliability

The daemon guarantees that ~/.watchfire/daemon.yaml (the connection discovery file) is only written after the gRPC server is fully ready and accepting connections. This eliminates race conditions where clients would read the file and attempt to connect before the port was open.

CLI and GUI both verify port readiness before proceeding after launching the daemon, so commands like watchfire agent start will not fail with "connection refused" errors even when starting the daemon for the first time.

Daemon Commands

# Start the daemon (no-op if already running)
watchfire daemon start

# Show daemon status
watchfire daemon status

# Stop the daemon
watchfire daemon stop

On this page