---
name: monitor-ledger
description: "The dogfood monitor methodology + on-disk live ledger column contract + dual-write + token/cache estimation heuristic + evidence-based context-reuse observation rules. The on-disk ledger is the single source of truth the report is assembled from — recorded live, per step, never reconstructed."
see_also:
  - dogfood-testing
  - report-template
protocol: sp:dogfood-testing@1.2
---

# Monitor + Ledger

The ledger is the **single source of truth** for the report. It is recorded **live on disk** — one
row per step, written the moment the step resolves — never reconstructed from memory at the end.
Reconstruction produces fiction: it cannot honestly distinguish a step that passed first-try from
one that took three attempts, and it loses the per-step signal that drives testee refinement.

**Disk SSOT (protocol @1.2).** Working-memory-only ledgers are a contract violation. The ledger
lives in the dual artifacts (see [report-template.md](report-template.md) → Always-on dual
artifacts):

| File | Path |
| ------ | ------ |
| Live | `.spur/run/dogfood/<run_id>.md` |
| Report | `docs/dogfood/YYYY-MM-DD-<testee-slug>-dogfood.md` |

## The live-ledger rule

1. **Open both artifacts in Phase 1**, before the first step runs (frontmatter `status: running` +
   empty ledger table in each).
2. **Write a row the instant a step resolves** (pass, fixed, unresolved, or N/A) — not after the run.
3. **Dual-write every step — EXCEPT inside a proof window (task 0804 R2).** Append/update the row
   on the **live** file first; the live file is SSOT. While a pipeline proof window is open (from
   the first proof capture until the final proof-sensitive action, including done/provenance
   checks), the tracked report mirror stays **frozen**: append each observation to the live ledger
   only, and sync/validate the mirror after the window closes (or after abort). Tracked reports are
   proof-input fingerprint inputs — writing them mid-window churns the fingerprint and voids the
   proof. If the report write fails at finalize, recover by recreating the mirror from the valid
   live content and re-validate; missing live evidence cannot manufacture complete.
4. **The report reads the on-disk ledger, not your memory.** Every number in the report traces to a
   ledger row on disk. If it is not in the ledger file, it does not go in the report.
