Insights & Metrics
Beacon captures per-task metrics, aggregates them per project and across the fleet, and exports CSV or Markdown reports. Inferno adds code-output analytics — commits, lines, and merges shipped per task.
Beacon (v4.0.0) turns every completed agent session into a structured metrics record, then aggregates those records into per-project and cross-project Insights views. Reports can be exported as CSV or Markdown, and a weekly digest summarises the fleet automatically. Inferno (v8.0.0) extends the same records with code-output analytics — what the agents actually shipped (commits, files, lines, merges), not just how many tasks closed.
Per-Task Metrics Capture
Every completed task gets a sibling <n>.metrics.yaml file next to its task YAML in .watchfire/tasks/. The file is written from a non-blocking goroutine inside handleTaskChanged, so capture never delays the task completion path.
The record carries a fixed set of fields:
| Field | Type | Description |
|---|---|---|
task_number | int | Task number this metric describes |
project_id | string | Project the task belongs to |
agent | string | Backend that ran the session (claude-code, codex, opencode, gemini, copilot, cursor) |
duration_ms | int | Wall-clock duration of the session |
tokens_in | int (nullable) | Prompt tokens consumed, nil when unavailable |
tokens_out | int (nullable) | Completion tokens produced, nil when unavailable |
cost_usd | float (nullable) | Estimated cost, nil when unavailable |
exit_reason | enum | One of completed / failed / stopped / timeout |
captured_at | timestamp | When the record was written |
Token and cost fields are pointers in the underlying TaskMetrics struct, so a backend that doesn't expose those numbers leaves them nil rather than emitting a zero that would skew rollups.
Code-Output Fields (v8 Inferno)
Inferno adds seven code-output fields to the same <n>.metrics.yaml sidecar, capturing what the agent shipped:
| Field | Type | Description |
|---|---|---|
commits | int | Commits on the task branch (git rev-list --count <merge-base>..watchfire/<n>) |
files_changed | int | Files touched, from the same diff stats the Inspect viewer and auto-PR body use |
lines_added | int | Lines added across the task branch's diff |
lines_removed | int | Lines removed across the task branch's diff |
net_lines | int | lines_added − lines_removed |
merged | bool | true when the local silent merge succeeds; false for auto-PR (push pending, not merged to the default branch) |
merge_kind | enum | silent or auto_pr — which of the two task-completion paths ran |
The task-done merge path (internal/daemon/agent/taskdone.go) snapshots these from the still-live watchfire/<n> branch before worktree cleanup — the snapshot must happen pre-merge, since the merge moves HEAD and would zero the commit count. The outcome fields are stamped on both completion paths, silent merge and GitHub auto-PR. Capture is best-effort: a git or diff error logs and records zeros rather than aborting the merge, and metrics.RecordCodeStats merges the code fields into the YAML under a mutex so it never clobbers the concurrent token capture.
Backward compatibility: metrics files written before v8.0 have no code fields and read back as zeros. Rollups count those tasks in a MetricsMissingCode honesty counter (mirroring tasks_missing_cost) so partial data never silently skews a total.
Metrics Package
Parsing lives in internal/daemon/metrics. Each backend has its own parser file alongside the shared capture goroutine:
| File | Backend | Coverage |
|---|---|---|
claude_code.go | Claude Code | Duration + tokens + cost |
codex.go | Codex | Duration + tokens + cost |
opencode.go | opencode | Duration + tokens + cost |
gemini.go | Gemini CLI | Duration + tokens + cost |
copilot.go | GitHub Copilot CLI | Stub — duration only; tokens / cost stay nil because Copilot has no transcript schema yet |
null.go | Fallback | Duration only, used when no backend parser matches (Cursor Agent sessions currently resolve here) |
capture.go | — | Goroutine that runs on handleTaskChanged and writes the file |
parser.go | — | Shared helpers across parsers |
Copilot is explicitly a stub. Until upstream exposes per-message token usage, Copilot rows in Insights show duration but contribute nothing to token or cost rollups. The tasks_missing_cost caveat surfaces this in the global rollup so partial-data projects don't silently flatten the chart.
Per-Project Insights View
internal/daemon/insights/project.go aggregates one project's <n>.metrics.yaml files into a window-scoped summary. The GUI surfaces this as a dedicated Insights tab on the Project View; the TUI binds the same overlay to i.
The view contains:
- KPI strip — totals for tasks, duration, tokens, and cost over the selected window
- Code KPI strip (v8) — Commits, Net lines (signed, with the
+added / −removedpair as a sub-line), Files touched, and Merge rate (merged / total, with a· N via PRhint) - Stacked-bar tasks-per-day — completed tasks per day, stacked by exit reason
- Code-churn-by-day chart (v8) — lines added stacked over lines removed per day, drawn next to the tasks-per-day bars
- Agent donut — share of tasks per backend
- Agent breakdown (v8) — the per-agent table gains Commits and Net columns, so output per agent is comparable, not just task count
- Duration histogram — distribution of task durations
- Time-window selector —
7d/30d/90d/All. The selection persists tolocalStorage[wf-insights-window]so it sticks across reloads <ExportPill>— header action that opens the export dialog scoped to the current project
The Project View Insights tab is documented in the GUI doc.
Cross-Project Insights Rollup
internal/daemon/insights/global.go aggregates the same metrics across every registered project, scoped to the same 7d / 30d / 90d / All windows. Results are cached at ~/.watchfire/insights-cache/_global.json so the dashboard renders without re-walking every project on each load.
Surfaces:
- GUI Dashboard rollup card — sits alongside the Beacon status bar at the top of the Dashboard. In v8 it gains a compact Commits / Net lines / Merge rate strip, a "Churn" top-projects row (ranked by net lines shipped, alongside the task-count "Top" row), and per-agent net-line figures in the agent legend. See the GUI Dashboard doc for the visual layout
- TUI fleet overlay — bound to Ctrl+f, mirrors the GUI rollup
- Top-projects pill list — names the projects driving fleet activity in the selected window
tasks_missing_costcaveat — banner that appears whenever a non-trivial slice of tasks is missing cost (typically Copilot sessions), so fleet-level cost numbers are never read as totals when they're really "what we could measure"MetricsMissingCodecaveat (v8) — every code surface is gated on real data and shows an honest "Code stats based on N of M tasks" caption; a fleet of pre-v8.0 tasks gets a quiet empty state instead of a wall of zeros
Report Export (CSV + Markdown)
Reports flow through a single RPC: InsightsService.ExportReport. The request carries a oneof scope — project_id, global, or single_task — and a format (CSV or MARKDOWN). The response carries filename, content (UTF-8 bytes), and mime (text/csv or text/markdown).
| Format | Conventions |
|---|---|
| Markdown | Rendered from templates under internal/daemon/insights/templates/ (global.md.tmpl, project.md.tmpl, single_task.md.tmpl) |
| CSV | Single file with # section: <name> header lines delimiting sub-tables (KPIs, per-day, per-agent, per-task) so multi-table content fits one CSV without losing structure |
Since v8, both formats carry code-output (header order stays stable — new CSV columns are appended):
- CSV — the per-task export gains the seven code columns (
commits,files_changed,lines_added,lines_removed,net_lines,merged,merge_kind); per-project and global exports gain a# section: codetotals block, the# section: agentsrows grow commits/lines columns, and the global# section: top_projectsrows grow commits/lines/net/merges - Markdown — reports gain a "## Code output" section (totals + per-agent churn columns; the global report also ranks top projects by churn)
A missing or malformed metrics file degrades to zeros and is counted in MetricsMissingCode, so partial or pre-v8.0 data never skews a total or breaks an export.
A single <ExportPill> component is reused on the Dashboard header and the Project View header. The TUI binds export to Ctrl+e with the same scope precedence as the GUI: the active Project View if one is selected, otherwise the dashboard (global). Single-task export is reachable from the task action menu and uses the single_task scope.
Weekly Digest
A weekly Markdown digest is rendered to ~/.watchfire/digests/<YYYY-MM-DD>.md regardless of toast suppression — even when notifications are muted, the file is still produced.
Since v8, the digest includes a "## Code output" block — commits, merges, +added / −removed lines (with net), and the week's top projects by net churn — so the weekly summary says what the fleet shipped, not only how many tasks closed.
The schedule is driven by digestRunner, which arms a re-armable time.Timer from models.DigestSchedule.NextFire. The runner is DST-stable and includes a 24-hour catch-up window so a daemon restart never silently skips a digest.
For the full notification story (toast routing, mute schedules, channel preferences) see the daemon notifications section.
Telegram Bridge
Supervise Watchfire from your phone — pair a Telegram bot with the daemon, pick a project, watch the agent conversation live, and just type to talk to a chat agent.
Daemon (watchfired)
The Watchfire daemon is the backend brain — managing projects, spawning agents, handling git workflows, and serving clients over gRPC.