Skip to main content
Watchfire
Concepts
Main content

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:

FieldTypeDescription
task_numberintTask number this metric describes
project_idstringProject the task belongs to
agentstringBackend that ran the session (claude-code, codex, opencode, gemini, copilot, cursor)
duration_msintWall-clock duration of the session
tokens_inint (nullable)Prompt tokens consumed, nil when unavailable
tokens_outint (nullable)Completion tokens produced, nil when unavailable
cost_usdfloat (nullable)Estimated cost, nil when unavailable
exit_reasonenumOne of completed / failed / stopped / timeout
captured_attimestampWhen 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:

FieldTypeDescription
commitsintCommits on the task branch (git rev-list --count <merge-base>..watchfire/<n>)
files_changedintFiles touched, from the same diff stats the Inspect viewer and auto-PR body use
lines_addedintLines added across the task branch's diff
lines_removedintLines removed across the task branch's diff
net_linesintlines_added − lines_removed
mergedbooltrue when the local silent merge succeeds; false for auto-PR (push pending, not merged to the default branch)
merge_kindenumsilent 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:

FileBackendCoverage
claude_code.goClaude CodeDuration + tokens + cost
codex.goCodexDuration + tokens + cost
opencode.goopencodeDuration + tokens + cost
gemini.goGemini CLIDuration + tokens + cost
copilot.goGitHub Copilot CLIStub — duration only; tokens / cost stay nil because Copilot has no transcript schema yet
null.goFallbackDuration only, used when no backend parser matches (Cursor Agent sessions currently resolve here)
capture.goGoroutine that runs on handleTaskChanged and writes the file
parser.goShared 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 / −removed pair as a sub-line), Files touched, and Merge rate (merged / total, with a · N via PR hint)
  • 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 selector7d / 30d / 90d / All. The selection persists to localStorage[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_cost caveat — 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"
  • MetricsMissingCode caveat (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 scopeproject_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).

FormatConventions
MarkdownRendered from templates under internal/daemon/insights/templates/ (global.md.tmpl, project.md.tmpl, single_task.md.tmpl)
CSVSingle 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: code totals block, the # section: agents rows grow commits/lines columns, and the global # section: top_projects rows 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.

On this page