FEATURES.md
Rama main · réplica de lectura del repositorio original.
# Features
The maintained inventory of what `assaio` does **today**, and since which release. This
is the current-state counterpart to the other two lifecycle documents: candidates live
in [BACKLOG.md](BACKLOG.md), the per-release delta lives in [CHANGELOG.md](CHANGELOG.md),
and every shipped user-facing capability gets (or updates) a row here in the same PR.
`v0.1` below means the v0.1.0 line — the first public release; the column starts
mattering the moment a second release exists.
## Commands
| Command | Since | What it does |
|---------|-------|--------------|
| `init` | v0.5 | A first run end to end: detects tools, prints the exact directories it will read before reading them, imports, writes the report, names what to run next. Writes no config when defaults work; `--non-interactive` for scripted setup. Since v0.31 it also lists and runs the parser plugins your config declares, so a plugin-only machine gets a first report. |
| `demo` | v0.1 | Full reports on bundled sample data — no logs needed. |
| `backfill` | v0.1 | Import all historical local session logs into the store. Since v0.4, incremental: an input already parsed unchanged by this build is skipped and reported as `unchanged=`; `--full` forces a complete re-parse. Since v0.20 it also stores each Claude Code session's step timeline — and since v0.26 each Codex rollout's — and prints `steps=`, bounded by `trace.horizon_days` (default 30, `0` keeps everything); `steps-past-horizon=` says how many it read and did not store. Since v0.24 it reports `restated-down=` — stored rows a re-read moved a figure *down* on, which is what a corrected attribution rule and a parser regression look like identically from the store's side. Since v0.27, conflicting project claims for one completed Claude sub-agent aggregate keep its project attribution empty and stable across re-imports; its usage remains counted once. Since v0.32 a re-read also corrects a stored model or label name, and a Claude Code row's stored time takes the earliest line that carries the response. `identity-changed=` and `steps-changed=` report replacements; `identity-kept=` reports names retained when a re-read had none. |
| `report` | v0.1 | Token/cost report; `--by day\|project\|tool\|model\|entrypoint` plus `task\|outcome\|difficulty` (v0.6; `member` withdrawn in v0.22 — see the refusal in BACKLOG.md), `--format table\|json\|csv`, `--compare` top movers. Since v0.15 table cells read at a glance: counts group thousands, costs round to cents from a dollar up and keep two significant digits below one, and a real amount under half a cent states a bound rather than rounding to zero. `json` and `csv` keep full precision. Since v0.24 a central store's member values leave every format as stable pseudonyms; `--identify` exports the stored value without that additional pseudonymization. It may be a legacy name or, after sync v2 migration, a keyed digest; the disclosure warns of both possibilities. Since v0.31 `json` and `csv` carry `cache_write_1h` and `cache_write_tiered`, so a single-model row's token columns reproduce its cost; the 1-hour figure is `null` where no source states the tier. |
| `effectiveness` | v0.1 | AI output vs cost — AI lines, edits, rejections, **`$`/100 AI lines** — per project by default; `--compare`. Since v0.6 also groupable by session label, so cost per line is comparable per kind of work. Since v0.15 the cost and `$`/100-lines cells use the same at-a-glance money formatting as `report`. Since v0.33 `$`/100 lines, per row and in TOTAL, divides only priced usage from sources that record changed lines, marks a ratio that leaves usage out with †, and JSON/CSV add `line_capable_cost` and `line_capable_unpriced_tokens`. Since v0.34, Copilot CLI lines are paired with the cost of every model it ran on the same day and project. That usage is left out of `$`/100 lines when one model has no known price and, under `--by model`, from every model's `$`/100 lines (`line_capable_shared_tokens`). TOTAL no longer depends on `--by`. |
| `analyze` | v0.1 | Runs the metric validators below plus configured exec metric plugins; `--list`, `--format text\|json`, `[name...]` subset. Since v0.16 the report leads with the few findings **worth a week's attention**, ordered by evidence strength and how much of the window each subject reaches, with those reasons shown — and a window whose reads are all fine, withheld or thin promotes nothing rather than the least weak one. It is an ordering, never a score; expected impact stays out until the outcome link exists. `--format json` keeps its array shape and stamps `lead` on the promoted reads. Since v0.5 every result carries a confidence envelope (coverage, sample size and unit, data age, parsing build, and a `high\|medium\|low\|insufficient` label), and since v0.16 an `insufficient` verdict tells history the capture could not reach — which `backfill` repairs — from a subject no source records. Since v0.6, `--task\|--outcome\|--difficulty` recompute every validator over just the sessions carrying that label. Since v0.9 the envelope also carries **signal coverage** — how much of the window a metric's own subject reaches — so a figure read off a sliver cannot travel as a fully covered verdict. Since v0.14 a projected "per month" figure spans the window's real days rather than only its active ones, so a working week is no longer priced as a calendar month. Since v0.21 a validator whose figures compare one span against an earlier one is marked `Trending` and carries how far back the store's own history reaches, because a comparison against a span the store began inside reads as growth from a base it never held. Since v0.22 every read states its **measurement layer** — activity, output, outcome or impact (ADR 0013) — and a window of N days is N whole day-buckets rather than N+1. Since v0.24 a metric only carries a good/bad verdict when its line is derived from the window's own distribution, cited from a published definition, or set by you (a plan price); the other fourteen report their figures under a `REPORTED` read with no colour, no promotion and a gauge pinned at half rather than one that moves, each naming the authority a verdict would need. |
| `recommend` | v0.24 | The few experiments this window's evidence supports, as typed records rather than prose: evidence, confidence, scope, one action, prerequisites, expected direction, risks, rollback, review window and the follow-up figure that would show whether it worked (ADR 0015). Rendered text projects the record and adds no claim to it; a record missing a required field is dropped rather than printed with a caveat. **Abstention is the default** — a thin window, a low-confidence verdict, or a metric missing a declared input produces nothing at all, and the empty output says it is an abstention rather than a clean bill. No predicted number: assaio has no counterfactual, so `effect` is a direction in words and the follow-up figure is named before the result is known. Every record is `proposed`; the accept/verify lifecycle needs stored state and is not shipped. |
| `reprice` | v0.25 | Re-prices the turns already in the store against another entry in the same price table — what this same set of turns would have cost on a different model, and how its projected monthly rate stands against a flat plan price. Arithmetic over observed events, never a counterfactual: a different model does not emit the same tokens for the same request, and assaio has never watched one do this work, so every figure states what it holds fixed and refuses to claim the rest. The share of the window it could not price is stated rather than dropped. Ends in one [ADR 0015](docs/adr/0015-structured-recommendations.md) experiment carrying its own rollback and follow-up. `--since`, `--against` and `--plan` (both repeatable), `--format text\|json`, `--db`. **No plan catalogue is vendored**: a published plan price changes without notice and a stale one read as current is a wrong recommendation about real money, so a candidate plan is a figure you pass in. Since v0.29, `reprice` never proposes models priced only at a dropped LiteLLM model's last listed price as routes, but still prices them when named with `--against`. |
| `check` | v0.1 | Budget gate with non-zero exit: `--max-tokens` (default basis) or `--max-cost` (labeled API-equivalent). Since v0.3, also the host for exec **rule** plugins: an `error` alert — or a rule that could not be evaluated — fails the gate. Since v0.13 the same rule holds for `--max-cost`: a window carrying usage the price table cannot price fails rather than passing on a partial figure. |
| `dashboard` | v0.1 | Writes the self-contained offline Assay HTML report; since v0.22 each ledger entry states its **measurement layer**; pseudonymized by default, `--no-anonymize` opt-out. A team panel appears only on a central store, and since v0.14 a member's row shows engagement only — lines and spend are the team's total, never a per-person figure. Since v0.16 the page opens with the same **worth a week's attention** ordering `analyze` leads with, and the faceplate shows a verdict and its gauge without the bare ratio beside it — purity is a different quantity in every validator, so the number invited a comparison between cells that means nothing. Since v0.34, the cost basis's cost per active day uses only days on which a source that counts tokens ran. It reports how many days it left out and carries the total's `*` when part of the cost is unpriced. Since v0.37, a team window with fewer than five distinct synced members shows aggregate 0, 1–2, 3–5 and 6+ session bands instead of per-member rows; team-wide output and cost remain visible. |
| `share` | v0.23 | Renders one window as something fit to post in public: a square reel (1:1 or 9:16) that walks through the numbers, the archetype and the session fingerprint; a still **poster**; and the post text beside them — or `--format text` for a block that survives SSH. **Redaction is structural, not a flag**: no field can carry a repository, member, path, branch, skill or sub-agent name, and an out-of-tree parser's tool label collapses to `a plugin source` because that name comes from the user's own config. Tools and models are named, because which coding agent and which model ran is a fact about a vendor. Every figure is quoted from what `analyze` published for the same window, never recomputed — the property that stops a shared number disagreeing with a reported one — and every rendered frame carries the measurement layer, whether the cost is complete or a floor, and a sample-data marker under `--demo`. An empty store imports the local logs first, so a fresh install reaches a finished card in one command. `--since`, `--output`, `--format html\|text`, `--demo`, `--no-open`; deliberately no `--db`. Since v0.28, its week-over-week block shows token volume on the activity layer and AI-written lines on the output layer, on separate lines for the last two complete UTC weeks. A figure prints `—` with the reason when its movement cannot be stated. Since v0.33 its per-100-lines figure uses the same population as `status`, appears in the hook line and post only when it leaves nothing out, and a window no source records lines for never shows "0 lines". |
| `reconcile` | v0.15 | Compares a vendor's own billing/usage export (a local CSV or JSON, no credential, no network) against assaio's estimate. Scope mismatch is computed before any delta; only causes with evidence are named; the residual is reported as **unexplained** and never closed by adjusting either side. Columns bind by header alias and the binding is printed — no vendor profile is claimed, because no real export has been read by this repository — with `--map` to override. States every run what no export can answer: line counts, edit attribution, per-session splits, or a flat-rate plan. |
| `serve` | v0.1 | Self-hosted team server: collects pushed usage and serves the aggregated dashboard. Since v0.24 every route but `/healthz` requires a bearer token, and requests are rate limited per secret (`server.rate_limit_per_minute`). Since v0.37, sync v2 writes require `server.members` with one token per keyed member digest; a shared `server.token` can read the dashboard but cannot write. `serve migrate-sync` rekeys an existing synced database offline before v2 writes. Still no TLS: run it behind a reverse proxy on a trusted network. |
| `sync` | v0.1 | Pushes local usage to a team server. Since v0.37, `--member` is only a stable local input to a keyed digest, never a cleartext name sent to the server; `--print-member-digest` shows that digest locally. Requires `ASSAIO_SYNC_IDENTITY_KEY` (private 32-byte hex key) and a matching per-member bearer token. POST `/v2/usage` excludes `GitBranch` and cleartext `Member` and has no v1 fallback. Since v0.35 it warns when two local repositories share a name it sends; the server still pools usage by name. |
| `doctor` | v0.1 | Detected tools, resolved log roots, store inventory and size, health, format-drift canaries (five since v0.22, one of which fires on a source that is found and has never yielded a record), accuracy caveats. Since v0.16 an `unpriced:` line reports how much of *your* store the price table cannot cost, and names the models a refresh must cover. `--strict` (v0.5) exits non-zero on suspected drift, on a configured source with no inputs, (v0.13) on a store it cannot read, or (v0.16) above `pricing.max_unpriced_share` of stored tokens carrying no price — default 5%, `0` disables. Since v0.10 a root it could not read is reported as a failed discovery rather than counted as zero files. Since v0.20 also the session-step timeline's size and the window it covers, since it is the largest thing assaio writes per unit of usage. Since v0.21 a `retention:` line states how long the source itself keeps its transcripts and how much of that this store still holds, because history a source has deleted is unrecoverable by any means; since v0.22 the timeline line also states its own horizon, and says plainly when `trace.horizon_days: 0` means there is none. Since v0.24 it takes `--db`, so the central store a team pushes into is diagnosable, and prints a `growth:` line: live bytes over the store's own span, a per-day rate, and a yearly figure labelled an upper bound because the rate averages a horizon-bounded table with an unbounded one. Since v0.29, `pricing:` counts models priced only at dropped LiteLLM models' last listed prices in `retained.json`, while entries without per-token rates count as unpriced, not $0. |
| `status` | v0.1 | Terminal overview: inventory, headline `$`/100 lines, hot / going-stale projects, session stats. Since v0.10 each session figure reads only the sources that record it, says so when that is fewer sessions than the window holds, and prints "not recorded" rather than a zero nobody measured. Since v0.22 the AI-line count and the `$`/100 ratio are withheld entirely when no source in the window records a changed line, and a window where only some do says what share of its tokens the figure reaches. Since v0.33, `$`/100 lines in the headline and each Hot project divides only priced usage from sources that record changed lines. † marks a ratio that leaves usage out; the note says what it leaves out. |
| `statusline` | v0.4 | One ambient line for a status bar: today's tokens, AI lines, cost basis, and data age. Local day (timestamp range, not a UTC bucket); read-only; never fails loudly. See [automation](docs/automation.md). |
| `explain` | v0.4 | The long-form page for a metric — what it measures, how to read it, what to do about it, its limits. Needs no store; no argument lists every metric. |
| `survival` | v0.2 | Directional local outcome check: how much of a git repo's window survives in `HEAD` (`git blame`), beside the AI lines the store recorded. Since v0.8 it reads commit observations, so it also reports what the window changed by file category and how many commits were reverts. Since v0.9 merge commits are reported apart from the rate — git publishes no line counts for a merge, so a hand-resolved conflict is counted in neither figure rather than inflating survival. Since v0.14 a file `git blame` cannot read is counted and named rather than passed over, so the rate never reads as covering files it never saw, and a non-ASCII path is decoded from git's quoted form instead of vanishing from it. Since v0.24 every run states the **age of what it measured** — the median commit age and the window's span — because survival is monotonic in commit age and a rate printed without it invites a comparison between two windows that measured different things. Since v0.35 it counts only AI lines resolved to its own repository, lists the lines under the name whose repository is not recorded as not counted, and prints `—` when no stored usage resolved to the repository. See [automation](docs/automation.md). |
| `evidence` | v0.27 | The first local Evidence Graph slice: compares a chosen repository's content-free commit observations with sessions carrying the same stored project name. A commit inside a session is a medium-confidence candidate; one commit within the bounded 48-hour following window is low-confidence; competing following commits remain `ambiguous`, and no candidate remains `unmatched`. Text and JSON show the population, candidate and resolved coverage, method, confidence, provenance, candidates, alternatives and abstention reason. It has no `--db`, stores no edge or commit, refuses member-bearing team rows, makes no network request without `--github` and is not a person-evaluation surface. Since v0.35 a session is a candidate only when its rows resolved to the repository the command runs in; a session under the same name whose repository never resolved is counted as `identityUnresolvedSessions`, a worktree reaches its repository's sessions, and a repository with no resolved usage gets an empty `project`. Since v0.36, a commit a forge wrote is judged by the time that means work: a squash is at most a low-confidence landing, a rebase merge counts at its written time, a forge merge commit is no candidate, an amended commit reaches both sessions, and each document states which forge it can recognise (GitHub.com only). Since v0.36, `--github` reads the repository's pull requests through your own `gh`, makes listed commits a `HEAD` reflog records as made here candidates, and names the pull request(s) each candidate belongs to (`session-commit/v3`, [ADR 0022](docs/adr/0022-pull-requests-through-gh.md)). Since v0.37, `--github` reports bounded PR review states and current-head check-run/status-context states, with total and listed counts that expose truncated connections. These are PR observations, not session outcomes. Since v0.38, B231's bounded slice adds a separate historical read of suites/runs on currently listed commits, with independent read coverage and total/listed counts at each layer. Complete usable reviews support distinct requested-change reviewed-revision counts. A current request-change snapshot share uses the entire named merged PR population, including alternatives, and requires a nonempty complete usable review population for every PR; ineligible PRs withhold the share and never shrink that denominator. Multiple merge parents identify a merge, while one cannot distinguish squash from rebase. Exact rounds, comparable PR workflow lifetimes and CI rates, authoritative squash-versus-rebase methods and trustworthy content-free PR revert relations remain B232 work. No delivery evidence is persisted, synced or added to dashboards. See [ADR 0024](docs/adr/0024-bounded-github-history.md). Since v0.39, the same base read adds the last 100 selected raw review-dismissal events per PR, with actual event times, nullable review joins and separate all-timeline, selected, source-slice, retained-node and truncation coverage. Missing coverage stays unavailable. These local PR outcome observations leave current review snapshots and session-commit/v3 matching unchanged and derive no rounds or new rates ([ADR 0025](docs/adr/0025-bounded-review-dismissals.md), [source study](docs/github-source-feasibility.md)). See [the evidence guide](docs/evidence.md) and [ADR 0018](docs/adr/0018-local-session-commit-evidence.md). |
| `repos` | v0.35 | Lists the project names local sessions are stored under, with each name's sessions, the `UNRESOLVED` column for sessions that resolved to no repository, and last activity; which names more than one repository shares; and how many sessions, and how many usage rows, record their repository. Given directories, prints the name each one's usage is stored under. The store keeps no path, so this is how `api` and `api (2)` are told apart; no key is printed and nothing is stored or sent ([ADR 0019](docs/adr/0019-repository-identity.md)). |
| `signals` | v0.7 | What assaio can report and what your own data supports. `list` names every signal; `describe <id>` says what it counts, where it is honest, and **what a zero means**; `coverage` reads your store and reports per signal whether it is fully, partly or not at all supported, naming the sources that can answer it. |
| `mark` | v0.6 | Labels a session with a task class, outcome and difficulty — category values only, never free text, never synced. Defaults to the newest session in the current repository; `--last`, an id prefix, `--list`, `--unmark`. Since v0.17, `--suggest` derives a label from what the store already holds (branch, skill, sub-agent, entrypoint) through a configurable rule engine, showing the evidence; `--accept-suggested` writes them and never replaces a label made by hand. Sources that disagree derive nothing. Since v0.35 its default target is the newest session with usage resolved to this repository, never one that only shares its name. |
| `digest` | v0.17 | Markdown summary of what **moved** since the last run — totals, model and project movers, verdict changes — for cron/launchd; delivery stays the user's script. Each run stores its own basis (bounded to 12, no session content). Declares a weak comparison rather than reporting through it: overlapping windows, differing window lengths, or a parser change between runs that corrected history. Since v0.22 the AI-line row is named an **output** measure and is withheld when no source records one — this is the surface written to be read out of context, so both facts travel with the figure. |
| `clear` | v0.1 | Deletes stored data; requires an explicit scope and `--yes`. Reports what stays held by the store afterwards. Session labels survive `--all` and are removed only by `--labels` (v0.6), which since v0.13 follows the same scope and only takes the label of a session the clear removes entirely. Since v0.13 a clear that is not time-scoped also forgets it read those inputs, so a plain `backfill` rebuilds; `--older-than` keeps them and says so. Since v0.20 it erases the session-step timeline under the same scope as the records, rather than leaving it behind. |
| `compact` | v0.5 | Reclaims disk space the store freed but still holds (`VACUUM` + WAL truncate); prints the size before and after. Deleting rows never shrinks a SQLite file on its own. |
| `config` | v0.1 | Prints the effective merged configuration and its source path. |
| `plugins` | v0.1 | `list` / `verify` for exec **parser** plugins (protocol conformance, nothing stored). Since v0.25, `init --kind parser\|metric\|rule --lang go\|python\|sh` prints a conforming skeleton for any of the three protocols, so a first plugin starts from something the boundary already accepts rather than from the prose. Since v0.31 `verify` exits non-zero when any record line violates the protocol. |
| `metrics` | v0.1 | `list` / `verify` for exec **metric** plugins (runs on your real window, prints violations + rendered result). |
| `docs` | v0.18 | `export --format json\|html` writes what this binary can enumerate about itself, read from the live registers rather than a maintained list: every signal with what a zero means, every source's depth per signal, every validator with its scope, the whole command tree with flags and defaults, every configuration key with its environment variable, and both halves of the metric contract — the in-tree `analyze.Input` and the plugin wire, side by side. Carries `assaio_docs` as its schema version and deliberately not the build's, so the committed copy is identical on every machine. |
| `version` | v0.1 | Prints the version (also `--version`). |
## Metric validators (`assaio analyze` + dashboard)
Each validator is one file in `internal/analyze/`, self-registered, rendered
generically by the CLI, JSON output, and the dashboard.
| Validator | Since | Question it answers | Built-in caveat |
|-----------|-------|--------------------|-----------------|
| `adoption` | v0.1 | How broad is AI usage (sessions, active days, project/tool breadth) and is it growing? | Breadth, not quality. |
| `burn-anomaly` | v0.3 | Which days burned far outside the window's typical day (robust median/MAD outlier test) — catching a runaway loop or an agent left running. Since v0.28, `week-over-week tokens` compares token volume across the last two complete UTC weeks. | A spike is a prompt to look, not a fault; needs 7+ active days for a baseline. The trend leaves out tools with no row in the window on or before the first day of the earlier week and prints `—` with the reason when the window, the store's history or the volume cannot support a direction. |
| `cache-hygiene` | v0.2 | Prompt-cache reuse: cache-read share of billed input, whether cache writes are reused, what share of writes bought the 1-hour lifetime (v0.12), and the vendor's own top reason for a miss (v0.12). | Cost signal, not quality; a big one-shot task legitimately shows low reuse. The tier and the miss cause are read from Claude Code only; a window whose sources state neither says so rather than reading zero. |
| `concentration` | v0.3 | How token spend spreads across projects, and where a project's share of tokens outruns its share of AI-written lines. | Concentration itself is neither good nor bad; undefined below 2 projects. A project running entirely on a source that records no lines is excluded from the gap rather than read as producing nothing. Project-level only, never people. Since v0.34 the gap compares a project's lines with its tokens from sources that record lines. |
| `context` | v0.1 | Are sessions healthy: turns, peak context, focused minutes, compaction rate? | Neutral below 3 sessions — no verdict from thin data. Since v0.10 each figure is read only from the sessions whose source records it, the reach is stated, and a window whose sources never mark a compaction gets no health verdict at all. |
| `coverage` | v0.2 | How much of the window is high-confidence data: token share from tools with full activity capture, share priced, and — when a window mixes them — the share from per-turn rather than whole-session records. | Says how complete the data is, never whether the work was good. |
| `explore-produce` | v0.3 | What the tool calls were for: reading and searching the codebase vs writing code in it, with reads-per-write. | Exploring earns the right to write; only flags the extreme. Covers Claude Code and Codex; states its own coverage, and un-backfilled history reads as unclassified. |
| `intent` | v0.6 | How much of the window carries a task label, and whether enough classes have enough sessions to compare kinds of work at all. | Reads readiness to stratify, never diligence: it has no unfavorable verdict, and unlabeled sessions stay fully counted everywhere. |
| `edit-loops` | v0.21 | How often a session goes back to a file it has already edited with a command run in between, and which sessions do it far more than the rest of this window. Definition adopted from [CodeBurn](https://github.com/getagentseal/codeburn) rather than invented. | A hard bug looks exactly like a loop; so do a single-file refactor, a hub file, and a red-green test cycle. No cost is claimed — spend inside those stretches measured at 1.04x the window's own per-step rate. Reads only sessions a person ran from a terminal — 25.0% there against 15.5% inside sub-agents on the audited store — and prints the share it excluded. |
| `friction` | v0.3 | How often tool calls fail outright, beside how often a human declines one — the tokens spent on calls that produced nothing. | Some failure is normal probing. Rates cover only tools that mark the outcome of every call (Claude Code today); Codex reports it for file edits alone and is excluded rather than counted as successful. |
| `model-fit` | v0.1 | Premium vs cheaper token share (by real price tier), lines-per-token contrast, sub-agent delegation share. | Reports the mix, never a saving: what another price table would have cost is `reprice`, which carries the span and the unpriced margin. Since v0.34 lines per 1M tokens count only usage whose source records lines for the model that ran. A caveat gives the share of tiered tokens left out. |
| `model-right-sizing` | v0.2 | Premium-model turns that produced little output — downgrade candidates a cheaper/faster model might handle. | Task difficulty is invisible; a prompt to review, not a verdict. On a flat plan it's about speed/limits, not $. |
| `recovery` | v0.21 | What a failed call, a refusal or a lost context costs next — the turns after one against what a turn costs anywhere in the window — plus the sessions that stopped on a failure and the share of work run on a summarized context. | Read per turn, not per step, deliberately: per step the same corpus reads 1.06x where per turn it reads 1.02x, because the steps after a failure are more heavily model turns and a tool call carries no tokens. A last stored step is not always a last step. |
| `reasoning-share` | v0.2 | Extended-thinking (reasoning) share of output among tools that report it, and how much of your output that covers. | Only Codex/reasoning models report it today; Claude Code doesn't. |
| `rework` | v0.1 | Within-session churn (AI lines undone in the same transcript) and human rejection rate. | Directional friction proxy; healthy iteration churns too. The rejection rate divides only by calls whose source records a refusal. |
| `rhythm` | v0.3 | When sessions run: off-hours and weekend share, the time-of-day shape of the work, and how long the longest focused sessions last (p95). | Aggregate workload signal, not an individual measure; hours read in the machine's local timezone. Session length reads only the sources that record focused minutes; timing still covers every session. |
| `session-taxonomy` | v0.2 | The mix of session kinds: conversational (no edits), light-edit, heavy-edit — how you actually use AI. | Descriptive, not a scorecard; conversational is real work. Only sessions whose source records edits are bucketed — elsewhere a zero is the tool's silence, not a conversation. A thrash bucket needs per-session rework (not stored yet). |
| `skill-economics` | v0.3 | Which skills and sub-agents the tokens went to, and how much code each produced — where shared tooling quietly concentrates spend. | Only Claude Code labels turns with a skill/sub-agent today; other tools are absent from the split, not zero. The share is of attributed tokens, and how small a slice of the window those are is declared as its signal coverage. |
| `subscription-fit` | v0.2 | For flat-plan users: the window's API-equivalent projected to a month vs the configured plan cost — a value multiple and an "is it paying off?" verdict. | API-equivalent is an estimate at public prices, not your bill; needs `pricing.monthly_subscription_cost`. |
| `throughput` | v0.1 | Total AI lines, lines per active day, top projects, week-over-week trend. Since v0.28, the trend compares the last two complete UTC weeks, dates its note, and prints `—` with the reason when it cannot be read. Since v0.33 lines per active day counts only days on which a source that records lines ran, and notes the days it leaves out. | Output rate, never a productivity score. Tools with no row in the window on or before the first day of the earlier week are left out of the trend's sums, and the note states the share of volume left out. |
| `turn-efficiency` | v0.2 | Getting more per prompt: one-shot rate, median turns per code-producing session, output tokens per turn. | Task size is invisible; directional, never a per-person score. Reads only sessions whose source records edits, so "no code-producing sessions" never means "no source that could tell". |
Exec **metric plugins** (below) render beside these, namespaced `plugin:<name>`.
## Extension surfaces
| Surface | Since | Contract |
|---------|-------|----------|
| In-tree parser (new data source) | v0.1 | One Go package under `internal/parser/` + golden & fuzz tests ([guide](docs/extending/data-source.md)). |
| Exec **parser** plugin, any language | v0.1 | `<command> scan`, handshake + JSONL records on stdout; `plugins:` in config (ADR 0003). Since v0.14 a record field the protocol does not define is a named violation rather than a silent zero, matching the metric and rule protocols. Since v0.31 a record must state at least one token counter, and a line holds exactly one JSON object. |
| Exec **metric** plugin, any language | v0.1 | `<command> analyze`, Input JSON on stdin → handshake + one Result on stdout; `metrics:` in config (ADR 0004). The Input gained six fields in v0.17 (`skills`, `agents`, `turnSizing`, `cacheMisses`, `delegation`, `planMonthlyCost`) and two in v0.21 (`trace`, `historyStart`), so a plugin reaches what the built-ins reach. **Breaking in v0.22:** a result must declare its `layer` (ADR 0013) or it is rejected whole. **Breaking in v0.24:** `trace` is sent only to a plugin whose `metrics:` entry declares `needs: [trace]` — it encodes to ~44 MB on a real store — and the envelope's new `withheld` field names what was kept back, so an absent section is never mistaken for an empty window. **Breaking in v0.25:** the envelope is a **projection**, not a dump, and the handshake moves to `4`. A plugin answers `<command> describe` with `{"needs":[…]}`, optionally narrowed by `fields` (columns) and `where` (rows), and receives exactly that; `needs:` in the `metrics:` entry becomes the reader's veto over that declaration rather than its source, so every protocol-3 plugin breaks until it grows a `describe` branch. On a real 30-day window the same envelope falls from 1.18 MB to 43 KB for a token-share metric. `assaio-agent plugins init --kind parser\|metric\|rule --lang go\|python\|sh` prints a conforming skeleton, and `docs/conformance/` publishes the vectors that judge one. Member names on every row are pseudonyms, not the name a teammate synced under. Since v0.34 `byModel`, `byProject` and `totals` carry `lineCapableLines`, `lineCapableTokens` and `lineCapableCost`. |
| Exec **rule** plugin, any language | v0.3 | `<command> evaluate`, the window's verdicts on stdin → handshake + one alerts document on stdout; `rules:` in config; gates `check` (ADR 0005). |
| In-tree metric validator | v0.1 | One file implementing `Validator`, registered via `init()` ([guide](docs/extending/metric-validator.md)). |
| Direct SQL on the store | v0.1 | Documented `usage_record` and (v0.20) `session_step` schemas, any SQLite client. |
| JSON/CSV pipes | v0.1 | `report`/`effectiveness` `--format json\|csv` into your own tooling. Since v0.14 the CSV carries `task`, `outcome` and `difficulty`, so a row grouped by a session annotation names the group it belongs to. |
| Per-team configuration | v0.1 | `config.yaml` + `ASSAIO_*` env vars (sources, plugins, metrics, privacy, server, sync, pricing). |
| Documentation generated from the repository | v0.19 | Every guide at [assaio.dev/docs](https://assaio.dev/docs) is rendered from the Markdown it is written in, and a cookbook of recipes — label rules, CI gates, rule plugins, complete validators and metric plugins — is **executed by the test suite**: the label rules are loaded and their derivations asserted, the plugins run against a fixture window and their output held to the protocol. A recipe that stops working fails the build. `make test` also fails when a document prints a flag the binary lacks, recommends a helper no package declares, or links anywhere the site does not serve. |
| Generated reference of every surface | v0.18 | `docs export --format json\|html` from the live registers — signals, source depth, validators and scope, the command tree with flags and defaults, config keys with their environment variables, and both halves of the metric contract. Published as [assaio.dev/docs/reference](https://assaio.dev/docs/reference) (at `/reference` until v0.19 moved it inside the documentation); `make test` fails when a published surface and the binary disagree, in both directions. |
## Data sources (parsers)
"Supported" is not one thing: a source that reports tokens but no edits cannot answer the
same questions as one that reports both. Every source therefore publishes its **depth**
(v0.5), which `doctor` prints for the tools it finds on your machine and which the
`coverage` validator reads instead of keeping its own list.
| Tool | Since | Depth | Tokens & cost | Activity (lines/edits/rework) | Ordered sequence | Attribution (skill / sub-agent) |
|------|-------|-------|---------------|-------------------------------|------------------|---------------------------------|
| Claude Code | v0.1 | **deep** | ✔ (incl. sub-agent turns) | ✔ full, incl. rejections | ✔ steps, outcomes and targets | ✔ |
| OpenAI Codex CLI | v0.1 | standard | ✔ (exact, delta-based) | ✔ except rejections; since v0.13 a created file contributes its lines, which it did not before | ✔ since v0.26: steps and targets; the outcome *signal* is not claimed — only a patch states whether it applied, and a command is marked *completed* whether or not it worked | — |
| Gemini CLI | v0.1 | standard | ✔ | — (sessions, turns and active minutes; no lines, edits or rework; `B39` / `B72` in [BACKLOG.md](BACKLOG.md)) | — | — |
| GitHub Copilot CLI | v0.6 | standard | ✔ (exact, per model, incl. reasoning) | ✔ lines added/removed per session; edit and tool-call counts are in the log but not extracted yet ([audit](docs/extending/source-fields.md)) | — | — |
| Cline | v0.1 | standard | ✔ (recomputed from tokens) | — (sessions, turns and active minutes; no lines, edits or rework; `B39` / `B72` in [BACKLOG.md](BACKLOG.md)) | — | — |
| Antigravity CLI (`agy`) | v0.25 | **activity-only** | — (no counter exists in the format; every token and cost figure withholds rather than counting zero) | ✔ turns, tool calls and edits per turn; no changed lines, rework or compaction | — | — |
| Exec parser plugins | v0.1 | declared per record | ✔ (validated at the boundary) | — (protocol carries tokens only) | — | — |
- **deep** — tokens, per-turn activity, and the labels that say what the work was.
- **standard** — reliable per-turn usage whose activity gaps are documented, not implied.
- **activity-only** — sessions and what was done in them, and no token or cost accounting at
all. Antigravity CLI is the only one: the tier exists because "standard" beside a source
whose every `$` figure withholds would be read as a source you can add to a cost report.
- **import-only** — billing or aggregate figures that cannot support session-level
conclusions. No in-tree parser is here today; a source that emits whole-session records
lands here, which is why granularity travels with every report row. It is not ranked against
activity-only: one knows what happened and not what it cost, the other the reverse.
Plugins are absent from the tiers by design; each plugin's depth depends on what its author
implemented. assaio still assumes every plugin answers the token, cache-read, reasoning and
cost signals, so a counter it never states appears as a measured zero (`B212`).
Within a tier the answer is per signal, not per column: reasoning tokens are reported by
Codex, Gemini CLI and Copilot CLI and by neither Claude Code nor Cline, a declined tool call
only by Claude Code and a context compaction only by Claude Code and Codex, and Copilot's
per-session totals carry changed lines but no edit, tool-call, turn or rework count. The
sequence column splits the same way: Codex records what it did and in what order and which
file it patched, and states an outcome only on a patch. Its commands offer one word, `completed`,
which says a call returned rather than that what it ran worked — so the outcome signal is refused
for it rather than computed over a source that can only ever look clean. Run
`assaio-agent signals coverage` for what your own mix supports; that matrix row is also what
makes a source syncable and targetable by `clear --tool`.
Shared parser guarantees: skip-and-count on corrupt lines, deterministic dedupe keys
(re-runs never double-count), `Granularity` honesty (session-level data never
masquerades as per-turn), fuzz tests with committed corpora. Cline task data is
discovered across VS Code, VS Code Insiders, VSCodium, and Cursor.
## The Assay dashboard
| Element | Since |
|---------|-------|
| Verdict faceplate (one gauge cell per validator, incl. metric plugins) | v0.1 |
| Ledger (figures, "how to read", bars, caveats, takeaway per validator) | v0.1 |
| Top-project drill-down + subpath breakdown | v0.1 |
| Team section (per-member, appears only on a central store) | v0.1 |
| Cost-basis footnote + honesty colophon | v0.1 |
| Light/dark theme, fully self-contained single file, i18n scaffold (EN) | v0.1 |
| Pseudonymized project/member names by default | v0.1 |
| Gauge takes its verdict's colour; captioned ratio; no phantom tile on an uneven grid | v0.15 |
| Takeaway banded in the verdict's colour; confidence coloured only when the basis is weak | v0.15 |
## Team server (MVP)
| Capability | Since |
|-----------|-------|
| `POST /v1/usage` — legacy write route; the v0.37 server returns 426 | v0.1 |
| `POST /v2/usage` — authenticated sync v2 with a token-bound keyed member digest and allowlisted, branch-free records; requires a protocol-2 store | v0.37 |
| `GET /` — served team dashboard, always anonymized; bearer token required since v0.24 (`/healthz` stays open for probes) | v0.1 |
| `GET /healthz` | v0.1 |
| Team-aware CLI: `--db` against a central store | v0.1 (`--by member` withdrawn in v0.22 — see the refusal in BACKLOG.md) |
| Explicitly not executed server-side: exec metric plugins (ADR 0004), exec rule plugins (ADR 0005) | v0.1 |
## Cost honesty & budgeting
| Capability | Since |
|-----------|-------|
| Every `$` disclosed as an estimate at public pay-as-you-go API prices | v0.1 |
| Unpriced models: `—` / `null` / excluded from totals — never a fake `$0` | v0.1; entries with no token rate since v0.29 ([correction](docs/corrections.md#models-with-no-token-rate-were-priced-at-0)) |
| `config.pricing` — subscription / negotiated-rate basis shown alongside the estimate | v0.1 |
| Token-first budgets in `check` (plan-independent), `$` budgets labeled API-equivalent | v0.1 |
| No counterfactual saving anywhere: a re-price states what the observed window costs against another table, never what you would have saved | v0.25 |
## Cross-cutting guarantees
- No network at runtime for local analysis (`sync`, `serve` and `evidence --github` reach it only
when you run them); no telemetry; prompts and model responses are
not extracted, while repository content in recorded diffs and file bodies is read only
transiently to derive counts and is never stored ([PRIVACY.md](PRIVACY.md)).
- A figure is computed only over the sources that record its field, states how much of the
window that reaches, and withholds its verdict rather than averaging in a source's silence
(v0.10, [ADR 0011](docs/adr/0011-capability-gated-metrics.md)). Since v0.11 that holds for
rates over stored columns as well as per-session figures, and an out-of-tree metric plugin
is told each source's capability on the wire (`answers`) so it can apply the same rule.
What each source's log carries and what is deliberately skipped is inventoried per source
([the audit](docs/extending/source-fields.md), v0.10).
- Shares never round in the direction that hides a signal: a small but nonzero share reads
`<1%` rather than `0%`, and a share just under whole reads `>99%` rather than `100%`
(v0.11). A whole sub-agent run is never counted as one turn (v0.11).
- Aggregate and pseudonymized by default; `--by member` is refused in every format; the team panel shows engagement, and output and spend only as a team total;
never a leaderboard.
- Plugins (parser, metric, and rule) are config-declared only — never PATH-scanned, never
downloaded; everything they emit is validated at the boundary and attributed to the
plugin that emitted it (`plugin:<name>`).
- Each usage row records which repository it came from, by a local key that never leaves the
store. A name two repositories share is shown split (`api`, `api (2)`, and `api (?)` for rows
that cannot be placed), and joins from a directory use only rows resolved to that repository
(v0.35, [ADR 0019](docs/adr/0019-repository-identity.md)).
- Joins between sessions and delivery data (candidate commits and their pull requests today) stay on
the machine that ran them: never stored, synced, exported or shown on a dashboard, and no output
names a person or ranks anyone (v0.36, [ADR 0020](docs/adr/0020-correlation-privacy.md)).
- Schema self-heal: an existing local database migrates itself forward.