Ctrl K
ComunidadGuías…

BACKLOG.md

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
# Backlog

The pool of concrete candidate work items behind [ROADMAP.md](ROADMAP.md). **Nothing here
is a commitment or a schedule.** The roadmap owns product order and exit criteria; this
file preserves implementation evidence and stable ids. Real feedback from people running
`assaio` can reshape, reorder or drop any item.

**How this file works**

- One item = one checkbox with a stable id (`B01`) — reference it in issues, PRs, and
  commit bodies. Ids are never reused.
- When an item ships, in the same PR: add a user-facing entry to `CHANGELOG.md` under
  `[Unreleased]`, add or update its row in [FEATURES.md](FEATURES.md), and delete the
  line here. If only part ships, split the item. The three files together are the
  lifecycle: BACKLOG (candidates) → CHANGELOG (per-release delta) → FEATURES (current
  state, with the release each capability arrived in).
- Every item inherits the honesty rules ([CONTRIBUTING.md](CONTRIBUTING.md)):
  directional framing, `—` over a fabricated number, aggregate and pseudonymized by
  default, never a leaderboard. The refusals at the bottom hold no matter what ships.
- Effort: **S**mall / **M**edium / **L**arge. Scope: **solo** / **team** / **both**.
- Want to pick one up? Comment on or open an issue first so the approach is agreed
  before the work — connectors additionally follow the
  [connector intake flow](docs/extending/data-source.md#the-intake-path-open-a-connector-issue-first).

## Shipped — "Trust the dataset" and "Intent"

Two promises are closed and their items deleted per the lifecycle rule above. Kept here as
one line so a reader knows why the numbering starts where it does: format-drift canaries,
`doctor --strict`, granularity provenance, the confidence envelope on every result, `init`,
the source-depth matrix (`B58`, `B59`, `B69`, `B81`, `B82`, `B83`, all v0.5), and session
annotations with per-kind-of-work stratification (`B80`, v0.6) alongside the GitHub Copilot
CLI parser (`B53`, v0.6). See [CHANGELOG.md](CHANGELOG.md) and [FEATURES.md](FEATURES.md).

## Shipped — "Correctness lockdown"

Nothing new is built on top of a surface that is known to be wrong. Every item here is a
defect in something already shipped, each with the reproduction that found it, and they are
listed in the pool below. They close first. The reason is v0.12: the flagship parser was 2x
wrong for eleven releases and every honesty guard this project owns said it was fine.

The eight that carried a wrong number or destroyed data closed in v0.13 (`B119`–`B123`,
`B127`, `B131`, `B132`). The remaining ten closed in v0.14 (`B124`–`B126`, `B128`–`B130`,
`B133`–`B136`), together with four defects the same review found that had no id yet: a
repeated Claude transcript line counted twice, a superseded sub-agent aggregate left in the
store, a team-server push that could never correct a partial figure, and an activity
correction that could not reach a stored row at all. **The milestone is closed** — what a
later review finds opens a new pool below rather than reopening this one.

## Alongside everything — "Somebody outside this repository"

The external-validation half of [roadmap milestone 1](ROADMAP.md#1-trust-and-activation), and
the only milestone in this file whose work is not this repository's to do. Two sources and the
reconciler are calibrated against a sample written in the source's own shape rather than a real
one, and no amount of local work changes that: the missing input is a file somebody else has.
`B144` is the half that already had an id; captures now arrive through the Capture contribution
issue template. Below are the half that never had an id and the reason a silence here would mean
anything.

- [ ] **B192 · the reconciler has never read a real export** — S · both — `B19` shipped the
  reader and the arithmetic against a **constructed** CSV whose column names and magnitudes were
  invented ([`internal/reconcile/testdata/README.md`](internal/reconcile/testdata/README.md)),
  which is why it binds columns by header alias and prints what it bound instead of shipping a
  named vendor profile. The alias list is therefore a guess about every vendor. One redacted
  export — date, model, token count, amount, currency, and nothing else — turns that sample
  `real` and settles the aliases for the vendor it came from. It has been a trailing clause on
  `B144` with no id of its own, which left a v1.0 condition tracked under an item about two
  other sources.

- [ ] **B193 · the ask has to reach somebody who has not already found us** — S · solo —
  publishing the call in [ROADMAP.md](ROADMAP.md) and `README.md` reaches people who are already
  here, and that is by definition the population that has not appeared. Take it once to where
  the users of the two uncalibrated tools are — those tools' own issue trackers and community
  channels, and the places the 2026-08-11 launch post already reached — and record, per channel,
  what was asked and what came back. That record is what makes milestone 1's validation gate
  readable: "nobody contributed" decides nothing unless somebody was asked, and a silence with
  no attempt behind it is evidence about neither the ask nor the reach.

## Now — "Calibrated measurement"

The guard the v0.12 class of bug needs, which provenance and coverage cannot provide. `B137`,
the conservation and metamorphic suite, shipped in v0.14 as `internal/calibration`, and `B19`,
the offline reconciliation against a vendor's own export, shipped after it — and `B139`, the
price table nothing watched, which was the largest error left in the `$` figure. All three are in
[CHANGELOG.md](CHANGELOG.md). One item remains and it cannot be closed here at all: `B144` needs
a redacted real capture, the same contribution `B19`'s column aliases need — which now carries
its own id (`B192`) and a milestone to arrive through, above.

- [ ] **B144 · calibrate Gemini CLI and Cline against a real capture** — S · both — both are
  calibrated today against a *constructed* sample in the source's shape, because the maintainer's
  machine holds neither a Gemini chat log carrying token counts nor a Cline install. A
  constructed trace proves the reading; only a real one also proves the shape is still what the
  vendor writes, and each trace already declares which it is (`capture: real|constructed`). Needs
  one redacted capture per source from anybody who runs them.

## After the agent skill — "Reusable contracts"

Proposed by the 2026-09-26 framework audit. Users can already write exec parsers, metrics and
rules and script over `report` and `analyze` JSON; a fuller analytical contract that outside
programs can rely on remains untested. [ROADMAP.md](ROADMAP.md#2b-a-public-analytical-export)
owns the order: the evidence graph (repository identity shipped in v0.35.0) and the agent skill,
then a small export, then typed plugin states and richer parser counters.

- [ ] **B211 · a versioned analytical snapshot** — M · both — a deterministic NDJSON export: a
  manifest (window end, grain, build, price-table date), then allow-listed usage and session
  records and, where stored, bounded step sequences. Each record states its source capability,
  cost basis including every cache tier, price and parser provenance, correction state and
  repository-identity status; the manifest counts rejected, unpriced and truncated input. An
  unknown price or an absent counter stays `null`, never `0`. Today's `report` rows do not meet
  that: `cache_write` exports 0 for Codex and Gemini CLI, which do not record it, and
  `reasoning` exports 0 for Claude Code and Cline, which do not record it. A CLI consumer
  fixture reproduces the eligible totals, including after an older row is corrected. Never
  exported: full ingest paths, secrets, private tables. Backup and incremental sync are separate
  contracts; an incremental export needs correction and deletion semantics, not a timestamp
  cursor. Today's aggregate outputs do not yet meet the v1 export guarantee in
  [compatibility](docs/compatibility.md#not-frozen-and-guaranteed-instead); this is the path to
  it. `B06` and `B99` then publish its schema and vectors.
- [ ] **B213 · typed completeness for plugin inputs and results** — M · both — a metric plugin
  denied an input it declared learns it from `withheld`, but its verdict records the refusal
  only as a caveat, and a rule plugin receives verdicts with no machine-readable state for it.
  The host records `denied`, `unavailable` and `failed` per input, with generation time,
  staleness, coverage and provenance; a plugin may state an observation, and the host sets
  confidence. One real rule or query consumer must tell the states apart without parsing prose.
  Only then a typed numeric figure (signal, unit, layer, scope, numerator, denominator), with
  today's display figures kept through an adapter. No secrets in error text.
- [ ] **B212 · a parser plugin can say what it does not measure** — M/L · both — v0.31 refuses a
  record that states no token counter at all rather than storing it as $0.00. The other half
  remains: a record from an activity-only source, and a counter the source does not keep,
  expressed as unsupported rather than refused or zeroed. Until then a counter omitted beside
  another is stored as 0, and every plugin is taken to answer cache-read and reasoning signals,
  so a plugin that never sends `reasoning_tokens` lowers the reasoning share and a missing
  `cache_read_tokens` reads as a 0% cache hit. The fix needs a versioned wire that declares
  which counters the source measures, the current reader kept through a stated deprecation path,
  and line, edit or tool-call counters designed against two real out-of-tree producers. Golden
  and fuzz vectors refuse unknown and content-bearing fields; ordered traces wait for a producer
  and a consumer.
- [ ] **B214 · measure the first run** — S/M · both — v0.31's `init` names and runs a configured
  parser plugin instead of reporting that no logs exist. What remains is measurement: on a
  stated fixture and machine, the time to a written report, the coverage it shows, and whether
  the reader reached a decision or a precise account of what is missing. Five solo users and
  three teams are targets for voluntary sessions, not telemetry and not results.
- [ ] **B217 · worked solutions built only on public contracts** — M · both — three examples
  that run in the test suite: a Python metric plus rule, a DuckDB query over the `B211`
  snapshot, and an offline project digest or CI check. They cover one repository and a monorepo,
  mixed tools, unpriced and activity-only records and unresolved repository identity, and import
  no internal Go package and no SQLite table. It counts as proof when at least two people
  outside the project adapt one and record whether it helped a decision. No marketplace, no
  automatic install or execution.

## Shipped — "Worth opening twice"

Everything that decided whether the analysis already shipped reaches anybody: findings that
arrive ordered with their reasons shown (`B148`), a digest that reports what *moved* and
declares when the comparison itself is weak (`B11`), a label derived through a rule engine
rather than typed (`B152`), the extension surface no longer weaker than the core it extends
(`B155`), and an assay that can be posted in public with redaction as the feature rather than
a flag (`B149`). All five are in [CHANGELOG.md](CHANGELOG.md). One piece of `B149` was dropped
deliberately rather than deferred: the item asked for repositories as a count *and a sorted
spend distribution* ("5 repositories, the heaviest 43% of spend"), and that distribution is a
re-identification surface — the ordering and proportions are exactly what somebody who knows
the setup reads a pseudonym back out of, which is the reason the same item refuses pseudonyms.
The count ships; the distribution does not, and will not.

## Then — "Everything the logs already say"

Depth before breadth, and before correlation. This milestone is the half of the product that
needs no server, no credential and no repository — and the half every other conclusion rests
on, since a link to a merged pull request is only as good as the session it links.

- [ ] **B147 · agent behaviour trace: the session as a sequence, not a total** — L · both —
  every figure assaio publishes today is an aggregate, and an aggregate cannot say *why* a
  session was expensive. The logs already carry the order: plan, search, read, edit, test
  failure, repeated tool call, compaction, retry, abandon. Store a content-free timeline —
  step kind, ordinal, model, token delta, outcome — and read detectors off it: retry loop,
  tool thrashing, the same file read N times, search with no following edit, a large edit
  before any test, compaction recovery, model switch after failure, dead-end run. No prompt,
  no code, no file contents; a path is a category, never a name. The refusal that has to
  hold: a detector fires on a *pattern*, and a pattern is not a fault — a hard bug
  legitimately looks like thrashing, so every detector ships with what it cannot distinguish.
  **Store cost is settled, and this item's own guess about it was right.** Measured on the
  maintainer's store after a full rebuild under the horizon: 1.88 stored step rows per usage
  record (335,527 against 178,016) — but a row multiplier is not a size measurement, and in
  bytes the table and its indexes are **101.9 MB against `usage_record`'s 58.3 MB**, roughly
  1.7x the table they describe. Hence a horizon
  (`trace.horizon_days`, default 30) pruned on every ingest rather than by a tidy-up command;
  30 days is also all that is recoverable, since Claude deletes transcripts at 30 by default --
  the history horizon and `doctor`'s retention line that state so shipped in v0.21.
  **Two things this item did not anticipate, both found by measuring before designing.** A
  session-grained denominator is a trap: 86% of sessions on the audited machine are SDK calls
  contributing 5% of rows, so "89% of sessions end without an edit" would be true, precise and
  worthless — every detector declares its scope from `entrypoint`/`sidechain` and renders the
  excluded share beside its figure. And a sub-agent needs its own timeline: 103 sessions hold
  39% of all rows, and blending them would make a sub-agent's thrashing read as a person's.
  **Landed** in v0.20.0: the step contract, the Claude reading, migration `0012`, the horizon and
  its prune. **Landed** in the release after it: the two detectors (`edit-loops`, `recovery`), the
  scope vocabulary and its shared denominator, the capability row and its three signals, the plugin
  boundary, and the widened target that made a read and a failed edit nameable at all. **Landed** in
  v0.26.0: the Codex reading, and what it settled is below. What remains is the two detectors this
  substrate can now carry but does not yet: the same file read N times (`docs/recipes/extensions.md` publishes it as a
  worked example rather than a built-in) and a large edit with nothing run after it, which needs a
  command *class* the store does not hold. A step carries no command identity, so "a command ran" is
  all any detector sees; making "was it a test" answerable is a closed vocabulary and its own ADR. Two things the readings do **not**
  do yet, stated because the ADR would otherwise imply they do: nothing reads
  `toolUseResult.interrupted`, `userModified` or Codex's `turn_aborted` (`B111`, `B113` — all
  measured at or near zero on the audited corpus), and the outcome vocabulary deliberately has
  no member they could fill.
  **What the Codex reading settled (v0.26.0)**, measured on its own 2,625 rollouts and 70,522
  lines rather than assumed from Claude's, none of it guessable from the 76 files this item was
  written against. The row multiplier is **1.66** (9,555 steps against 5,765 records) and in bytes
  **2.00 MB for 8,096 steps, 260 B each**, measured on a store built from Codex alone rather than
  differenced across two runs — stated because this item's own correction above is that a row
  multiplier is not a size. Codex *does* state how it was started (`session_meta.source`: `exec`
  on 2,604 rollouts, `cli` on 13), so `internal/trace`'s claim that it writes no entrypoint was
  wrong on a current install and its sequences are scoped rather than landing in `unstated`. Codex
  states an outcome on a patch and on nothing else — `completed` means a call returned, and 57 of
  840 commands exited non-zero under it — so the depth row claims `ai.steps.count` and
  `ai.step.target`, refuses `ai.step.outcome`, and `recovery` now narrows to the sources that
  answer it rather than counting Codex's silence as a clean run. A patch applies to 1.91 files at
  once, so the sequence records one step per file where the record counts one write per call: two
  answers to two questions, and the only way a repeat-edit reading sees the file that was returned
  to. **The substrate is thinner than the corpus suggests**, and the honest figure is the second
  one: 1,904 of Codex's 9,555 steps (19.9%) are a person's terminal session, and after the 30-day
  horizon only 608 of them in 5 sequences reach a detector. Of the 34 rollouts that edit anything,
  17 return to one file three or more times and the heaviest patches one file 36 times — a
  measurement over the whole corpus, not a figure any shipped detector publishes, since most of
  those rollouts are `exec` and out of every detector's declared scope.
- [ ] **B107 · Codex cache-write tokens are never read** — S · solo —
  `payload.info.total_token_usage.cache_write_input_tokens` is reported on every Codex
  `token_count` and `usage.Record` gets no value for it. **Measure before claiming a
  correction**: on the audited corpus the field carried a value on 238 events and was zero on
  every one, so this fixes no number here — the reason to read it is that a zero nobody looked
  at and a zero the vendor reported are different facts, and a different plan or model may not
  share it. Two things have to be settled first, and neither is guessable from that corpus:
  whether `input_tokens` already contains the write (it does contain `cached_input_tokens`,
  which the parser subtracts), and how history gets the value, since the local restate covers
  activity and granularity but deliberately not tokens. A `backfill --full` alone will not do
  it.
- [ ] **B109 · Copilot CLI is deeper than its matrix row** — M · both — the audit found named
  tool calls (`data.toolRequests[].name`), per-call line counts
  (`data.toolTelemetry.metrics.linesAdded`/`linesRemoved`), per-model request counts
  (`data.modelMetrics.<model>.requests.count`), a published cache TTL
  (`data.modelCacheState[].cacheTtlSeconds`) and sub-agent parentage
  (`data.parentAgentTaskId`) — every one of them a signal its depth row currently declares it
  cannot answer. Per-call lines are the interesting half: they would end the "credited whole to
  the model with the most requests" compromise. The corpus behind this is three sessions, so
  verify against a real one before writing the row.
- [ ] **B110 · Gemini CLI: the parsed shape is missing from a current install** — S/M · solo —
  the two files matching the discovery glob on the audited machine contain no token field at
  all (`files=2 records=0`), and the wider `~/.gemini` tree carries a different vocabulary
  (`CHECKPOINT`, `PLANNER_RESPONSE`, `CONVERSATION_HISTORY`) with no `tokens` object anywhere.
  Either the recording moved or these files were never the token source; settle which against a
  fresh session before touching the parser. Not establishable from this machine — it needs a
  redacted capture from someone running Gemini CLI, which is `B144`'s standing caveat.
  **The second half shipped in v0.22 as the `barren` canary, and this item's stated cause for it
  was wrong.** It read "each needs a 20-file sample floor"; two of the four floors are 50 and the
  discovery canary's total-loss branch has none, and an A/B with all four set to `1` produced
  `no canary fired` on both builds. The blocker was never the floor but the baseline guard: a
  source that has always yielded zero has a baseline of zero and there is no drop to detect. A
  fix written from the wording above would have shipped green and changed nothing. Recorded
  because the corrected cause is the reusable part.
- [ ] **B111 · the human correction, recorded rather than proxied** — S · solo — **the claim
  this item was written on is false, measured 2026-08-12 and corrected here rather than
  discovered after shipping.** `toolUseResult.userModified` was described as marking an edit the
  person changed after the AI wrote it "on a meaningful share of Claude edit results". The
  *field* is on a meaningful share — 2,691 of 13,038 tool results in a 400-transcript sample —
  and its *value* is `false` on every one: `"userModified":true` appears in **0 of 5,706
  transcripts, ~5 GB**. A signal built on it would be structurally zero and nobody would notice.
  The field is now read into the step timeline's outcome so a different plan, model or user
  reveals it (the `B107` template: a zero nobody looked at and a zero the vendor reported are
  different facts), and it stays out of the signal catalog until one does. `rework` remains the
  proxy it says it is.
- [ ] **B112 · attribution beyond skill and sub-agent, and the build that wrote the line** — M ·
  solo — Claude stamps `attributionPlugin` and `attributionMcpServer`/`attributionMcpTool` on a
  large share of turns, which would widen `skill-economics` from two dimensions to four and give
  MCP servers a cost of their own. Alongside it, `version` on every line is the harness-version
  cohort input `B96` assumes needs a server — it is already on disk, offline, per turn.
- [ ] **B113 · how a turn ended** — S/M · both — **measured 2026-08-12; the "stopped" half is
  near-empty and the item is downgraded accordingly.** Across the whole corpus:
  `"interrupted":true` in **0 of 5,706** transcripts, `"stop_reason":"max_tokens"` in **5 of
  5,706**, Codex `turn_aborted` **9 occurrences across 2,625 rollouts** (all `reason: interrupted`; the
  76-file figure this line carried is superseded by `B147`'s corpus). A
  signal promising to "distinguish a turn that finished from one that was stopped" would ship a
  permanently empty bucket. What does fire is `toolDenialKind` (automode-blocked, user-rejected,
  automode-unavailable, permission-rule) and `stop_reason` as a continuation-versus-finish
  discriminator (tool_use 18,330 against end_turn 512). All of it is read into the step
  timeline's outcome vocabulary; none of it claims a signal yet. Codex's
  `changes.<path>.{type,move_path}` is untouched by this measurement and still belongs here: an
  add, an update and a rename are one undifferentiated edit today.
- [ ] **B114 · Codex states its own limits** — M · both — `payload.info.model_context_window` is
  the model's context limit on every `token_count`, which is what `B16` proposes to vendor a
  table for; and `payload.rate_limits.{plan_type,primary.used_percent,primary.resets_at,credits}`
  is the only place any source says how close a session ran to a plan's ceiling. That is the
  subscription question `subscription-fit` answers from a configured number today, answered
  from the vendor's own accounting instead. Both are per-session state rather than usage, so the
  storage shape needs deciding first.

## After that — "Evidence Graph: from session to shipped change"

The outcome layer, pulled ahead of recommendations: a suggestion built on activity and
output proxies alone is a guess with a confident voice. First connect a session to the
change it produced — commit, pull request, review, CI, merge, survival — carrying the
confidence and the ambiguity of every link. Everything here ships with its error bars or it
does not ship.

- [ ] **B226 · an explicit team mapping for repository names** — M · team — `sync` sends a
  project's name and the team server pools usage by it, so two local repositories with one name
  are one project there; `sync` only warns. A member-declared mapping from a local checkout to a
  team name must relabel every stored row of that repository, not only the rolling window a push
  re-sends, and must say what happens to rows whose repository never resolved, or one repository
  shows under two names on the server. It sends no key, id or path, changes PRIVACY.md and
  docs/threat-model.md in the same commit, and waits for the team server's re-push and retention
  rules (milestone 4).
- [ ] **B102 · AnalyzerContext: retire the store types from the analyzer surface** — L · both
  — the half of `B90` deliberately not shipped with the catalog. Validators already do not
  query SQLite; what leaks is *types*, since `analyze.Input` carries `[]store.UsageRow`,
  `[]store.SessionRow` and `[]store.AttributionRow`, and those same shapes are the published
  metric-plugin wire (ADR 0004). Replacing them is therefore a breaking change to a public
  surface for no user-visible gain, so it waits for a real second consumer to design against
  — the git evidence collector (`B91`) — rather than being guessed at now. Ships with a
  deprecation window and a conformance fixture, and **lands with the protocol freeze (`B23`)**
  rather than before it — settled by [ADR 0016](docs/adr/0016-usage-is-a-store-row-not-an-event.md),
  which puts that breaking type change behind the evidence graph and with the contract
  freeze. See [ADR 0008](docs/adr/0008-signal-catalog.md). A public export adapter (`B211`) does not wait for this: it reads the store types and publishes records of its own.
  **Three requirements came out of building the git collector** rather than being guessed at:
  (1) a validator receives `[]store.UsageRow`, so there is no shape in which it could read a
  commit observation at all — the context has to serve *heterogeneous* observation streams
  keyed by type, which is a bigger job than swapping the row types; (2) `signals coverage`
  weights support by tokens (`analyze.TokensByTool`), so a source that has no tokens cannot
  appear in it, which is why no `vcs.*` signal is registered yet and what a presence notion
  would have to replace; (3) capability lives on `parser.Answers`, and a collector is not a
  parser — the registry has to move out of `internal/parser` or learn about non-parser
  sources before the first `vcs.*` signal is declared.
  **v0.24 took the first step and deliberately not the whole one**: `analyze.Capability` is the
  closed vocabulary of what an analyzer reads, `Needs` is how a validator declares it, and the
  runner records what a window could not supply (`Result.Withheld`) instead of leaving each
  validator to invent a sentence about an empty field. Four validators declare; the rest declare
  nothing and are held to nothing, which is the property that makes this migratable. What is
  still open is the part this item is named for: `Input` still carries the store row types, and
  retiring them is still a breaking change to the metric-plugin wire. The capability set is now
  the seam that change would move through — and it already earns its keep on the wire, where
  `needs:` gates the 44 MB step timeline.
- [ ] **B103 · commit observations that outlive one pass** — M · both — the half of `B91`
  deliberately not shipped: a configured repository list (rather than one `--repo` at a time),
  a keyed pseudonymous commit digest for the syncable case, and durable storage. Each waits
  for the consumer that decides its shape — v0.27's first `B85` slice deliberately collects and
  links in one pass, while replay across passes still needs a durable shape; ADR 0020 decides what
  identity may leave the machine — and each costs a
  migration, a size bound and a cleanup path, so none is worth guessing at. Path-level storage
  stays out entirely until something needs it: `B91` never records a path, so there is no
  opt-in to design yet. See [ADR 0009](docs/adr/0009-local-git-evidence-collector.md).
- [ ] **B232 · establish remaining PR outcome sources and corpus** — M · both — B231's
  bounded observation slice shipped in v0.38. The additive raw dismissal slice records actual
  source-stated PR events, nullable review joins and separate timeline/filter/slice coverage
  ([ADR 0025](docs/adr/0025-bounded-review-dismissals.md)); it derives no rounds or new rates.
  Remaining gates: authoritative review transitions and round boundaries with real
  changes_requested-dismissal, null-review and over-100-dismissal cases; a complete workflow
  source covering lost/force-pushed commits, attempts and PR associations with comparable
  populations; an authoritative read-side applied method with distinguishable real one-parent
  squash/rebase cases; and an explicit content-free read-side original/revert relation.
  Each track needs explicit coverage and real positive, negative and unknown conformance cases
  before further derived rates. Synthetic cases cannot close real-corpus gaps. No causal session
  outcomes. See [the finite source study](docs/github-source-feasibility.md).
- [ ] **B230 · attribution by this clone's configured git identity** — M · both — git can match
  the configured author identity and return only hashes, which would tell a teammate's commits from
  the user's where time alone cannot. It is the first use of identity in attribution, so it needs its
  own ADR, a stated miss for commits made under another address, and a corpus scenario where it must
  not resolve two people's commits (ADR 0022 rejected it for `evidence --github`).
- [ ] **B225 · an exec port for issue-tracker observations** — L · both — resolved issues are
  the one delivery edge with no item here. It follows the ADR 0003 posture: an ADR and a
  feasibility spike first (can GitHub and Jira return issue→PR links without reading issue
  bodies?), then a subprocess protocol emitting text-free observations: a keyed issue digest,
  type, state transitions with times, and explicit links to a branch, PR or commit found by the
  connector. It carries no title, body, label text or assignee, is validated at the boundary
  and is namespaced like `plugin:`. Joins go only through PR or branch edges (after `B85` and
  `B92`), with coverage and abstention; never by time. Digests and transition times still
  identify work, so the edge is local-only and ADR 0020 applies to it. GitHub Issues
  and Jira ship as out-of-tree reference connectors.
- [ ] **B85 · attribution engine + edges (session → commit / PR)** — L · both — versioned,
  confidence-bearing candidate and confirmed edges carrying their method (explicit marker,
  repo/branch match, bounded temporal proximity, file-category compatibility, identity
  compatibility, parent/child session, manual confirmation), the alternatives they beat, and
  their ambiguity status. Never force one session onto one PR; many-to-many stays
  many-to-many; an unattributed session stays unattributed; user correction wins and survives
  replay after an algorithm change. **Inherits from B80**: the optional issue-id or branch
  reference was deliberately cut from session annotations rather than shipped as a bare
  string, because a session→change pointer is a claim that must carry its method and
  confidence, not a category label — see [ADR 0006](docs/adr/0006-session-annotations.md).
  **The conformance corpus (`B93`, shipped) is the specification to build against**, and
  writing it surfaced a requirement that was not obvious: "identity compatibility" is not an
  available method today, because a commit observation carries no author at all. The
  `overlapping-users` scenario therefore *requires* ambiguity, and separating those sessions
  means changing what an observation carries — a privacy decision under ADR 0020,
  not a better ranking. See [ADR 0010](docs/adr/0010-attribution-conformance-corpus.md).
  **v0.27 ships the local session→commit portion, not this whole item**: algorithm
  `session-commit/v1` emits confidence-bearing matched, ambiguous and unmatched results from
  project plus bounded time evidence, keeps alternatives, honours the corpus's confirmed links,
  and stores nothing.
  `evidence --github` (`session-commit/v3`, ADR 0022) names the pull requests a candidate
  commit belongs to. Explicit markers, branch, identity and file-category compatibility,
  persisted corrections, stored edges and durable replay remain open.
- [ ] **B94 · `outcomes` funnel + `evidence explain`** — M · both — the first visible path:
  sessions → sessions with edits → linked commits → linked PRs → passing CI → merged →
  surviving, sliced by tool, model, project, task annotation and confidence band, always
  showing the unattributed and insufficient-evidence share. `evidence explain <edge>` says why
  a link was or was not made. No causal language anywhere in it.
  **v0.27 ships only the first visible row of this path**: `evidence` shows session→commit
  population coverage, candidates, alternatives, method, provenance, confidence and abstention
  reason in text or JSON. There is no edit-stage funnel, slicing, edge identifier or separate
  `evidence explain`, and no review, CI, merge or survival join yet; `evidence --github` names the pull requests
  a candidate commit belongs to and their state, and derives no rate from them.
- [ ] **B153 · what a change costs after it is written** — M/L · both — "AI lines" counts
  what was produced, never what it took to land. Once a session links to a pull request, the
  downstream burden becomes countable as named signals rather than adjectives: review rounds
  and requested changes (`review_tax`), CI failures and the repair cycles after them
  (`ci_repair_tax`), revert rate, time to merge, and the two ratios that make cost mean
  something — `cost_per_merged_change` and `cost_per_surviving_change`. Each with its own
  coverage and confidence, and each **age-matched against human-authored changes of the same
  age**, which is already the standing rule for bug density and is the only way these are not
  a smear. They are observations about a process, never a measure of a person; the
  per-person refusal holds here exactly as everywhere else.
- [ ] **B18 · survival: per-day correlation + age-matching** — M · both — per-day AI-heavy
  vs quiet-day survival comparison, an age/settle threshold so recent commits are not counted
  as "survived", and rename-following in blame. Only ever age-matched.
- [ ] **B21 · test-touch** — M · both — share of AI edits touching test files via
  privacy-safe category counts (test/source/docs/config) classified at parse time — paths are
  never stored. Needs a PRIVACY.md note. The first quality-adjacent signal without a server,
  and deliberately a floor rather than a verdict: research finds agent-written tests lean on
  mocks more than human ones, so "tests were touched" is not "the change was verified".
## Then — "The agent skill, over the evidence"

Roadmap milestone 2A. The skill ships after the delivery joins, so inside an agent it shows
spend beside what that spend delivered, not spend alone.

- [ ] **B223 · the `assaio` agent skill** — M · both — `skills/assaio/SKILL.md` in this
  repository, installed with `npx skills add assaio/assaio --skill assaio` into any host that
  reads the Agent Skills format. It detects `assaio-agent` or installs it after asking (`brew`,
  or a release checked against `checksums.txt` and `gh attestation verify`), walks the first-run
  path, maps a question to a command with `--format json`, quotes each figure with its layer,
  confidence, coverage and `—`, relays `recommend` records verbatim, and refuses per-person
  ranking. It says that whatever it reads from `assaio-agent` enters the model's context and
  reaches the model provider, project names included, and prefers aggregated and pseudonymized
  outputs. v1 ships no hooks or scripts. Proof: evals on Claude Code, Codex and Gemini CLI that
  check each control holds per host, the skills.sh audits passing, and `B214` timing the skill
  path beside the plain CLI path. A skills.sh install count is the skills CLI's telemetry, not
  users. First check whether `npx skills add assaio/assaio` also offers the harness skills under
  `.claude/skills/`; if it does, the product skill moves to its own repository.

## Then — "Harness intelligence & verified improvement"

Agent configuration — `AGENTS.md`, rules, skills, sub-agents, hooks, MCP servers — is a
versioned engineering input, not magic that is assumed to help. Once outcomes are linkable,
the question "did changing it actually improve anything" becomes answerable, and a
recommendation can rest on outcome rather than on activity.

- [ ] **B95 · harness inventory v1** — M · both — detect the configuration artifacts a
  repository carries and store only their shape: type, scope, keyed hash/version, size band,
  modified time, tool compatibility, and whether the artifact was observably invoked. Content
  is **not** stored by default — an `AGENTS.md` is a document, and this is an inventory.
- [ ] **B96 · harness-to-outcome cohorts** — L · both — compare repository or time cohorts
  before and after a harness version changed: token and cache profile, retry and failed-tool
  behaviour, time to first edit and first test, CI-repair burden, review tax, delivery yield,
  survival. Every result states sample size, confounders and attribution confidence, and is
  labelled an observation, never proof.
- [ ] **B150 · which model for which kind of work, from your own history** — M/L · both —
  a person running Claude, Codex, Gemini, Copilot and Cline has no basis for choosing between
  them beyond feel. The parts are already stored: task class (`mark`), model, cost, retries
  and failed tools, rework, and — once outcomes link — review and survival. Compare models
  *within* a task class and repository class, never globally, and publish the comparison with
  its sample size. The rule that keeps it honest is a floor, not a disclaimer: below a
  minimum of comparable sessions it reports insufficient evidence and names no winner. A
  global "best model" is never emitted; benchmarks measure other people's work.
- [ ] **B84 · deterministic recommendations** — M/L · both — one concrete next step per
  finding: rules over verdicts and outcomes emitting observed pattern, evidence, confidence,
  one action, the follow-up metric and a review window; dismissable, and the dismissal sticks.
  No LLM anywhere in the decision path — an LLM may later only explain a suggestion that
  already exists. Abstains when outcome coverage is inadequate; that abstention rate being
  *too low* is a warning sign, not a win.
- [ ] **B97 · experiment framework v1** — L · both — what turns a recommendation into
  evidence: named hypothesis, target cohort, baseline window, treatment version, success and
  guardrail metrics, a minimum-observation requirement, pre/post or alternating-period mode,
  and a result carrying its confidence and caveats. Nothing mutates a config automatically.
- [ ] **B87 · pre/post window comparison** — M · both — the measurement half of B97 on its
  own: compare a metric across matched windows around a deliberate change, with sample-size
  and seasonality warnings and mandatory association-not-causation framing.
- [ ] **B20 · compaction-recovery-cost** — M · both — tokens spent in the turns right after
  a compaction vs baseline: the true price of overflowing context.
- [ ] **B17 · progress ("skill curve")** — M · solo — you vs you four weeks ago across
  adoption breadth, turn-efficiency, cache-hygiene, rework — a small panel of deltas,
  deliberately not a single score. Strictly self-relative, never cross-person.
- [ ] **B44 · read-only MCP/query interface** — M · solo — ask your own usage questions from
  an agent: source coverage, recent cost and workflow changes, active experiments, the
  evidence behind a finding. Never exposes prompts, code, secrets, or — in anonymized mode —
  project names.

## After that — "Team adoption, without surveillance"

A team self-hosts it, sees cross-tool adoption and delivery outcomes, and cannot build a
leaderboard from it.

- [ ] **B22 · server hardening** — M · team — TLS/reverse-proxy guidance, per-member tokens
  and roles, retention and deletion policy, chunked/resumable sync, health and migration
  checks, backup/restore, bounded ingestion queues.
- [ ] **B09 · team-evenness** — M · team — Lorenz/Gini spread of usage across pseudonymized
  members with a minimum-cohort (≥5) guard. "Broad adoption, or two power users?" — never a
  ranked list.
- [ ] **B10 · tool-coverage** — S · team — members per tool on a central store; shadow-tool
  and unused-seat detection, only where the evidence is explicit.
- [ ] **B40 · onboarding-curve** — M · team — usage growth vs weeks-since-first-sync, in
  aggregated bands, pseudonymized.
- [ ] **B41 · team efficiency spread** — M · team — distribution bands of turn-efficiency
  signals across members; "the team needs prompting practice" without naming anyone.

## Later — "Cost truth, policy & interoperability"

Every `$` today is an estimate at public pay-as-you-go prices, which a flat-rate
subscription makes structurally different from real spend — and assaio's own model should
speak a standard rather than only its own dialect.

- [ ] **B138 · credentialed billing pull** — M/L · both — the network half of the
  reconciliation `B19` moved up to "Calibrated measurement": an opt-in pull of the Anthropic/OpenAI usage and cost
  APIs so the reconciliation runs unattended instead of from a downloaded export. Network- and
  credential-gated; pulls vendor aggregates only, never uploads logs. Deliberately second: the
  offline import carries most of the value and needs nobody's key.
- [ ] **B16 · context-utilization** — M · both — vendored model context-window table (like
  the price table) → peak context vs model limit, near-limit share, and honest right-sizing
  hints. Prerequisite for pricing long-context and cache tiers instead of one flat rate. For
  Codex the table is unnecessary: the log states `model_context_window` on every `token_count`
  (`B114`), which is a stronger source than a snapshot assaio maintains.
- [ ] **B98 · OpenTelemetry GenAI mapping + content-free ingest** — M/L · both — publish a
  field mapping between assaio's canonical events and the OTel GenAI semantic conventions
  (model, tokens, tool calls, agent and operation identity, session), then ingest only the
  subset needed to prove it against a Claude Code telemetry fixture. Prompt, completion, tool
  input and tool result attributes are **dropped by default**, and the dropped set is
  documented rather than implied. Opens a path to sources whose local files cannot be parsed.
- [ ] **B99 · connector SDK + conformance kit** — M · both — generated scaffold, JSON
  Schemas, typed SDKs for a couple of languages, a fixture redaction helper, a depth-manifest
  validator, parser-drift tests and a privacy manifest. What makes a community connector
  possible without touching core packages. Supersedes the "more parsers in core" reflex.

- [ ] **B210 · week-over-week cost movement** — S · both — `B184` shipped the
  share card's week-over-week block with token volume and AI lines, but the cost ($)
  movement requested by `B149` did not ship. A $ delta over two weeks is a delta of
  two floors unless both weeks are fully priced: the price table can miss models,
  cache reads cost a fraction, and model mix moves price per token by an order of
  magnitude. Show the movement only when both weeks are 100% priced, or state the
  unpriced share on the image.

## v1.0 — "Measurements worth depending on"

The one milestone that keeps a version number, because there the number *is* the promise. It is
**not** only the semver guarantee any more: after v0.12 the bar leads with calibration, because
a frozen contract over an uncalibrated measurement is a stable wrong answer, which is worse than
a breaking correct one. The release gates are summarized in
[ROADMAP.md](ROADMAP.md#what-v10-has-to-mean); the contract freeze follows the outcome and
operational proof rather than substituting for them. `B24` used to sit here and no longer does — its own entry says it is not a v1.0
condition, so it moved to the reserved pool below rather than contradicting itself in place.

- [ ] **B23 · protocol freeze** — M · both — declare the exec plugin protocols (parser,
  metric, rule), the canonical event and signal contracts and the sync API stable under semver,
  with conformance fixtures and deprecation windows. **The SQLite schema is deliberately
  excluded**: v0.12 needed a migration that rewrote stored rows to correct a semantic error and
  it will not be the last, so freezing the store would make every future correction a breaking
  change and create pressure to leave a wrong number in place. It stays internally versioned
  and migratable — a promise about correctability, not the absence of one.

## Pool — reserved: waiting on a condition, not on time

Neither of these is scheduled, and neither is dropped. Each waits for a fact to become true —
a scale that does not exist yet, and a mechanism that has not been chosen — so listing them
anywhere else would misrepresent them as work anyone is about to pick up. `B24` in particular
was under the v1.0 heading while its own text said it is not a v1.0 condition.

- [ ] **B25 · Postgres backend** — L · team — once a single SQLite file stops being enough
  for a central store. Not before: the current shape has not been shown to hurt.
- [ ] **B24 · in-process plugin API (research first)** — L · both — **no longer a v1.0
  condition**: it is a research question, not a readiness bar. The mechanism is **not**
  decided, and native Go plugins are explicitly not a v1 requirement: they need cgo, do not
  work on Windows, and require host and plugin built with identical toolchain and dependency
  versions, which contradicts shipping one static binary per platform. Evaluate a sandboxed
  WebAssembly/WASI component instead — it brings capability limits and resource budgets — and
  keep exec plugins as the universal baseline either way. Deliverable is a decision record,
  not code.

## Pool — validators from data already stored (unscheduled)

- [ ] **B28 · rhythm: day × hour heatmap** — S · both — the shipped `rhythm` validator
  reports off-hours/weekend share, four time-of-day bands, and p95 focused minutes; still
  to add the full day-of-week × hour heatmap. Explicitly never an attendance view.
- [ ] **B29 · session-taxonomy: thrash + trend** — M · both — the shipped `session-taxonomy`
  validator buckets conversational / light-edit / heavy-edit; still to add a thrash bucket
  (needs per-session rework, not stored yet) and the mix's week-over-week trend.
- [ ] **B30 · delegation: trends and per-session economics** — S · solo — `sidechain` now
  marks sub-agent turns exactly and `skill-economics` reports per-agent tokens and lines;
  still to add the delegation-share trend and tokens per delegated vs main-loop session.
  Task difficulty is invisible — a prompt to look, not a verdict.
- [ ] **B31 · throughput-per-hour** — S · both — AI lines per focused hour
  (ActiveMinutes); labeled an activity rate, never a productivity score.
- [ ] **B32 · rework-bursts** — S/M · both — rework clustered over time and per-session
  p90; healthy iteration churns too.
- [ ] **B33 · reasoning-share: per-project + trend** — S · solo — the shipped
  `reasoning-share` validator reports the overall reasoning share of reporting-tool output
  plus its coverage; still to add the per-model/project breakdown and a week-over-week trend.
- [ ] **B34 · model-freshness / lock-in** — S · both — single-model dependence and
  share of usage on unknown/legacy-priced models.
- [ ] **B35 · entrypoint-mix** — S · both — CLI vs IDE vs hook usage and where
  friction (rejections) differs; the stored `entrypoint` column is unused today.
- [ ] **B36 · marathon: long- vs short-session efficiency** — S · solo — p95 session
  active minutes ships in `rhythm`; still to compare output and thrash between long and
  short sessions, since thrash correlates with marathons.
- [ ] **B37 · branch-mix** — S · both — AI lines on the default branch vs feature
  branches; a process signal (direct-to-main AI work), local only.
- [ ] **B02 · cache-hygiene trend** — S · both — add the day-over-day cache-read-share
  trend to the shipped `cache-hygiene` validator, which already reports the current-window
  share, the cache-write-waste flag, the 1-hour write share and the top stated miss cause
  (`B108`, v0.12). Day-grain approximation stands.

## Pool — needs a schema or parser extension

- [ ] **B224 · find where sources record reasoning effort** — S · both — no parser stores the
  effort setting a turn ran at, so no figure can say a high effort bought nothing. Collect
  fixtures from real captures and define the setting first; store it only where a source writes
  a stable field, and declare it unanswered for every other source.
- [ ] **B79 · local-day bucketing** — M · both — `UsageRow.Day` is `substr(ts,1,10)` over
  timestamps normalised to UTC, so every day-based figure (active days, lines/active-day,
  week-over-week, burn-anomaly's baseline) counts UTC calendar days while `rhythm` reads
  session starts in local time. Blocked on data, not effort: Claude Code and Codex both log
  `Z` timestamps, so the offset a session actually happened in **does not exist in the
  source** and cannot be recovered for history. Converting to the reporting machine's zone
  would help a solo user but make the team server arbitrary (bucketing every member in the
  server's zone) where UTC is at least uniform. A real fix needs the tools to log an offset,
  or an explicit reporting-timezone setting with the trade-off stated. Disclosed meanwhile
  as caveats on `burn-anomaly` and in `doctor`.
- [ ] **B78 · sub-agent tool-call split** — S · solo — a Claude sub-agent's aggregate record
  keeps `ToolCalls=0` and an all-zero purpose split, so its `toolStats` read/search/bash/edit
  counts never reach `explore-produce`. Populating them means also setting `ToolCalls`, which
  today would double-count against the parent turn — needs the accounting decided first. The
  `B105` audit named the fields and confirmed they are always present together:
  `toolUseResult.toolStats.{readCount,searchCount,bashCount,editFileCount,otherToolCount}`
  beside the `linesAdded`/`linesRemoved` already read, plus `totalToolUseCount`. Nothing is
  missing but the decision.
- [ ] **B39 · Cline activity extraction** — M · both — close the "cost but no lines" gap
  for Cline: `ui_messages.json` `say:"tool"` payloads (`newFileCreated` /
  `editedExistingFile` / `appliedDiff`) and `api_conversation_history.json` tool_use blocks
  carry the paths and diffs, so lines added/removed are derivable. Multiplies every activity
  validator. Gemini CLI's default recording carries only tool-call *names*, not diffs — its
  edit activity needs the opt-in telemetry export (B72), so it is split out.
- [ ] **B72 · Gemini activity via OpenTelemetry** — M · both — Gemini CLI's structured edit
  data (`ToolCallEvent.model_added_lines`/`model_removed_lines`, `FileOperationEvent`) lives
  only in its **opt-in** OTel export, not the default session JSONL. An optional connector
  could read a user-configured OTLP file export where enabled; strictly opt-in and clearly
  labeled, since most installs won't have it on.

## Pool — team

- [ ] **B42 · server-side exec metrics** — M · team — lift the ADR 0004 non-goal safely: trusted
  server configuration, budgeted background runs, and validated results cached for reads. An
  HTTP read never starts a program, and a member's upload never installs one. Team data reuse
  starts from a server-side run of the `B211` export; an authenticated, bounded HTTP query
  follows a named consumer. `B22` owns scopes, resumable sync, retention and recovery.
- [ ] **B12 · GitHub Action** — M · team — packaged action running `check` as a gate
  plus a PR comment with movers/effectiveness for the changed window.

## Pool — from the 2026-08 needs research

Sourced from public evidence rather than from this machine, which is the point: the roadmap
had been growing from one maintainer's dogfooding, and that is a sample of one.

- [ ] **B157 · Cline's second root** — S · both — the standalone Cline CLI stores tasks under
  `~/.cline/tasks/<id>/`, overridable by `--data-dir` / `CLINE_DATA_DIR`
  ([docs.cline.bot/cli/cli-reference](https://docs.cline.bot/cli/cli-reference), retrieved
  2026-08-11); assaio discovers only the VS Code root, so a Cline-CLI user gets zero rows and
  no warning. Discovery work, not new parsing.
- [ ] **B158 · Copilot's OTel file exporter** — M · both — `COPILOT_OTEL_ENABLED=true` with a
  file exporter writes token, cache and reasoning counts to `~/.copilot/otel/*.jsonl`
  ([ccusage.com/guide/copilot](https://ccusage.com/guide/copilot/); GitHub shipped
  enterprise-managed OTel export [2026-07-08](https://github.blog/changelog/2026-07-08-enterprise-managed-opentelemetry-export-for-vs-code-and-cli/)).
  A second, opt-in path beside the `session.shutdown` totals already parsed — and the template
  for every tool that speaks OTel, which is where cloud-executed sessions surface at all.
  Carries a calibration caveat: Claude Code 2.1.216 fixed telemetry double-counting on streams
  emitting multiple cumulative `message_delta` frames, so OTel figures predating it are inflated.
- [ ] **B159 · an AI-tool inventory an auditor accepts** — S/M · both — ISO 42001 has moved
  from standard to procurement requirement, and buyers now ask for an inventory of the AI
  systems and services a supplier relies on. `report` already knows tools, models, projects and
  first/last-seen; a stable `inventory` export (tools × models × repos × window, counts only,
  no persons, no paths) answers that questionnaire offline. Aggregate-only by construction, so
  it clears the never-a-performance-metric refusal without a flag. The EU AI Act angle was
  checked and is **not** applicable: Article 50 targets generated content and deepfakes.
- [ ] **B160 · the reopen window: cost per accepted change** — M · both — the measure that
  decides whether AI improved delivery sits just outside session data: not tokens, but the
  second and third attempt at the same change. What ships today stops at the session boundary —
  `rework` counts within-session churn and rejections and says so, and `survival` checks git
  blame directionally. Neither answers how often a file an agent touched is touched again
  within two weeks, which is what turns cost per session into cost per accepted change — the
  number an engineering lead can defend at a budget review. This is the **git engine**, separate
  from log parsing by construction, and its first universal analyser. Raised in public feedback
  on the launch post, 2026-08-11; converges with the evidence-graph milestone below.
- [ ] **B163 · a digest that can tell a prune from a change** — M · both — v0.17 made `clear`
  drop the digest's comparison basis, which fixes the case assaio itself causes. It does not
  cover a store edited by anything else, or the general question: the digest compares two
  reports and has no way to know the population under them changed. A row count per snapshot
  would catch a shrink, but cannot distinguish a prune from a genuine fall in usage — which is
  why this is a design question rather than a patch, and why the shipped answer is a caveat
  about what could not be checked instead of a claim about what happened.
- [ ] **B162 · a destructive command that cannot be pointed at the wrong store** — S/M · both —
  `clear` has no `--db`: it always acts on the default local store, and there is no environment
  variable either. v0.17 made it print the path and record count before deleting, which is the
  cheap half. The open question is the other half — whether a command that deletes should accept
  a target at all (making the mistake possible in a new way), or should refuse when the store it
  found is not the one the invocation implies. Raised by a real incident: a tooling run set a
  variable that does not exist, expected `clear --all --yes` to apply to a copy, and emptied a
  513,617-row store. `--yes` skipping the prompt is the other half of why it was silent.

## Pool — from the 2026-08-12 needs research

A second sweep, sourced the same way as the pool above: public evidence, each claim carrying
the URL it was read from. Where it contradicted an item already here, the correction went onto
that item (`B60`) rather than becoming a new one.

- [ ] **B164 · the autonomy validator** — M · both — Anthropic published a vocabulary for how
  autonomously an agent ran — turn duration, auto-approve and interrupt rate, agent-initiated
  stops — and states that the API has "no reliable way to link independent requests into
  coherent agent sessions"
  ([anthropic.com](https://www.anthropic.com/research/measuring-agent-autonomy)). Local
  transcripts do link them, which is the whole argument. Every input is either stored or one
  field away: `toolDenialKind`'s `automode-blocked`/`automode-unavailable` is the auto-approve
  half, already parsed. Depends on `B147`'s timeline; it is the first analyser that should read
  it, because the definition is citable rather than invented here.
- [ ] **B172 · a project name is read with the host's path rules** — S · solo — `carryForward.project`
  takes `filepath.Base(cwd)`, where `cwd` comes from the transcript and `filepath` answers for the
  platform assaio runs on. A Windows transcript read on Linux therefore yields the whole
  `C:\\w\\app` string as the project name rather than `app`, and every per-project figure splits
  along the platform that read the log. Found while fixing the same host-dependence in the step
  target reading, which CI caught on Windows (`targetKey` is the shape the fix should take here).
  Pre-existing, not introduced by that change, and it needs a `backfill` to restate the names it
  already stored.
- [ ] **B171 · the served dashboard cannot show the detectors** — S/M · both — `serve` rebuilds
  `GET /` on every request for any token holder, with no cache, so it deliberately does not read
  the step sequences: `store.Timelines` is a full scan of the step table plus a GROUP BY
  over the window's records, about 2.5s on a 339,000-step store, and it costs that before it can
  know whether a step exists. On the store this surface is built for the answer is always none --
  `sync` carries usage records and not sequences (ADR 0012) -- so today the detectors correctly
  report that and say why. Making them work on a self-hosted store that also ingests locally needs
  the thing this handler lacks and the CLI does not need: a built snapshot with a lifetime, which is
  the same machinery a cached dashboard would want anyway.
- [ ] **B170 · a call that runs nothing is classified as a command** — S · solo — `parser.StepKind`
  maps `BashOutput` and `KillShell` onto the same `command` kind as `Bash`, because the tool-purpose
  classification predates the sequence reading and only had to answer "was this a shell tool".
  `edit-loops` adopts CodeBurn's definition, which turns on *a shell command having run* between two
  edits of one file, so polling a background shell's buffered output counts as one and inflates the
  repeat rate for anyone running a dev server or a long test in the background. Measured: zero
  `BashOutput`/`KillShell` calls across ~1,500 recent transcripts on the maintainer's machine
  against 35,215 `Bash` calls, which is exactly why the cross-check against SQL could not catch it —
  both readings key on the same stored `kind`. Fixing it properly means splitting the kind, which
  moves `tool_commands` on every usage record and needs a backfill plus a re-derivation of the
  calibration corpus; the detector states the limitation in the meantime.
- [ ] **B169 · the cleanup orphans a sub-agent transcript** — S · solo — measured on the
  maintainer's machine 2026-08-12: of 5,774 Claude Code transcripts, 286 are older than the 30-day
  `cleanupPeriodDays` boundary and **every one of them is a sub-agent file** under
  `<session>/subagents/`, with no session transcript older than the boundary at all. So the cleanup
  walks sessions and leaves their sub-agents behind. Consequences to check rather than assume: a
  sub-agent sequence whose parent's own rows were never ingested carries whichever entrypoint its
  own lines state, and `trace.Scope` classifies it as a sub-agent regardless, so nothing is
  misfiled today. What is unmeasured is whether an orphan is *discovered* at all once the parent
  directory holds no session file, and whether the 45-day floor observed here is the cleanup's real
  behaviour or an artifact of when it last ran.
- [ ] **B165 · OpenCode and Kilo Code: one parser, two tools** — M · both — OpenCode keeps a
  relational store at `~/.local/share/opencode/opencode.db` whose `session` table carries
  `cost`, a five-way token split and `directory`, with per-message cost/tokens beside it
  (verified against
  [session/sql.ts @v1.18.16](https://raw.githubusercontent.com/sst/opencode/v1.18.16/packages/core/src/session/sql.ts)).
  Kilo Code v7 rebased onto the same schema at `~/.local/share/kilo/kilo.db` (see `B60`). One
  reading covers the largest unparsed population found. Ships with the double-count guard both
  need: the pre-v7 Kilo era coexists on disk and v7's importer can restate it.
- [ ] **B166 · Goose: the cleanest per-request ledger in the field** — M · both —
  `~/.local/share/goose/sessions/sessions.db` holds a literal `usage_ledger`: one row per
  request with model, a cache read/write split, `cost`, **a `cost_source` provenance column**
  and an **`is_compaction` flag**, with `sessions.working_dir` giving the project. The
  provenance column is the notable part — it is the only surveyed source that states where its
  own cost figure came from, which is exactly what assaio's honesty rules ask every fact to
  carry. Its `is_compaction` also feeds `B164`. Goose left session JSONL at v1.10.0 and the
  legacy files linger unmanaged, so discovery must not read
  both eras as separate sessions.
- [ ] **B167 · Aider default-history measurement caveat** — M · both — default chat history
  rounds token counts of at least 10k to whole thousands, so a connector reading it can report
  only approximate counts. B14's opt-in analytics log is the source for exact token counts;
  its `.aider.chat.history.md` description must not imply that default history contains no
  token information.

## Pool — CLI & DX

- [ ] **B45 · TUI** — L · both — interactive terminal dashboard (validators +
  project drill), the flagship DX piece once the small wins land.
- [ ] **B46 · completions + man pages** — S · both — cobra generators, shipped via
  goreleaser.
- [ ] **B47 · exports** — M · team — an OpenMetrics endpoint on `serve` for Grafana and a
  Parquet dump, each after a named consumer; the NDJSON snapshot is `B211`.
- [ ] **B66 · Scoop bucket (Windows)** — S · both — `scoops:` block in goreleaser
  publishing to a new `assaio/scoop-bucket` repo. Prerequisite: extend the release
  PAT's repository access to that bucket, or the next tag's release fails at the
  publish step.
- [ ] **B67 · winget manifest** — M · both — automated manifest PR to
  `microsoft/winget-pkgs` on release (goreleaser supports it); a heavier review loop
  than Scoop, so Scoop first.
- [ ] **B06 · JSON Schemas for the metric envelope and result** — S · both — publish
  machine-readable schemas under `docs/schemas/` for the Input a metric plugin is sent and the
  Result it returns, so a plugin can be validated by a generic tool rather than by reading
  prose. `docs/conformance/` already publishes accept/refuse vectors for the same boundary; a
  schema is the other half, and the two must not be able to disagree. The scaffolder this item
  used to carry shipped in v0.25 as `plugins init --kind parser|metric|rule --lang
  go|python|sh`.
- [ ] **B08 · a second locale** — S · both — add one `i18n.Catalog` beside `en` and a case in
  `i18n.For`, proving the scaffolding with a real language. v0.4 built the catalog (dashboard
  chrome, statusline words, the 18 explain pages); what remains is translating it and choosing
  how a locale is selected. Data-derived validator text stays out of scope: translating it
  needs message templates for the interpolated numbers.
- [ ] **B86 · contributor entry points** — S · both — public issues cut from the top
  backlog items, labels (`good first issue`, `help wanted`, `connector`, `metric`,
  `parser-drift`, `research-needed`, `privacy-review`), Discussions categories, and a short
  architecture map showing where a parser, metric, rule, connector or dashboard change belongs.

## Pool — dashboard

- [ ] **B48 · sparklines** — M · both — per-day series for key figures as inline SVG;
  the dashboard stays fully self-contained.
- [ ] **B49 · multi-window tabs** — M · both — 7d/30d/90d generated into one HTML
  with a client-side toggle.
- [ ] **B50 · top-N drilldowns** — S/M · both — accordion drill for the top 3–5
  projects instead of only the top one.
- [ ] **B51 · print stylesheet** — S · team — a PDF-able layout for management
  readouts.

## Pool — connectors

Each follows the [connector intake flow](docs/extending/data-source.md#the-intake-path-open-a-connector-issue-first);
a tool used by one organization is usually better served by an out-of-tree
[exec plugin](docs/extending/parser-plugin.md#write-a-plugin-any-language).

- [ ] **B52 · opencode** — M — `~/.local/share/opencode/storage/message/**` JSON (plus a
  newer relational `opencode.db`). Assistant messages carry `tokens{input,output,reasoning,
  cache{read,write}}`, `cost`, `modelID`, and — richest of any candidate — the `edit` tool
  persists a structured `filediff{additions,deletions,patch}`, so lines +/- are stored
  directly, no diff parsing. The best activity target after the current four.
- [ ] **B194 · Antigravity's two readable figures, and the two questions they raise** — M ·
  both — the `agy` parser ships (ADR 0017) reading the transcript only, and three findings from
  the 2026-09-02 corpus sweep are parked here rather than lost. **(a) Observed context
  occupancy.** `gen_metadata` field path `1.9.10.1` is prompt/context tokens used: monotonic
  across steps in **198/198** conversations with more than one row, and `used ≤ window` in
  **638/638** observations against `1.9.10.4`, the model's context window, which takes exactly
  {80000, 128000, 160000, 256000} and pairs with the model name in the same blob **218/218**
  times. `store.SessionRow.PeakContextTokens` is *derived* on every other source
  (`cache_read + input`); this would be *observed*, and one column holding both with no field
  to say which is which is the blend AGENTS.md forbids. Needs a provenance flag on the column
  first. **(b) The model name.** Field paths `3.28` / `1.19` hold it (`gemini-3.7-flash-high`
  ×208, `gemini-3.7-flash` ×45, `claude-opus-4-6-thinking` ×52, plus `gpt-oss-120b-medium`,
  `claude-sonnet-4-6`, `gemini-pro-agent`) — so this source runs models from three vendors and
  assaio reports none of them. Reading it means opening another tool's live WAL-mode SQLite
  file and walking an unnamed protobuf field, for a name covering 218 of 500 conversations and
  often several per conversation with no request count to choose between them. **(c) A
  session-level failure signal.** 384 of 1,537 transcript entries are `ERROR_MESSAGE` — a
  stream or platform failure, not a tool call returning an error, and the catalog has no id for
  it. `ai.tool_errors.count` is the wrong home; a new signal is a catalog decision. All three
  were verified against Antigravity CLI 1.1.23, whose binary self-updates.
- [ ] **B55 · Cursor (Admin API)** — M — local storage verified to lack token counts;
  vendor-aggregate granularity, tagged as such.
- [ ] **B195 · Cursor's local store — the format passes and the corpus does not** — S · both —
  measured 2026-09-02 against the same two-part test every source has to pass: a stable
  discoverable format **and** a real captured corpus. The format half passes and is the most
  discoverable of any candidate — `~/.cursor/ai-tracking/ai-code-tracking.db` is a plain SQLite
  file at a fixed path with named columns and no protobuf:
  `ai_code_hashes(hash, source, fileExtension, fileName, requestId, conversationId, timestamp,
  model)` and `scored_commits(commitHash, branchName, scoredAt, linesAdded, linesDeleted,
  tabLinesAdded, tabLinesDeleted, composerLinesAdded, composerLinesDeleted, humanLinesAdded,
  humanLinesDeleted, blankLinesAdded, blankLinesDeleted, commitMessage)`. The corpus half fails
  on the audited machine: 1,493 `ai_code_hashes` rows spanning 2026-07-15..2026-07-29 — 14 days
  that stopped five weeks before the measurement — 85 `scored_commits`, and `conversation_summaries`
  and `tracked_file_content` both empty. Summed over all 85 scored commits, `linesAdded` = 53,059
  while `composerLinesAdded` = 6,636, `tabLinesAdded` = 0 and `humanLinesAdded` = 0: the AI-versus-
  human split the columns promise is not written here. **No token count and no cost column exists
  anywhere in the schema, on any machine**, so Cursor can never answer the token half — `B55`
  covers that through the Admin API instead. **What would change the answer:** a machine where
  `humanLinesAdded` and `tabLinesAdded` are populated across a real span. `scored_commits` is, in
  shape, exactly what roadmap milestone 2 needs — AI versus human lines *per commit*, already
  attributed by the vendor — which makes it worth reading for the survival question the day
  somebody brings that corpus, and never for cost.
- [ ] **B151 · what a plugin declares, and a badge that goes red when a vendor moves** — M ·
  both — two halves of the same gap. A plugin today is verified for *protocol* conformance
  (`plugins verify`, `metrics verify`) and declares nothing about itself: whether it touches
  the network, what it reads, which schema version it supports, what it emits. A manifest
  carrying permissions, privacy class, input and output signals, supported schema and a
  checksum makes an install a decision instead of a leap. The second half is a compatibility
  run over **public fixtures** per source, so a vendor changing a format turns a badge red
  and notifies the owner rather than being discovered by a wrong number months later — the
  in-tree drift canaries already do this for the six built-in sources and stop at the repo
  boundary. Blocked on nothing except the fixture question: a public fixture is a real log,
  so what may be in one has to be settled before any are accepted.
- [ ] **B57 · community plugin registry page** — S — a docs page listing community
  exec plugins (parsers and metrics) once a few exist, seeded with the weekend-usage
  example.
- [ ] **B60 · Roo Code Cline-family storage** — M · both — verify a sample of Roo Code's
  `rooveterinaryinc.roo-cline` task-directory storage. Its `api_req_started` shape follows
  Cline, with `apiProtocol` ignored. Kilo's pre-v7 Cline-family storage needs its own sample;
  Kilo v7 uses the OpenCode-family schema tracked in B165.
- [ ] **B61 · Qwen Code (Gemini family)** — M — a Gemini CLI fork, but **not** the same
  on-disk shape: chats live at `~/.qwen/projects/<hash>/chats/<id>.jsonl` (not
  `tmp/*/chats`), and tokens are in a raw-API `usageMetadata` object, not Gemini's
  normalized `tokens{…}`. It also persists tool-call args, so unlike Gemini it carries edit
  activity. Needs its own parser, not the Gemini one; verify a real sample first.
- [ ] **B65 · Cursor local activity source** — M — `~/.cursor/ai-tracking/`'s
  AI-code-tracking database is a potential **activity-only** source (AI-attributed
  code, no token counts — Cursor's local storage is verified to lack them). Would ship
  as lines/activity with `granularity`/provenance honestly tagged, complementing the
  Admin-API cost path (B55). Research item.
- [ ] **B14 · Aider connector** — M · both — in-tree parser. The parseable token source is
  the opt-in `~/.aider/analytics.jsonl` (`message_send` events with `properties.{main_model,
  prompt_tokens,completion_tokens,total_tokens,cost}`, `time` in epoch seconds, no session
  id or cache split); `.aider.chat.history.md` is markdown-only. No structured per-edit field
  — Aider auto-commits, so lines +/- come from git, not the logs.
- [ ] **B154 · unverified local-storage candidates** — M · both — Factory droid, Kiro,
  Continue, Amp and Crush remain under the existing intake rule: a candidate earns its own
  connector item when someone provides a redacted local capture with evidence of token
  counts. Goose is no longer an unverified candidate: its SQLite usage ledger is tracked in
  B166. The retired B54, B56
  and B62–B64 ids are not reused.

## Pool — code health (from earlier reviews)

Deferred cleanups surfaced by the max-effort review of the 0.2 work; no behavior change, so
they wait behind features but keep the growing metric surface maintainable.

- [ ] **B73 · validator meta** — S · both — embed a `meta{name,title,describe,howToRead}`
  value in each validator so the eighteen metrics stop repeating the three trivial interface
  methods and the duplicated `Result` header; one source of truth per metric. The long-form
  explain text is deliberately *not* part of this: it lives in the i18n catalog so it can be
  translated, and keeping it there is what let `internal/analyze` stay free of any
  presentation dependency.
- [ ] **B74 · member aggregate** — S · team — collapse `dashboard/team.go`'s four parallel
  per-member maps (`lines`/`cost`/`hasCost`/`unpriced`) into one `map[string]memberAgg`, so a
  member's totals travel as a unit.
- [ ] **B75 · humanize helpers** — S · both — `internal/humanize` landed in v0.4 with
  `analyze`'s two formatters (`compactCount`, `money`) moved onto it byte-identically. v0.11
  folded in `report.formatCompactTokens` (the `status` peak-context figure now reads `85.0K`
  rather than `85k`) and moved every percentage onto `humanize.Percent`. v0.13 took
  `dashboard.formatCompactUSD` with `B131`, since the rounding that made it a duplicate was
  also what printed a real cost as `$0`; the survivor is `humanize.USDCompact`. Still out:
  `humanize.USD` and `USDCompact` disagree about the same amount — `USD` keeps cents below
  $100 and has no M tier, `USDCompact` keeps them below $1 and has one — so `status` and the
  dashboard still render one figure two ways, and reconciling them changes shipped output.
- [ ] **B77 · readWhenEnough** — S · both — factor the "gate the favorable read behind a
  minimum-sample floor, else neutral" block shared verbatim by session-taxonomy,
  turn-efficiency, and model-right-sizing into one helper beside `readFor`.
- [ ] **B106 · two files past the size budget outside the analysis packages** — S · both —
  `internal/i18n/en_explain_work.go` (221) and `internal/cli/analyze.go` (208) each carry more
  than one responsibility at more than the ~200-line budget. (`internal/plugin/metric_input.go`
  came off this list in v0.11, split into the envelope and its wire mapping when the capability
  field landed.) The i18n one is the interesting case rather than the largest: it is a block of
  prose per metric, so splitting it by metric group is a different judgement from splitting
  code, and doing it badly makes the catalog harder to translate (`B08`), not easier.
- [ ] **B117 · `metrics verify` prints a blank confidence label** — S · both — the verify path
  renders a plugin's `Result` without `analyze.Stamp`, so the label the summary leads with is
  empty (`Confidence:  · 3 usage rows · activity coverage 0%`) while the coverage axes beside
  it read as a real zero. Confirmed against v0.10 as well, so it is not new. Either stamp the
  window like `analyze` does, or render the unstamped case as what it is — a conformance
  check on the document, not a verdict about anyone's data.

## Pool — from the v0.13 whole-codebase review

Same rule as the pool below: reproduced against the maintainer's own store before being
written down. What v0.13 fixed is in [CHANGELOG.md](CHANGELOG.md) and not repeated here. `B139`
left this pool for the calibration milestone above — an unwatched price table is a figure checked
against nothing, which is that milestone's subject rather than a cleanup — and shipped from it.

- [ ] **B140 · a Claude `<synthetic>` message is counted as a turn** — S · solo — Claude Code
  writes locally-generated assistant messages (API errors, refusals to continue) with
  `"model": "<synthetic>"` and an all-zero usage block. **Measured:** 551 of 163,976 records
  (0.34%) on the audited store carry zero tokens, zero tool calls and zero lines — 565 counting
  the other zero-token Claude rows. **One live consequence already found**: `<synthetic>` has no
  price, so it sets `Row.HasUnpriced` while carrying no spend, and the first cut of v0.13's cost
  gate keyed on that flag and failed every run on a fully priced store. The gate now keys on
  unpriced tokens; every *other* reader of `HasUnpriced` — `report`, `status`, the dashboard —
  still prints the "includes unpriced usage" asterisk on a window where nothing is missing.
  Beyond that they are real events but not API requests, so
  every per-record denominator (tokens per turn, the confidence envelope's "N usage rows",
  the coverage weighting) is 0.34% larger than the population that could feed its numerator.
  **Below the threshold of a wrong number here and recorded so it is not rediscovered as one**;
  it matters if the share ever grows, which a rate-limited or erroring session would do. The
  decision is whether a synthetic turn is a turn — `B113`'s "how a turn ended" is the same
  question from the other side, so they probably answer together.

## Pool — from the v0.14 whole-codebase review

The defects this review found are fixed and listed in [CHANGELOG.md](CHANGELOG.md). What it
found that was **not** a defect — three places where the code did exactly what it said, and the
question was whether what it said was still the right call — was decided rather than deferred,
and all three shipped in v0.14:

- `B141` — the team panel's per-member row now shows engagement only; output and spend are the
  team's total. A pseudonym is not anonymous to a colleague who knows the roster.
- `B142` — a projected "per month" is a calendar month, not thirty *active* days, which had been
  inflating the figure a flat plan is compared against.
- `B143` — the parser plugin protocol rejects a field it does not define, as the metric and rule
  protocols already did.

## Pool — from the v0.22 whole-codebase review

What this review found and v0.22 fixed is in [CHANGELOG.md](CHANGELOG.md) and not repeated
here. These are the items it measured and deliberately did not fix: two too large for the
release, five that are judgement calls about invented thresholds rather than defects, and one
file-size norm.

- [ ] **B173 · `usage_record` is unbounded and has no measured bound anywhere** — M · solo —
  measured on the maintainer's store: **1.31 MB/day over 44.7 days → ~476 MB/year**, with no
  horizon, no automatic prune, and only a manual `clear --older-than` to reduce it. Today's big
  table is the *bounded* one — `session_step` is capped at 30 days ≈ 136.3 MB steady state,
  re-measured for v0.26 — and
  the unbounded one crosses it in about 78 more usage-days and never comes back down. The rate is
  one machine's; the unboundedness is structural. What makes this hard rather than a second
  horizon: a usage record is the only thing a re-import can rebuild *and* the only thing a report
  reads, so a retention rule has to answer what a five-year cost trend is worth against a store
  nobody can carry. Deciding that is the work; the size is not in dispute.
  **v0.24 made it visible without deciding it**: `doctor` projects the store's own measured
  growth from live bytes (2.9 MB/day → at most 1.0 GB/year on the maintainer's store, stated as
  an upper bound because the rate averages the horizon-bounded timeline with this table) and
  takes `--db`, so the central
  store an operator cannot walk over to is diagnosable. What remains is the decision this item
  is named for -- what to keep, at what resolution, for how long -- which is a policy, not a
  line of SQL.
- [ ] **B190 · read the uncovered branches, not the coverage number** — M · solo — measured on
  this build: `internal/parser` 47.7%, `internal/recommend` 60.6%, `internal/usage` 70.0%,
  `internal/reconcile` 70.7%, `internal/config` 72.0%, `internal/vcs` 74.1%, `internal/cli`
  75.8%. The number is not the finding and no gate is wanted (Goodhart, and
  [ADR 0002](docs/adr/0002-code-standards-and-enforcement.md) says so): the work is to open the
  uncovered branches in the packages that carry a *contract* rather than a rendering — the
  shared parser limits, reconciliation's failure paths, CLI error paths, and the recommendation
  families whose abstention rules are the whole point — and to give each one a test or a written
  reason. `internal/layer` and `internal/trace` report no direct package coverage at all and are
  exercised only through consumers, which is a different risk from a low percentage.

- [ ] **B187 · attribute usage to the MCP server that caused it** — M · both — Claude Code's own
  `/usage` breaks recent consumption down by skill, sub-agent, plugin **and MCP server**; assaio
  has the first two (`skill-economics`) and not the last. An MCP tool call is named
  `mcp__<server>__<tool>` in the transcript, so the server is recoverable from the tool name
  without any new source — the work is a vocabulary decision (is an MCP server a fourth
  attribution dimension or a facet of the tool taxonomy?), a store column, and the same
  minimum-volume rules the skill dimension already carries. Verified against Claude Code
  2.1.238.

- [ ] **B186 · one assistant message, two transcripts, two different counts** — M · solo — found
  by the downward-restatement canary `B116` asked for, on its first run against the maintainer's
  store. A Claude sub-agent's transcript and its parent session both carry the same assistant
  `msg_` id, and the two files see different amounts of it: for `msg_011Ce6Ytmoyd4FXNBViS2XzB`
  the parent holds 4 `tool_use` blocks and the sub-agent file holds 1. The dedupe key is the
  message id alone, so whichever file the walk reads last wins the restate, and the stored
  `tool_calls` oscillates between 4 and 1 forever — the canary reports the same 6 rows on every
  `backfill --full`, unchanged while the store gains new records around them. **Measured: 6 of
  192,277 records (0.003%), a swing of ~18 tool calls against a window of 216,790**, which is
  why this is a defect to fix rather than a figure to distrust. The decision is which file is
  the authority on a message that appears in two: the parent transcript is the authority on the
  parent's turn and the sub-agent file on its own, so the key probably has to carry the scope it
  was read in — the same shape the sync path already uses to keep two members' rows apart.
  Whatever the answer, the canary is the regression test: it must reach zero and stay there.
  Measured on 2026-09-27 across the maintainer's transcripts: 70 message ids are carried by more
  than one file, 19 of them with different timestamps (0.5–138 s apart), almost all forked
  sub-agent transcripts. `B116` made the stored time take the earliest carrier; the assigned
  activity counts still follow whichever file was read last, and so do a sub-agent aggregate's
  context columns across its parent transcripts, which is what `restated-down` counts on a full
  re-read. The same holds for one stored step key, which `steps-changed=2` shows on every full
  re-read.

- [ ] **B185 · give the withdrawn verdicts a line derived from your own history** — L · both —
  v0.24 withdrew the good/bad call from fourteen metrics because their thresholds were numbers
  picked once (`B176`–`B178`, `B180`, and the same class in `reasoning-share` and
  `session-taxonomy`). The figures stayed; what is gone is a colour this project could not
  defend. The credible line is not a published constant — there is none for most of these — but
  **the same store's earlier windows**: "your tool-error rate is 22%, against 9% over your own
  prior 90 days" cites a source, derives from a distribution, and is the claim a reader can act
  on. It needs per-metric history that assaio does not keep: today only the *current* window is
  recomputed, and `digest` stores a bounded basis per run rather than a per-metric series.
  So the work is (a) decide what a metric's own history is and what it costs to store — `B173`'s
  size question, one table smaller — (b) restate it when a parser fix corrects the past, and (c)
  a floor of history below which the comparison is withheld rather than made against two weeks.
  Until it exists, `analyze` leads on the four metrics whose lines *are* derived or cited, and
  that shortness is the honest state, not a gap to paper over.

- [ ] **B196 · attach a published threshold to a metric, once one fits** — M · solo — the
  register (`internal/threshold`) refuses every candidate today, and
  `TestNoRegisteredCandidateGradesAnything` keeps it that way until someone changes it
  deliberately. What would let one attach: a churn or survival figure whose numerator,
  denominator, window, data source and layer match a metric assaio already computes — or an
  assaio-side change that closes the gap, chiefly age-matched survival (a fixed post-commit
  horizon rather than "until now") and AI attribution on the git side, which together would
  make `survival` comparable to GitClear's two-week churn on all five properties instead of
  three.

- [ ] **B201 · two per-record relationships are checked only at the server boundary** — S · solo
  — `Sidechain ∈ {0,1}` and "the tool-purpose split sums to `ToolCalls`" live in
  `internal/server/validate.go`, although `internal/usage/bounds.go`'s own header says it exists
  because the plugin and sync boundaries were drifting. Not reachable today: the plugin wire
  record carries neither field, so `CheckCounts` is never asked about them from that direction.
  It is latent — the wire has grown before, and a field added to it would inherit no check.
  Related: a rejection names the rule but not the offending field (`negative count field`), which
  on a `sync` push tells a team member what was wrong but not where.

- [ ] **B202 · `skill` and `agent` labels reach the store with no length bound** — S · solo —
  they are the only log-sourced *text* that is stored, and on the local path they are taken
  verbatim from the vendor's `attributionSkill`/`attributionAgent`. The 512-byte clamp exists
  only where a record arrives from outside the process (`server.maxStringField`,
  `plugin.maxWireStringLen`). PRIVACY.md calls them "short text labels", which describes vendor
  behaviour rather than an enforced invariant. Note `parser.VocabularyToken` is the **wrong**
  clamp here — its charset excludes `-`, so it would drop `code-review`; a plain length bound is
  the fitting hardening.

- [ ] **B203 · the codex yield canary cannot separate a format break from a changed habit** — S ·
  solo — it fires today on the maintainer's store (`3.5 record(s)/file across 1170 file(s),
  history is ~22.9`) and `doctor --strict` exits 1 on it. Investigated 2026-09-02 and it is **not
  drift**: 4142 `token_count` events in the corpus against 4054 parsed records is 97.9% yield, and
  1159 of 1170 files carry `originator=codex_exec` — hundreds of short one-shot batch runs instead
  of a few long interactive sessions. The canary is right to fire, because it cannot tell the two
  apart; what is missing is a second figure that can. Events-per-file beside records-per-file
  would have answered it in one line.

- [ ] **B204 · `codex` writes an ordered step timeline nobody reads** — M · solo — measured
  2026-09-02: `item_completed` events appear in 984 of 1170 rollout files, up to 98 per file,
  under the `codex exec` originator that now dominates this corpus. The depth matrix says "no
  ordered step sequence is read from its logs yet, so the behaviour detectors have nothing to read
  for it" — that gap is now closable without a new source. Wants a golden fixture and a fuzz seed
  from a real capture, and a check that the sequence is genuinely ordered rather than emission
  order.

- [ ] **B205 · retention is reported for `claude-code` only** — S · solo —
  `internal/cli/doctor_store.go` hardcodes it, so the line that tells a reader "35 of those days
  came from transcripts already deleted and exist only here now" is silent about the other five
  sources. Circumstantial evidence that `agy` rotates: `brain/` holds exactly 500 conversation
  directories and the earliest transcript entry is 30 days after the installation id was created.
  Not proof — but if it rotates, agy history older than the vendor's window exists only in the
  store, and nothing says so before a deletion.

- [ ] **B207 · the shared ingest path still documents itself as Cline-only** — XS · solo —
  `internal/ingest/state.go:30` ("one discovered file or Cline task directory"), `:52` ("dirInput
  signs a Cline task directory"), `internal/ingest/parse.go:23` ("one file's (or cline
  directory's)"). `dirInput` now also signs an Antigravity conversation, whose tree includes
  `transcript_full.jsonl` — a file the parser never reads, so a change to it invalidates the
  signature. That is correct conservative behaviour, but the comment no longer names what it
  signs. `docs/extending/data-source.md` was generalised when `agy` landed; the Go comments were
  not.

- [ ] **B208 · `adoption` asserts a capability where it measured the data** — S · solo —
  `internal/analyze/adoption.go:62` derives `breadthKnown` from `inv.Unattributed < len(in.Usage)`
  ("no row named a project") and then prints "no source here records a working directory". Those
  are different statements, and the purity effect is real — a breadth of 0 is replaced by
  `neutralPurity`. Not a live defect: only `claude-code`, `codex` and `copilot-cli` write `Cwd`,
  so today every window where the two coincide really is one whose sources record no cwd. It
  becomes one the day a cwd-carrying source is added, and the failure will be silent because the
  note asserts the conclusion rather than the premise. The rest of v0.25.0 moved these decisions
  onto `parser.Answers`; this one was left because it needs a project signal id that does not
  exist yet.

- [ ] **B209 · `CollapseForTable` is an invariant held by a doc comment and one caller** — XS ·
  solo — `internal/cli/report.go` must call it before `RenderTable` or the day table prints the
  duplicate rows v0.25.0 removed. `Aggregate` already returns rows unchanged for `by == "day"`, so
  `RenderTable` could call it itself and the invariant would be unbreakable. Also worth noting:
  `collapse_test.go`'s fixtures carry no `Tokened` row, so every token cell in them renders `—`
  and a regression in the token columns would not show there.
- [ ] **B219 · the restate counts vanish with the terminal** — S/M · solo — `backfill` prints
  `restated-down=`, `identity-changed=`, `identity-kept=` and `steps-changed=` but keeps none of
  them. Documented automation runs backfill from launchd, cron and hooks with output discarded,
  and the first run after an upgrade is the one that restates. Persist the last run's counts per
  source (a migration) and show them in `doctor` and beside `digest`'s build caveat.
- [ ] **B220 · a team server never applies the sub-agent conflict rule** — S · team —
  `restateActivitySQL` recognises a Claude sub-agent aggregate by `dedupe_key LIKE 'agent:%'`,
  but the sync endpoint prefixes every key with `<member>:`, so on a central store the
  competing-project rule never fires and a project the member's own store abandoned is kept.
  Match `%:agent:%` there, as migration 0006 and the delegation reader already do, with one
  shared definition.

## Refusals (will not build, regardless of demand)

- No "estimated time saved" headline — the logs contain no counterfactual.
- No lines-of-code or token leaderboards; nothing ranked per named individual, ever.
- No per-person analytics outside a deliberate, governed team-mode opt-in.
- No cohort/percentile comparisons without a minimum cohort size and explicit consent.
- No wrapping `analyze --format json` in an object. The ordering (`B148`) wanted a
  `{worthAttention, results}` document and it would read better, but the array is what every
  script already reads and a shape change breaks them all silently. The ordering rides on the
  results it promotes instead, as `lead`. Decided during v0.16; revisit only at a major, and
  only with the break stated in the changelog before anyone hits it.