lerp Docs Why lerp latest

Telemetry

At run exit, lerp appends one JSON line to a local telemetry file — a structured record of the finished run’s duration, token usage, cost, and outcome.

Design stance

Telemetry is history, not state (SCOPE.md invariant 1). Four principles:

  • Written once at run exit by deterministic code. The settling lane’s goroutine totals token usage from the stream log, notes exit timing and exit code, and appends the line before freeing the lane.
  • Read by nothing in lerp. The loop and the TUI do not query, parse, or depend on it. Losing the file costs a chart, never a ticket.
  • Never posted to Linear. Linear receives stage-boundary decisions — plans, pull requests, verdicts. Process measurements belong to local history (SCOPE.md invariant 7), never on a ticket.
  • Never trusts agent prose. Measurements come from config, the loop’s own evidence records, and deterministic decoders of the stream logs — never from agents reporting their own spend.

Where the file lives

$XDG_STATE_HOME/lerp/runs.jsonl

With XDG_STATE_HOME unset, ~/.local/state/lerp/runs.jsonl, on both macOS and Linux.

The file and its parent directories are created on the first finished run. Writes are serialized across lanes with a process mutex and use append mode (O_APPEND), so multiple lerp processes on different repositories can append concurrently without tearing lines.

The line format

The format is a stable interface: changes are additive only, and keys are never renamed or repurposed.

Fields a runner or settlement path could not supply are omitted (omitempty) rather than zero-faked: a command-template runner naming no vendor omits vendor, and a killed run with no exit file omits exit_code and duration_ms.

Field Type Description
at string ISO 8601 UTC timestamp when the run finished or was reaped (e.g. "2026-08-27T10:04:11Z"). Always present.
repo string Absolute path to the repository directory (e.g. "/Users/matt/src/donewell/lerp"). Always present.
team string Linear team key prefix from the ticket (e.g. "LERP"). Always present.
ticket string Linear ticket identifier (e.g. "LERP-138"). Always present.
queue string Name of the queue that ran the ticket (e.g. "implement"). Always present.
runner string Configured runner name from lerp.toml (e.g. "claude"). Always present.
vendor string Built-in vendor adapter name ("claude", "codex", "antigravity"), when configured with vendor.
model string Model identifier reported by the stream summary or configured on the runner (e.g. "claude-opus-4-6").
session string Session ID, conversation ID, or thread ID for the run.
duration_ms integer Total run duration in milliseconds, read from the exit file timestamp minus started time.
tokens integer Total token usage summed across all tool calls in the run’s stream log.
cost_usd number Total cost in USD reported by the runner’s stream.
exit_code integer Process exit code (0 for clean exit, non-zero for failure). Omitted if the run was killed before recording an exit file.
status string Linear status where the ticket came to rest at run exit (e.g. "In Review", "Needs Attention").

Example line

{"at":"2026-08-27T10:04:11Z","repo":"/Users/matt/src/donewell/lerp","team":"LERP","ticket":"LERP-138","queue":"implement","runner":"claude","vendor":"claude","model":"claude-opus-4-6","session":"7420e6f8","duration_ms":742318,"tokens":1284310,"cost_usd":3.71,"exit_code":0,"status":"In Review"}

Querying with jq

The file is standard JSON Lines, so jq is the dashboard.

Total cost per ticket

jq -s 'group_by(.ticket)[] | {
  ticket: .[0].ticket,
  cost_usd: (map(.cost_usd // 0) | add | (.*100 | round)/100),
  runs: length
}' ~/.local/state/lerp/runs.jsonl

Tokens per queue over the past 7 days

jq --arg since "$(date -u -v-7d +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date -u -d '7 days ago' +%Y-%m-%dT%H:%M:%SZ)" \
   -s '[.[] | select(.at >= $since)] | group_by(.queue)[] | {
  queue: .[0].queue,
  total_tokens: (map(.tokens // 0) | add),
  total_cost_usd: (map(.cost_usd // 0) | add | (.*100 | round)/100),
  runs: length
}' ~/.local/state/lerp/runs.jsonl

Spend and runs by model

jq -s 'group_by(.model // "unspecified")[] | {
  model: .[0].model // "unspecified",
  total_cost_usd: (map(.cost_usd // 0) | add | (.*100 | round)/100),
  total_tokens: (map(.tokens // 0) | add),
  runs: length
}' ~/.local/state/lerp/runs.jsonl

Clean exits by queue

jq -s 'group_by(.queue)[] | {
  queue: .[0].queue,
  clean_exits: (map(select(.exit_code == 0)) | length),
  total_runs: length
}' ~/.local/state/lerp/runs.jsonl