Architecture
Understand Watchfire's architecture — a daemon orchestrates coding agents while thin clients connect over gRPC.
Architecture
Watchfire follows a daemon + thin client architecture. A single daemon process (watchfired) manages all orchestration, while lightweight clients (CLI/TUI and GUI) connect over gRPC to display state and accept user input.
Components
| Component | Binary | Role | Tech |
|---|---|---|---|
| Daemon | watchfired | Orchestration, PTY management, terminal emulation, git workflows, gRPC server, system tray, desktop notifications | Go |
| CLI/TUI | watchfire | CLI commands + TUI mode. Project-scoped thin client | Go + Bubbletea |
| GUI | Watchfire.app | Multi-project thin client (shows all projects) | Electron |
Data Flow
The daemon is the single source of truth. Clients are stateless views that subscribe to updates.
Pluggable Agent Backends
Every agent-specific detail — which binary to launch, how to assemble the command line, how to deliver the system prompt, where its transcripts live, and what the sandbox must allow for it — is encapsulated in a Backend implementation. Backends register themselves with a process-wide registry at startup, and the daemon looks them up by name when spawning a session.
| Aspect | Behavior |
|---|---|
| Interface | Backend in internal/daemon/agent/backend/ — one struct per agent |
| Registry | Process-wide Register/Get/List keyed by name (claude-code, codex, opencode, gemini, copilot) |
| Shipped backends | Claude Code, OpenAI Codex, opencode, Gemini CLI, GitHub Copilot CLI |
| Command construction | Backends own BuildCommand — binary path, args, env, whether the initial prompt is embedded or pasted after launch |
| System prompt delivery | Watchfire composes one canonical prompt; each backend's InstallSystemPrompt delivers it (CLI flag for Claude Code, AGENTS.md for Codex/opencode/Copilot, system.md for Gemini) |
| Transcript discovery | Backends own LocateTranscript and FormatTranscript — the daemon copies and renders whatever the backend returns |
| Sandbox extras | SandboxExtras() contributes writable paths, cache patterns, and env vars to strip; the sandbox layer merges these with the base policy |
Agent Resolution Chain
When the daemon spawns a session, it resolves the backend through a four-step chain:
task.agent → project.default_agent → settings.defaults.default_agent → claude-code
Empty string at any level defers to the next. Chat, wildfire-refine, and wildfire-generate sessions aren't scoped to a single task, so they skip the task step and start from the project default.
Per-Session Homes
Four of the five backends get an isolated per-session home so the Watchfire system prompt never contaminates the user's real configuration, while auth and global settings continue to flow from the shared location:
| Backend | Isolation | Location | What's isolated | What's reused |
|---|---|---|---|---|
| Claude Code | None needed | — | — | --append-system-prompt flag delivers the prompt; user's ~/.claude/ is used directly |
| Codex | CODEX_HOME | ~/.watchfire/codex-home/<session>/ | AGENTS.md, session transcripts | Auth + config from real ~/.codex/ |
| opencode | OPENCODE_CONFIG_DIR + OPENCODE_DATA_DIR | ~/.watchfire/opencode-home/<session>/ | AGENTS.md, permission config, per-message JSON | Auth/providers/agents symlinked from ~/.config/opencode/ |
| Gemini | GEMINI_SYSTEM_MD | ~/.watchfire/gemini-home/<session>/system.md | Watchfire system prompt | Auth, settings, hierarchical GEMINI.md from ~/.gemini/ |
| GitHub Copilot CLI | COPILOT_HOME + COPILOT_CUSTOM_INSTRUCTIONS_DIRS | ~/.watchfire/copilot-home/<session>/ | AGENTS.md, per-session state | GitHub login, MCP config, and session history symlinked from ~/.copilot/ |
Adding a Backend
A new backend is one file in internal/daemon/agent/backend/<name>.go — implement the Backend interface, register in init(), and contribute sandbox extras. Chat, task, start-all, and wildfire modes work against the registry generically, so no wiring changes are needed in the manager, sandbox, or UX surfaces.
PTY and Terminal Emulation
Watchfire uses a real PTY (pseudo-terminal) to run coding agents, with terminal emulation to parse escape codes:
The screen buffer is a 2D grid of cells, each with character, foreground/background color, and style attributes (bold, italic, underline, inverse). The cursor position is also tracked.
Network
| Aspect | Decision |
|---|---|
| Protocol | gRPC + gRPC-Web (multiplexed on same port) |
| Port | Dynamic allocation (OS assigns free port) |
| Discovery | Connection info written to ~/.watchfire/daemon.yaml (only after the gRPC port is accepting connections) |
| Health check | Ping RPC — lightweight empty-to-empty call for connection verification |
| Clients | CLI/TUI use native gRPC, GUI uses gRPC-Web |
File Watching
The daemon uses fsnotify to watch for changes to task files:
| Aspect | Behavior |
|---|---|
| Mechanism | fsnotify with debouncing |
| Global watched | ~/.watchfire/projects.yaml |
| Per-project watched | .watchfire/project.yaml, .watchfire/tasks/*.yaml |
| Robustness | Handles create-then-rename pattern (common with AI tools) |
| Re-watch on chain | When agents chain (wildfire/start-all), re-watches to pick up new directories |
| Polling fallback | Task-mode agents poll task YAML every 5s as safety net |
| Reaction | File changes trigger real-time updates to connected clients |
Crash Recovery
| Scenario | Behavior |
|---|---|
| Daemon crashes mid-task | On restart, user must manually restart task |
| Agent crashes | Daemon detects PTY exit, stops task |
JSONL Transcript Logging
Session logs capture the agent's structured JSONL transcript in addition to raw PTY scrollback. This provides a clean, formatted conversation history for reviewing what an agent did during a session, across all supported backends — not just Claude Code.
Transcript discovery: On agent exit, the daemon calls the active backend's LocateTranscript to find the session's JSONL file:
| Backend | Source location |
|---|---|
| Claude Code | ~/.claude/projects/<encoded-cwd>/<sessionId>.jsonl matched by customTitle |
| Codex | <CODEX_HOME>/sessions/**/rollout-*.jsonl (per-session home) |
| opencode | Per-message JSON under <OPENCODE_DATA_DIR>/storage/message/ collated into a synthesized transcript.jsonl |
| Gemini | ~/.gemini/tmp/<project_hash>/chats/session-*.jsonl (or legacy logs.json) |
The backend copies or synthesizes a JSONL file into the Watchfire logs directory.
Log viewing: The ReadLog RPC prefers the .jsonl transcript and dispatches to the backend's FormatTranscript to render it as a readable User/Assistant conversation with tool-call summaries. If no JSONL transcript is available, it falls back to the .log PTY scrollback.
Both file types are stored side-by-side in ~/.watchfire/logs/<project_id>/ — see the directory structure below for details.
Restart Protection
Wildfire and start-all modes automatically stop chaining after repeated failures on the same task to prevent infinite loops caused by rate limits, crashes, or auth expiry.
| Aspect | Behavior |
|---|---|
| Trigger | Same task restarted 3+ times consecutively without reaching status: done |
| Action | Stop wildfire/start-all chaining, start chat-mode agent instead |
| Counter | Per-project in-memory map (reset on task progression) |
| Reset | Counter resets when a different task is chained (successful progression) or agent is stopped by user |
| Logging | Warning logged with task number and restart count when limit reached |
Directory Structures
Global (~/.watchfire/)
~/.watchfire/
├── daemon.yaml # Connection info (host, port, PID)
├── agents.yaml # Running agents state
├── projects.yaml # Projects index
├── settings.yaml # Global settings
├── installation_id # Stable UUID for analytics
└── logs/ # Session logs
└── <project_id>/
├── <task_number>-<session>-<timestamp>.log # PTY scrollback (fallback)
└── <task_number>-<session>-<timestamp>.jsonl # Agent JSONL transcript (preferred)
Log filename examples:
0001-1-2026-02-03T13-05-00.log— task 1, session 1 (PTY scrollback)0001-1-2026-02-03T13-05-00.jsonl— task 1, session 1 (agent JSONL transcript)chat-1-2026-02-03T15-00-00.log— chat mode (no task)
Per-backend session homes live alongside logs/:
~/.watchfire/
├── codex-home/<session>/ # Per-session CODEX_HOME (Codex)
├── opencode-home/<session>/ # Per-session config + data dirs (opencode)
└── gemini-home/<session>/ # Per-session system.md (Gemini)
Per-Project (<project>/.watchfire/)
<project>/
├── .watchfire/
│ ├── project.yaml # Project configuration
│ ├── tasks/ # Task YAML files
│ │ ├── 0001.yaml
│ │ ├── 0002.yaml
│ │ └── ...
│ ├── memory.md # Persistent project knowledge across agent sessions
│ ├── secrets/ # Secrets and setup instructions
│ │ └── instructions.md
│ └── worktrees/ # Git worktrees (one per active task)
│ └── <task_number>/
└── <project files>