5. **Cardinality (@1.2).** The ledger's data-row count MUST equal the `**Steps:** N derived, N executed` declared
   in the report's §2 Execution Summary. N/A steps are not dropped — each gets its own row with
   `Outcome: N/A`. A count mismatch refuses `status: complete` at finalize (see
6. **R2 drift row (task 0296).** A ledger row tagged `drift:external` in the `Step` column documents
   workspace drift detected during the run — files changed by an external writer that neither the
   driver nor testee ledger rows name. The row carries `Outcome: drift`, `Fix Applied: <drifted paths>`,
   `Finding: P2 — workspace drift detected during run; attribution to external writer`,
   and `Basis: <fingerprint diff>`. A drift row never changes a step's outcome and never results in
   a `FIXED` / `PASS` outcome — it is purely documentary. Cache columns carry `—` (not estimated).
   See [SKILL.md §Workspace-drift guard](../SKILL.md#workspace-drift-guard-r2--task-0296).

### Worktree advisory — planning-time surfacing (task 0804 R5)

The worktree advisory (SKILL.md §Worktree advisory) is surfaced at planning time, not only when
drift is detected. Phase 1 prints it whenever the **driver or the testee may mutate** and the tree
is dirty (`git status --porcelain` non-empty) **or** the testee itself drives a pipeline —
including observe-only driver mode over a mutating testee. The advisory is **not a hard gate** (the
refuse-gate semantics from task 0293 are unchanged); the isolated checkout is simply the preferred
setup for mutating dogfoods because no concurrent writer can reach it, so attribution stays clean
— which is why the §Mutating `--fix` mode contract recommends it for those two refuse-gate cases.
Dirtiness alone is not proof of a concurrent writer and must not be reported as one; known
concurrent writes still follow the project's one-writer rule. A clean, read-only run adds no
warning.

### Fast-run exemption (task 0294 R6a)

The per-step live-write mandate (rules 1–4) exists to bound information loss when a mid-run crash
terminates the driver before finalize. That risk is real for long runs (multi-step pipelines,
mutating testees); it is **marginal for fast runs** where wall-clock is short enough that an
operator would naturally watch the run to completion.

**Codified exemption.** A run with **total wall-clock < 3 minutes** MAY batch-write all ledger
rows at finalize, provided **both** of the following hold:

1. The report's §2 Execution Summary carries an explicit note:
   `Ledger write mode: batch-finalize (fast-run exemption, total wall-clock < 3 min)`.
2. The driver is still prepared to reconstruct per-step ordering honestly — batched does not mean
   fictional. If the driver cannot reconstruct attempts/outcomes per step from its own tool-call
   history, the exemption does NOT apply and the strict per-step rule is back in force (rule 4:
   "if it is not in the ledger file, it does not go in the report").

This matches how the 0280 fast-run actually behaved (batch-finalize, still validated `complete`)
and preserves the strict mandate for long runs where mid-run crash loss is the real risk. A run
≥ 3 min that batch-writes is a **protocol violation the driver must self-report** as a P3 finding
in the report's §6 Findings (no exemption applies).

## Column contract

```
| Step | Attempts | Outcome | Fix Applied | Finding | Fresh Tokens | Cached Tokens | Cache % | Basis | Wall-clock |
```

| Column | Meaning |
| -------- | --------- |
| `Step` | The derived step label (Phase 1) or `N` for a single-step testee. |
| `Attempts` | How many times the step was run (1 = first-try; >1 = retried under the fix budget). |
| `Outcome` | `PASS` / `FIXED` / `UNRESOLVED` / `N/A`. (`FIXED` = failed then passed within budget.) |
| `Fix Applied` | `file:line` + one-line summary, or `—`. |
| `Finding` | One-line finding surfaced at this step, or `—`. A finding does **not** change `Outcome`. |
| `Fresh Tokens` | Estimated fresh context for the step. Prefix with `~`. |
| `Cached Tokens` | Estimated reused context for the step. Prefix with `~`. |
| `Cache %` | Estimated context-reuse share: `Cached Tokens / (Fresh Tokens + Cached Tokens)`, rounded to the nearest whole percent. This is a **heuristic estimate** of how much context the step reused — it is not observed provider cache behavior (task 0913, R3). An `~unknown` row carries `—`, never `0%` — unknown basis is not an observed zero. |
| `Basis` | Observable basis for the estimate: command output, prior file read reused, generated text, etc. |
| `Wall-clock` | Elapsed time for the step. |

## Token + context-reuse estimation heuristic

A skill **cannot read its own exact token meter** — derive an estimate and label every number
`~estimate`. These chars/4 figures estimate **context volume and reuse within the driver's own
session**; they do not measure provider cache hits, cache cost, or realized savings (task 0913,
R3). The accepted methodology is deterministic from the ledger rows:

1. Estimate **Fresh Tokens** from new material consumed or produced by the step:
   - text read from files or command output: `ceil(characters / 4)`, rounded to the nearest 100;
   - generated prose/code/report text: `ceil(characters / 4)`, rounded to the nearest 100;
   - short command/control overhead: add `~100` per tool invocation that produced non-empty output.
2. Estimate **Cached Tokens** only for material already present in the current session and actively
   reused by reference in this step. Use the same `ceil(characters / 4)` basis and round to the
   nearest 100. Do not count fresh command output, newly read files, or regenerated scaffolding as
   cached.
3. Compute each row: `Cache % = round(Cached Tokens / (Fresh Tokens + Cached Tokens) * 100)`. A row
   whose basis is unknown carries `—`, never `0%`.
4. Compute the report aggregate from **observable rows only** — `~unknown` rows are excluded from
   both sums (or surfaced as a separate unknown bucket), never folded in as `Cached ~0`:
   `aggregate cache% = round(sum(Cached Tokens) / sum(Fresh Tokens + Cached Tokens) * 100)`.

The **trend across runs** is the signal, not the absolute value: a falling estimated reuse share
suggests context bloat creeping in; a rising one, efficient reference reuse. Trend claims from
estimates stay labeled as such — they never become causal waste findings on their own.

> Never print a precise token number you cannot substantiate. The numbers exist to show a *trend*,
> not to bill anyone.

### Multi-source Cost block (report §2)

Ledger estimates alone are **confidence: LOW**. When assembling the report Cost block
([report-template.md](report-template.md) §2):

| Source | When to use | Confidence | Scope label |
| -------- | ------------- | ------------ | ------------- |
| Ledger `chars/4` heuristic | Always | LOW | per-step trend |
| `ccusage` daily/session | If CLI available and returns data | MEDIUM | day or session — **not** per-step |
| Agent usage fields in tool results | If present (never invent) | MEDIUM | as reported by the tool |

If no external meter is available, print `Meter: n/a`. Never merge a day-level meter into a
per-step ledger cell as if it were measured per step.

### Chained-step rows (implement-heavy derived steps)

When a derived step is implement-heavy (it runs a pipeline leg, writes code, or otherwise mutates more
than its own arguments), its row is tagged `chained:<step>` in the Step column and its Fresh/Cached
columns reflect the **chained leg's** cost, not the driver's. The driver's monitoring cost for that
step stays on the driver's own row.

- Observable chained usage (subagent output in driver context, or the operator explicitly provided
  the artifact) → estimate Fresh/Cached from that output normally.
- Unobservable chained usage (subagent ran in a different session, usage data never surfaced) →
  label Fresh `~unknown`, Cached `~unknown`, Basis `chained-leg usage not observable from driver`,
  and **exclude the row from the aggregate cache%** (or surface it as a separate unknown bucket) —
  unknown cache use is not an observed zero. **MUST** emit a P3 finding: `P3 — chained-step cost
  not observable` (task 0278 R3). Do not invent totals.

Never fold a chained row into the driver's row; the whole point of dogfooding a pipeline-driving
testee is to see the testee's own cost separately from the driver's monitoring cost. See
[SKILL.md §Cost segmentation for implement-heavy steps](../SKILL.md#cost-segmentation-for-implement-heavy-steps).

## Anti-fiction rule

Never reuse a convenient cache percentage such as `45%` because it "feels right." A cache percentage
is valid only when it can be recomputed from the ledger row sums of observable rows. If the basis is
missing, mark the row `~unknown`, exclude it from the aggregate (or surface it as a separate unknown
bucket), and explain the missing basis — never fold it in as `Cached Tokens = ~0`: unknown cache use
is not an observed zero-percent hit rate, and a low-cache diagnosis needs observed data.

## Context-reuse observation rule (task 0913, R3 — replaces the fixed-threshold cache-health rule)

The estimated reuse share is a **heuristic** (chars/4 over the driver's own session); it cannot
prove provider cache behavior, unnecessary re-reading, or realized savings. Threshold crossings
(40%, 50%, or any other fixed line) therefore **never auto-generate a causal waste finding**.
Instead:

- **Estimated-only evidence** (chars/4 ledger, no external meter): a low estimated reuse share may
  be reported only as a **labeled hypothesis** finding carrying the `[unverifiable]` tag, naming
  the confirmation it needs (per-step provider telemetry, a real meter, or a measured reread
  trace). Example shape: `P3 — estimated reuse share 36%, below the run's own trend — hypothesis:
  repeated task-list refetching; confirm with per-step metering` `[unverifiable]`.
- **Measured evidence** (ccusage session/day delta, or agent usage fields in tool results): report
  the measured figures with their scope label and confidence, and keep them out of per-step ledger
  cells. A waste finding may then name the measured evidence — still without claiming causality
  the meter does not show.
- **Unavailable evidence**: print `Meter: n/a` and totals `n/a` rather than a fabricated share;
  route the missing-measurement gap to its owner as an improvement proposal.

Unobservable chained-step cost keeps its **mandatory** P3 finding (`P3 — chained-step cost not
observable`, task 0278 R3) — that rule reports missing evidence, not causal waste, and stays.

These findings feed the report's §6 Findings (see [report-template.md](report-template.md)).

## Pipeline-run provenance observations (task 0913, R5)

When the testee drives a task pipeline, the driver may adopt the supported findings from the
0912 workflow baseline (`docs/reports/i31/0912-workflow-baseline.md`) as bounded observations —
with artifact anchors and owner handoffs, never as invented performance conclusions:

- **Run-row closure / structured emission (supported, pilot-selected):** if the observed pipeline
  run leaves a non-terminal run row or emits no `action_runs` rows, record the observation with
  the run id as anchor and hand off to the owners named in the baseline (driver adoption: D62;
  row-closure defect: P). These are the baseline's pilot-eligible findings (F1/F2).
- **Performance conclusions (INSUFFICIENT_EVIDENCE in the baseline):** no token/USD, percentage,
  speedup, or fleet claim may be derived from a dogfood run. Record the limitation explicitly
  (what was unmeasured and why) and route the missing evidence to the baseline's named gaps
  (session/cost joins E6; scoped-gate experiment F3/F4; fleet serve session S5).
- Historical comparison, recurrence, and cohort trends stay with history-anatomy and existing
  doctor tooling — the dogfood report owns only what its own ledger observed.

## Cache-conservation discipline (how to keep cache% high)

The context-reuse observation rule above interprets the signal; this section is the mitigation. The
dogfooding driver
(the agent running Phase 2/3) controls most of the cache% it later reports — low cache% is usually
the driver re-fetching data it already holds. Apply these while monitoring each step:

### Driver cache checklist (task 0278 R7)

When the estimated reuse share looks low (or trends down across runs), apply this checklist
**before** re-reading:

| # | Action | Why |
| --- | -------- | ----- |
| 1 | Reuse the Step-1 `spur task show --json` capture for the rest of the run | Avoids re-tokenizing the full task body |
| 2 | Do not re-Read SKILL.md / report-template after Phase 1 loaded them | Skill body is large; keep one copy in context |
| 3 | Prefer `--json` CLI over re-parsing freeform prose | Smaller, stable payloads |
| 4 | Dual-write ledger rows without re-reading the whole report each step | Append/patch; don't full-file re-load |
| 5 | Skip redundant `bun test` full suite between steps when a focused file suite already green | Run the broad suite once at the end |
| 6 | For batch testees (`verifyall` / `runall` / `refineall`): freeze `task list --json` once at resolve | Re-listing the set per task is the #1 low-reuse-estimate pattern on feature dogfoods |
| 7 | On re-verify of done tasks: re-read only cited `file:line` anchors, not full Solution blobs | Anchor-first re-verify keeps the reuse estimate honest and high |

1. **Reuse CLI output already in context.** If a prior step (or a prior tool call this step)
   captured `spur task show`/`check`/`list` output, do **not** re-invoke the same command for that
   data — reference the prior result. Re-invocation is the #1 cause of low estimated-reuse steps.
   Only re-fetch
   when the underlying state *changed* (e.g. you just wrote a section and need the new
   `requiredSections`).
2. **Don't re-ground shared scaffolding per step.** Command docs, the skill preamble, and the
   testee's own argument-hint are loaded once into your context — they do not need to be re-read or
   re-quoted for each step. Re-sending unchanged preamble registers as fresh tokens, not cached.
3. **Prefer `--json` + targeted fields over full human output.** When you must fetch, ask for the
   smallest shape that answers the question (`--json` and read one field), not the full
   human-formatted dump.
4. **Estimate `~cached` honestly against this discipline.** If *you* re-read a file or re-sent
   scaffolding this step, that portion is **not** cached — mark cache% down. The estimate is only
   useful as a trend if it reflects what actually happened.

The point is not to game the number — it is to drive the testee (and your own monitoring) toward
reusing context, which is the context-efficiency gain the estimated reuse share stands for. The
share itself remains a heuristic trend signal (task 0913, R3), not a measured saving.

## Worked ledger example

```
| Step | Attempts | Outcome | Fix Applied | Finding | Fresh Tokens | Cached Tokens | Cache % | Basis | Wall-clock |
|------|----------|---------|-------------|---------|--------------|---------------|---------|-------|------------|
| 1 resolve | 1 | PASS | — | — | ~600 | ~400 | 40% | task JSON output + prior command docs reused | ~3s |
| 2 analyze | 1 | PASS | — | over-specified for refine | ~1100 | ~700 | 39% | task file read + prior task summary reused | ~5s |
| 3 synthesize | 2 | FIXED | spur-dev/SKILL.md:88 thread --agent | — | ~1500 | ~600 | 29% | edit diff + prior plan reused | ~8s |
| 4 profile | 1 | PASS | — | — | ~500 | ~350 | 41% | command output + prior profile reused | ~2s |
```

Aggregate: total = `3700 + 2050 = 5750`; cached = `2050`; estimated reuse share =
`round(2050 / 5750 * 100) = 36%` `[~estimate]`. A low estimated reuse share is reported only as a
labeled-hypothesis `[unverifiable]` finding (or backed by a real meter) — never as an automatic
causal waste claim (task 0913, R3).
